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 1a24085e..37a4d3bb 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 @@ -1,110 +1,109 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.ImageProducer; -import java.util.ArrayList; -import java.util.List; - -/** - * AbstractImageSource - *

- * - * @author Harald Kuhr - * @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 consumers = new ArrayList(); - protected int width; - protected int height; - protected int xOff; - protected int yOff; - - // ImageProducer interface - public void addConsumer(final ImageConsumer pConsumer) { - if (consumers.contains(pConsumer)) { - return; - } - - consumers.add(pConsumer); - - try { - initConsumer(pConsumer); - sendPixels(pConsumer); - - if (isConsumer(pConsumer)) { - pConsumer.imageComplete(ImageConsumer.STATICIMAGEDONE); - - // Get rid of "sticky" consumers... - if (isConsumer(pConsumer)) { - pConsumer.imageComplete(ImageConsumer.IMAGEERROR); - removeConsumer(pConsumer); - } - } - } - catch (Exception e) { - e.printStackTrace(); - - if (isConsumer(pConsumer)) { - pConsumer.imageComplete(ImageConsumer.IMAGEERROR); - } - } - } - - public void removeConsumer(final ImageConsumer pConsumer) { - consumers.remove(pConsumer); - } - - /** - * 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(final ImageConsumer pConsumer) { - // ignore - } - - public void startProduction(final ImageConsumer pConsumer) { - addConsumer(pConsumer); - } - - public boolean isConsumer(final ImageConsumer pConsumer) { - return consumers.contains(pConsumer); - } - - protected abstract void initConsumer(ImageConsumer pConsumer); - - protected abstract void sendPixels(ImageConsumer pConsumer); -} +/* + * 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 of the copyright holder 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 HOLDER 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.ImageProducer; +import java.util.ArrayList; +import java.util.List; + +/** + * AbstractImageSource + * + * @author Harald Kuhr + * @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 consumers = new ArrayList(); + protected int width; + protected int height; + protected int xOff; + protected int yOff; + + // ImageProducer interface + public void addConsumer(final ImageConsumer pConsumer) { + if (consumers.contains(pConsumer)) { + return; + } + + consumers.add(pConsumer); + + try { + initConsumer(pConsumer); + sendPixels(pConsumer); + + if (isConsumer(pConsumer)) { + pConsumer.imageComplete(ImageConsumer.STATICIMAGEDONE); + + // Get rid of "sticky" consumers... + if (isConsumer(pConsumer)) { + pConsumer.imageComplete(ImageConsumer.IMAGEERROR); + removeConsumer(pConsumer); + } + } + } + catch (Exception e) { + e.printStackTrace(); + + if (isConsumer(pConsumer)) { + pConsumer.imageComplete(ImageConsumer.IMAGEERROR); + } + } + } + + public void removeConsumer(final ImageConsumer pConsumer) { + consumers.remove(pConsumer); + } + + /** + * 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(final ImageConsumer pConsumer) { + // ignore + } + + public void startProduction(final ImageConsumer pConsumer) { + addConsumer(pConsumer); + } + + public boolean isConsumer(final ImageConsumer pConsumer) { + return consumers.contains(pConsumer); + } + + protected abstract void initConsumer(ImageConsumer pConsumer); + + protected abstract void sendPixels(ImageConsumer pConsumer); +} 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 271c7e09..e01f0713 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 @@ -1,170 +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 of the copyright holder 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 HOLDER 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.RGBImageFilter; - - -/** - * Adjusts the contrast and brightness of an image. - *

- * For brightness, the valid range is {@code -2.0,..,0.0,..,2.0}. - * A value of {@code 0.0} means no change. - * Negative values will make the pixels darker. - * Maximum negative value ({@code -2}) will make all filtered pixels black. - * Positive values will make the pixels brighter. - * Maximum positive value ({@code 2}) will make all filtered pixels white. - *

- * For contrast, the valid range is {@code -1.0,..,0.0,..,1.0}. - * A value of {@code 0.0} means no change. - * Negative values will reduce contrast. - * Maximum negative value ({@code -1}) will make all filtered pixels grey - * (no contrast). - * Positive values will increase contrast. - * Maximum positive value ({@code 1}) will make all filtered pixels primary - * colors (either black, white, cyan, magenta, yellow, red, blue or green). - * - * @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/BrightnessContrastFilter.java#1 $ - * - * @todo consider doing something similar to http://archives.java.sun.com/cgi-bin/wa?A2=ind0302&L=jai-interest&F=&S=&P=15947 - */ - -public class BrightnessContrastFilter extends RGBImageFilter { - - // TODO: Replace with RescaleOp? - - // This filter can filter IndexColorModel, as it is does not depend on - // the pixels' location - { - canFilterIndexColorModel = true; - } - - // Use a pre-calculated lookup table for performance - private final int[] LUT; - - /** - * Creates a BrightnessContrastFilter with default values - * ({@code brightness=0.3, contrast=0.3}). - *

- * This will slightly increase both brightness and contrast. - */ - public BrightnessContrastFilter() { - this(0.3f, 0.3f); - } - - /** - * Creates a BrightnessContrastFilter with the given values for brightness - * and contrast. - *

- * For brightness, the valid range is {@code -2.0,..,0.0,..,2.0}. - * A value of {@code 0.0} means no change. - * Negative values will make the pixels darker. - * Maximum negative value ({@code -2}) will make all filtered pixels black. - * Positive values will make the pixels brighter. - * Maximum positive value ({@code 2}) will make all filtered pixels white. - *

- * For contrast, the valid range is {@code -1.0,..,0.0,..,1.0}. - * A value of {@code 0.0} means no change. - * Negative values will reduce contrast. - * Maximum negative value ({@code -1}) will make all filtered pixels grey - * (no contrast). - * Positive values will increase contrast. - * Maximum positive value ({@code 1}) will make all filtered pixels primary - * colors (either black, white, cyan, magenta, yellow, red, blue or green). - * - * @param pBrightness adjust the brightness of the image, in the range - * {@code -2.0,..,0.0,..,2.0}. - * @param pContrast adjust the contrast of the image, in the range - * {@code -1.0,..,0.0,..,1.0}. - */ - public BrightnessContrastFilter(float pBrightness, float pContrast) { - LUT = createLUT(pBrightness, pContrast); - } - - private static int[] createLUT(float pBrightness, float pContrast) { - int[] lut = new int[256]; - - // Hmmm.. This approximates Photoshop values.. Not good though.. - double contrast = pContrast > 0 ? Math.pow(pContrast, 7.0) * 127.0 : pContrast; - - // Convert range [-1,..,0,..,1] -> [0,..,1,..,2] - double brightness = pBrightness + 1.0; - - for (int i = 0; i < 256; i++) { - lut[i] = clamp((int) (127.5 * brightness + (i - 127) * (contrast + 1.0))); - } - - // Special case, to ensure only primary colors for max contrast - if (pContrast == 1f) { - lut[127] = lut[126]; - } - - return lut; - } - - private static int clamp(int i) { - if (i < 0) { - return 0; - } - if (i > 255) { - return 255; - } - return i; - } - - /** - * Filters one pixel, adjusting brightness and contrast according to this - * filter. - * - * @param pX x - * @param pY y - * @param pARGB pixel value in default color space - * - * @return the filtered pixel value in the default color space - */ - public int filterRGB(int pX, int pY, int pARGB) { - // Get color components - int r = pARGB >> 16 & 0xFF; - int g = pARGB >> 8 & 0xFF; - int b = pARGB & 0xFF; - - // Scale to new contrast - r = LUT[r]; - g = LUT[g]; - b = LUT[b]; - - // Return ARGB pixel, leave transparency as is - return (pARGB & 0xFF000000) | (r << 16) | (g << 8) | b; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.RGBImageFilter; + + +/** + * Adjusts the contrast and brightness of an image. + *

+ * For brightness, the valid range is {@code -2.0,..,0.0,..,2.0}. + * A value of {@code 0.0} means no change. + * Negative values will make the pixels darker. + * Maximum negative value ({@code -2}) will make all filtered pixels black. + * Positive values will make the pixels brighter. + * Maximum positive value ({@code 2}) will make all filtered pixels white. + *

+ *

+ * For contrast, the valid range is {@code -1.0,..,0.0,..,1.0}. + * A value of {@code 0.0} means no change. + * Negative values will reduce contrast. + * Maximum negative value ({@code -1}) will make all filtered pixels grey + * (no contrast). + * Positive values will increase contrast. + * Maximum positive value ({@code 1}) will make all filtered pixels primary + * colors (either black, white, cyan, magenta, yellow, red, blue or green). + *

+ * + * @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/BrightnessContrastFilter.java#1 $ + */ +// TODO: consider doing something similar to http://archives.java.sun.com/cgi-bin/wa?A2=ind0302&L=jai-interest&F=&S=&P=15947 +public class BrightnessContrastFilter extends RGBImageFilter { + + // TODO: Replace with RescaleOp? + + // This filter can filter IndexColorModel, as it is does not depend on + // the pixels' location + { + canFilterIndexColorModel = true; + } + + // Use a pre-calculated lookup table for performance + private final int[] LUT; + + /** + * Creates a BrightnessContrastFilter with default values + * ({@code brightness=0.3, contrast=0.3}). + *

+ * This will slightly increase both brightness and contrast. + *

+ */ + public BrightnessContrastFilter() { + this(0.3f, 0.3f); + } + + /** + * Creates a BrightnessContrastFilter with the given values for brightness + * and contrast. + *

+ * For brightness, the valid range is {@code -2.0,..,0.0,..,2.0}. + * A value of {@code 0.0} means no change. + * Negative values will make the pixels darker. + * Maximum negative value ({@code -2}) will make all filtered pixels black. + * Positive values will make the pixels brighter. + * Maximum positive value ({@code 2}) will make all filtered pixels white. + *

+ *

+ * For contrast, the valid range is {@code -1.0,..,0.0,..,1.0}. + * A value of {@code 0.0} means no change. + * Negative values will reduce contrast. + * Maximum negative value ({@code -1}) will make all filtered pixels grey + * (no contrast). + * Positive values will increase contrast. + * Maximum positive value ({@code 1}) will make all filtered pixels primary + * colors (either black, white, cyan, magenta, yellow, red, blue or green). + *

+ * + * @param pBrightness adjust the brightness of the image, in the range + * {@code -2.0,..,0.0,..,2.0}. + * @param pContrast adjust the contrast of the image, in the range + * {@code -1.0,..,0.0,..,1.0}. + */ + public BrightnessContrastFilter(float pBrightness, float pContrast) { + LUT = createLUT(pBrightness, pContrast); + } + + private static int[] createLUT(float pBrightness, float pContrast) { + int[] lut = new int[256]; + + // Hmmm.. This approximates Photoshop values.. Not good though.. + double contrast = pContrast > 0 ? Math.pow(pContrast, 7.0) * 127.0 : pContrast; + + // Convert range [-1,..,0,..,1] -> [0,..,1,..,2] + double brightness = pBrightness + 1.0; + + for (int i = 0; i < 256; i++) { + lut[i] = clamp((int) (127.5 * brightness + (i - 127) * (contrast + 1.0))); + } + + // Special case, to ensure only primary colors for max contrast + if (pContrast == 1f) { + lut[127] = lut[126]; + } + + return lut; + } + + private static int clamp(int i) { + if (i < 0) { + return 0; + } + if (i > 255) { + return 255; + } + return i; + } + + /** + * Filters one pixel, adjusting brightness and contrast according to this + * filter. + * + * @param pX x + * @param pY y + * @param pARGB pixel value in default color space + * + * @return the filtered pixel value in the default color space + */ + public int filterRGB(int pX, int pY, int pARGB) { + // Get color components + int r = pARGB >> 16 & 0xFF; + int g = pARGB >> 8 & 0xFF; + int b = pARGB & 0xFF; + + // Scale to new contrast + 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 22d9d565..a8d16cb7 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 @@ -44,14 +44,16 @@ 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 $ 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 d23b236a..27104fea 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 @@ -1,92 +1,91 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 javax.swing.*; -import java.awt.*; -import java.awt.geom.AffineTransform; -import java.awt.image.BufferedImage; - -/** - * An {@code Icon} implementation backed by a {@code BufferedImage}. - *

- * - * @author Harald Kuhr - * @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 image; - private int width; - private int height; - private final boolean fast; - - public BufferedImageIcon(BufferedImage pImage) { - this(pImage, pImage != null ? pImage.getWidth() : 0, pImage != null ? pImage.getHeight() : 0); - } - - public BufferedImageIcon(BufferedImage pImage, int pWidth, int pHeight) { - this(pImage, pWidth, pHeight, pImage.getWidth() == pWidth && pImage.getHeight() == pHeight); - } - - public BufferedImageIcon(BufferedImage pImage, int pWidth, int pHeight, boolean useFastRendering) { - 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"); - - fast = useFastRendering; - } - - public int getIconHeight() { - return height; - } - - public int getIconWidth() { - return width; - } - - public void paintIcon(Component c, Graphics g, int x, int y) { - if (fast || !(g instanceof Graphics2D)) { - //System.out.println("Scaling fast"); - 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(width / (double) image.getWidth(), height / (double) image.getHeight()); - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g2.drawImage(image, xform, 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 of the copyright holder 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 HOLDER 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 javax.swing.*; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; + +/** + * An {@code Icon} implementation backed by a {@code BufferedImage}. + * + * @author Harald Kuhr + * @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 image; + private int width; + private int height; + private final boolean fast; + + public BufferedImageIcon(BufferedImage pImage) { + this(pImage, pImage != null ? pImage.getWidth() : 0, pImage != null ? pImage.getHeight() : 0); + } + + public BufferedImageIcon(BufferedImage pImage, int pWidth, int pHeight) { + this(pImage, pWidth, pHeight, pImage.getWidth() == pWidth && pImage.getHeight() == pHeight); + } + + public BufferedImageIcon(BufferedImage pImage, int pWidth, int pHeight, boolean useFastRendering) { + 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"); + + fast = useFastRendering; + } + + public int getIconHeight() { + return height; + } + + public int getIconWidth() { + return width; + } + + public void paintIcon(Component c, Graphics g, int x, int y) { + if (fast || !(g instanceof Graphics2D)) { + //System.out.println("Scaling fast"); + 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(width / (double) image.getWidth(), height / (double) image.getHeight()); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(image, xform, null); + } + } +} 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 aa43d527..50c21390 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 @@ -1,483 +1,485 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.geom.Point2D; -import java.awt.geom.Rectangle2D; -import java.awt.image.*; -import java.util.Random; - -/** - * This {@code BufferedImageOp/RasterOp} implements basic - * Floyd-Steinberg error-diffusion algorithm for dithering. - *

- * The weights used are 7/16, 3/16, 5/16 and 1/16, distributed like this: - * - *

- * - * - * - *
 X7/16
3/165/161/16
- *

- * See Computer Graphics (Foley et al.) - * for more information. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * - * @version $Id: DiffusionDither.java#1 $ - */ -public class DiffusionDither implements BufferedImageOp, RasterOp { - - 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(final IndexColorModel pICM) { - // Store color model - indexColorModel = pICM; - } - - /** - * Creates a {@code DiffusionDither}, with no fixed - * {@code IndexColorModel}. The color model will be generated for each - * filtering, unless the destination image already has an - * {@code IndexColorModel}. - */ - public DiffusionDither() { - this(null); - } - - /** - * Sets the scan mode. If the parameter is true, error distribution for - * every even line will be left-to-right, while odd lines will be - * right-to-left. - * The default is {@code true}. - * - * @param pUse {@code true} if scan mode should be alternating left/right - */ - public void setAlternateScans(boolean pUse) { - alternateScans = pUse; - } - - /** - * Creates a compatible {@code BufferedImage} to dither into. - * Only {@code IndexColorModel} allowed. - * - * @return a compatible {@code BufferedImage} - * - * @throws ImageFilterException if {@code pDestCM} is not {@code null} or - * an instance of {@code IndexColorModel}. - */ - public final BufferedImage createCompatibleDestImage(BufferedImage pSource, ColorModel pDestCM) { - if (pDestCM == null) { - return new BufferedImage(pSource.getWidth(), pSource.getHeight(), - BufferedImage.TYPE_BYTE_INDEXED, - getICM(pSource)); - } - else if (pDestCM instanceof IndexColorModel) { - return new BufferedImage(pSource.getWidth(), pSource.getHeight(), - BufferedImage.TYPE_BYTE_INDEXED, - (IndexColorModel) pDestCM); - } - else { - throw new ImageFilterException("Only IndexColorModel allowed."); - } - } - - /** - * Creates a compatible {@code Raster} to dither into. - * Only {@code IndexColorModel} allowed. - * - * @param pSrc the source raster - * - * @return a {@code WritableRaster} - */ - public final WritableRaster createCompatibleDestRaster(Raster pSrc) { - return createCompatibleDestRaster(pSrc, getICM(pSrc)); - } - - /** - * 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()); - } - - - /** - * Returns the bounding box of the filtered destination image. Since - * this is not a geometric operation, the bounding box does not - * change. - * @param pSrc the {@code BufferedImage} to be filtered - * @return the bounds of the filtered definition image. - */ - public final Rectangle2D getBounds2D(BufferedImage pSrc) { - return getBounds2D(pSrc.getRaster()); - } - - /** - * Returns the bounding box of the filtered destination Raster. Since - * this is not a geometric operation, the bounding box does not - * change. - * @param pSrc the {@code Raster} to be filtered - * @return the bounds of the filtered definition {@code Raster}. - */ - public final Rectangle2D getBounds2D(Raster pSrc) { - return pSrc.getBounds(); - } - - /** - * Returns the location of the destination point given a - * point in the source. If {@code dstPt} is not - * {@code null}, it will be used to hold the return value. - * Since this is not a geometric operation, the {@code srcPt} - * will equal the {@code dstPt}. - * @param pSrcPt a {@code Point2D} that represents a point - * in the source image - * @param pDstPt a {@code Point2D}that represents the location - * in the destination - * @return the {@code Point2D} in the destination that - * corresponds to the specified point in the source. - */ - public final Point2D getPoint2D(Point2D pSrcPt, Point2D pDstPt) { - // Create new Point, if needed - if (pDstPt == null) { - pDstPt = new Point2D.Float(); - } - - // Copy location - pDstPt.setLocation(pSrcPt.getX(), pSrcPt.getY()); - - // Return dest - return pDstPt; - } - - /** - * Returns the rendering mHints for this op. - * @return the {@code RenderingHints} object associated - * with this op. - */ - public final RenderingHints getRenderingHints() { - return null; - } - - /** - * Converts an int ARGB to int triplet. - */ - private static int[] toRGBArray(int pARGB, int[] pBuffer) { - pBuffer[0] = ((pARGB & 0x00ff0000) >> 16); - pBuffer[1] = ((pARGB & 0x0000ff00) >> 8); - pBuffer[2] = ((pARGB & 0x000000ff)); - //pBuffer[3] = ((pARGB & 0xff000000) >> 24); // alpha - - return pBuffer; - } - - /** - * Converts a int triplet to int ARGB. - */ - private static int toIntARGB(int[] pRGB) { - return 0xff000000 // All opaque - | (pRGB[0] << 16) - | (pRGB[1] << 8) - | (pRGB[2]); - /* - | ((int) (pRGB[0] << 16) & 0x00ff0000) - | ((int) (pRGB[1] << 8) & 0x0000ff00) - | ((int) (pRGB[2] ) & 0x000000ff); - */ - } - - - /** - * Performs a single-input/single-output dither operation, applying basic - * Floyd-Steinberg error-diffusion to the image. - * - * @param pSource the source image - * @param pDest the destination image - * - * @return the destination image, or a new image, if {@code pDest} was - * {@code null}. - */ - public final BufferedImage filter(BufferedImage pSource, BufferedImage pDest) { - // Create destination image, if none provided - if (pDest == null) { - pDest = createCompatibleDestImage(pSource, getICM(pSource)); - } - else if (!(pDest.getColorModel() instanceof IndexColorModel)) { - throw new ImageFilterException("Only IndexColorModel allowed."); - } - - // Filter rasters - filter(pSource.getRaster(), pDest.getRaster(), (IndexColorModel) pDest.getColorModel()); - - return pDest; - } - - /** - * Performs a single-input/single-output dither operation, applying basic - * Floyd-Steinberg error-diffusion to the image. - * - * @param pSource the source raster, assumed to be in sRGB - * @param pDest the destination raster, may be {@code null} - * - * @return the destination raster, or a new raster, if {@code pDest} was - * {@code null}. - */ - public final WritableRaster filter(final Raster pSource, WritableRaster pDest) { - return filter(pSource, pDest, getICM(pSource)); - } - - private IndexColorModel getICM(BufferedImage pSource) { - return (indexColorModel != null ? indexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK)); - } - private IndexColorModel getICM(Raster pSource) { - return (indexColorModel != null ? indexColorModel : createIndexColorModel(pSource)); - } - - private IndexColorModel createIndexColorModel(Raster pSource) { - BufferedImage image = new BufferedImage(pSource.getWidth(), pSource.getHeight(), - BufferedImage.TYPE_INT_ARGB); - image.setData(pSource); - 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. - * - * @param pSource the source raster, assumed to be in sRGB - * @param pDest the destination raster, may be {@code null} - * @param pColorModel the indexed color model to use - * - * @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) { - int width = pSource.getWidth(); - int height = pSource.getHeight(); - - // Create destination raster if needed - if (pDest == null) { - pDest = createCompatibleDestRaster(pSource, pColorModel); - } - - // Initialize Floyd-Steinberg error vectors. - // +2 to handle the previous pixel and next pixel case minimally - // When reference for column, add 1 to reference as this buffer is - // offset from actual column position by one to allow FS to not check - // left/right edge conditions - int[][] currErr = new int[width + 2][3]; - int[][] nextErr = new int[width + 2][3]; - - // Random errors in [-1 .. 1] - for first row - for (int i = 0; i < width + 2; i++) { - // Note: This is broken for the strange cases where nextInt returns Integer.MIN_VALUE - currErr[i][0] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; - currErr[i][1] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; - currErr[i][2] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; - } - - // Temp buffers - final int[] diff = new int[3]; // No alpha - final int[] inRGB = new int[4]; - final int[] outRGB = new int[4]; - Object pixel = null; - boolean forward = true; - - // Loop through image data - for (int y = 0; y < height; y++) { - // Clear out next error rows for colour errors - for (int i = nextErr.length; --i >= 0;) { - nextErr[i][0] = 0; - nextErr[i][1] = 0; - nextErr[i][2] = 0; - } - - // Set up start column and limit - int x; - int limit; - if (forward) { - x = 0; - limit = width; - } - else { - x = width - 1; - limit = -1; - } - - // TODO: Use getPixels instead of getPixel for better performance? - - // Loop over row - while (true) { - // Get RGB from original raster - // DON'T KNOW IF THIS WILL WORK FOR ALL TYPES. - pSource.getPixel(x, y, inRGB); - - // Get error for this pixel & add error to rgb - for (int i = 0; i < 3; i++) { - // Make a 28.4 FP number, add Error (with fraction), - // rounding and truncate to int - inRGB[i] = ((inRGB[i] << 4) + currErr[x + 1][i] + 0x08) >> 4; - - // Clamp - if (inRGB[i] > 255) { - inRGB[i] = 255; - } - else if (inRGB[i] < 0) { - inRGB[i] = 0; - } - } - - // Get pixel value... - // It is VERY important that we are using a IndexColorModel that - // support reverse color lookup for speed. - pixel = pColorModel.getDataElements(toIntARGB(inRGB), pixel); - - // ...set it... - pDest.setDataElements(x, y, pixel); - - // ..and get back the closet match - pDest.getPixel(x, y, outRGB); - - // Convert the value to default sRGB - // Should work for all transfertypes supported by IndexColorModel - toRGBArray(pColorModel.getRGB(outRGB[0]), outRGB); - - // Find diff - diff[0] = inRGB[0] - outRGB[0]; - diff[1] = inRGB[1] - outRGB[1]; - diff[2] = inRGB[2] - outRGB[2]; - - // Apply F-S error diffusion - // Serpentine scan: left-right - if (forward) { - // Row 1 (y) - // Update error in this pixel (x + 1) - currErr[x + 2][0] += diff[0] * 7; - currErr[x + 2][1] += diff[1] * 7; - currErr[x + 2][2] += diff[2] * 7; - - // Row 2 (y + 1) - // Update error in this pixel (x - 1) - nextErr[x][0] += diff[0] * 3; - nextErr[x][1] += diff[1] * 3; - nextErr[x][2] += diff[2] * 3; - // Update error in this pixel (x) - nextErr[x + 1][0] += diff[0] * 5; - nextErr[x + 1][1] += diff[1] * 5; - nextErr[x + 1][2] += diff[2] * 5; - // Update error in this pixel (x + 1) - // TODO: Consider calculating this using - // error term = error - sum(error terms 1, 2 and 3) - // See Computer Graphics (Foley et al.), p. 573 - nextErr[x + 2][0] += diff[0]; // * 1; - nextErr[x + 2][1] += diff[1]; // * 1; - nextErr[x + 2][2] += diff[2]; // * 1; - - // Next - x++; - - // Done? - if (x >= limit) { - break; - } - - } - else { - // Row 1 (y) - // Update error in this pixel (x - 1) - currErr[x][0] += diff[0] * 7; - currErr[x][1] += diff[1] * 7; - currErr[x][2] += diff[2] * 7; - - // Row 2 (y + 1) - // Update error in this pixel (x + 1) - nextErr[x + 2][0] += diff[0] * 3; - nextErr[x + 2][1] += diff[1] * 3; - nextErr[x + 2][2] += diff[2] * 3; - // Update error in this pixel (x) - nextErr[x + 1][0] += diff[0] * 5; - nextErr[x + 1][1] += diff[1] * 5; - nextErr[x + 1][2] += diff[2] * 5; - // Update error in this pixel (x - 1) - // TODO: Consider calculating this using - // error term = error - sum(error terms 1, 2 and 3) - // See Computer Graphics (Foley et al.), p. 573 - nextErr[x][0] += diff[0]; // * 1; - nextErr[x][1] += diff[1]; // * 1; - nextErr[x][2] += diff[2]; // * 1; - - // Previous - x--; - - // Done? - if (x <= limit) { - break; - } - } - } - - // Make next error info current for next iteration - int[][] temperr; - temperr = currErr; - currErr = nextErr; - nextErr = temperr; - - // Toggle direction - if (alternateScans) { - forward = !forward; - } - } - - return pDest; - } +/* + * 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 of the copyright holder 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 HOLDER 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.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.*; +import java.util.Random; + +/** + * This {@code BufferedImageOp/RasterOp} implements basic + * Floyd-Steinberg error-diffusion algorithm for dithering. + *

+ * The weights used are 7/16, 3/16, 5/16 and 1/16, distributed like this: + * + *

+ * + * + * + * + *
Floyd-Steinberg error-diffusion weights
 x7/16
3/165/161/16
+ *

+ * See Computer Graphics (Foley et al.) + * for more information. + *

+ * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: DiffusionDither.java#1 $ + */ +public class DiffusionDither implements BufferedImageOp, RasterOp { + + 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(final IndexColorModel pICM) { + // Store color model + indexColorModel = pICM; + } + + /** + * Creates a {@code DiffusionDither}, with no fixed + * {@code IndexColorModel}. The color model will be generated for each + * filtering, unless the destination image already has an + * {@code IndexColorModel}. + */ + public DiffusionDither() { + this(null); + } + + /** + * Sets the scan mode. If the parameter is true, error distribution for + * every even line will be left-to-right, while odd lines will be + * right-to-left. + * The default is {@code true}. + * + * @param pUse {@code true} if scan mode should be alternating left/right + */ + public void setAlternateScans(boolean pUse) { + alternateScans = pUse; + } + + /** + * Creates a compatible {@code BufferedImage} to dither into. + * Only {@code IndexColorModel} allowed. + * + * @return a compatible {@code BufferedImage} + * + * @throws ImageFilterException if {@code pDestCM} is not {@code null} or + * an instance of {@code IndexColorModel}. + */ + public final BufferedImage createCompatibleDestImage(BufferedImage pSource, ColorModel pDestCM) { + if (pDestCM == null) { + return new BufferedImage(pSource.getWidth(), pSource.getHeight(), + BufferedImage.TYPE_BYTE_INDEXED, + getICM(pSource)); + } + else if (pDestCM instanceof IndexColorModel) { + return new BufferedImage(pSource.getWidth(), pSource.getHeight(), + BufferedImage.TYPE_BYTE_INDEXED, + (IndexColorModel) pDestCM); + } + else { + throw new ImageFilterException("Only IndexColorModel allowed."); + } + } + + /** + * Creates a compatible {@code Raster} to dither into. + * Only {@code IndexColorModel} allowed. + * + * @param pSrc the source raster + * + * @return a {@code WritableRaster} + */ + public final WritableRaster createCompatibleDestRaster(Raster pSrc) { + return createCompatibleDestRaster(pSrc, getICM(pSrc)); + } + + /** + * 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()); + } + + + /** + * Returns the bounding box of the filtered destination image. Since + * this is not a geometric operation, the bounding box does not + * change. + * @param pSrc the {@code BufferedImage} to be filtered + * @return the bounds of the filtered definition image. + */ + public final Rectangle2D getBounds2D(BufferedImage pSrc) { + return getBounds2D(pSrc.getRaster()); + } + + /** + * Returns the bounding box of the filtered destination Raster. Since + * this is not a geometric operation, the bounding box does not + * change. + * @param pSrc the {@code Raster} to be filtered + * @return the bounds of the filtered definition {@code Raster}. + */ + public final Rectangle2D getBounds2D(Raster pSrc) { + return pSrc.getBounds(); + } + + /** + * Returns the location of the destination point given a + * point in the source. If {@code dstPt} is not + * {@code null}, it will be used to hold the return value. + * Since this is not a geometric operation, the {@code srcPt} + * will equal the {@code dstPt}. + * @param pSrcPt a {@code Point2D} that represents a point + * in the source image + * @param pDstPt a {@code Point2D}that represents the location + * in the destination + * @return the {@code Point2D} in the destination that + * corresponds to the specified point in the source. + */ + public final Point2D getPoint2D(Point2D pSrcPt, Point2D pDstPt) { + // Create new Point, if needed + if (pDstPt == null) { + pDstPt = new Point2D.Float(); + } + + // Copy location + pDstPt.setLocation(pSrcPt.getX(), pSrcPt.getY()); + + // Return dest + return pDstPt; + } + + /** + * Returns the rendering mHints for this op. + * @return the {@code RenderingHints} object associated + * with this op. + */ + public final RenderingHints getRenderingHints() { + return null; + } + + /** + * Converts an int ARGB to int triplet. + */ + private static int[] toRGBArray(int pARGB, int[] pBuffer) { + pBuffer[0] = ((pARGB & 0x00ff0000) >> 16); + pBuffer[1] = ((pARGB & 0x0000ff00) >> 8); + pBuffer[2] = ((pARGB & 0x000000ff)); + //pBuffer[3] = ((pARGB & 0xff000000) >> 24); // alpha + + return pBuffer; + } + + /** + * Converts a int triplet to int ARGB. + */ + private static int toIntARGB(int[] pRGB) { + return 0xff000000 // All opaque + | (pRGB[0] << 16) + | (pRGB[1] << 8) + | (pRGB[2]); + /* + | ((int) (pRGB[0] << 16) & 0x00ff0000) + | ((int) (pRGB[1] << 8) & 0x0000ff00) + | ((int) (pRGB[2] ) & 0x000000ff); + */ + } + + + /** + * Performs a single-input/single-output dither operation, applying basic + * Floyd-Steinberg error-diffusion to the image. + * + * @param pSource the source image + * @param pDest the destination image + * + * @return the destination image, or a new image, if {@code pDest} was + * {@code null}. + */ + public final BufferedImage filter(BufferedImage pSource, BufferedImage pDest) { + // Create destination image, if none provided + if (pDest == null) { + pDest = createCompatibleDestImage(pSource, getICM(pSource)); + } + else if (!(pDest.getColorModel() instanceof IndexColorModel)) { + throw new ImageFilterException("Only IndexColorModel allowed."); + } + + // Filter rasters + filter(pSource.getRaster(), pDest.getRaster(), (IndexColorModel) pDest.getColorModel()); + + return pDest; + } + + /** + * Performs a single-input/single-output dither operation, applying basic + * Floyd-Steinberg error-diffusion to the image. + * + * @param pSource the source raster, assumed to be in sRGB + * @param pDest the destination raster, may be {@code null} + * + * @return the destination raster, or a new raster, if {@code pDest} was + * {@code null}. + */ + public final WritableRaster filter(final Raster pSource, WritableRaster pDest) { + return filter(pSource, pDest, getICM(pSource)); + } + + private IndexColorModel getICM(BufferedImage pSource) { + return (indexColorModel != null ? indexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK)); + } + private IndexColorModel getICM(Raster pSource) { + return (indexColorModel != null ? indexColorModel : createIndexColorModel(pSource)); + } + + private IndexColorModel createIndexColorModel(Raster pSource) { + BufferedImage image = new BufferedImage(pSource.getWidth(), pSource.getHeight(), + BufferedImage.TYPE_INT_ARGB); + image.setData(pSource); + 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. + * + * @param pSource the source raster, assumed to be in sRGB + * @param pDest the destination raster, may be {@code null} + * @param pColorModel the indexed color model to use + * + * @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) { + int width = pSource.getWidth(); + int height = pSource.getHeight(); + + // Create destination raster if needed + if (pDest == null) { + pDest = createCompatibleDestRaster(pSource, pColorModel); + } + + // Initialize Floyd-Steinberg error vectors. + // +2 to handle the previous pixel and next pixel case minimally + // When reference for column, add 1 to reference as this buffer is + // offset from actual column position by one to allow FS to not check + // left/right edge conditions + int[][] currErr = new int[width + 2][3]; + int[][] nextErr = new int[width + 2][3]; + + // Random errors in [-1 .. 1] - for first row + for (int i = 0; i < width + 2; i++) { + // Note: This is broken for the strange cases where nextInt returns Integer.MIN_VALUE + currErr[i][0] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; + currErr[i][1] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; + currErr[i][2] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; + } + + // Temp buffers + final int[] diff = new int[3]; // No alpha + final int[] inRGB = new int[4]; + final int[] outRGB = new int[4]; + Object pixel = null; + boolean forward = true; + + // Loop through image data + for (int y = 0; y < height; y++) { + // Clear out next error rows for colour errors + for (int i = nextErr.length; --i >= 0;) { + nextErr[i][0] = 0; + nextErr[i][1] = 0; + nextErr[i][2] = 0; + } + + // Set up start column and limit + int x; + int limit; + if (forward) { + x = 0; + limit = width; + } + else { + x = width - 1; + limit = -1; + } + + // TODO: Use getPixels instead of getPixel for better performance? + + // Loop over row + while (true) { + // Get RGB from original raster + // DON'T KNOW IF THIS WILL WORK FOR ALL TYPES. + pSource.getPixel(x, y, inRGB); + + // Get error for this pixel & add error to rgb + for (int i = 0; i < 3; i++) { + // Make a 28.4 FP number, add Error (with fraction), + // rounding and truncate to int + inRGB[i] = ((inRGB[i] << 4) + currErr[x + 1][i] + 0x08) >> 4; + + // Clamp + if (inRGB[i] > 255) { + inRGB[i] = 255; + } + else if (inRGB[i] < 0) { + inRGB[i] = 0; + } + } + + // Get pixel value... + // It is VERY important that we are using a IndexColorModel that + // support reverse color lookup for speed. + pixel = pColorModel.getDataElements(toIntARGB(inRGB), pixel); + + // ...set it... + pDest.setDataElements(x, y, pixel); + + // ..and get back the closet match + pDest.getPixel(x, y, outRGB); + + // Convert the value to default sRGB + // Should work for all transfertypes supported by IndexColorModel + toRGBArray(pColorModel.getRGB(outRGB[0]), outRGB); + + // Find diff + diff[0] = inRGB[0] - outRGB[0]; + diff[1] = inRGB[1] - outRGB[1]; + diff[2] = inRGB[2] - outRGB[2]; + + // Apply F-S error diffusion + // Serpentine scan: left-right + if (forward) { + // Row 1 (y) + // Update error in this pixel (x + 1) + currErr[x + 2][0] += diff[0] * 7; + currErr[x + 2][1] += diff[1] * 7; + currErr[x + 2][2] += diff[2] * 7; + + // Row 2 (y + 1) + // Update error in this pixel (x - 1) + nextErr[x][0] += diff[0] * 3; + nextErr[x][1] += diff[1] * 3; + nextErr[x][2] += diff[2] * 3; + // Update error in this pixel (x) + nextErr[x + 1][0] += diff[0] * 5; + nextErr[x + 1][1] += diff[1] * 5; + nextErr[x + 1][2] += diff[2] * 5; + // Update error in this pixel (x + 1) + // TODO: Consider calculating this using + // error term = error - sum(error terms 1, 2 and 3) + // See Computer Graphics (Foley et al.), p. 573 + nextErr[x + 2][0] += diff[0]; // * 1; + nextErr[x + 2][1] += diff[1]; // * 1; + nextErr[x + 2][2] += diff[2]; // * 1; + + // Next + x++; + + // Done? + if (x >= limit) { + break; + } + + } + else { + // Row 1 (y) + // Update error in this pixel (x - 1) + currErr[x][0] += diff[0] * 7; + currErr[x][1] += diff[1] * 7; + currErr[x][2] += diff[2] * 7; + + // Row 2 (y + 1) + // Update error in this pixel (x + 1) + nextErr[x + 2][0] += diff[0] * 3; + nextErr[x + 2][1] += diff[1] * 3; + nextErr[x + 2][2] += diff[2] * 3; + // Update error in this pixel (x) + nextErr[x + 1][0] += diff[0] * 5; + nextErr[x + 1][1] += diff[1] * 5; + nextErr[x + 1][2] += diff[2] * 5; + // Update error in this pixel (x - 1) + // TODO: Consider calculating this using + // error term = error - sum(error terms 1, 2 and 3) + // See Computer Graphics (Foley et al.), p. 573 + nextErr[x][0] += diff[0]; // * 1; + nextErr[x][1] += diff[1]; // * 1; + nextErr[x][2] += diff[2]; // * 1; + + // Previous + x--; + + // Done? + if (x <= limit) { + break; + } + } + } + + // Make next error info current for next iteration + int[][] temperr; + temperr = currErr; + currErr = nextErr; + nextErr = temperr; + + // Toggle direction + if (alternateScans) { + forward = !forward; + } + } + + return pDest; + } } \ No newline at end of file diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java b/common/common-image/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java index 2ff7c85d..94be2459 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java @@ -1,84 +1,86 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.*; - -/** - * GraphicsUtil - * - * @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/GraphicsUtil.java#1 $ - */ -public final class GraphicsUtil { - - /** - * Enables anti-aliasing in the {@code Graphics} object. - *

- * Anti-aliasing is enabled by casting to {@code Graphics2D} and setting - * the rendering hint {@code RenderingHints.KEY_ANTIALIASING} to - * {@code RenderingHints.VALUE_ANTIALIAS_ON}. - * - * @param pGraphics the graphics object - * @throws ClassCastException if {@code pGraphics} is not an instance of - * {@code Graphics2D}. - * - * @see java.awt.RenderingHints#KEY_ANTIALIASING - */ - public static void enableAA(final Graphics pGraphics) { - ((Graphics2D) pGraphics).setRenderingHint( - RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON - ); - } - - /** - * Sets the alpha in the {@code Graphics} object. - *

- * Alpha is set by casting to {@code Graphics2D} and setting the composite - * to the rule {@code AlphaComposite.SRC_OVER} multiplied by the given - * alpha. - * - * @param pGraphics the graphics object - * @param pAlpha the alpha level, {@code alpha} must be a floating point - * number in the inclusive range [0.0, 1.0]. - * @throws ClassCastException if {@code pGraphics} is not an instance of - * {@code Graphics2D}. - * - * @see java.awt.AlphaComposite#SRC_OVER - * @see java.awt.AlphaComposite#getInstance(int, float) - */ - public static void setAlpha(final Graphics pGraphics, final float pAlpha) { - ((Graphics2D) pGraphics).setComposite( - AlphaComposite.getInstance(AlphaComposite.SRC_OVER, pAlpha) - ); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.*; + +/** + * GraphicsUtil + * + * @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/GraphicsUtil.java#1 $ + */ +public final class GraphicsUtil { + + /** + * Enables anti-aliasing in the {@code Graphics} object. + *

+ * Anti-aliasing is enabled by casting to {@code Graphics2D} and setting + * the rendering hint {@code RenderingHints.KEY_ANTIALIASING} to + * {@code RenderingHints.VALUE_ANTIALIAS_ON}. + *

+ * + * @param pGraphics the graphics object + * @throws ClassCastException if {@code pGraphics} is not an instance of + * {@code Graphics2D}. + * + * @see java.awt.RenderingHints#KEY_ANTIALIASING + */ + public static void enableAA(final Graphics pGraphics) { + ((Graphics2D) pGraphics).setRenderingHint( + RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON + ); + } + + /** + * Sets the alpha in the {@code Graphics} object. + *

+ * Alpha is set by casting to {@code Graphics2D} and setting the composite + * to the rule {@code AlphaComposite.SRC_OVER} multiplied by the given + * alpha. + *

+ * + * @param pGraphics the graphics object + * @param pAlpha the alpha level, {@code alpha} must be a floating point + * number in the inclusive range [0.0, 1.0]. + * @throws ClassCastException if {@code pGraphics} is not an instance of + * {@code Graphics2D}. + * + * @see java.awt.AlphaComposite#SRC_OVER + * @see java.awt.AlphaComposite#getInstance(int, float) + */ + public static void setAlpha(final Graphics pGraphics, final float pAlpha) { + ((Graphics2D) pGraphics).setComposite( + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, pAlpha) + ); + } +} 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 6eedf33e..27332422 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 @@ -1,131 +1,132 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.RGBImageFilter; - -/** - * This class can convert a color image to grayscale. - *

- * Uses ITU standard conversion: (222 * Red + 707 * Green + 71 * Blue) / 1000. - * - * @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/GrayFilter.java#1 $ - * - */ -public class GrayFilter extends RGBImageFilter { - - // This filter can filter IndexColorModel - { - canFilterIndexColorModel = true; - } - - private int low = 0; - private float range = 1.0f; - - /** - * Constructs a GrayFilter using ITU color-conversion. - */ - public GrayFilter() { - } - - /** - * Constructs a GrayFilter using ITU color-conversion, and a dynamic range between - * pLow and pHigh. - * - * @param pLow float in the range 0..1 - * @param pHigh float in the range 0..1 and >= pLow - */ - public GrayFilter(float pLow, float pHigh) { - if (pLow > pHigh) { - pLow = 0f; - } - // Make sure high and low are inside range - if (pLow < 0f) { - pLow = 0f; - } - else if (pLow > 1f) { - pLow = 1f; - } - if (pHigh < 0f) { - pHigh = 0f; - } - else if (pHigh > 1f) { - pHigh = 1f; - } - - low = (int) (pLow * 255f); - range = pHigh - pLow; - - } - - /** - * Constructs a GrayFilter using ITU color-conversion, and a dynamic - * range between pLow and pHigh. - * - * @param pLow integer in the range 0..255 - * @param pHigh inteeger in the range 0..255 and >= pLow - */ - public GrayFilter(int pLow, int pHigh) { - this(pLow / 255f, pHigh / 255f); - } - - /** - * Filters one pixel using ITU color-conversion. - * - * @param pX x - * @param pY y - * @param pARGB pixel value in default color space - * - * @return the filtered pixel value in the default color space - */ - public int filterRGB(int pX, int pY, int pARGB) { - // Get color components - int r = pARGB >> 16 & 0xFF; - int g = pARGB >> 8 & 0xFF; - int b = pARGB & 0xFF; - - // ITU standard: Gray scale=(222*Red+707*Green+71*Blue)/1000 - int gray = (222 * r + 707 * g + 71 * b) / 1000; - - //int gray = (int) ((float) (r + g + b) / 3.0f); - - if (range != 1.0f) { - // Apply range - gray = low + (int) (gray * range); - } - - // Return ARGB pixel - return (pARGB & 0xFF000000) | (gray << 16) | (gray << 8) | gray; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.RGBImageFilter; + +/** + * This class can convert a color image to grayscale. + *

+ * Uses ITU standard conversion: (222 * Red + 707 * Green + 71 * Blue) / 1000. + *

+ * + * @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/GrayFilter.java#1 $ + * + */ +public class GrayFilter extends RGBImageFilter { + + // This filter can filter IndexColorModel + { + canFilterIndexColorModel = true; + } + + private int low = 0; + private float range = 1.0f; + + /** + * Constructs a GrayFilter using ITU color-conversion. + */ + public GrayFilter() { + } + + /** + * Constructs a GrayFilter using ITU color-conversion, and a dynamic range between + * pLow and pHigh. + * + * @param pLow float in the range 0..1 + * @param pHigh float in the range 0..1 and >= pLow + */ + public GrayFilter(float pLow, float pHigh) { + if (pLow > pHigh) { + pLow = 0f; + } + // Make sure high and low are inside range + if (pLow < 0f) { + pLow = 0f; + } + else if (pLow > 1f) { + pLow = 1f; + } + if (pHigh < 0f) { + pHigh = 0f; + } + else if (pHigh > 1f) { + pHigh = 1f; + } + + low = (int) (pLow * 255f); + range = pHigh - pLow; + + } + + /** + * Constructs a GrayFilter using ITU color-conversion, and a dynamic + * range between pLow and pHigh. + * + * @param pLow integer in the range 0..255 + * @param pHigh integer in the range 0..255 and >= pLow + */ + public GrayFilter(int pLow, int pHigh) { + this(pLow / 255f, pHigh / 255f); + } + + /** + * Filters one pixel using ITU color-conversion. + * + * @param pX x + * @param pY y + * @param pARGB pixel value in default color space + * + * @return the filtered pixel value in the default color space + */ + public int filterRGB(int pX, int pY, int pARGB) { + // Get color components + int r = pARGB >> 16 & 0xFF; + int g = pARGB >> 8 & 0xFF; + int b = pARGB & 0xFF; + + // ITU standard: Gray scale=(222*Red+707*Green+71*Blue)/1000 + int gray = (222 * r + 707 * g + 71 * b) / 1000; + + //int gray = (int) ((float) (r + g + b) / 3.0f); + + if (range != 1.0f) { + // Apply range + gray = low + (int) (gray * range); + } + + // Return ARGB pixel + return (pARGB & 0xFF000000) | (gray << 16) | (gray << 8) | gray; + } +} 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 0ddc9be3..cef0b7e2 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 @@ -1,1942 +1,1956 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.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. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: common/common-image/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; - public final static int ROTATE_180 = 180; - - public final static int FLIP_VERTICAL = -1; - public final static int FLIP_HORIZONTAL = 1; - - /** - * Alias for {@link ConvolveOp#EDGE_ZERO_FILL}. - * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int) - * @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. - * Note that while this mode typically provides better quality than the - * standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so - * at the expense of higher memory consumption and considerable more computation. - * @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. - * Note that while this mode typically provides better quality than the - * standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so - * at the expense of higher memory consumption and considerable more computation. - * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int) - * @see #EDGE_REFLECT - */ - public static final int EDGE_WRAP = 3; // as JAI BORDER_WRAP - - /** - * Java default dither - */ - public final static int DITHER_DEFAULT = IndexImage.DITHER_DEFAULT; - - /** - * No dither - */ - public final static int DITHER_NONE = IndexImage.DITHER_NONE; - - /** - * Error diffusion dither - */ - public final static int DITHER_DIFFUSION = IndexImage.DITHER_DIFFUSION; - - /** - * Error diffusion dither with alternating scans - */ - public final static int DITHER_DIFFUSION_ALTSCANS = IndexImage.DITHER_DIFFUSION_ALTSCANS; - - /** - * Default color selection - */ - public final static int COLOR_SELECTION_DEFAULT = IndexImage.COLOR_SELECTION_DEFAULT; - - /** - * Prioritize speed - */ - public final static int COLOR_SELECTION_FAST = IndexImage.COLOR_SELECTION_FAST; - - /** - * Prioritize quality - */ - public final static int COLOR_SELECTION_QUALITY = IndexImage.COLOR_SELECTION_QUALITY; - - /** - * Default transparency (none) - */ - public final static int TRANSPARENCY_DEFAULT = IndexImage.TRANSPARENCY_DEFAULT; - - /** - * Discard any alpha information - */ - public final static int TRANSPARENCY_OPAQUE = IndexImage.TRANSPARENCY_OPAQUE; - - /** - * Convert alpha to bitmask - */ - public final static int TRANSPARENCY_BITMASK = IndexImage.TRANSPARENCY_BITMASK; - - /** - * Keep original alpha (not supported yet) - */ - protected final static int TRANSPARENCY_TRANSLUCENT = IndexImage.TRANSPARENCY_TRANSLUCENT; - - /** Passed to the createXxx methods, to indicate that the type does not matter */ - private final static int BI_TYPE_ANY = -1; - /* - public final static int BI_TYPE_ANY_TRANSLUCENT = -1; - public final static int BI_TYPE_ANY_BITMASK = -2; - public final static int BI_TYPE_ANY_OPAQUE = -3;*/ - - /** Tells wether this WM may support acceleration of some images */ - private static boolean VM_SUPPORTS_ACCELERATION = true; - - /** The sharpen matrix */ - private static final float[] SHARPEN_MATRIX = new float[] { - 0.0f, -0.3f, 0.0f, - -0.3f, 2.2f, -0.3f, - 0.0f, -0.3f, 0.0f - }; - - /** - * The sharpen kernel. Uses the following 3 by 3 matrix: - * - * - * - * - *
0.0-0.30.0
-0.32.2-0.3
0.0-0.30.0
- */ - private static final Kernel SHARPEN_KERNEL = new Kernel(3, 3, SHARPEN_MATRIX); - - /** - * Component that can be used with the MediaTracker etc. - */ - private static final Component NULL_COMPONENT = new Component() {}; - - /** Our static image tracker */ - private static MediaTracker sTracker = new MediaTracker(NULL_COMPONENT); - - /** */ - protected static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); - /** */ - protected static final Point LOCATION_UPPER_LEFT = new Point(0, 0); - - /** */ - private static final GraphicsConfiguration DEFAULT_CONFIGURATION = getDefaultGraphicsConfiguration(); - - private static GraphicsConfiguration getDefaultGraphicsConfiguration() { - try { - GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); - if (!env.isHeadlessInstance()) { - return env.getDefaultScreenDevice().getDefaultConfiguration(); - } - } - catch (LinkageError e) { - // Means we are not in a 1.4+ VM, so skip testing for headless again - VM_SUPPORTS_ACCELERATION = false; - } - return null; - } - - /** Creates an ImageUtil. Private constructor. */ - private ImageUtil() { - } - - /** - * Converts the {@code RenderedImage} to a {@code BufferedImage}. - * The new image will have the same {@code ColorModel}, - * {@code Raster} and properties as the original image, if possible. - *

- * If the image is already a {@code BufferedImage}, it is simply returned - * and no conversion takes place. - * - * @param pOriginal the image to convert. - * - * @return a {@code BufferedImage} - */ - public static BufferedImage toBuffered(RenderedImage pOriginal) { - // Don't convert if it already is a BufferedImage - if (pOriginal instanceof BufferedImage) { - return (BufferedImage) pOriginal; - } - if (pOriginal == null) { - throw new IllegalArgumentException("original == null"); - } - - // Copy properties - Hashtable properties; - String[] names = pOriginal.getPropertyNames(); - if (names != null && names.length > 0) { - properties = new Hashtable(names.length); - - for (String name : names) { - properties.put(name, pOriginal.getProperty(name)); - } - } - else { - properties = null; - } - - // NOTE: This is a workaround for the broken Batik '*Red' classes, that - // throw NPE if copyData(null) is used. This may actually be faster too. - // See RenderedImage#copyData / RenderedImage#getData - Raster data = pOriginal.getData(); - WritableRaster raster; - if (data instanceof WritableRaster) { - raster = (WritableRaster) data; - } - else { - raster = data.createCompatibleWritableRaster(); - raster = pOriginal.copyData(raster); - } - - // Create buffered image - ColorModel colorModel = pOriginal.getColorModel(); - return new BufferedImage(colorModel, raster, - colorModel.isAlphaPremultiplied(), - properties); - } - - /** - * Converts the {@code RenderedImage} to a {@code BufferedImage} of the - * given type. - *

- * 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. - * @param pType the type of buffered image - * - * @return a {@code BufferedImage} - * - * @throws IllegalArgumentException if {@code pOriginal == null} - * or {@code pType} is not a valid type for {@code BufferedImage} - * - * @see java.awt.image.BufferedImage#getType() - */ - public static BufferedImage toBuffered(RenderedImage pOriginal, int pType) { - // Don't convert if it already is BufferedImage and correct type - if ((pOriginal instanceof BufferedImage) && ((BufferedImage) pOriginal).getType() == pType) { - return (BufferedImage) pOriginal; - } - if (pOriginal == null) { - throw new IllegalArgumentException("original == null"); - } - - // Create a buffered image - BufferedImage image = createBuffered(pOriginal.getWidth(), - pOriginal.getHeight(), - pType, Transparency.TRANSLUCENT); - - // Draw the image onto the buffer - // NOTE: This is faster than doing a raster conversion in most cases - Graphics2D g = image.createGraphics(); - try { - g.setComposite(AlphaComposite.Src); - g.drawRenderedImage(pOriginal, IDENTITY_TRANSFORM); - } - finally { - g.dispose(); - } - - return image; - } - - /** - * Converts the {@code BufferedImage} to a {@code BufferedImage} of the - * 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 already a {@code BufferedImage} of the given type, it - * is simply returned and no conversion takes place. - *

- * This method simply invokes - * {@link #toBuffered(RenderedImage,int) toBuffered((RenderedImage) pOriginal, pType)}. - * - * @param pOriginal the image to convert. - * @param pType the type of buffered image - * - * @return a {@code BufferedImage} - * - * @throws IllegalArgumentException if {@code pOriginal == null} - * or if {@code pType} is not a valid type for {@code BufferedImage} - * - * @see java.awt.image.BufferedImage#getType() - */ - public static BufferedImage toBuffered(BufferedImage pOriginal, int pType) { - return toBuffered((RenderedImage) pOriginal, pType); - } - - /** - * Converts the {@code Image} to a {@code BufferedImage}. - * The new image will have the same {@code ColorModel}, {@code Raster} and - * properties as the original image, if possible. - *

- * If the image is already a {@code BufferedImage}, it is simply returned - * and no conversion takes place. - * - * @param pOriginal the image to convert. - * - * @return a {@code BufferedImage} - * - * @throws IllegalArgumentException if {@code pOriginal == null} - * @throws ImageConversionException if the image cannot be converted - */ - public static BufferedImage toBuffered(Image pOriginal) { - // Don't convert if it already is BufferedImage - if (pOriginal instanceof BufferedImage) { - return (BufferedImage) pOriginal; - } - if (pOriginal == null) { - throw new IllegalArgumentException("original == null"); - } - - //System.out.println("--> Doing full BufferedImage conversion..."); - - BufferedImageFactory factory = new BufferedImageFactory(pOriginal); - return factory.getBufferedImage(); - } - - /** - * Creates a deep copy of the given image. The image will have the same - * color model and raster type, but will not share image (pixel) data - * with the input image. - * - * @param pImage the image to clone. - * - * @return a new {@code BufferedImage} - * - * @throws IllegalArgumentException if {@code pImage} is {@code null} - */ - public static BufferedImage createCopy(final BufferedImage pImage) { - if (pImage == null) { - throw new IllegalArgumentException("image == null"); - } - - ColorModel cm = pImage.getColorModel(); - - BufferedImage img = new BufferedImage(cm, - cm.createCompatibleWritableRaster(pImage.getWidth(), pImage.getHeight()), - cm.isAlphaPremultiplied(), null); - - drawOnto(img, pImage); - - return img; - } - - /** - * Creates a {@code WritableRaster} for the given {@code ColorModel} and - * pixel data. - *

- * This method is optimized for the most common cases of {@code ColorModel} - * and pixel data combinations. The raster's backing {@code DataBuffer} is - * created directly from the pixel data, as this is faster and more - * resource-friendly than using - * {@code ColorModel.createCompatibleWritableRaster(w, h)}. - *

- * For uncommon combinations, the method will fallback to using - * {@code ColorModel.createCompatibleWritableRaster(w, h)} and - * {@code WritableRaster.setDataElements(w, h, pixels)} - *

- * Note that the {@code ColorModel} and pixel data are not cloned - * (in most cases). - * - * @param pWidth the requested raster width - * @param pHeight the requested raster height - * @param pPixels the pixels, as an array, of a type supported by the - * different {@link DataBuffer} - * @param pColorModel the color model to use - * @return a new {@code WritableRaster} - * - * @throws NullPointerException if either {@code pColorModel} or - * {@code pPixels} are {@code null}. - * @throws RuntimeException if {@code pWidth} and {@code pHeight} does not - * match the pixel data in {@code pPixels}. - * - * @see ColorModel#createCompatibleWritableRaster(int, int) - * @see ColorModel#createCompatibleSampleModel(int, int) - * @see WritableRaster#setDataElements(int, int, Object) - * @see DataBuffer - */ - static WritableRaster createRaster(int pWidth, int pHeight, Object pPixels, ColorModel pColorModel) { - // NOTE: This is optimized code for most common cases. - // We create a DataBuffer from the pixel array directly, - // and creating a raster based on the DataBuffer and ColorModel. - // Creating rasters this way is faster and more resource-friendly, as - // cm.createCompatibleWritableRaster allocates an - // "empty" DataBuffer with a storage array of w*h. This array is - // later discarded, and replaced in the raster.setDataElements() call. - // The "old" way is kept as a more compatible fall-back mode. - - DataBuffer buffer = null; - WritableRaster raster = null; - - int bands; - if (pPixels instanceof int[]) { - int[] data = (int[]) pPixels; - buffer = new DataBufferInt(data, data.length); - bands = pColorModel.getNumComponents(); - } - else if (pPixels instanceof short[]) { - short[] data = (short[]) pPixels; - buffer = new DataBufferUShort(data, data.length); - bands = data.length / (pWidth * pHeight); - } - else if (pPixels instanceof byte[]) { - byte[] data = (byte[]) pPixels; - buffer = new DataBufferByte(data, data.length); - - // NOTE: This only holds for gray and indexed with one byte per pixel... - if (pColorModel instanceof IndexColorModel) { - bands = 1; - } - else { - bands = data.length / (pWidth * pHeight); - } - } - else { - // Fallback mode, slower & requires more memory, but compatible - bands = -1; - - // Create raster from color model, w and h - raster = pColorModel.createCompatibleWritableRaster(pWidth, pHeight); - raster.setDataElements(0, 0, pWidth, pHeight, pPixels); // Note: This is known to throw ClassCastExceptions.. - } - - if (raster == null) { - if (pColorModel instanceof IndexColorModel && isIndexedPacked((IndexColorModel) pColorModel)) { - raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pColorModel.getPixelSize(), LOCATION_UPPER_LEFT); - } - else if (pColorModel instanceof PackedColorModel) { - PackedColorModel pcm = (PackedColorModel) pColorModel; - raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pWidth, pcm.getMasks(), LOCATION_UPPER_LEFT); - } - else { - // (A)BGR order... For TYPE_3BYTE_BGR/TYPE_4BYTE_ABGR/TYPE_4BYTE_ABGR_PRE. - int[] bandsOffsets = new int[bands]; - for (int i = 0; i < bands;) { - bandsOffsets[i] = bands - (++i); - } - - raster = Raster.createInterleavedRaster(buffer, pWidth, pHeight, pWidth * bands, bands, bandsOffsets, LOCATION_UPPER_LEFT); - } - } - - return raster; - } - - private static boolean isIndexedPacked(IndexColorModel pColorModel) { - return (pColorModel.getPixelSize() == 1 || pColorModel.getPixelSize() == 2 || pColorModel.getPixelSize() == 4); - } - - /** - * Workaround for bug: TYPE_3BYTE_BGR, TYPE_4BYTE_ABGR and - * TYPE_4BYTE_ABGR_PRE are all converted to TYPE_CUSTOM when using the - * default createCompatibleWritableRaster from ComponentColorModel. - * - * @param pOriginal the orignal image - * @param pModel the original color model - * @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 width, int height) { - if (pModel == null || equals(pOriginal.getColorModel(), pModel)) { - int[] bOffs; - switch (pOriginal.getType()) { - case BufferedImage.TYPE_3BYTE_BGR: - bOffs = new int[]{2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return - return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, - 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, - 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(width, height); - } - } - - return pModel.createCompatibleWritableRaster(width, height); - } - - /** - * Converts the {@code Image} to a {@code BufferedImage} of the 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 already a {@code BufferedImage} of the given type, it - * is simply returned and no conversion takes place. - * - * @param pOriginal the image to convert. - * @param pType the type of buffered image - * - * @return a {@code BufferedImage} - * - * @throws IllegalArgumentException if {@code pOriginal == null} - * or if {@code pType} is not a valid type for {@code BufferedImage} - * - * @see java.awt.image.BufferedImage#getType() - */ - public static BufferedImage toBuffered(Image pOriginal, int pType) { - return toBuffered(pOriginal, pType, null); - } - - /** - * - * @param pOriginal the original image - * @param pType the type of {@code BufferedImage} to create - * @param pICM the optional {@code IndexColorModel} to use. If not - * {@code null} the {@code pType} must be compatible with the color model - * @return a {@code BufferedImage} - * @throws IllegalArgumentException if {@code pType} is not compatible with - * the color model - */ - private static BufferedImage toBuffered(Image pOriginal, int pType, IndexColorModel pICM) { - // 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))) { - return (BufferedImage) pOriginal; - } - if (pOriginal == null) { - throw new IllegalArgumentException("original == null"); - } - - //System.out.println("--> Doing full BufferedImage conversion, using Graphics.drawImage()."); - - // Create a buffered image - // NOTE: The getWidth and getHeight methods, will wait for the image - BufferedImage image; - if (pICM == null) { - image = createBuffered(getWidth(pOriginal), getHeight(pOriginal), pType, Transparency.TRANSLUCENT);//new BufferedImage(getWidth(pOriginal), getHeight(pOriginal), pType); - } - else { - image = new BufferedImage(getWidth(pOriginal), getHeight(pOriginal), pType, pICM); - } - - // Draw the image onto the buffer - drawOnto(image, pOriginal); - - return image; - } - - /** - * Draws the source image onto the buffered image, using - * {@code AlphaComposite.Src} and coordinates {@code 0, 0}. - * - * @param pDestination the image to draw on - * @param pSource the source image to draw - * - * @throws NullPointerException if {@code pDestination} or {@code pSource} is {@code null} - */ - static void drawOnto(final BufferedImage pDestination, final Image pSource) { - Graphics2D g = pDestination.createGraphics(); - try { - g.setComposite(AlphaComposite.Src); - g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); - g.drawImage(pSource, 0, 0, null); - } - finally { - g.dispose(); - } - } - - /** - * Creates a flipped version of the given image. - * - * @param pImage the image to flip - * @param pAxis the axis to flip around - * @return a new {@code BufferedImage} - */ - public static BufferedImage createFlipped(final Image pImage, final int pAxis) { - switch (pAxis) { - case FLIP_HORIZONTAL: - case FLIP_VERTICAL: - // TODO case FLIP_BOTH:?? same as rotate 180? - break; - default: - throw new IllegalArgumentException("Illegal direction: " + pAxis); - } - BufferedImage source = toBuffered(pImage); - AffineTransform transform; - if (pAxis == FLIP_HORIZONTAL) { - transform = AffineTransform.getTranslateInstance(0, source.getHeight()); - transform.scale(1, -1); - } - else { - transform = AffineTransform.getTranslateInstance(source.getWidth(), 0); - transform.scale(-1, 1); - } - AffineTransformOp transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); - return transformOp.filter(source, null); - } - - - /** - * Rotates the image 90 degrees, clockwise (aka "rotate right"), - * counter-clockwise (aka "rotate left") or 180 degrees, depending on the - * {@code pDirection} argument. - *

- * The new image will be completely covered with pixels from the source - * image. - * - * @param pImage the source image. - * @param pDirection the direction, must be either {@link #ROTATE_90_CW}, - * {@link #ROTATE_90_CCW} or {@link #ROTATE_180} - * - * @return a new {@code BufferedImage} - * - */ - public static BufferedImage createRotated(final Image pImage, final int pDirection) { - switch (pDirection) { - case ROTATE_90_CW: - case ROTATE_90_CCW: - case ROTATE_180: - return createRotated(pImage, Math.toRadians(pDirection)); - default: - throw new IllegalArgumentException("Illegal direction: " + pDirection); - } - } - - /** - * Rotates the image to the given angle. Areas not covered with pixels from - * the source image will be left transparent, if possible. - * - * @param pImage the source image - * @param pAngle the angle of rotation, in radians - * - * @return a new {@code BufferedImage}, unless {@code pAngle == 0.0} - */ - public static BufferedImage createRotated(final Image pImage, final double pAngle) { - return createRotated0(toBuffered(pImage), pAngle); - } - - private static BufferedImage createRotated0(final BufferedImage pSource, final double pAngle) { - if ((Math.abs(Math.toDegrees(pAngle)) % 360) == 0) { - return pSource; - } - - final boolean fast = ((Math.abs(Math.toDegrees(pAngle)) % 90) == 0.0); - final int w = pSource.getWidth(); - final int h = pSource.getHeight(); - - // Compute new width and height - double sin = Math.abs(Math.sin(pAngle)); - double cos = Math.abs(Math.cos(pAngle)); - - int newW = (int) Math.floor(w * cos + h * sin); - int newH = (int) Math.floor(h * cos + w * sin); - - AffineTransform transform = AffineTransform.getTranslateInstance((newW - w) / 2.0, (newH - h) / 2.0); - transform.rotate(pAngle, w / 2.0, h / 2.0); - - // TODO: Figure out if this is correct - BufferedImage dest = createTransparent(newW, newH); - - // See: http://weblogs.java.net/blog/campbell/archive/2007/03/java_2d_tricker_1.html - Graphics2D g = dest.createGraphics(); - try { - g.transform(transform); - if (!fast) { - // Max quality - g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, - RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); - g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, - RenderingHints.VALUE_INTERPOLATION_BILINEAR); - g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, - RenderingHints.VALUE_ANTIALIAS_ON); - g.setPaint(new TexturePaint(pSource, - new Rectangle2D.Float(0, 0, pSource.getWidth(), pSource.getHeight()))); - g.fillRect(0, 0, pSource.getWidth(), pSource.getHeight()); - } - else { - g.drawImage(pSource, 0, 0, null); - } - } - finally { - g.dispose(); - } - - return dest; - } - - /** - * 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 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 - * @param pWidth width in pixels - * @param pHeight height in pixels - * @param pHints scaling ints - * - * @return a {@code BufferedImage} - * - * @throws NullPointerException if {@code pImage} is {@code null}. - * - * @see #createResampled(java.awt.Image, int, int, int) - * @see Image#getScaledInstance(int,int,int) - * @see Image#SCALE_AREA_AVERAGING - * @see Image#SCALE_DEFAULT - * @see Image#SCALE_FAST - * @see Image#SCALE_REPLICATE - * @see Image#SCALE_SMOOTH - */ - public static BufferedImage createScaled(Image pImage, int pWidth, int pHeight, int pHints) { - ColorModel cm; - int type = BI_TYPE_ANY; - if (pImage instanceof RenderedImage) { - cm = ((RenderedImage) pImage).getColorModel(); - if (pImage instanceof BufferedImage) { - type = ((BufferedImage) pImage).getType(); - } - } - else { - BufferedImageFactory factory = new BufferedImageFactory(pImage); - cm = factory.getColorModel(); - } - - BufferedImage scaled = createResampled(pImage, pWidth, pHeight, pHints); - - // 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(); - WritableRaster raster; - if (pImage instanceof BufferedImage) { - raster = createCompatibleWritableRaster((BufferedImage) pImage, cm, pWidth, pHeight); - } - else { - raster = cm.createCompatibleWritableRaster(pWidth, pHeight); - } - - BufferedImage temp = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); - - if (cm instanceof IndexColorModel && pHints == Image.SCALE_SMOOTH) { - // TODO: DiffusionDither does not support transparency at the moment, this will create bad results - new DiffusionDither((IndexColorModel) cm).filter(scaled, temp); - } - else { - drawOnto(temp, scaled); - } - - scaled = temp; - //long end = System.currentTimeMillis(); - //System.out.println("Time: " + (end - start) + " ms"); - } - - return scaled; - } - - private static boolean equals(ColorModel pLeft, ColorModel pRight) { - if (pLeft == pRight) { - return true; - } - - if (!pLeft.equals(pRight)) { - return false; - } - - // Now, the models are equal, according to the equals method - // Test indexcolormodels for equality, the maps must be equal - if (pLeft instanceof IndexColorModel) { - IndexColorModel icm1 = (IndexColorModel) pLeft; - IndexColorModel icm2 = (IndexColorModel) pRight; // NOTE: Safe, they're equal - - - final int mapSize1 = icm1.getMapSize(); - final int mapSize2 = icm2.getMapSize(); - - if (mapSize1 != mapSize2) { - return false; - } - - for (int i = 0; i > mapSize1; i++) { - if (icm1.getRGB(i) != icm2.getRGB(i)) { - return false; - } - } - - return true; - - } - - return true; - } - - /** - * Creates a scaled instance of the given {@code Image}, and converts it to - * a {@code BufferedImage} if needed. - * - * @param pImage the {@code Image} to scale - * @param pWidth width in pixels - * @param pHeight height in pixels - * @param pHints scaling mHints - * - * @return a {@code BufferedImage} - * - * @throws NullPointerException if {@code pImage} is {@code null}. - * - * @see Image#SCALE_AREA_AVERAGING - * @see Image#SCALE_DEFAULT - * @see Image#SCALE_FAST - * @see Image#SCALE_REPLICATE - * @see Image#SCALE_SMOOTH - * @see ResampleOp - */ - public static BufferedImage createResampled(Image pImage, int pWidth, int pHeight, int pHints) { - // NOTE: TYPE_4BYTE_ABGR or TYPE_3BYTE_BGR is more efficient when accelerated... - BufferedImage image = pImage instanceof BufferedImage - ? (BufferedImage) pImage - : toBuffered(pImage, BufferedImage.TYPE_4BYTE_ABGR); - return createResampled(image, pWidth, pHeight, pHints); - } - - /** - * Creates a scaled instance of the given {@code RenderedImage}, and - * converts it to a {@code BufferedImage} if needed. - * - * @param pImage the {@code RenderedImage} to scale - * @param pWidth width in pixels - * @param pHeight height in pixels - * @param pHints scaling mHints - * - * @return a {@code BufferedImage} - * - * @throws NullPointerException if {@code pImage} is {@code null}. - * - * @see Image#SCALE_AREA_AVERAGING - * @see Image#SCALE_DEFAULT - * @see Image#SCALE_FAST - * @see Image#SCALE_REPLICATE - * @see Image#SCALE_SMOOTH - * @see ResampleOp - */ - public static BufferedImage createResampled(RenderedImage pImage, int pWidth, int pHeight, int pHints) { - // NOTE: TYPE_4BYTE_ABGR or TYPE_3BYTE_BGR is more efficient when accelerated... - BufferedImage image = pImage instanceof BufferedImage - ? (BufferedImage) pImage - : toBuffered(pImage, pImage.getColorModel().hasAlpha() ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); - return createResampled(image, pWidth, pHeight, pHints); - } - - /** - * Creates a scaled instance of the given {@code BufferedImage}. - * - * @param pImage the {@code BufferedImage} to scale - * @param pWidth width in pixels - * @param pHeight height in pixels - * @param pHints scaling mHints - * - * @return a {@code BufferedImage} - * - * @throws NullPointerException if {@code pImage} is {@code null}. - * - * @see Image#SCALE_AREA_AVERAGING - * @see Image#SCALE_DEFAULT - * @see Image#SCALE_FAST - * @see Image#SCALE_REPLICATE - * @see Image#SCALE_SMOOTH - * @see ResampleOp - */ - public static BufferedImage createResampled(BufferedImage pImage, int pWidth, int pHeight, int pHints) { - // Hints are converted between java.awt.Image hints and filter types - return new ResampleOp(pWidth, pHeight, convertAWTHints(pHints)).filter(pImage, null); - } - - private static int convertAWTHints(int pHints) { - switch (pHints) { - case Image.SCALE_FAST: - case Image.SCALE_REPLICATE: - return ResampleOp.FILTER_POINT; - case Image.SCALE_AREA_AVERAGING: - return ResampleOp.FILTER_BOX; - //return ResampleOp.FILTER_CUBIC; - case Image.SCALE_SMOOTH: - return ResampleOp.FILTER_LANCZOS; - default: - //return ResampleOp.FILTER_TRIANGLE; - return ResampleOp.FILTER_QUADRATIC; - } - } - - /** - * Extracts an {@code IndexColorModel} from the given image. - * - * @param pImage the image to get the color model from - * @param pColors the maximum number of colors in the resulting color model - * @param pHints hints controlling transparency and color selection - * - * @return the extracted {@code IndexColorModel} - * - * @see #COLOR_SELECTION_DEFAULT - * @see #COLOR_SELECTION_FAST - * @see #COLOR_SELECTION_QUALITY - * @see #TRANSPARENCY_DEFAULT - * @see #TRANSPARENCY_OPAQUE - * @see #TRANSPARENCY_BITMASK - * @see #TRANSPARENCY_TRANSLUCENT - */ - public static IndexColorModel getIndexColorModel(Image pImage, int pColors, int pHints) { - return IndexImage.getIndexColorModel(pImage, pColors, pHints); - } - - /** - * Creates an indexed version of the given image (a {@code BufferedImage} - * with an {@code IndexColorModel}. - * The resulting image will have a maximum of 256 different colors. - * Transparent parts of the original will be replaced with solid black. - * Default (possibly HW accelerated) dither will be used. - * - * @param pImage the image to convert - * - * @return an indexed version of the given image - */ - public static BufferedImage createIndexed(Image pImage) { - return IndexImage.getIndexedImage(toBuffered(pImage), 256, Color.black, IndexImage.DITHER_DEFAULT); - } - - /** - * Creates an indexed version of the given image (a {@code BufferedImage} - * with an {@code IndexColorModel}. - * - * @param pImage the image to convert - * @param pColors number of colors in the resulting image - * @param pMatte color to replace transparent parts of the original. - * @param pHints hints controlling dither, transparency and color selection - * - * @return an indexed version of the given image - * - * @see #COLOR_SELECTION_DEFAULT - * @see #COLOR_SELECTION_FAST - * @see #COLOR_SELECTION_QUALITY - * @see #DITHER_NONE - * @see #DITHER_DEFAULT - * @see #DITHER_DIFFUSION - * @see #DITHER_DIFFUSION_ALTSCANS - * @see #TRANSPARENCY_DEFAULT - * @see #TRANSPARENCY_OPAQUE - * @see #TRANSPARENCY_BITMASK - * @see #TRANSPARENCY_TRANSLUCENT - */ - public static BufferedImage createIndexed(Image pImage, int pColors, Color pMatte, int pHints) { - return IndexImage.getIndexedImage(toBuffered(pImage), pColors, pMatte, pHints); - } - - /** - * Creates an indexed version of the given image (a {@code BufferedImage} - * with an {@code IndexColorModel}. - * - * @param pImage the image to convert - * @param pColors the {@code IndexColorModel} to be used in the resulting - * image. - * @param pMatte color to replace transparent parts of the original. - * @param pHints hints controlling dither, transparency and color selection - * - * @return an indexed version of the given image - * - * @see #COLOR_SELECTION_DEFAULT - * @see #COLOR_SELECTION_FAST - * @see #COLOR_SELECTION_QUALITY - * @see #DITHER_NONE - * @see #DITHER_DEFAULT - * @see #DITHER_DIFFUSION - * @see #DITHER_DIFFUSION_ALTSCANS - * @see #TRANSPARENCY_DEFAULT - * @see #TRANSPARENCY_OPAQUE - * @see #TRANSPARENCY_BITMASK - * @see #TRANSPARENCY_TRANSLUCENT - */ - public static BufferedImage createIndexed(Image pImage, IndexColorModel pColors, Color pMatte, int pHints) { - return IndexImage.getIndexedImage(toBuffered(pImage), pColors, pMatte, pHints); - } - - /** - * Creates an indexed version of the given image (a {@code BufferedImage} - * with an {@code IndexColorModel}. - * - * @param pImage the image to convert - * @param pColors an {@code Image} used to get colors from. If the image is - * has an {@code IndexColorModel}, it will be uesd, otherwise an - * {@code IndexColorModel} is created from the image. - * @param pMatte color to replace transparent parts of the original. - * @param pHints hints controlling dither, transparency and color selection - * - * @return an indexed version of the given image - * - * @see #COLOR_SELECTION_DEFAULT - * @see #COLOR_SELECTION_FAST - * @see #COLOR_SELECTION_QUALITY - * @see #DITHER_NONE - * @see #DITHER_DEFAULT - * @see #DITHER_DIFFUSION - * @see #DITHER_DIFFUSION_ALTSCANS - * @see #TRANSPARENCY_DEFAULT - * @see #TRANSPARENCY_OPAQUE - * @see #TRANSPARENCY_BITMASK - * @see #TRANSPARENCY_TRANSLUCENT - */ - public static BufferedImage createIndexed(Image pImage, Image pColors, Color pMatte, int pHints) { - return IndexImage.getIndexedImage(toBuffered(pImage), - IndexImage.getIndexColorModel(pColors, 255, pHints), - pMatte, pHints); - } - - /** - * Sharpens an image using a convolution matrix. - * The sharpen kernel used, is defined by the following 3 by 3 matrix: - * - * - * - * - *
0.0-0.30.0
-0.32.2-0.3
0.0-0.30.0
- *

- * This is the same result returned as - * {@code sharpen(pOriginal, 0.3f)}. - * - * @param pOriginal the BufferedImage to sharpen - * - * @return a new BufferedImage, containing the sharpened image. - */ - public static BufferedImage sharpen(BufferedImage pOriginal) { - return convolve(pOriginal, SHARPEN_KERNEL, EDGE_REFLECT); - } - - /** - * Sharpens an image using a convolution matrix. - * The sharpen kernel used, is defined by the following 3 by 3 matrix: - * - * - * - * - * - * - *
0.0-{@code pAmount}0.0
-{@code pAmount}4.0 * {@code pAmount} + 1.0-{@code pAmount}
0.0-{@code pAmount}0.0
- * - * @param pOriginal the BufferedImage to sharpen - * @param pAmount the amount of sharpening - * - * @return a BufferedImage, containing the sharpened image. - */ - public static BufferedImage sharpen(BufferedImage pOriginal, float pAmount) { - if (pAmount == 0f) { - return pOriginal; - } - - // Create the convolution matrix - float[] data = new float[] { - 0.0f, -pAmount, 0.0f, -pAmount, 4f * pAmount + 1f, -pAmount, 0.0f, -pAmount, 0.0f - }; - - // Do the filtering - return convolve(pOriginal, new Kernel(3, 3, data), EDGE_REFLECT); - } - - /** - * Creates a blurred version of the given image. - * - * @param pOriginal the original image - * - * @return a new {@code BufferedImage} with a blurred version of the given image - */ - public static BufferedImage blur(BufferedImage pOriginal) { - return blur(pOriginal, 1.5f); - } - - // Some work to do... Is okay now, for range 0...1, anything above creates - // artifacts. - // The idea here is that the sum of all terms in the matrix must be 1. - - /** - * Creates a blurred version of the given image. - * - * @param pOriginal the original image - * @param pRadius the amount to blur - * - * @return a new {@code BufferedImage} with a blurred version of the given image - */ - public static BufferedImage blur(BufferedImage pOriginal, float pRadius) { - if (pRadius <= 1f) { - return pOriginal; - } - - // TODO: Re-implement using two-pass one-dimensional gaussion blur - // See: http://en.wikipedia.org/wiki/Gaussian_blur#Implementation - // Also see http://www.jhlabs.com/ip/blurring.html - - // TODO: Rethink... Fixed amount and scale matrix instead? -// pAmount = 1f - pAmount; -// float pAmount = 1f - pRadius; -// -// // Normalize amount -// float normAmt = (1f - pAmount) / 24; -// -// // Create the convolution matrix -// float[] data = new float[] { -// normAmt / 2, normAmt, normAmt, normAmt, normAmt / 2, -// normAmt, normAmt, normAmt * 2, normAmt, normAmt, -// normAmt, normAmt * 2, pAmount, normAmt * 2, normAmt, -// normAmt, normAmt, normAmt * 2, normAmt, normAmt, -// normAmt / 2, normAmt, normAmt, normAmt, normAmt / 2 -// }; -// -// // Do the filtering -// return convolve(pOriginal, new Kernel(5, 5, data), EDGE_REFLECT); - - Kernel horizontal = makeKernel(pRadius); - Kernel vertical = new Kernel(horizontal.getHeight(), horizontal.getWidth(), horizontal.getKernelData(null)); - - BufferedImage temp = addBorder(pOriginal, horizontal.getWidth() / 2, vertical.getHeight() / 2, EDGE_REFLECT); - - temp = convolve(temp, horizontal, EDGE_NO_OP); - temp = convolve(temp, vertical, EDGE_NO_OP); - - return temp.getSubimage( - horizontal.getWidth() / 2, vertical.getHeight() / 2, pOriginal.getWidth(), pOriginal.getHeight() - ); - } - - /** - * Make a Gaussian blur {@link Kernel}. - * - * @param radius the blur radius - * @return a new blur {@code Kernel} - */ - private static Kernel makeKernel(float radius) { - int r = (int) Math.ceil(radius); - int rows = r * 2 + 1; - float[] matrix = new float[rows]; - float sigma = radius / 3; - float sigma22 = 2 * sigma * sigma; - float sigmaPi2 = (float) (2 * Math.PI * sigma); - float sqrtSigmaPi2 = (float) Math.sqrt(sigmaPi2); - float radius2 = radius * radius; - float total = 0; - int index = 0; - for (int row = -r; row <= r; row++) { - float distance = row * row; - if (distance > radius2) { - matrix[index] = 0; - } - else { - matrix[index] = (float) Math.exp(-(distance) / sigma22) / sqrtSigmaPi2; - } - total += matrix[index]; - index++; - } - for (int i = 0; i < rows; i++) { - matrix[i] /= total; - } - - return new Kernel(rows, 1, matrix); - } - - - /** - * Convolves an image, using a convolution matrix. - * - * @param pOriginal the BufferedImage to sharpen - * @param pKernel the kernel - * @param pEdgeOperation the edge operation. Must be one of {@link #EDGE_NO_OP}, - * {@link #EDGE_ZERO_FILL}, {@link #EDGE_REFLECT} or {@link #EDGE_WRAP} - * - * @return a new BufferedImage, containing the sharpened image. - */ - public static BufferedImage convolve(BufferedImage pOriginal, Kernel pKernel, int pEdgeOperation) { - // Allow for 2 more edge operations - BufferedImage original; - switch (pEdgeOperation) { - case EDGE_REFLECT: - case EDGE_WRAP: - original = addBorder(pOriginal, pKernel.getWidth() / 2, pKernel.getHeight() / 2, pEdgeOperation); - break; - default: - original = pOriginal; - break; - } - - // Create convolution operation - ConvolveOp convolve = new ConvolveOp(pKernel, pEdgeOperation, null); - - // Workaround for what seems to be a Java2D bug: - // ConvolveOp needs explicit destination image type for some "uncommon" - // image types. However, TYPE_3BYTE_BGR is what javax.imageio.ImageIO - // normally returns for color JPEGs... :-/ - BufferedImage result = null; - if (original.getType() == BufferedImage.TYPE_3BYTE_BGR) { - result = createBuffered( - pOriginal.getWidth(), pOriginal.getHeight(), - pOriginal.getType(), pOriginal.getColorModel().getTransparency() - ); - } - - // Do the filtering (if result is null, a new image will be created) - BufferedImage image = convolve.filter(original, result); - - if (pOriginal != original) { - // Remove the border - image = image.getSubimage( - pKernel.getWidth() / 2, pKernel.getHeight() / 2, pOriginal.getWidth(), pOriginal.getHeight() - ); - } - - return image; - } - - private static BufferedImage addBorder(final BufferedImage pOriginal, final int pBorderX, final int pBorderY, final int pEdgeOperation) { - // TODO: Might be faster if we could clone raster and strech it... - int w = pOriginal.getWidth(); - int h = pOriginal.getHeight(); - - ColorModel cm = pOriginal.getColorModel(); - WritableRaster raster = cm.createCompatibleWritableRaster(w + 2 * pBorderX, h + 2 * pBorderY); - BufferedImage bordered = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); - - Graphics2D g = bordered.createGraphics(); - try { - g.setComposite(AlphaComposite.Src); - g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); - - // Draw original in center - 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 (pEdgeOperation) { - case EDGE_REFLECT: - // Top/left (empty) - g.drawImage(pOriginal, pBorderX, 0, pBorderX + w, pBorderY, 0, 0, w, 1, null); // Top/center - // Top/right (empty) - - g.drawImage(pOriginal, -w + pBorderX, pBorderY, pBorderX, h + pBorderY, 0, 0, 1, h, null); // Center/left - // Center/center (already drawn) - g.drawImage(pOriginal, w + pBorderX, pBorderY, 2 * pBorderX + w, h + pBorderY, w - 1, 0, w, h, null); // Center/right - - // Bottom/left (empty) - g.drawImage(pOriginal, pBorderX, pBorderY + h, pBorderX + w, 2 * pBorderY + h, 0, h - 1, w, h, null); // Bottom/center - // Bottom/right (empty) - break; - case EDGE_WRAP: - g.drawImage(pOriginal, -w + pBorderX, -h + pBorderY, null); // Top/left - g.drawImage(pOriginal, pBorderX, -h + pBorderY, null); // Top/center - g.drawImage(pOriginal, w + pBorderX, -h + pBorderY, null); // Top/right - - g.drawImage(pOriginal, -w + pBorderX, pBorderY, null); // Center/left - // Center/center (already drawn) - g.drawImage(pOriginal, w + pBorderX, pBorderY, null); // Center/right - - g.drawImage(pOriginal, -w + pBorderX, h + pBorderY, null); // Bottom/left - g.drawImage(pOriginal, pBorderX, h + pBorderY, null); // Bottom/center - g.drawImage(pOriginal, w + pBorderX, h + pBorderY, null); // Bottom/right - break; - default: - throw new IllegalArgumentException("Illegal edge operation " + pEdgeOperation); - } - - } - finally { - g.dispose(); - } - - //ConvolveTester.showIt(bordered, "jaffe"); - - return bordered; - } - - /** - * Adds contrast - * - * @param pOriginal the BufferedImage to add contrast to - * - * @return an {@code Image}, containing the contrasted image. - */ - public static Image contrast(Image pOriginal) { - return contrast(pOriginal, 0.3f); - } - - /** - * Changes the contrast of the image - * - * @param pOriginal the {@code Image} to change - * @param pAmount the amount of contrast in the range [-1.0..1.0]. - * - * @return an {@code Image}, containing the contrasted image. - */ - public static Image contrast(Image pOriginal, float pAmount) { - // No change, return original - if (pAmount == 0f) { - return pOriginal; - } - - // Create filter - RGBImageFilter filter = new BrightnessContrastFilter(0f, pAmount); - - // Return contrast adjusted image - return filter(pOriginal, filter); - } - - - /** - * Changes the brightness of the original image. - * - * @param pOriginal the {@code Image} to change - * @param pAmount the amount of brightness in the range [-2.0..2.0]. - * - * @return an {@code Image} - */ - public static Image brightness(Image pOriginal, float pAmount) { - // No change, return original - if (pAmount == 0f) { - return pOriginal; - } - - // Create filter - RGBImageFilter filter = new BrightnessContrastFilter(pAmount, 0f); - - // Return brightness adjusted image - return filter(pOriginal, filter); - } - - - /** - * Converts an image to grayscale. - * - * @see GrayFilter - * @see RGBImageFilter - * - * @param pOriginal the image to convert. - * @return a new Image, containing the gray image data. - */ - public static Image grayscale(Image pOriginal) { - // Create filter - RGBImageFilter filter = new GrayFilter(); - - // Convert to gray - return filter(pOriginal, filter); - } - - /** - * Filters an image, using the given {@code ImageFilter}. - * - * @param pOriginal the original image - * @param pFilter the filter to apply - * - * @return the new {@code Image} - */ - public static Image filter(Image pOriginal, ImageFilter pFilter) { - // Create a filtered source - ImageProducer source = new FilteredImageSource(pOriginal.getSource(), pFilter); - - // Create new image - return Toolkit.getDefaultToolkit().createImage(source); - } - - /** - * Tries to use H/W-accelerated code for an image for display purposes. - * Note that transparent parts of the image might be replaced by solid - * color. Additional image information not used by the current diplay - * hardware may be discarded, like extra bith depth etc. - * - * @param pImage any {@code Image} - * @return a {@code BufferedImage} - */ - public static BufferedImage accelerate(Image pImage) { - return accelerate(pImage, null, DEFAULT_CONFIGURATION); - } - - /** - * Tries to use H/W-accelerated code for an image for display purposes. - * Note that transparent parts of the image might be replaced by solid - * color. Additional image information not used by the current diplay - * hardware may be discarded, like extra bith depth etc. - * - * @param pImage any {@code Image} - * @param pConfiguration the {@code GraphicsConfiguration} to accelerate - * for - * - * @return a {@code BufferedImage} - */ - public static BufferedImage accelerate(Image pImage, GraphicsConfiguration pConfiguration) { - return accelerate(pImage, null, pConfiguration); - } - - /** - * Tries to use H/W-accelerated code for an image for display purposes. - * Note that transparent parts of the image will be replaced by solid - * color. Additional image information not used by the current diplay - * hardware may be discarded, like extra bith depth etc. - * - * @param pImage any {@code Image} - * @param pBackgroundColor the background color to replace any transparent - * parts of the image. - * May be {@code null}, in such case the color is undefined. - * @param pConfiguration the graphics configuration - * May be {@code null}, in such case the color is undefined. - * - * @return a {@code BufferedImage} - */ - static BufferedImage accelerate(Image pImage, Color pBackgroundColor, GraphicsConfiguration pConfiguration) { - // Skip acceleration if the layout of the image and color model is already ok - if (pImage instanceof BufferedImage) { - BufferedImage buffered = (BufferedImage) pImage; - // TODO: What if the createCompatibleImage insist on TYPE_CUSTOM...? :-P - if (buffered.getType() != BufferedImage.TYPE_CUSTOM && equals(buffered.getColorModel(), pConfiguration.getColorModel(buffered.getTransparency()))) { - return buffered; - } - } - if (pImage == null) { - throw new IllegalArgumentException("image == null"); - } - - int w = ImageUtil.getWidth(pImage); - int h = ImageUtil.getHeight(pImage); - - // Create accelerated version - BufferedImage temp = createClear(w, h, BI_TYPE_ANY, getTransparency(pImage), pBackgroundColor, pConfiguration); - drawOnto(temp, pImage); - - return temp; - } - - private static int getTransparency(Image pImage) { - if (pImage instanceof BufferedImage) { - BufferedImage bi = (BufferedImage) pImage; - return bi.getTransparency(); - } - return Transparency.OPAQUE; - } - - /** - * Creates a transparent image. - * - * @param pWidth the requested width of the image - * @param pHeight the requested height of the image - * - * @throws IllegalArgumentException if {@code pType} is not a valid type - * for {@code BufferedImage} - * - * @return the new image - */ - public static BufferedImage createTransparent(int pWidth, int pHeight) { - return createTransparent(pWidth, pHeight, BI_TYPE_ANY); - } - - /** - * Creates a transparent image. - * - * @see BufferedImage#BufferedImage(int,int,int) - * - * @param pWidth the requested width of the image - * @param pHeight the requested height of the image - * @param pType the type of {@code BufferedImage} to create - * - * @throws IllegalArgumentException if {@code pType} is not a valid type - * for {@code BufferedImage} - * - * @return the new image - */ - public static BufferedImage createTransparent(int pWidth, int pHeight, int pType) { - // Create - BufferedImage image = createBuffered(pWidth, pHeight, pType, Transparency.TRANSLUCENT); - - // Clear image with transparent alpha by drawing a rectangle - Graphics2D g = image.createGraphics(); - try { - g.setComposite(AlphaComposite.Clear); - g.fillRect(0, 0, pWidth, pHeight); - } - finally { - g.dispose(); - } - - return image; - } - - /** - * Creates a clear image with the given background color. - * - * @see BufferedImage#BufferedImage(int,int,int) - * - * @param pWidth the requested width of the image - * @param pHeight the requested height of the image - * @param pBackground the background color. The color may be translucent. - * May be {@code null}, in such case the color is undefined. - * - * @throws IllegalArgumentException if {@code pType} is not a valid type - * for {@code BufferedImage} - * - * @return the new image - */ - public static BufferedImage createClear(int pWidth, int pHeight, Color pBackground) { - return createClear(pWidth, pHeight, BI_TYPE_ANY, pBackground); - } - - /** - * Creates a clear image with the given background color. - * - * @see BufferedImage#BufferedImage(int,int,int) - * - * @param pWidth the width of the image to create - * @param pHeight the height of the image to create - * @param pType the type of image to create (one of the constants from - * {@link BufferedImage} or {@link #BI_TYPE_ANY}) - * @param pBackground the background color. The color may be translucent. - * May be {@code null}, in such case the color is undefined. - * - * @throws IllegalArgumentException if {@code pType} is not a valid type - * for {@code BufferedImage} - * - * @return the new image - */ - public static BufferedImage createClear(int pWidth, int pHeight, int pType, Color pBackground) { - return createClear(pWidth, pHeight, pType, Transparency.OPAQUE, pBackground, DEFAULT_CONFIGURATION); - } - - static BufferedImage createClear(int pWidth, int pHeight, int pType, int pTransparency, Color pBackground, GraphicsConfiguration pConfiguration) { - // Create - int transparency = (pBackground != null) ? pBackground.getTransparency() : pTransparency; - BufferedImage image = createBuffered(pWidth, pHeight, pType, transparency, pConfiguration); - - if (pBackground != null) { - // Clear image with clear color, by drawing a rectangle - Graphics2D g = image.createGraphics(); - try { - g.setComposite(AlphaComposite.Src); // Allow color to be translucent - g.setColor(pBackground); - g.fillRect(0, 0, pWidth, pHeight); - } - finally { - g.dispose(); - } - } - - return image; - } - - /** - * Creates a {@code BufferedImage} of the given size and type. If possible, - * uses accelerated versions of BufferedImage from GraphicsConfiguration. - * - * @param pWidth the width of the image to create - * @param pHeight the height of the image to create - * @param pType the type of image to create (one of the constants from - * {@link BufferedImage} or {@link #BI_TYPE_ANY}) - * @param pTransparency the transparency type (from {@link Transparency}) - * - * @return a {@code BufferedImage} - */ - private static BufferedImage createBuffered(int pWidth, int pHeight, int pType, int pTransparency) { - return createBuffered(pWidth, pHeight, pType, pTransparency, DEFAULT_CONFIGURATION); - } - - static BufferedImage createBuffered(int pWidth, int pHeight, int pType, int pTransparency, - GraphicsConfiguration pConfiguration) { - if (VM_SUPPORTS_ACCELERATION && pType == BI_TYPE_ANY) { - GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); - if (supportsAcceleration(env)) { - return getConfiguration(pConfiguration).createCompatibleImage(pWidth, pHeight, pTransparency); - } - } - - return new BufferedImage(pWidth, pHeight, getImageType(pType, pTransparency)); - } - - private static GraphicsConfiguration getConfiguration(final GraphicsConfiguration pConfiguration) { - return pConfiguration != null ? pConfiguration : DEFAULT_CONFIGURATION; - } - - private static int getImageType(int pType, int pTransparency) { - // TODO: Handle TYPE_CUSTOM? - if (pType != BI_TYPE_ANY) { - return pType; - } - else { - switch (pTransparency) { - case Transparency.OPAQUE: - return BufferedImage.TYPE_INT_RGB; - case Transparency.BITMASK: - case Transparency.TRANSLUCENT: - return BufferedImage.TYPE_INT_ARGB; - default: - throw new IllegalArgumentException("Unknown transparency type: " + pTransparency); - } - } - } - - /** - * Tests if the given {@code GraphicsEnvironment} supports accelleration - * - * @param pEnv the environment - * @return {@code true} if the {@code GraphicsEnvironment} supports - * acceleration - */ - private static boolean supportsAcceleration(GraphicsEnvironment pEnv) { - try { - // Acceleration only supported in non-headless environments, on 1.4+ VMs - return /*VM_SUPPORTS_ACCELERATION &&*/ !pEnv.isHeadlessInstance(); - } - catch (LinkageError ignore) { - // Means we are not in a 1.4+ VM, so skip testing for headless again - VM_SUPPORTS_ACCELERATION = false; - } - - // If the invocation fails, assume no accelleration is possible - return false; - } - - /** - * Gets the width of an Image. - * This method has the side-effect of completely loading the image. - * - * @param pImage an image. - * - * @return the width of the image, or -1 if the width could not be - * determined (i.e. an error occured while waiting for the - * image to load). - */ - public static int getWidth(Image pImage) { - int width = pImage.getWidth(NULL_COMPONENT); - if (width < 0) { - if (!waitForImage(pImage)) { - return -1; // Error while waiting - } - width = pImage.getWidth(NULL_COMPONENT); - } - - return width; - } - - /** - * Gets the height of an Image. - * This method has the side-effect of completely loading the image. - * - * @param pImage an image. - * - * @return the height of the image, or -1 if the height could not be - * determined (i.e. an error occured while waiting for the - * image to load). - */ - public static int getHeight(Image pImage) { - int height = pImage.getHeight(NULL_COMPONENT); - if (height < 0) { - if (!waitForImage(pImage)) { - return -1; // Error while waiting - } - height = pImage.getHeight(NULL_COMPONENT); - } - - return height; - } - - /** - * Waits for an image to load completely. - * Will wait forever. - * - * @param pImage an Image object to wait for. - * - * @return true if the image was loaded successfully, false if an error - * occured, or the wait was interrupted. - * - * @see #waitForImage(Image,long) - */ - public static boolean waitForImage(Image pImage) { - return waitForImages(new Image[]{pImage}, -1L); - } - - /** - * Waits for an image to load completely. - * Will wait the specified time. - * - * @param pImage an Image object to wait for. - * @param pTimeOut the time to wait, in milliseconds. - * - * @return true if the image was loaded successfully, false if an error - * occurred, or the wait was interrupted. - * - * @see #waitForImages(Image[],long) - */ - public static boolean waitForImage(Image pImage, long pTimeOut) { - return waitForImages(new Image[]{pImage}, pTimeOut); - } - - /** - * Waits for a number of images to load completely. - * Will wait forever. - * - * @param pImages an array of Image objects to wait for. - * - * @return true if the images was loaded successfully, false if an error - * occurred, or the wait was interrupted. - * - * @see #waitForImages(Image[],long) - */ - public static boolean waitForImages(Image[] pImages) { - return waitForImages(pImages, -1L); - } - - /** - * Waits for a number of images to load completely. - * Will wait the specified time. - * - * @param pImages an array of Image objects to wait for - * @param pTimeOut the time to wait, in milliseconds - * - * @return true if the images was loaded successfully, false if an error - * occurred, or the wait was interrupted. - */ - public static boolean waitForImages(Image[] pImages, long pTimeOut) { - // TODO: Need to make sure that we don't wait for the same image many times - // Use hashcode as id? Don't remove images from tracker? Hmmm... - boolean success = true; - - // Create a local id for use with the mediatracker - int imageId; - - // NOTE: This is very experimental... - imageId = pImages.length == 1 ? System.identityHashCode(pImages[0]) : System.identityHashCode(pImages); - - // Add images to tracker - for (Image image : pImages) { - sTracker.addImage(image, imageId); - - // Start loading immediately - if (sTracker.checkID(imageId, false)) { - // Image is done, so remove again - sTracker.removeImage(image, imageId); - } - } - - try { - if (pTimeOut < 0L) { - // Just wait - sTracker.waitForID(imageId); - } - else { - // Wait until timeout - // NOTE: waitForID(int, long) return value is undocumented. - // I assume that it returns true, if the image(s) loaded - // successfully before the timeout, however, I always check - // isErrorID later on, just in case... - success = sTracker.waitForID(imageId, pTimeOut); - } - } - catch (InterruptedException ie) { - // Interrupted while waiting, image not loaded - success = false; - } - finally { - // Remove images from mediatracker - for (Image pImage : pImages) { - sTracker.removeImage(pImage, imageId); - } - } - - // If the wait was successfull, and no errors were reported for the - // images, return true - return success && !sTracker.isErrorID(imageId); - } - - /** - * Tests whether the image has any transparent or semi-transparent pixels. - * - * @param pImage the image - * @param pFast if {@code true}, the method tests maximum 10 x 10 pixels, - * evenly spaced out in the image. - * - * @return {@code true} if transparent pixels are found, otherwise - * {@code false}. - */ - public static boolean hasTransparentPixels(RenderedImage pImage, boolean pFast) { - if (pImage == null) { - return false; - } - - // First, test if the ColorModel supports alpha... - ColorModel cm = pImage.getColorModel(); - if (!cm.hasAlpha()) { - return false; - } - - if (cm.getTransparency() != Transparency.BITMASK - && cm.getTransparency() != Transparency.TRANSLUCENT) { - return false; - } - - // ... if so, test the pixels of the image hard way - Object data = null; - - // Loop over tiles (noramally, BufferedImages have only one) - for (int yT = pImage.getMinTileY(); yT < pImage.getNumYTiles(); yT++) { - for (int xT = pImage.getMinTileX(); xT < pImage.getNumXTiles(); xT++) { - // Test pixels of each tile - Raster raster = pImage.getTile(xT, yT); - int xIncrement = pFast ? Math.max(raster.getWidth() / 10, 1) : 1; - int yIncrement = pFast ? Math.max(raster.getHeight() / 10, 1) : 1; - - for (int y = 0; y < raster.getHeight(); y += yIncrement) { - for (int x = 0; x < raster.getWidth(); x += xIncrement) { - // Copy data for each pixel, without allocation array - data = raster.getDataElements(x, y, data); - - // Test alpha value - if (cm.getAlpha(data) != 0xff) { - return true; - } - } - } - } - } - - return false; - } - - /** - * Creates a translucent version of the given color. - * - * @param pColor the original color - * @param pTransparency the transparency level ({@code 0 - 255}) - * @return a translucent color - * - * @throws NullPointerException if {@code pColor} is {@code null} - */ - public static Color createTranslucent(Color pColor, int pTransparency) { - //return new Color(pColor.getRed(), pColor.getGreen(), pColor.getBlue(), pTransparency); - return new Color(((pTransparency & 0xff) << 24) | (pColor.getRGB() & 0x00ffffff), true); - } - - /** - * Blends two ARGB values half and half, to create a tone in between. - * - * @param pRGB1 color 1 - * @param pRGB2 color 2 - * @return the new rgb value - */ - static int blend(int pRGB1, int pRGB2) { - // Slightly modified from http://www.compuphase.com/graphic/scale3.htm - // to support alpha values - return (((pRGB1 ^ pRGB2) & 0xfefefefe) >> 1) + (pRGB1 & pRGB2); - } - - /** - * Blends two colors half and half, to create a tone in between. - * - * @param pColor color 1 - * @param pOther color 2 - * @return a new {@code Color} - */ - public static Color blend(Color pColor, Color pOther) { - return new Color(blend(pColor.getRGB(), pOther.getRGB()), true); - - /* - return new Color((pColor.getRed() + pOther.getRed()) / 2, - (pColor.getGreen() + pOther.getGreen()) / 2, - (pColor.getBlue() + pOther.getBlue()) / 2, - (pColor.getAlpha() + pOther.getAlpha()) / 2); - */ - } - - /** - * Blends two colors, controlled by the blending factor. - * A factor of {@code 0.0} will return the first color, - * a factor of {@code 1.0} will return the second. - * - * @param pColor color 1 - * @param pOther color 2 - * @param pBlendFactor {@code [0...1]} - * @return a new {@code Color} - */ - public static Color blend(Color pColor, Color pOther, float pBlendFactor) { - float inverseBlend = (1f - pBlendFactor); - return new Color( - clamp((pColor.getRed() * inverseBlend) + (pOther.getRed() * pBlendFactor)), - clamp((pColor.getGreen() * inverseBlend) + (pOther.getGreen() * pBlendFactor)), - clamp((pColor.getBlue() * inverseBlend) + (pOther.getBlue() * pBlendFactor)), - clamp((pColor.getAlpha() * inverseBlend) + (pOther.getAlpha() * pBlendFactor)) - ); - } - - private static int clamp(float f) { - return (int) f; - } +/* + * 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 of the copyright holder 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 HOLDER 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.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. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: common/common-image/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; + public final static int ROTATE_180 = 180; + + public final static int FLIP_VERTICAL = -1; + public final static int FLIP_HORIZONTAL = 1; + + /** + * Alias for {@link ConvolveOp#EDGE_ZERO_FILL}. + * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int) + * @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. + * Note that while this mode typically provides better quality than the + * standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so + * at the expense of higher memory consumption and considerable more computation. + * @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. + * Note that while this mode typically provides better quality than the + * standard modes {@code EDGE_ZERO_FILL} and {@code EDGE_NO_OP}, it does so + * at the expense of higher memory consumption and considerable more computation. + * @see #convolve(java.awt.image.BufferedImage, java.awt.image.Kernel, int) + * @see #EDGE_REFLECT + */ + public static final int EDGE_WRAP = 3; // as JAI BORDER_WRAP + + /** + * Java default dither + */ + public final static int DITHER_DEFAULT = IndexImage.DITHER_DEFAULT; + + /** + * No dither + */ + public final static int DITHER_NONE = IndexImage.DITHER_NONE; + + /** + * Error diffusion dither + */ + public final static int DITHER_DIFFUSION = IndexImage.DITHER_DIFFUSION; + + /** + * Error diffusion dither with alternating scans + */ + public final static int DITHER_DIFFUSION_ALTSCANS = IndexImage.DITHER_DIFFUSION_ALTSCANS; + + /** + * Default color selection + */ + public final static int COLOR_SELECTION_DEFAULT = IndexImage.COLOR_SELECTION_DEFAULT; + + /** + * Prioritize speed + */ + public final static int COLOR_SELECTION_FAST = IndexImage.COLOR_SELECTION_FAST; + + /** + * Prioritize quality + */ + public final static int COLOR_SELECTION_QUALITY = IndexImage.COLOR_SELECTION_QUALITY; + + /** + * Default transparency (none) + */ + public final static int TRANSPARENCY_DEFAULT = IndexImage.TRANSPARENCY_DEFAULT; + + /** + * Discard any alpha information + */ + public final static int TRANSPARENCY_OPAQUE = IndexImage.TRANSPARENCY_OPAQUE; + + /** + * Convert alpha to bitmask + */ + public final static int TRANSPARENCY_BITMASK = IndexImage.TRANSPARENCY_BITMASK; + + /** + * Keep original alpha (not supported yet) + */ + protected final static int TRANSPARENCY_TRANSLUCENT = IndexImage.TRANSPARENCY_TRANSLUCENT; + + /** Passed to the createXxx methods, to indicate that the type does not matter */ + private final static int BI_TYPE_ANY = -1; + /* + public final static int BI_TYPE_ANY_TRANSLUCENT = -1; + public final static int BI_TYPE_ANY_BITMASK = -2; + public final static int BI_TYPE_ANY_OPAQUE = -3;*/ + + /** Tells wether this WM may support acceleration of some images */ + private static boolean VM_SUPPORTS_ACCELERATION = true; + + /** The sharpen matrix */ + private static final float[] SHARPEN_MATRIX = new float[] { + 0.0f, -0.3f, 0.0f, + -0.3f, 2.2f, -0.3f, + 0.0f, -0.3f, 0.0f + }; + + /** + * The sharpen kernel. Uses the following 3 by 3 matrix: + * + * + * + * + * + *
Sharpen Kernel Matrix
0.0-0.30.0
-0.32.2-0.3
0.0-0.30.0
+ */ + private static final Kernel SHARPEN_KERNEL = new Kernel(3, 3, SHARPEN_MATRIX); + + /** + * Component that can be used with the MediaTracker etc. + */ + private static final Component NULL_COMPONENT = new Component() {}; + + /** Our static image tracker */ + private static MediaTracker sTracker = new MediaTracker(NULL_COMPONENT); + + /** */ + protected static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); + /** */ + protected static final Point LOCATION_UPPER_LEFT = new Point(0, 0); + + /** */ + private static final GraphicsConfiguration DEFAULT_CONFIGURATION = getDefaultGraphicsConfiguration(); + + private static GraphicsConfiguration getDefaultGraphicsConfiguration() { + try { + GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); + if (!env.isHeadlessInstance()) { + return env.getDefaultScreenDevice().getDefaultConfiguration(); + } + } + catch (LinkageError e) { + // Means we are not in a 1.4+ VM, so skip testing for headless again + VM_SUPPORTS_ACCELERATION = false; + } + return null; + } + + /** Creates an ImageUtil. Private constructor. */ + private ImageUtil() { + } + + /** + * Converts the {@code RenderedImage} to a {@code BufferedImage}. + * The new image will have the same {@code ColorModel}, + * {@code Raster} and properties as the original image, if possible. + *

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

+ * + * @param pOriginal the image to convert. + * + * @return a {@code BufferedImage} + */ + public static BufferedImage toBuffered(RenderedImage pOriginal) { + // Don't convert if it already is a BufferedImage + if (pOriginal instanceof BufferedImage) { + return (BufferedImage) pOriginal; + } + if (pOriginal == null) { + throw new IllegalArgumentException("original == null"); + } + + // Copy properties + Hashtable properties; + String[] names = pOriginal.getPropertyNames(); + if (names != null && names.length > 0) { + properties = new Hashtable(names.length); + + for (String name : names) { + properties.put(name, pOriginal.getProperty(name)); + } + } + else { + properties = null; + } + + // NOTE: This is a workaround for the broken Batik '*Red' classes, that + // throw NPE if copyData(null) is used. This may actually be faster too. + // See RenderedImage#copyData / RenderedImage#getData + Raster data = pOriginal.getData(); + WritableRaster raster; + if (data instanceof WritableRaster) { + raster = (WritableRaster) data; + } + else { + raster = data.createCompatibleWritableRaster(); + raster = pOriginal.copyData(raster); + } + + // Create buffered image + ColorModel colorModel = pOriginal.getColorModel(); + return new BufferedImage(colorModel, raster, + colorModel.isAlphaPremultiplied(), + properties); + } + + /** + * Converts the {@code RenderedImage} to a {@code BufferedImage} of the + * given type. + *

+ * 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. + * @param pType the type of buffered image + * + * @return a {@code BufferedImage} + * + * @throws IllegalArgumentException if {@code pOriginal == null} + * or {@code pType} is not a valid type for {@code BufferedImage} + * + * @see java.awt.image.BufferedImage#getType() + */ + public static BufferedImage toBuffered(RenderedImage pOriginal, int pType) { + // Don't convert if it already is BufferedImage and correct type + if ((pOriginal instanceof BufferedImage) && ((BufferedImage) pOriginal).getType() == pType) { + return (BufferedImage) pOriginal; + } + if (pOriginal == null) { + throw new IllegalArgumentException("original == null"); + } + + // Create a buffered image + BufferedImage image = createBuffered(pOriginal.getWidth(), + pOriginal.getHeight(), + pType, Transparency.TRANSLUCENT); + + // Draw the image onto the buffer + // NOTE: This is faster than doing a raster conversion in most cases + Graphics2D g = image.createGraphics(); + try { + g.setComposite(AlphaComposite.Src); + g.drawRenderedImage(pOriginal, IDENTITY_TRANSFORM); + } + finally { + g.dispose(); + } + + return image; + } + + /** + * Converts the {@code BufferedImage} to a {@code BufferedImage} of the + * 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 already a {@code BufferedImage} of the given type, it + * is simply returned and no conversion takes place. + *

+ *

+ * This method simply invokes + * {@link #toBuffered(RenderedImage,int) toBuffered((RenderedImage) pOriginal, pType)}. + *

+ * + * @param pOriginal the image to convert. + * @param pType the type of buffered image + * + * @return a {@code BufferedImage} + * + * @throws IllegalArgumentException if {@code pOriginal == null} + * or if {@code pType} is not a valid type for {@code BufferedImage} + * + * @see java.awt.image.BufferedImage#getType() + */ + public static BufferedImage toBuffered(BufferedImage pOriginal, int pType) { + return toBuffered((RenderedImage) pOriginal, pType); + } + + /** + * Converts the {@code Image} to a {@code BufferedImage}. + * The new image will have the same {@code ColorModel}, {@code Raster} and + * properties as the original image, if possible. + *

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

+ * + * @param pOriginal the image to convert. + * + * @return a {@code BufferedImage} + * + * @throws IllegalArgumentException if {@code pOriginal == null} + * @throws ImageConversionException if the image cannot be converted + */ + public static BufferedImage toBuffered(Image pOriginal) { + // Don't convert if it already is BufferedImage + if (pOriginal instanceof BufferedImage) { + return (BufferedImage) pOriginal; + } + if (pOriginal == null) { + throw new IllegalArgumentException("original == null"); + } + + //System.out.println("--> Doing full BufferedImage conversion..."); + + BufferedImageFactory factory = new BufferedImageFactory(pOriginal); + return factory.getBufferedImage(); + } + + /** + * Creates a deep copy of the given image. The image will have the same + * color model and raster type, but will not share image (pixel) data + * with the input image. + * + * @param pImage the image to clone. + * + * @return a new {@code BufferedImage} + * + * @throws IllegalArgumentException if {@code pImage} is {@code null} + */ + public static BufferedImage createCopy(final BufferedImage pImage) { + if (pImage == null) { + throw new IllegalArgumentException("image == null"); + } + + ColorModel cm = pImage.getColorModel(); + + BufferedImage img = new BufferedImage(cm, + cm.createCompatibleWritableRaster(pImage.getWidth(), pImage.getHeight()), + cm.isAlphaPremultiplied(), null); + + drawOnto(img, pImage); + + return img; + } + + /** + * Creates a {@code WritableRaster} for the given {@code ColorModel} and + * pixel data. + *

+ * This method is optimized for the most common cases of {@code ColorModel} + * and pixel data combinations. The raster's backing {@code DataBuffer} is + * created directly from the pixel data, as this is faster and more + * resource-friendly than using + * {@code ColorModel.createCompatibleWritableRaster(w, h)}. + *

+ *

+ * For uncommon combinations, the method will fallback to using + * {@code ColorModel.createCompatibleWritableRaster(w, h)} and + * {@code WritableRaster.setDataElements(w, h, pixels)} + *

+ *

+ * Note that the {@code ColorModel} and pixel data are not cloned + * (in most cases). + *

+ * + * @param pWidth the requested raster width + * @param pHeight the requested raster height + * @param pPixels the pixels, as an array, of a type supported by the + * different {@link DataBuffer} + * @param pColorModel the color model to use + * @return a new {@code WritableRaster} + * + * @throws NullPointerException if either {@code pColorModel} or + * {@code pPixels} are {@code null}. + * @throws RuntimeException if {@code pWidth} and {@code pHeight} does not + * match the pixel data in {@code pPixels}. + * + * @see ColorModel#createCompatibleWritableRaster(int, int) + * @see ColorModel#createCompatibleSampleModel(int, int) + * @see WritableRaster#setDataElements(int, int, Object) + * @see DataBuffer + */ + static WritableRaster createRaster(int pWidth, int pHeight, Object pPixels, ColorModel pColorModel) { + // NOTE: This is optimized code for most common cases. + // We create a DataBuffer from the pixel array directly, + // and creating a raster based on the DataBuffer and ColorModel. + // Creating rasters this way is faster and more resource-friendly, as + // cm.createCompatibleWritableRaster allocates an + // "empty" DataBuffer with a storage array of w*h. This array is + // later discarded, and replaced in the raster.setDataElements() call. + // The "old" way is kept as a more compatible fall-back mode. + + DataBuffer buffer = null; + WritableRaster raster = null; + + int bands; + if (pPixels instanceof int[]) { + int[] data = (int[]) pPixels; + buffer = new DataBufferInt(data, data.length); + bands = pColorModel.getNumComponents(); + } + else if (pPixels instanceof short[]) { + short[] data = (short[]) pPixels; + buffer = new DataBufferUShort(data, data.length); + bands = data.length / (pWidth * pHeight); + } + else if (pPixels instanceof byte[]) { + byte[] data = (byte[]) pPixels; + buffer = new DataBufferByte(data, data.length); + + // NOTE: This only holds for gray and indexed with one byte per pixel... + if (pColorModel instanceof IndexColorModel) { + bands = 1; + } + else { + bands = data.length / (pWidth * pHeight); + } + } + else { + // Fallback mode, slower & requires more memory, but compatible + bands = -1; + + // Create raster from color model, w and h + raster = pColorModel.createCompatibleWritableRaster(pWidth, pHeight); + raster.setDataElements(0, 0, pWidth, pHeight, pPixels); // Note: This is known to throw ClassCastExceptions.. + } + + if (raster == null) { + if (pColorModel instanceof IndexColorModel && isIndexedPacked((IndexColorModel) pColorModel)) { + raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pColorModel.getPixelSize(), LOCATION_UPPER_LEFT); + } + else if (pColorModel instanceof PackedColorModel) { + PackedColorModel pcm = (PackedColorModel) pColorModel; + raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pWidth, pcm.getMasks(), LOCATION_UPPER_LEFT); + } + else { + // (A)BGR order... For TYPE_3BYTE_BGR/TYPE_4BYTE_ABGR/TYPE_4BYTE_ABGR_PRE. + int[] bandsOffsets = new int[bands]; + for (int i = 0; i < bands;) { + bandsOffsets[i] = bands - (++i); + } + + raster = Raster.createInterleavedRaster(buffer, pWidth, pHeight, pWidth * bands, bands, bandsOffsets, LOCATION_UPPER_LEFT); + } + } + + return raster; + } + + private static boolean isIndexedPacked(IndexColorModel pColorModel) { + return (pColorModel.getPixelSize() == 1 || pColorModel.getPixelSize() == 2 || pColorModel.getPixelSize() == 4); + } + + /** + * Workaround for bug: TYPE_3BYTE_BGR, TYPE_4BYTE_ABGR and + * TYPE_4BYTE_ABGR_PRE are all converted to TYPE_CUSTOM when using the + * default createCompatibleWritableRaster from ComponentColorModel. + * + * @param pOriginal the orignal image + * @param pModel the original color model + * @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 width, int height) { + if (pModel == null || equals(pOriginal.getColorModel(), pModel)) { + int[] bOffs; + switch (pOriginal.getType()) { + case BufferedImage.TYPE_3BYTE_BGR: + bOffs = new int[]{2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return + return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, + 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, + 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(width, height); + } + } + + return pModel.createCompatibleWritableRaster(width, height); + } + + /** + * Converts the {@code Image} to a {@code BufferedImage} of the 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 already a {@code BufferedImage} of the given type, it + * is simply returned and no conversion takes place. + *

+ * + * @param pOriginal the image to convert. + * @param pType the type of buffered image + * + * @return a {@code BufferedImage} + * + * @throws IllegalArgumentException if {@code pOriginal == null} + * or if {@code pType} is not a valid type for {@code BufferedImage} + * + * @see java.awt.image.BufferedImage#getType() + */ + public static BufferedImage toBuffered(Image pOriginal, int pType) { + return toBuffered(pOriginal, pType, null); + } + + /** + * + * @param pOriginal the original image + * @param pType the type of {@code BufferedImage} to create + * @param pICM the optional {@code IndexColorModel} to use. If not + * {@code null} the {@code pType} must be compatible with the color model + * @return a {@code BufferedImage} + * @throws IllegalArgumentException if {@code pType} is not compatible with + * the color model + */ + private static BufferedImage toBuffered(Image pOriginal, int pType, IndexColorModel pICM) { + // 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))) { + return (BufferedImage) pOriginal; + } + if (pOriginal == null) { + throw new IllegalArgumentException("original == null"); + } + + //System.out.println("--> Doing full BufferedImage conversion, using Graphics.drawImage()."); + + // Create a buffered image + // NOTE: The getWidth and getHeight methods, will wait for the image + BufferedImage image; + if (pICM == null) { + image = createBuffered(getWidth(pOriginal), getHeight(pOriginal), pType, Transparency.TRANSLUCENT);//new BufferedImage(getWidth(pOriginal), getHeight(pOriginal), pType); + } + else { + image = new BufferedImage(getWidth(pOriginal), getHeight(pOriginal), pType, pICM); + } + + // Draw the image onto the buffer + drawOnto(image, pOriginal); + + return image; + } + + /** + * Draws the source image onto the buffered image, using + * {@code AlphaComposite.Src} and coordinates {@code 0, 0}. + * + * @param pDestination the image to draw on + * @param pSource the source image to draw + * + * @throws NullPointerException if {@code pDestination} or {@code pSource} is {@code null} + */ + static void drawOnto(final BufferedImage pDestination, final Image pSource) { + Graphics2D g = pDestination.createGraphics(); + try { + g.setComposite(AlphaComposite.Src); + g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); + g.drawImage(pSource, 0, 0, null); + } + finally { + g.dispose(); + } + } + + /** + * Creates a flipped version of the given image. + * + * @param pImage the image to flip + * @param pAxis the axis to flip around + * @return a new {@code BufferedImage} + */ + public static BufferedImage createFlipped(final Image pImage, final int pAxis) { + switch (pAxis) { + case FLIP_HORIZONTAL: + case FLIP_VERTICAL: + // TODO case FLIP_BOTH:?? same as rotate 180? + break; + default: + throw new IllegalArgumentException("Illegal direction: " + pAxis); + } + BufferedImage source = toBuffered(pImage); + AffineTransform transform; + if (pAxis == FLIP_HORIZONTAL) { + transform = AffineTransform.getTranslateInstance(0, source.getHeight()); + transform.scale(1, -1); + } + else { + transform = AffineTransform.getTranslateInstance(source.getWidth(), 0); + transform.scale(-1, 1); + } + AffineTransformOp transformOp = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + return transformOp.filter(source, null); + } + + + /** + * Rotates the image 90 degrees, clockwise (aka "rotate right"), + * counter-clockwise (aka "rotate left") or 180 degrees, depending on the + * {@code pDirection} argument. + *

+ * The new image will be completely covered with pixels from the source + * image. + *

+ * + * @param pImage the source image. + * @param pDirection the direction, must be either {@link #ROTATE_90_CW}, + * {@link #ROTATE_90_CCW} or {@link #ROTATE_180} + * + * @return a new {@code BufferedImage} + * + */ + public static BufferedImage createRotated(final Image pImage, final int pDirection) { + switch (pDirection) { + case ROTATE_90_CW: + case ROTATE_90_CCW: + case ROTATE_180: + return createRotated(pImage, Math.toRadians(pDirection)); + default: + throw new IllegalArgumentException("Illegal direction: " + pDirection); + } + } + + /** + * Rotates the image to the given angle. Areas not covered with pixels from + * the source image will be left transparent, if possible. + * + * @param pImage the source image + * @param pAngle the angle of rotation, in radians + * + * @return a new {@code BufferedImage}, unless {@code pAngle == 0.0} + */ + public static BufferedImage createRotated(final Image pImage, final double pAngle) { + return createRotated0(toBuffered(pImage), pAngle); + } + + private static BufferedImage createRotated0(final BufferedImage pSource, final double pAngle) { + if ((Math.abs(Math.toDegrees(pAngle)) % 360) == 0) { + return pSource; + } + + final boolean fast = ((Math.abs(Math.toDegrees(pAngle)) % 90) == 0.0); + final int w = pSource.getWidth(); + final int h = pSource.getHeight(); + + // Compute new width and height + double sin = Math.abs(Math.sin(pAngle)); + double cos = Math.abs(Math.cos(pAngle)); + + int newW = (int) Math.floor(w * cos + h * sin); + int newH = (int) Math.floor(h * cos + w * sin); + + AffineTransform transform = AffineTransform.getTranslateInstance((newW - w) / 2.0, (newH - h) / 2.0); + transform.rotate(pAngle, w / 2.0, h / 2.0); + + // TODO: Figure out if this is correct + BufferedImage dest = createTransparent(newW, newH); + + // See: http://weblogs.java.net/blog/campbell/archive/2007/03/java_2d_tricker_1.html + Graphics2D g = dest.createGraphics(); + try { + g.transform(transform); + if (!fast) { + // Max quality + g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, + RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, + RenderingHints.VALUE_ANTIALIAS_ON); + g.setPaint(new TexturePaint(pSource, + new Rectangle2D.Float(0, 0, pSource.getWidth(), pSource.getHeight()))); + g.fillRect(0, 0, pSource.getWidth(), pSource.getHeight()); + } + else { + g.drawImage(pSource, 0, 0, null); + } + } + finally { + g.dispose(); + } + + return dest; + } + + /** + * 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 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 + * @param pWidth width in pixels + * @param pHeight height in pixels + * @param pHints scaling ints + * + * @return a {@code BufferedImage} + * + * @throws NullPointerException if {@code pImage} is {@code null}. + * + * @see #createResampled(java.awt.Image, int, int, int) + * @see Image#getScaledInstance(int,int,int) + * @see Image#SCALE_AREA_AVERAGING + * @see Image#SCALE_DEFAULT + * @see Image#SCALE_FAST + * @see Image#SCALE_REPLICATE + * @see Image#SCALE_SMOOTH + */ + public static BufferedImage createScaled(Image pImage, int pWidth, int pHeight, int pHints) { + ColorModel cm; + int type = BI_TYPE_ANY; + if (pImage instanceof RenderedImage) { + cm = ((RenderedImage) pImage).getColorModel(); + if (pImage instanceof BufferedImage) { + type = ((BufferedImage) pImage).getType(); + } + } + else { + BufferedImageFactory factory = new BufferedImageFactory(pImage); + cm = factory.getColorModel(); + } + + BufferedImage scaled = createResampled(pImage, pWidth, pHeight, pHints); + + // 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(); + WritableRaster raster; + if (pImage instanceof BufferedImage) { + raster = createCompatibleWritableRaster((BufferedImage) pImage, cm, pWidth, pHeight); + } + else { + raster = cm.createCompatibleWritableRaster(pWidth, pHeight); + } + + BufferedImage temp = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); + + if (cm instanceof IndexColorModel && pHints == Image.SCALE_SMOOTH) { + // TODO: DiffusionDither does not support transparency at the moment, this will create bad results + new DiffusionDither((IndexColorModel) cm).filter(scaled, temp); + } + else { + drawOnto(temp, scaled); + } + + scaled = temp; + //long end = System.currentTimeMillis(); + //System.out.println("Time: " + (end - start) + " ms"); + } + + return scaled; + } + + private static boolean equals(ColorModel pLeft, ColorModel pRight) { + if (pLeft == pRight) { + return true; + } + + if (!pLeft.equals(pRight)) { + return false; + } + + // Now, the models are equal, according to the equals method + // Test indexcolormodels for equality, the maps must be equal + if (pLeft instanceof IndexColorModel) { + IndexColorModel icm1 = (IndexColorModel) pLeft; + IndexColorModel icm2 = (IndexColorModel) pRight; // NOTE: Safe, they're equal + + + final int mapSize1 = icm1.getMapSize(); + final int mapSize2 = icm2.getMapSize(); + + if (mapSize1 != mapSize2) { + return false; + } + + for (int i = 0; i > mapSize1; i++) { + if (icm1.getRGB(i) != icm2.getRGB(i)) { + return false; + } + } + + return true; + + } + + return true; + } + + /** + * Creates a scaled instance of the given {@code Image}, and converts it to + * a {@code BufferedImage} if needed. + * + * @param pImage the {@code Image} to scale + * @param pWidth width in pixels + * @param pHeight height in pixels + * @param pHints scaling mHints + * + * @return a {@code BufferedImage} + * + * @throws NullPointerException if {@code pImage} is {@code null}. + * + * @see Image#SCALE_AREA_AVERAGING + * @see Image#SCALE_DEFAULT + * @see Image#SCALE_FAST + * @see Image#SCALE_REPLICATE + * @see Image#SCALE_SMOOTH + * @see ResampleOp + */ + public static BufferedImage createResampled(Image pImage, int pWidth, int pHeight, int pHints) { + // NOTE: TYPE_4BYTE_ABGR or TYPE_3BYTE_BGR is more efficient when accelerated... + BufferedImage image = pImage instanceof BufferedImage + ? (BufferedImage) pImage + : toBuffered(pImage, BufferedImage.TYPE_4BYTE_ABGR); + return createResampled(image, pWidth, pHeight, pHints); + } + + /** + * Creates a scaled instance of the given {@code RenderedImage}, and + * converts it to a {@code BufferedImage} if needed. + * + * @param pImage the {@code RenderedImage} to scale + * @param pWidth width in pixels + * @param pHeight height in pixels + * @param pHints scaling mHints + * + * @return a {@code BufferedImage} + * + * @throws NullPointerException if {@code pImage} is {@code null}. + * + * @see Image#SCALE_AREA_AVERAGING + * @see Image#SCALE_DEFAULT + * @see Image#SCALE_FAST + * @see Image#SCALE_REPLICATE + * @see Image#SCALE_SMOOTH + * @see ResampleOp + */ + public static BufferedImage createResampled(RenderedImage pImage, int pWidth, int pHeight, int pHints) { + // NOTE: TYPE_4BYTE_ABGR or TYPE_3BYTE_BGR is more efficient when accelerated... + BufferedImage image = pImage instanceof BufferedImage + ? (BufferedImage) pImage + : toBuffered(pImage, pImage.getColorModel().hasAlpha() ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); + return createResampled(image, pWidth, pHeight, pHints); + } + + /** + * Creates a scaled instance of the given {@code BufferedImage}. + * + * @param pImage the {@code BufferedImage} to scale + * @param pWidth width in pixels + * @param pHeight height in pixels + * @param pHints scaling mHints + * + * @return a {@code BufferedImage} + * + * @throws NullPointerException if {@code pImage} is {@code null}. + * + * @see Image#SCALE_AREA_AVERAGING + * @see Image#SCALE_DEFAULT + * @see Image#SCALE_FAST + * @see Image#SCALE_REPLICATE + * @see Image#SCALE_SMOOTH + * @see ResampleOp + */ + public static BufferedImage createResampled(BufferedImage pImage, int pWidth, int pHeight, int pHints) { + // Hints are converted between java.awt.Image hints and filter types + return new ResampleOp(pWidth, pHeight, convertAWTHints(pHints)).filter(pImage, null); + } + + private static int convertAWTHints(int pHints) { + switch (pHints) { + case Image.SCALE_FAST: + case Image.SCALE_REPLICATE: + return ResampleOp.FILTER_POINT; + case Image.SCALE_AREA_AVERAGING: + return ResampleOp.FILTER_BOX; + //return ResampleOp.FILTER_CUBIC; + case Image.SCALE_SMOOTH: + return ResampleOp.FILTER_LANCZOS; + default: + //return ResampleOp.FILTER_TRIANGLE; + return ResampleOp.FILTER_QUADRATIC; + } + } + + /** + * Extracts an {@code IndexColorModel} from the given image. + * + * @param pImage the image to get the color model from + * @param pColors the maximum number of colors in the resulting color model + * @param pHints hints controlling transparency and color selection + * + * @return the extracted {@code IndexColorModel} + * + * @see #COLOR_SELECTION_DEFAULT + * @see #COLOR_SELECTION_FAST + * @see #COLOR_SELECTION_QUALITY + * @see #TRANSPARENCY_DEFAULT + * @see #TRANSPARENCY_OPAQUE + * @see #TRANSPARENCY_BITMASK + * @see #TRANSPARENCY_TRANSLUCENT + */ + public static IndexColorModel getIndexColorModel(Image pImage, int pColors, int pHints) { + return IndexImage.getIndexColorModel(pImage, pColors, pHints); + } + + /** + * Creates an indexed version of the given image (a {@code BufferedImage} + * with an {@code IndexColorModel}. + * The resulting image will have a maximum of 256 different colors. + * Transparent parts of the original will be replaced with solid black. + * Default (possibly HW accelerated) dither will be used. + * + * @param pImage the image to convert + * + * @return an indexed version of the given image + */ + public static BufferedImage createIndexed(Image pImage) { + return IndexImage.getIndexedImage(toBuffered(pImage), 256, Color.black, IndexImage.DITHER_DEFAULT); + } + + /** + * Creates an indexed version of the given image (a {@code BufferedImage} + * with an {@code IndexColorModel}. + * + * @param pImage the image to convert + * @param pColors number of colors in the resulting image + * @param pMatte color to replace transparent parts of the original. + * @param pHints hints controlling dither, transparency and color selection + * + * @return an indexed version of the given image + * + * @see #COLOR_SELECTION_DEFAULT + * @see #COLOR_SELECTION_FAST + * @see #COLOR_SELECTION_QUALITY + * @see #DITHER_NONE + * @see #DITHER_DEFAULT + * @see #DITHER_DIFFUSION + * @see #DITHER_DIFFUSION_ALTSCANS + * @see #TRANSPARENCY_DEFAULT + * @see #TRANSPARENCY_OPAQUE + * @see #TRANSPARENCY_BITMASK + * @see #TRANSPARENCY_TRANSLUCENT + */ + public static BufferedImage createIndexed(Image pImage, int pColors, Color pMatte, int pHints) { + return IndexImage.getIndexedImage(toBuffered(pImage), pColors, pMatte, pHints); + } + + /** + * Creates an indexed version of the given image (a {@code BufferedImage} + * with an {@code IndexColorModel}. + * + * @param pImage the image to convert + * @param pColors the {@code IndexColorModel} to be used in the resulting + * image. + * @param pMatte color to replace transparent parts of the original. + * @param pHints hints controlling dither, transparency and color selection + * + * @return an indexed version of the given image + * + * @see #COLOR_SELECTION_DEFAULT + * @see #COLOR_SELECTION_FAST + * @see #COLOR_SELECTION_QUALITY + * @see #DITHER_NONE + * @see #DITHER_DEFAULT + * @see #DITHER_DIFFUSION + * @see #DITHER_DIFFUSION_ALTSCANS + * @see #TRANSPARENCY_DEFAULT + * @see #TRANSPARENCY_OPAQUE + * @see #TRANSPARENCY_BITMASK + * @see #TRANSPARENCY_TRANSLUCENT + */ + public static BufferedImage createIndexed(Image pImage, IndexColorModel pColors, Color pMatte, int pHints) { + return IndexImage.getIndexedImage(toBuffered(pImage), pColors, pMatte, pHints); + } + + /** + * Creates an indexed version of the given image (a {@code BufferedImage} + * with an {@code IndexColorModel}. + * + * @param pImage the image to convert + * @param pColors an {@code Image} used to get colors from. If the image is + * has an {@code IndexColorModel}, it will be uesd, otherwise an + * {@code IndexColorModel} is created from the image. + * @param pMatte color to replace transparent parts of the original. + * @param pHints hints controlling dither, transparency and color selection + * + * @return an indexed version of the given image + * + * @see #COLOR_SELECTION_DEFAULT + * @see #COLOR_SELECTION_FAST + * @see #COLOR_SELECTION_QUALITY + * @see #DITHER_NONE + * @see #DITHER_DEFAULT + * @see #DITHER_DIFFUSION + * @see #DITHER_DIFFUSION_ALTSCANS + * @see #TRANSPARENCY_DEFAULT + * @see #TRANSPARENCY_OPAQUE + * @see #TRANSPARENCY_BITMASK + * @see #TRANSPARENCY_TRANSLUCENT + */ + public static BufferedImage createIndexed(Image pImage, Image pColors, Color pMatte, int pHints) { + return IndexImage.getIndexedImage(toBuffered(pImage), + IndexImage.getIndexColorModel(pColors, 255, pHints), + pMatte, pHints); + } + + /** + * Sharpens an image using a convolution matrix. + * The sharpen kernel used, is defined by the following 3 by 3 matrix: + * + * + * + * + * + *
Sharpen Kernel Matrix
0.0-0.30.0
-0.32.2-0.3
0.0-0.30.0
+ *

+ * This is the same result returned as + * {@code sharpen(pOriginal, 0.3f)}. + *

+ * + * @param pOriginal the BufferedImage to sharpen + * + * @return a new BufferedImage, containing the sharpened image. + */ + public static BufferedImage sharpen(BufferedImage pOriginal) { + return convolve(pOriginal, SHARPEN_KERNEL, EDGE_REFLECT); + } + + /** + * Sharpens an image using a convolution matrix. + * The sharpen kernel used, is defined by the following 3 by 3 matrix: + * + * + * + * + * + * + * + *
Sharpen Kernel Matrix
0.0-{@code pAmount}0.0
-{@code pAmount}4.0 * {@code pAmount} + 1.0-{@code pAmount}
0.0-{@code pAmount}0.0
+ * + * @param pOriginal the BufferedImage to sharpen + * @param pAmount the amount of sharpening + * + * @return a BufferedImage, containing the sharpened image. + */ + public static BufferedImage sharpen(BufferedImage pOriginal, float pAmount) { + if (pAmount == 0f) { + return pOriginal; + } + + // Create the convolution matrix + float[] data = new float[] { + 0.0f, -pAmount, 0.0f, -pAmount, 4f * pAmount + 1f, -pAmount, 0.0f, -pAmount, 0.0f + }; + + // Do the filtering + return convolve(pOriginal, new Kernel(3, 3, data), EDGE_REFLECT); + } + + /** + * Creates a blurred version of the given image. + * + * @param pOriginal the original image + * + * @return a new {@code BufferedImage} with a blurred version of the given image + */ + public static BufferedImage blur(BufferedImage pOriginal) { + return blur(pOriginal, 1.5f); + } + + // Some work to do... Is okay now, for range 0...1, anything above creates + // artifacts. + // The idea here is that the sum of all terms in the matrix must be 1. + + /** + * Creates a blurred version of the given image. + * + * @param pOriginal the original image + * @param pRadius the amount to blur + * + * @return a new {@code BufferedImage} with a blurred version of the given image + */ + public static BufferedImage blur(BufferedImage pOriginal, float pRadius) { + if (pRadius <= 1f) { + return pOriginal; + } + + // TODO: Re-implement using two-pass one-dimensional gaussion blur + // See: http://en.wikipedia.org/wiki/Gaussian_blur#Implementation + // Also see http://www.jhlabs.com/ip/blurring.html + + // TODO: Rethink... Fixed amount and scale matrix instead? +// pAmount = 1f - pAmount; +// float pAmount = 1f - pRadius; +// +// // Normalize amount +// float normAmt = (1f - pAmount) / 24; +// +// // Create the convolution matrix +// float[] data = new float[] { +// normAmt / 2, normAmt, normAmt, normAmt, normAmt / 2, +// normAmt, normAmt, normAmt * 2, normAmt, normAmt, +// normAmt, normAmt * 2, pAmount, normAmt * 2, normAmt, +// normAmt, normAmt, normAmt * 2, normAmt, normAmt, +// normAmt / 2, normAmt, normAmt, normAmt, normAmt / 2 +// }; +// +// // Do the filtering +// return convolve(pOriginal, new Kernel(5, 5, data), EDGE_REFLECT); + + Kernel horizontal = makeKernel(pRadius); + Kernel vertical = new Kernel(horizontal.getHeight(), horizontal.getWidth(), horizontal.getKernelData(null)); + + BufferedImage temp = addBorder(pOriginal, horizontal.getWidth() / 2, vertical.getHeight() / 2, EDGE_REFLECT); + + temp = convolve(temp, horizontal, EDGE_NO_OP); + temp = convolve(temp, vertical, EDGE_NO_OP); + + return temp.getSubimage( + horizontal.getWidth() / 2, vertical.getHeight() / 2, pOriginal.getWidth(), pOriginal.getHeight() + ); + } + + /** + * Make a Gaussian blur {@link Kernel}. + * + * @param radius the blur radius + * @return a new blur {@code Kernel} + */ + private static Kernel makeKernel(float radius) { + int r = (int) Math.ceil(radius); + int rows = r * 2 + 1; + float[] matrix = new float[rows]; + float sigma = radius / 3; + float sigma22 = 2 * sigma * sigma; + float sigmaPi2 = (float) (2 * Math.PI * sigma); + float sqrtSigmaPi2 = (float) Math.sqrt(sigmaPi2); + float radius2 = radius * radius; + float total = 0; + int index = 0; + for (int row = -r; row <= r; row++) { + float distance = row * row; + if (distance > radius2) { + matrix[index] = 0; + } + else { + matrix[index] = (float) Math.exp(-(distance) / sigma22) / sqrtSigmaPi2; + } + total += matrix[index]; + index++; + } + for (int i = 0; i < rows; i++) { + matrix[i] /= total; + } + + return new Kernel(rows, 1, matrix); + } + + + /** + * Convolves an image, using a convolution matrix. + * + * @param pOriginal the BufferedImage to sharpen + * @param pKernel the kernel + * @param pEdgeOperation the edge operation. Must be one of {@link #EDGE_NO_OP}, + * {@link #EDGE_ZERO_FILL}, {@link #EDGE_REFLECT} or {@link #EDGE_WRAP} + * + * @return a new BufferedImage, containing the sharpened image. + */ + public static BufferedImage convolve(BufferedImage pOriginal, Kernel pKernel, int pEdgeOperation) { + // Allow for 2 more edge operations + BufferedImage original; + switch (pEdgeOperation) { + case EDGE_REFLECT: + case EDGE_WRAP: + original = addBorder(pOriginal, pKernel.getWidth() / 2, pKernel.getHeight() / 2, pEdgeOperation); + break; + default: + original = pOriginal; + break; + } + + // Create convolution operation + ConvolveOp convolve = new ConvolveOp(pKernel, pEdgeOperation, null); + + // Workaround for what seems to be a Java2D bug: + // ConvolveOp needs explicit destination image type for some "uncommon" + // image types. However, TYPE_3BYTE_BGR is what javax.imageio.ImageIO + // normally returns for color JPEGs... :-/ + BufferedImage result = null; + if (original.getType() == BufferedImage.TYPE_3BYTE_BGR) { + result = createBuffered( + pOriginal.getWidth(), pOriginal.getHeight(), + pOriginal.getType(), pOriginal.getColorModel().getTransparency() + ); + } + + // Do the filtering (if result is null, a new image will be created) + BufferedImage image = convolve.filter(original, result); + + if (pOriginal != original) { + // Remove the border + image = image.getSubimage( + pKernel.getWidth() / 2, pKernel.getHeight() / 2, pOriginal.getWidth(), pOriginal.getHeight() + ); + } + + return image; + } + + private static BufferedImage addBorder(final BufferedImage pOriginal, final int pBorderX, final int pBorderY, final int pEdgeOperation) { + // TODO: Might be faster if we could clone raster and strech it... + int w = pOriginal.getWidth(); + int h = pOriginal.getHeight(); + + ColorModel cm = pOriginal.getColorModel(); + WritableRaster raster = cm.createCompatibleWritableRaster(w + 2 * pBorderX, h + 2 * pBorderY); + BufferedImage bordered = new BufferedImage(cm, raster, cm.isAlphaPremultiplied(), null); + + Graphics2D g = bordered.createGraphics(); + try { + g.setComposite(AlphaComposite.Src); + g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); + + // Draw original in center + 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 (pEdgeOperation) { + case EDGE_REFLECT: + // Top/left (empty) + g.drawImage(pOriginal, pBorderX, 0, pBorderX + w, pBorderY, 0, 0, w, 1, null); // Top/center + // Top/right (empty) + + g.drawImage(pOriginal, -w + pBorderX, pBorderY, pBorderX, h + pBorderY, 0, 0, 1, h, null); // Center/left + // Center/center (already drawn) + g.drawImage(pOriginal, w + pBorderX, pBorderY, 2 * pBorderX + w, h + pBorderY, w - 1, 0, w, h, null); // Center/right + + // Bottom/left (empty) + g.drawImage(pOriginal, pBorderX, pBorderY + h, pBorderX + w, 2 * pBorderY + h, 0, h - 1, w, h, null); // Bottom/center + // Bottom/right (empty) + break; + case EDGE_WRAP: + g.drawImage(pOriginal, -w + pBorderX, -h + pBorderY, null); // Top/left + g.drawImage(pOriginal, pBorderX, -h + pBorderY, null); // Top/center + g.drawImage(pOriginal, w + pBorderX, -h + pBorderY, null); // Top/right + + g.drawImage(pOriginal, -w + pBorderX, pBorderY, null); // Center/left + // Center/center (already drawn) + g.drawImage(pOriginal, w + pBorderX, pBorderY, null); // Center/right + + g.drawImage(pOriginal, -w + pBorderX, h + pBorderY, null); // Bottom/left + g.drawImage(pOriginal, pBorderX, h + pBorderY, null); // Bottom/center + g.drawImage(pOriginal, w + pBorderX, h + pBorderY, null); // Bottom/right + break; + default: + throw new IllegalArgumentException("Illegal edge operation " + pEdgeOperation); + } + + } + finally { + g.dispose(); + } + + //ConvolveTester.showIt(bordered, "jaffe"); + + return bordered; + } + + /** + * Adds contrast + * + * @param pOriginal the BufferedImage to add contrast to + * + * @return an {@code Image}, containing the contrasted image. + */ + public static Image contrast(Image pOriginal) { + return contrast(pOriginal, 0.3f); + } + + /** + * Changes the contrast of the image + * + * @param pOriginal the {@code Image} to change + * @param pAmount the amount of contrast in the range [-1.0..1.0]. + * + * @return an {@code Image}, containing the contrasted image. + */ + public static Image contrast(Image pOriginal, float pAmount) { + // No change, return original + if (pAmount == 0f) { + return pOriginal; + } + + // Create filter + RGBImageFilter filter = new BrightnessContrastFilter(0f, pAmount); + + // Return contrast adjusted image + return filter(pOriginal, filter); + } + + + /** + * Changes the brightness of the original image. + * + * @param pOriginal the {@code Image} to change + * @param pAmount the amount of brightness in the range [-2.0..2.0]. + * + * @return an {@code Image} + */ + public static Image brightness(Image pOriginal, float pAmount) { + // No change, return original + if (pAmount == 0f) { + return pOriginal; + } + + // Create filter + RGBImageFilter filter = new BrightnessContrastFilter(pAmount, 0f); + + // Return brightness adjusted image + return filter(pOriginal, filter); + } + + + /** + * Converts an image to grayscale. + * + * @see GrayFilter + * @see RGBImageFilter + * + * @param pOriginal the image to convert. + * @return a new Image, containing the gray image data. + */ + public static Image grayscale(Image pOriginal) { + // Create filter + RGBImageFilter filter = new GrayFilter(); + + // Convert to gray + return filter(pOriginal, filter); + } + + /** + * Filters an image, using the given {@code ImageFilter}. + * + * @param pOriginal the original image + * @param pFilter the filter to apply + * + * @return the new {@code Image} + */ + public static Image filter(Image pOriginal, ImageFilter pFilter) { + // Create a filtered source + ImageProducer source = new FilteredImageSource(pOriginal.getSource(), pFilter); + + // Create new image + return Toolkit.getDefaultToolkit().createImage(source); + } + + /** + * Tries to use H/W-accelerated code for an image for display purposes. + * Note that transparent parts of the image might be replaced by solid + * color. Additional image information not used by the current diplay + * hardware may be discarded, like extra bith depth etc. + * + * @param pImage any {@code Image} + * @return a {@code BufferedImage} + */ + public static BufferedImage accelerate(Image pImage) { + return accelerate(pImage, null, DEFAULT_CONFIGURATION); + } + + /** + * Tries to use H/W-accelerated code for an image for display purposes. + * Note that transparent parts of the image might be replaced by solid + * color. Additional image information not used by the current diplay + * hardware may be discarded, like extra bith depth etc. + * + * @param pImage any {@code Image} + * @param pConfiguration the {@code GraphicsConfiguration} to accelerate + * for + * + * @return a {@code BufferedImage} + */ + public static BufferedImage accelerate(Image pImage, GraphicsConfiguration pConfiguration) { + return accelerate(pImage, null, pConfiguration); + } + + /** + * Tries to use H/W-accelerated code for an image for display purposes. + * Note that transparent parts of the image will be replaced by solid + * color. Additional image information not used by the current diplay + * hardware may be discarded, like extra bith depth etc. + * + * @param pImage any {@code Image} + * @param pBackgroundColor the background color to replace any transparent + * parts of the image. + * May be {@code null}, in such case the color is undefined. + * @param pConfiguration the graphics configuration + * May be {@code null}, in such case the color is undefined. + * + * @return a {@code BufferedImage} + */ + static BufferedImage accelerate(Image pImage, Color pBackgroundColor, GraphicsConfiguration pConfiguration) { + // Skip acceleration if the layout of the image and color model is already ok + if (pImage instanceof BufferedImage) { + BufferedImage buffered = (BufferedImage) pImage; + // TODO: What if the createCompatibleImage insist on TYPE_CUSTOM...? :-P + if (buffered.getType() != BufferedImage.TYPE_CUSTOM && equals(buffered.getColorModel(), pConfiguration.getColorModel(buffered.getTransparency()))) { + return buffered; + } + } + if (pImage == null) { + throw new IllegalArgumentException("image == null"); + } + + int w = ImageUtil.getWidth(pImage); + int h = ImageUtil.getHeight(pImage); + + // Create accelerated version + BufferedImage temp = createClear(w, h, BI_TYPE_ANY, getTransparency(pImage), pBackgroundColor, pConfiguration); + drawOnto(temp, pImage); + + return temp; + } + + private static int getTransparency(Image pImage) { + if (pImage instanceof BufferedImage) { + BufferedImage bi = (BufferedImage) pImage; + return bi.getTransparency(); + } + return Transparency.OPAQUE; + } + + /** + * Creates a transparent image. + * + * @param pWidth the requested width of the image + * @param pHeight the requested height of the image + * + * @throws IllegalArgumentException if {@code pType} is not a valid type + * for {@code BufferedImage} + * + * @return the new image + */ + public static BufferedImage createTransparent(int pWidth, int pHeight) { + return createTransparent(pWidth, pHeight, BI_TYPE_ANY); + } + + /** + * Creates a transparent image. + * + * @see BufferedImage#BufferedImage(int,int,int) + * + * @param pWidth the requested width of the image + * @param pHeight the requested height of the image + * @param pType the type of {@code BufferedImage} to create + * + * @throws IllegalArgumentException if {@code pType} is not a valid type + * for {@code BufferedImage} + * + * @return the new image + */ + public static BufferedImage createTransparent(int pWidth, int pHeight, int pType) { + // Create + BufferedImage image = createBuffered(pWidth, pHeight, pType, Transparency.TRANSLUCENT); + + // Clear image with transparent alpha by drawing a rectangle + Graphics2D g = image.createGraphics(); + try { + g.setComposite(AlphaComposite.Clear); + g.fillRect(0, 0, pWidth, pHeight); + } + finally { + g.dispose(); + } + + return image; + } + + /** + * Creates a clear image with the given background color. + * + * @see BufferedImage#BufferedImage(int,int,int) + * + * @param pWidth the requested width of the image + * @param pHeight the requested height of the image + * @param pBackground the background color. The color may be translucent. + * May be {@code null}, in such case the color is undefined. + * + * @throws IllegalArgumentException if {@code pType} is not a valid type + * for {@code BufferedImage} + * + * @return the new image + */ + public static BufferedImage createClear(int pWidth, int pHeight, Color pBackground) { + return createClear(pWidth, pHeight, BI_TYPE_ANY, pBackground); + } + + /** + * Creates a clear image with the given background color. + * + * @see BufferedImage#BufferedImage(int,int,int) + * + * @param pWidth the width of the image to create + * @param pHeight the height of the image to create + * @param pType the type of image to create (one of the constants from + * {@link BufferedImage} or {@link #BI_TYPE_ANY}) + * @param pBackground the background color. The color may be translucent. + * May be {@code null}, in such case the color is undefined. + * + * @throws IllegalArgumentException if {@code pType} is not a valid type + * for {@code BufferedImage} + * + * @return the new image + */ + public static BufferedImage createClear(int pWidth, int pHeight, int pType, Color pBackground) { + return createClear(pWidth, pHeight, pType, Transparency.OPAQUE, pBackground, DEFAULT_CONFIGURATION); + } + + static BufferedImage createClear(int pWidth, int pHeight, int pType, int pTransparency, Color pBackground, GraphicsConfiguration pConfiguration) { + // Create + int transparency = (pBackground != null) ? pBackground.getTransparency() : pTransparency; + BufferedImage image = createBuffered(pWidth, pHeight, pType, transparency, pConfiguration); + + if (pBackground != null) { + // Clear image with clear color, by drawing a rectangle + Graphics2D g = image.createGraphics(); + try { + g.setComposite(AlphaComposite.Src); // Allow color to be translucent + g.setColor(pBackground); + g.fillRect(0, 0, pWidth, pHeight); + } + finally { + g.dispose(); + } + } + + return image; + } + + /** + * Creates a {@code BufferedImage} of the given size and type. If possible, + * uses accelerated versions of BufferedImage from GraphicsConfiguration. + * + * @param pWidth the width of the image to create + * @param pHeight the height of the image to create + * @param pType the type of image to create (one of the constants from + * {@link BufferedImage} or {@link #BI_TYPE_ANY}) + * @param pTransparency the transparency type (from {@link Transparency}) + * + * @return a {@code BufferedImage} + */ + private static BufferedImage createBuffered(int pWidth, int pHeight, int pType, int pTransparency) { + return createBuffered(pWidth, pHeight, pType, pTransparency, DEFAULT_CONFIGURATION); + } + + static BufferedImage createBuffered(int pWidth, int pHeight, int pType, int pTransparency, + GraphicsConfiguration pConfiguration) { + if (VM_SUPPORTS_ACCELERATION && pType == BI_TYPE_ANY) { + GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment(); + if (supportsAcceleration(env)) { + return getConfiguration(pConfiguration).createCompatibleImage(pWidth, pHeight, pTransparency); + } + } + + return new BufferedImage(pWidth, pHeight, getImageType(pType, pTransparency)); + } + + private static GraphicsConfiguration getConfiguration(final GraphicsConfiguration pConfiguration) { + return pConfiguration != null ? pConfiguration : DEFAULT_CONFIGURATION; + } + + private static int getImageType(int pType, int pTransparency) { + // TODO: Handle TYPE_CUSTOM? + if (pType != BI_TYPE_ANY) { + return pType; + } + else { + switch (pTransparency) { + case Transparency.OPAQUE: + return BufferedImage.TYPE_INT_RGB; + case Transparency.BITMASK: + case Transparency.TRANSLUCENT: + return BufferedImage.TYPE_INT_ARGB; + default: + throw new IllegalArgumentException("Unknown transparency type: " + pTransparency); + } + } + } + + /** + * Tests if the given {@code GraphicsEnvironment} supports accelleration + * + * @param pEnv the environment + * @return {@code true} if the {@code GraphicsEnvironment} supports + * acceleration + */ + private static boolean supportsAcceleration(GraphicsEnvironment pEnv) { + try { + // Acceleration only supported in non-headless environments, on 1.4+ VMs + return /*VM_SUPPORTS_ACCELERATION &&*/ !pEnv.isHeadlessInstance(); + } + catch (LinkageError ignore) { + // Means we are not in a 1.4+ VM, so skip testing for headless again + VM_SUPPORTS_ACCELERATION = false; + } + + // If the invocation fails, assume no accelleration is possible + return false; + } + + /** + * Gets the width of an Image. + * This method has the side-effect of completely loading the image. + * + * @param pImage an image. + * + * @return the width of the image, or -1 if the width could not be + * determined (i.e. an error occured while waiting for the + * image to load). + */ + public static int getWidth(Image pImage) { + int width = pImage.getWidth(NULL_COMPONENT); + if (width < 0) { + if (!waitForImage(pImage)) { + return -1; // Error while waiting + } + width = pImage.getWidth(NULL_COMPONENT); + } + + return width; + } + + /** + * Gets the height of an Image. + * This method has the side-effect of completely loading the image. + * + * @param pImage an image. + * + * @return the height of the image, or -1 if the height could not be + * determined (i.e. an error occured while waiting for the + * image to load). + */ + public static int getHeight(Image pImage) { + int height = pImage.getHeight(NULL_COMPONENT); + if (height < 0) { + if (!waitForImage(pImage)) { + return -1; // Error while waiting + } + height = pImage.getHeight(NULL_COMPONENT); + } + + return height; + } + + /** + * Waits for an image to load completely. + * Will wait forever. + * + * @param pImage an Image object to wait for. + * + * @return true if the image was loaded successfully, false if an error + * occured, or the wait was interrupted. + * + * @see #waitForImage(Image,long) + */ + public static boolean waitForImage(Image pImage) { + return waitForImages(new Image[]{pImage}, -1L); + } + + /** + * Waits for an image to load completely. + * Will wait the specified time. + * + * @param pImage an Image object to wait for. + * @param pTimeOut the time to wait, in milliseconds. + * + * @return true if the image was loaded successfully, false if an error + * occurred, or the wait was interrupted. + * + * @see #waitForImages(Image[],long) + */ + public static boolean waitForImage(Image pImage, long pTimeOut) { + return waitForImages(new Image[]{pImage}, pTimeOut); + } + + /** + * Waits for a number of images to load completely. + * Will wait forever. + * + * @param pImages an array of Image objects to wait for. + * + * @return true if the images was loaded successfully, false if an error + * occurred, or the wait was interrupted. + * + * @see #waitForImages(Image[],long) + */ + public static boolean waitForImages(Image[] pImages) { + return waitForImages(pImages, -1L); + } + + /** + * Waits for a number of images to load completely. + * Will wait the specified time. + * + * @param pImages an array of Image objects to wait for + * @param pTimeOut the time to wait, in milliseconds + * + * @return true if the images was loaded successfully, false if an error + * occurred, or the wait was interrupted. + */ + public static boolean waitForImages(Image[] pImages, long pTimeOut) { + // TODO: Need to make sure that we don't wait for the same image many times + // Use hashcode as id? Don't remove images from tracker? Hmmm... + boolean success = true; + + // Create a local id for use with the mediatracker + int imageId; + + // NOTE: This is very experimental... + imageId = pImages.length == 1 ? System.identityHashCode(pImages[0]) : System.identityHashCode(pImages); + + // Add images to tracker + for (Image image : pImages) { + sTracker.addImage(image, imageId); + + // Start loading immediately + if (sTracker.checkID(imageId, false)) { + // Image is done, so remove again + sTracker.removeImage(image, imageId); + } + } + + try { + if (pTimeOut < 0L) { + // Just wait + sTracker.waitForID(imageId); + } + else { + // Wait until timeout + // NOTE: waitForID(int, long) return value is undocumented. + // I assume that it returns true, if the image(s) loaded + // successfully before the timeout, however, I always check + // isErrorID later on, just in case... + success = sTracker.waitForID(imageId, pTimeOut); + } + } + catch (InterruptedException ie) { + // Interrupted while waiting, image not loaded + success = false; + } + finally { + // Remove images from mediatracker + for (Image pImage : pImages) { + sTracker.removeImage(pImage, imageId); + } + } + + // If the wait was successfull, and no errors were reported for the + // images, return true + return success && !sTracker.isErrorID(imageId); + } + + /** + * Tests whether the image has any transparent or semi-transparent pixels. + * + * @param pImage the image + * @param pFast if {@code true}, the method tests maximum 10 x 10 pixels, + * evenly spaced out in the image. + * + * @return {@code true} if transparent pixels are found, otherwise + * {@code false}. + */ + public static boolean hasTransparentPixels(RenderedImage pImage, boolean pFast) { + if (pImage == null) { + return false; + } + + // First, test if the ColorModel supports alpha... + ColorModel cm = pImage.getColorModel(); + if (!cm.hasAlpha()) { + return false; + } + + if (cm.getTransparency() != Transparency.BITMASK + && cm.getTransparency() != Transparency.TRANSLUCENT) { + return false; + } + + // ... if so, test the pixels of the image hard way + Object data = null; + + // Loop over tiles (noramally, BufferedImages have only one) + for (int yT = pImage.getMinTileY(); yT < pImage.getNumYTiles(); yT++) { + for (int xT = pImage.getMinTileX(); xT < pImage.getNumXTiles(); xT++) { + // Test pixels of each tile + Raster raster = pImage.getTile(xT, yT); + int xIncrement = pFast ? Math.max(raster.getWidth() / 10, 1) : 1; + int yIncrement = pFast ? Math.max(raster.getHeight() / 10, 1) : 1; + + for (int y = 0; y < raster.getHeight(); y += yIncrement) { + for (int x = 0; x < raster.getWidth(); x += xIncrement) { + // Copy data for each pixel, without allocation array + data = raster.getDataElements(x, y, data); + + // Test alpha value + if (cm.getAlpha(data) != 0xff) { + return true; + } + } + } + } + } + + return false; + } + + /** + * Creates a translucent version of the given color. + * + * @param pColor the original color + * @param pTransparency the transparency level ({@code 0 - 255}) + * @return a translucent color + * + * @throws NullPointerException if {@code pColor} is {@code null} + */ + public static Color createTranslucent(Color pColor, int pTransparency) { + //return new Color(pColor.getRed(), pColor.getGreen(), pColor.getBlue(), pTransparency); + return new Color(((pTransparency & 0xff) << 24) | (pColor.getRGB() & 0x00ffffff), true); + } + + /** + * Blends two ARGB values half and half, to create a tone in between. + * + * @param pRGB1 color 1 + * @param pRGB2 color 2 + * @return the new rgb value + */ + static int blend(int pRGB1, int pRGB2) { + // Slightly modified from http://www.compuphase.com/graphic/scale3.htm + // to support alpha values + return (((pRGB1 ^ pRGB2) & 0xfefefefe) >> 1) + (pRGB1 & pRGB2); + } + + /** + * Blends two colors half and half, to create a tone in between. + * + * @param pColor color 1 + * @param pOther color 2 + * @return a new {@code Color} + */ + public static Color blend(Color pColor, Color pOther) { + return new Color(blend(pColor.getRGB(), pOther.getRGB()), true); + + /* + return new Color((pColor.getRed() + pOther.getRed()) / 2, + (pColor.getGreen() + pOther.getGreen()) / 2, + (pColor.getBlue() + pOther.getBlue()) / 2, + (pColor.getAlpha() + pOther.getAlpha()) / 2); + */ + } + + /** + * Blends two colors, controlled by the blending factor. + * A factor of {@code 0.0} will return the first color, + * a factor of {@code 1.0} will return the second. + * + * @param pColor color 1 + * @param pOther color 2 + * @param pBlendFactor {@code [0...1]} + * @return a new {@code Color} + */ + public static Color blend(Color pColor, Color pOther, float pBlendFactor) { + float inverseBlend = (1f - pBlendFactor); + return new Color( + clamp((pColor.getRed() * inverseBlend) + (pOther.getRed() * pBlendFactor)), + clamp((pColor.getGreen() * inverseBlend) + (pOther.getGreen() * pBlendFactor)), + clamp((pColor.getBlue() * inverseBlend) + (pOther.getBlue() * pBlendFactor)), + clamp((pColor.getAlpha() * inverseBlend) + (pOther.getAlpha() * pBlendFactor)) + ); + } + + private static int clamp(float f) { + return (int) f; + } } \ No newline at end of file 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 3838c9e3..2a359ca3 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 @@ -1,1517 +1,1527 @@ -/* - * 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 of the copyright holder 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 HOLDER 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. - */ -/* -****************************************************************************** -* -* ============================================================================ -* The Apache Software License, Version 1.1 -* ============================================================================ -* -* Copyright (C) 2000 The Apache Software Foundation. All rights reserved. -* -* Redistribution and use in source and binary forms, with or without modifica- -* tion, are permitted provided that the following conditions are met: -* -* 1. Redistributions of source code must retain the above copyright notice, -* this list of conditions and the following disclaimer. -* -* 2. Redistributions in binary form must reproduce the above copyright notice, -* this list of conditions and the following disclaimer in the documentation -* and/or other materials provided with the distribution. -* -* 3. The end-user documentation included with the redistribution, if any, must -* include the following acknowledgment: "This product includes software -* developed by the Apache Software Foundation (http://www.apache.org/)." -* Alternately, this acknowledgment may appear in the software itself, if -* and wherever such third-party acknowledgments normally appear. -* -* 4. The names "Batik" and "Apache Software Foundation" must not be used to -* endorse or promote products derived from this software without prior -* written permission. For written permission, please contact -* apache@apache.org. -* -* 5. Products derived from this software may not be called "Apache", nor may -* "Apache" appear in their name, without prior written permission of the -* Apache Software Foundation. -* -* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, -* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -* APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU- -* DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS -* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -* -* This software consists of voluntary contributions made by many individuals -* on behalf of the Apache Software Foundation. For more information on the -* Apache Software Foundation, please see . -* -****************************************************************************** -* -*/ - -package com.twelvemonkeys.image; - -import com.twelvemonkeys.io.FileUtil; -import com.twelvemonkeys.lang.StringUtil; - -import javax.imageio.ImageIO; -import java.awt.*; -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; - -/** - * This class implements an adaptive palette generator to reduce images - * to a variable number of colors. - * It can also render images into fixed color pallettes. - *

- * Support for the default JVM (ordered/pattern) dither, Floyd-Steinberg like - * error-diffusion and no dither, controlled by the hints - * {@link #DITHER_DIFFUSION}, - * {@link #DITHER_NONE} and - * {@link #DITHER_DEFAULT}. - *

- * Color selection speed/accuracy can be controlled using the hints - * {@link #COLOR_SELECTION_FAST}, - * {@link #COLOR_SELECTION_QUALITY} and - * {@link #COLOR_SELECTION_DEFAULT}. - *

- * Transparency support can be controlled using the hints - * {@link #TRANSPARENCY_OPAQUE}, - * {@link #TRANSPARENCY_BITMASK} and - * {@link #TRANSPARENCY_TRANSLUCENT}. - *

- *


- *

- *

- * This product includes software developed by the Apache Software Foundation.
- * 

- * This software consists of voluntary contributions made by many individuals - * on behalf of the Apache Software Foundation. For more information on the - * Apache Software Foundation, please see http://www.apache.org/ - *

- * - * @author Thomas DeWeese - * @author Jun Inamori - * @author Harald Kuhr - * @version $Id: IndexImage.java#1 $ - * @see DiffusionDither - */ -class IndexImage { - - /** - * Dither mask - */ - protected final static int DITHER_MASK = 0xFF; - - /** - * Java default dither - */ - public final static int DITHER_DEFAULT = 0x00; - - /** - * No dither - */ - public final static int DITHER_NONE = 0x01; - - /** - * Error diffusion dither - */ - public final static int DITHER_DIFFUSION = 0x02; - - /** - * Error diffusion dither with alternating scans - */ - public final static int DITHER_DIFFUSION_ALTSCANS = 0x03; - - /** - * Color Selection mask - */ - protected final static int COLOR_SELECTION_MASK = 0xFF00; - - /** - * Default color selection - */ - public final static int COLOR_SELECTION_DEFAULT = 0x0000; - - /** - * Prioritize speed - */ - public final static int COLOR_SELECTION_FAST = 0x0100; - - /** - * Prioritize quality - */ - public final static int COLOR_SELECTION_QUALITY = 0x0200; - - /** - * Transparency mask - */ - protected final static int TRANSPARENCY_MASK = 0xFF0000; - - /** - * Default transparency (none) - */ - public final static int TRANSPARENCY_DEFAULT = 0x000000; - - /** - * Discard any alpha information - */ - public final static int TRANSPARENCY_OPAQUE = 0x010000; - - /** - * Convert alpha to bitmask - */ - public final static int TRANSPARENCY_BITMASK = 0x020000; - - /** - * Keep original alpha (not supported yet) - */ - protected final static int TRANSPARENCY_TRANSLUCENT = 0x030000; - - /** - * Used to track a color and the number of pixels of that colors - */ - private static class Counter { - - /** - * Field val - */ - public int val; - - /** - * Field count - */ - public int count = 1; - - /** - * Constructor Counter - * - * @param val the initial value - */ - public Counter(int val) { - this.val = val; - } - - /** - * Method add - * - * @param val the new value - * @return {@code true} if the value was added, otherwise {@code false} - */ - public boolean add(int val) { - // See if the value matches us... - if (this.val != val) { - return false; - } - - count++; - - return true; - } - } - - /** - * 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}; - int[] max = {255, 255, 255}; - boolean done = false; - List[] colors = null; - int count = 0; - static final int RED = 0; - static final int GRN = 1; - static final int BLU = 2; - - /** - * Define a new cube. - * - * @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) { - this.colors = colors; - this.count = count; - } - - /** - * If this returns true then the cube can not be subdivided any - * further - * - * @return true if cube can not be subdivided any further - */ - public boolean isDone() { - return done; - } - - /** - * Splits the cube into two parts. This cube is - * changed to be one half and the returned cube is the other half. - * This tries to pick the right channel to split on. - * - * @return the {@code Cube} containing the other half - */ - public Cube split() { - int dr = max[0] - min[0] + 1; - int dg = max[1] - min[1] + 1; - int db = max[2] - min[2] + 1; - int c0, c1, splitChannel; - - // Figure out which axis is the longest and split along - // that axis (this tries to keep cubes square-ish). - if (dr >= dg) { - c0 = GRN; - if (dr >= db) { - splitChannel = RED; - c1 = BLU; - } - else { - splitChannel = BLU; - c1 = RED; - } - } - else if (dg >= db) { - splitChannel = GRN; - c0 = RED; - c1 = BLU; - } - else { - splitChannel = BLU; - 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; - } - - /** - * Splits the image according to the parameters. It tries - * to find a location where half the pixels are on one side - * and half the pixels are on the other. - * - * @param splitChannel split channel - * @param c0 channel 0 - * @param c1 channel 1 - * @return the {@code Cube} containing the other half - */ - public Cube splitChannel(int splitChannel, int c0, int c1) { - if (min[splitChannel] == max[splitChannel]) { - return null; - } - int splitSh4 = (2 - splitChannel) * 4; - int c0Sh4 = (2 - c0) * 4; - int c1Sh4 = (2 - c1) * 4; - - // int splitSh8 = (2-splitChannel)*8; - // int c0Sh8 = (2-c0)*8; - // int c1Sh8 = (2-c1)*8; - // - int half = count / 2; - - // Each entry is the number of pixels that have that value - // in the split channel within the cube (so pixels - // that have that value in the split channel aren't counted - // if they are outside the cube in the other color channels. - int counts[] = new int[256]; - int tcount = 0; - - // System.out.println("Cube: [" + - // min[0] + "-" + max[0] + "] [" + - // min[1] + "-" + max[1] + "] [" + - // min[2] + "-" + max[2] + "]"); - int[] minIdx = {min[0] >> 4, min[1] >> 4, min[2] >> 4}; - int[] maxIdx = {max[0] >> 4, max[1] >> 4, max[2] >> 4}; - int minR = min[0], minG = min[1], minB = min[2]; - int maxR = max[0], maxG = max[1], maxB = max[2]; - int val; - int[] vals = {0, 0, 0}; - - for (int i = minIdx[splitChannel]; i <= maxIdx[splitChannel]; i++) { - int idx1 = i << splitSh4; - - for (int j = minIdx[c0]; j <= maxIdx[c0]; j++) { - int idx2 = idx1 | (j << c0Sh4); - - for (int k = minIdx[c1]; k <= maxIdx[c1]; k++) { - int idx = idx2 | (k << c1Sh4); - List v = colors[idx]; - - if (v == null) { - continue; - } - - for (Counter c : v) { - val = c.val; - vals[0] = (val & 0xFF0000) >> 16; - vals[1] = (val & 0xFF00) >> 8; - vals[2] = (val & 0xFF); - if (((vals[0] >= minR) && (vals[0] <= maxR)) && ((vals[1] >= minG) && (vals[1] <= maxG)) - && ((vals[2] >= minB) && (vals[2] <= maxB))) { - - // The val lies within this cube so count it. - counts[vals[splitChannel]] += c.count; - tcount += c.count; - } - } - } - } - - // We've found the half way point. Note that the - // rest of counts is not filled out. - if (tcount >= half) { - break; - } - } - tcount = 0; - int lastAdd = -1; - - // These indicate what the top value for the low cube and - // the low value of the high cube should be in the split channel - // (they may not be one off if there are 'dead' spots in the - // counts array.) - int splitLo = min[splitChannel], splitHi = max[splitChannel]; - - for (int i = min[splitChannel]; i <= max[splitChannel]; i++) { - 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; - } - continue; - } - if (tcount + c < half) { - lastAdd = i; - tcount += c; - 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) { - - // All pixels are at this value so make min/max - // reflect that. - this.max[splitChannel] = i; - return null;// no split to make. - } - else { - - // There are values about this one so - // split above. - splitLo = i; - splitHi = i + 1; - break; - } - } - splitLo = lastAdd; - splitHi = i; - } - 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; - splitHi = i; - break; - } - } - - // Include c in counts - tcount += c; - splitLo = i; - splitHi = i + 1; - } - break; - } - - // System.out.println("Split: " + splitChannel + "@" - // + splitLo + "-"+splitHi + - // " Count: " + tcount + " of " + count + - // " LA: " + lastAdd); - // Create the new cube and update everyone's bounds & counts. - Cube ret = new Cube(colors, tcount); - - this.count = this.count - tcount; - ret.min[splitChannel] = this.min[splitChannel]; - ret.max[splitChannel] = splitLo; - this.min[splitChannel] = splitHi; - ret.min[c0] = this.min[c0]; - ret.max[c0] = this.max[c0]; - ret.min[c1] = this.min[c1]; - ret.max[c1] = this.max[c1]; - - return ret; - } - - /** - * Returns the average color for this cube - * - * @return the average - */ - public int averageColor() { - 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]; - int[] minIdx = {minR >> 4, minG >> 4, minB >> 4}; - int[] maxIdx = {maxR >> 4, maxG >> 4, maxB >> 4}; - int val, ired, igrn, iblu; - float weight; - - for (int i = minIdx[0]; i <= maxIdx[0]; i++) { - int idx1 = i << 8; - - for (int j = minIdx[1]; j <= maxIdx[1]; j++) { - int idx2 = idx1 | (j << 4); - - for (int k = minIdx[2]; k <= maxIdx[2]; k++) { - int idx = idx2 | k; - List v = colors[idx]; - - if (v == null) { - continue; - } - - 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; - grn += ((float) igrn) * weight; - blu += ((float) iblu) * weight; - } - } - } - } - } - - // System.out.println("RGB: [" + red + ", " + - // grn + ", " + blu + "]"); - return (((int) (red + 0.5f)) << 16 | ((int) (grn + 0.5f)) << 8 | ((int) (blu + 0.5f))); - } - }// end Cube - - /** - * You cannot create this - */ - private IndexImage() { - } - - /** - * @param pImage the image to get {@code IndexColorModel} from - * @param pNumberOfColors the number of colors for the {@code IndexColorModel} - * @param pFast {@code true} if fast - * @return an {@code IndexColorModel} - * @see #getIndexColorModel(Image,int,int) - * - * @deprecated Use {@link #getIndexColorModel(Image,int,int)} instead! - * 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); - } - - /** - * Gets an {@code IndexColorModel} from the given image. If the image has an - * {@code IndexColorModel}, this will be returned. Otherwise, an {@code IndexColorModel} - * is created, using an adaptive palette. - * - * @param pImage the image to get {@code IndexColorModel} from - * @param pNumberOfColors the number of colors for the {@code IndexColorModel} - * @param pHints one of {@link #COLOR_SELECTION_FAST}, - * {@link #COLOR_SELECTION_QUALITY} or - * {@link #COLOR_SELECTION_DEFAULT}. - * @return The {@code IndexColorModel} from the given image, or a newly created - * {@code IndexColorModel} using an adaptive palette. - * @throws ImageConversionException if an exception occurred during color - * model extraction. - */ - public static IndexColorModel getIndexColorModel(Image pImage, int pNumberOfColors, int pHints) throws ImageConversionException { - IndexColorModel icm = null; - RenderedImage image = null; - - if (pImage instanceof RenderedImage) { - image = (RenderedImage) pImage; - ColorModel cm = image.getColorModel(); - - if (cm instanceof IndexColorModel) { - // Test if we have right number of colors - if (((IndexColorModel) cm).getMapSize() <= pNumberOfColors) { - //System.out.println("IndexColorModel from BufferedImage"); - icm = (IndexColorModel) cm;// Done - } - } - - // Else create from buffered image, hard way, see below - } - else { - // Create from image using BufferedImageFactory - BufferedImageFactory factory = new BufferedImageFactory(pImage); - ColorModel cm = factory.getColorModel(); - - if ((cm instanceof IndexColorModel) && ((IndexColorModel) cm).getMapSize() <= pNumberOfColors) { - //System.out.println("IndexColorModel from Image"); - icm = (IndexColorModel) cm;// Done - } - else { - // Else create from (buffered) image, hard way - image = factory.getBufferedImage(); - } - } - - // We now have at least a buffered image, create model from it - if (icm == null) { - icm = createIndexColorModel(ImageUtil.toBuffered(image), pNumberOfColors, pHints); - } - else if (!(icm instanceof InverseColorMapIndexColorModel)) { - // If possible, use faster code - icm = new InverseColorMapIndexColorModel(icm); - } - - return icm; - } - - /** - * Creates an {@code IndexColorModel} from the given image, using an adaptive - * palette. - * - * @param pImage the image to get {@code IndexColorModel} from - * @param pNumberOfColors the number of colors for the {@code IndexColorModel} - * @param pHints use fast mode if possible (might give slightly lower - * quality) - * @return a new {@code IndexColorModel} created from the given image - */ - private static IndexColorModel createIndexColorModel(BufferedImage pImage, int pNumberOfColors, int pHints) { - // TODO: Use ImageUtil.hasTransparentPixels(pImage, true) || - // -- haraldK, 20021024, experimental, try to use one transparent pixel - boolean useTransparency = isTransparent(pHints); - - if (useTransparency) { - pNumberOfColors--; - } - - //System.out.println("Transp: " + useTransparency + " colors: " + pNumberOfColors); - int width = pImage.getWidth(); - int height = pImage.getHeight(); - - // Using 4 bits from R, G & B. - @SuppressWarnings("unchecked") - List[] colors = new List[1 << 12];// [4096] - - // Speedup, doesn't decrease image quality much - int step = 1; - - if (isFast(pHints)) { - step += (width * height / 16384);// 128x128px - } - int sampleCount = 0; - int rgb; - - //for (int x = 0; x < width; x++) { - //for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - for (int y = x % step; y < height; y += step) { - // Count the number of color samples - sampleCount++; - - // Get ARGB pixel from image - rgb = (pImage.getRGB(x, y) & 0xFFFFFF); - - // Get index from high four bits of each component. - int index = (((rgb & 0xF00000) >>> 12) | ((rgb & 0x00F000) >>> 8) | ((rgb & 0x0000F0) >>> 4)); - - // Get the 'hash vector' for that key. - List v = colors[index]; - - if (v == null) { - // No colors in this bin yet so create vector and - // add color. - v = new ArrayList(); - v.add(new Counter(rgb)); - colors[index] = v; - } - else { - // Find our color in the bin or create a counter for it. - Iterator i = v.iterator(); - - while (true) { - if (i.hasNext()) { - // try adding our color to each counter... - if (((Counter) i.next()).add(rgb)) { - break; - } - } - else { - v.add(new Counter(rgb)); - break; - } - } - } - } - } - - // All colours found, reduce to pNumberOfColors - int numberOfCubes = 1; - int fCube = 0; - Cube[] cubes = new Cube[pNumberOfColors]; - - cubes[0] = new Cube(colors, sampleCount); - - //cubes[0] = new Cube(colors, width * height); - while (numberOfCubes < pNumberOfColors) { - while (cubes[fCube].isDone()) { - fCube++; - - if (fCube == numberOfCubes) { - break; - } - } - - if (fCube == numberOfCubes) { - break; - } - - Cube cube = cubes[fCube]; - Cube newCube = cube.split(); - - if (newCube != null) { - if (newCube.count > cube.count) { - Cube tmp = cube; - - cube = newCube; - newCube = tmp; - } - - int j = fCube; - int count = cube.count; - - for (int i = fCube + 1; i < numberOfCubes; i++) { - if (cubes[i].count < count) { - break; - } - cubes[j++] = cubes[i]; - } - - cubes[j++] = cube; - count = newCube.count; - - while (j < numberOfCubes) { - if (cubes[j].count < count) { - break; - } - j++; - } - - System.arraycopy(cubes, j, cubes, j + 1, numberOfCubes - j); - - cubes[j/*++*/] = newCube; - numberOfCubes++; - } - } - - // Create RGB arrays with correct number of colors - // If we have transparency, the last color will be the transparent one - byte[] r = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes]; - byte[] g = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes]; - byte[] b = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes]; - - for (int i = 0; i < numberOfCubes; i++) { - int val = cubes[i].averageColor(); - - r[i] = (byte) ((val >> 16) & 0xFF); - g[i] = (byte) ((val >> 8) & 0xFF); - b[i] = (byte) ((val) & 0xFF); - - //System.out.println("Color [" + i + "]: #" + - // (((val>>16)<16)?"0":"") + - // Integer.toHexString(val)); - } - - // For some reason using less than 8 bits causes a bug in the dither - // - transparency added to all totally black colors? - int numOfBits = 8; - - // -- haraldK, 20021024, as suggested by Thomas E. Deweese - // plus adding a transparent pixel - IndexColorModel icm; - if (useTransparency) { - icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b, r.length - 1); - } - else { - icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b); - } - return icm; - } - - /** - * Converts the input image (must be {@code TYPE_INT_RGB} or - * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive - * palette (8 bit) from the color data in the image, and uses default - * dither. - *

- * The image returned is a new image, the input image is not modified. - * - * @param pImage the BufferedImage to index and get color information from. - * @return the indexed BufferedImage. The image will be of type - * {@code BufferedImage.TYPE_BYTE_INDEXED}, and use an - * {@code IndexColorModel}. - * @see BufferedImage#TYPE_BYTE_INDEXED - * @see IndexColorModel - */ - public static BufferedImage getIndexedImage(BufferedImage pImage) { - return getIndexedImage(pImage, 256, DITHER_DEFAULT); - } - - /** - * Tests if the hint {@code COLOR_SELECTION_QUALITY} is not - * set. - * - * @param pHints hints - * @return true if the hint {@code COLOR_SELECTION_QUALITY} - * is not set. - */ - private static boolean isFast(int pHints) { - return (pHints & COLOR_SELECTION_MASK) != COLOR_SELECTION_QUALITY; - } - - /** - * Tests if the hint {@code TRANSPARENCY_BITMASK} or - * {@code TRANSPARENCY_TRANSLUCENT} is set. - * - * @param pHints hints - * @return true if the hint {@code TRANSPARENCY_BITMASK} or - * {@code TRANSPARENCY_TRANSLUCENT} is set. - */ - static boolean isTransparent(int pHints) { - return (pHints & TRANSPARENCY_BITMASK) != 0 || (pHints & TRANSPARENCY_TRANSLUCENT) != 0; - } - - /** - * Converts the input image (must be {@code TYPE_INT_RGB} or - * {@code TYPE_INT_ARGB}) to an indexed image. If the palette image - * uses an {@code IndexColorModel}, this will be used. Otherwise, generating an - * adaptive palette (8 bit) from the given palette image. - * Dithering, transparency and color selection is controlled with the - * {@code pHints}parameter. - *

- * The image returned is a new image, the input image is not modified. - * - * @param pImage the BufferedImage to index - * @param pPalette the Image to read color information from - * @param pMatte the background color, used where the original image was - * transparent - * @param pHints hints that control output quality and speed. - * @return the indexed BufferedImage. The image will be of type - * {@code BufferedImage.TYPE_BYTE_INDEXED} or - * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an - * {@code IndexColorModel}. - * @throws ImageConversionException if an exception occurred during color - * model extraction. - * @see #DITHER_DIFFUSION - * @see #DITHER_NONE - * @see #COLOR_SELECTION_FAST - * @see #COLOR_SELECTION_QUALITY - * @see #TRANSPARENCY_OPAQUE - * @see #TRANSPARENCY_BITMASK - * @see BufferedImage#TYPE_BYTE_INDEXED - * @see BufferedImage#TYPE_BYTE_BINARY - * @see IndexColorModel - */ - public static BufferedImage getIndexedImage(BufferedImage pImage, Image pPalette, Color pMatte, int pHints) - throws ImageConversionException { - return getIndexedImage(pImage, getIndexColorModel(pPalette, 256, pHints), pMatte, pHints); - } - - /** - * Converts the input image (must be {@code TYPE_INT_RGB} or - * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive - * palette with the given number of colors. - * Dithering, transparency and color selection is controlled with the - * {@code pHints}parameter. - *

- * The image returned is a new image, the input image is not modified. - * - * @param pImage the BufferedImage to index - * @param pNumberOfColors the number of colors for the image - * @param pMatte the background color, used where the original image was - * transparent - * @param pHints hints that control output quality and speed. - * @return the indexed BufferedImage. The image will be of type - * {@code BufferedImage.TYPE_BYTE_INDEXED} or - * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an - * {@code IndexColorModel}. - * @see #DITHER_DIFFUSION - * @see #DITHER_NONE - * @see #COLOR_SELECTION_FAST - * @see #COLOR_SELECTION_QUALITY - * @see #TRANSPARENCY_OPAQUE - * @see #TRANSPARENCY_BITMASK - * @see BufferedImage#TYPE_BYTE_INDEXED - * @see BufferedImage#TYPE_BYTE_BINARY - * @see IndexColorModel - */ - public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, Color pMatte, int pHints) { - // NOTE: We need to apply matte before creating color model, otherwise we - // won't have colors for potential faded transitions - IndexColorModel icm; - - if (pMatte != null) { - icm = getIndexColorModel(createSolid(pImage, pMatte), pNumberOfColors, pHints); - } - else { - icm = getIndexColorModel(pImage, pNumberOfColors, pHints); - } - - // If we found less colors, then no need to dither - if ((pHints & DITHER_MASK) != DITHER_NONE && (icm.getMapSize() < pNumberOfColors)) { - pHints = (pHints & ~DITHER_MASK) | DITHER_NONE; - } - return getIndexedImage(pImage, icm, pMatte, pHints); - } - - /** - * Converts the input image (must be {@code TYPE_INT_RGB} or - * {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied - * {@code IndexColorModel}'s palette. - * Dithering, transparency and color selection is controlled with the - * {@code pHints} parameter. - *

- * The image returned is a new image, the input image is not modified. - * - * @param pImage the BufferedImage to index - * @param pColors an {@code IndexColorModel} containing the color information - * @param pMatte the background color, used where the original image was - * transparent. Also note that any transparent antialias will be - * rendered against this color. - * @param pHints RenderingHints that control output quality and speed. - * @return the indexed BufferedImage. The image will be of type - * {@code BufferedImage.TYPE_BYTE_INDEXED} or - * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an - * {@code IndexColorModel}. - * @see #DITHER_DIFFUSION - * @see #DITHER_NONE - * @see #COLOR_SELECTION_FAST - * @see #COLOR_SELECTION_QUALITY - * @see #TRANSPARENCY_OPAQUE - * @see #TRANSPARENCY_BITMASK - * @see BufferedImage#TYPE_BYTE_INDEXED - * @see BufferedImage#TYPE_BYTE_BINARY - * @see IndexColorModel - */ - public static BufferedImage getIndexedImage(BufferedImage pImage, IndexColorModel pColors, Color pMatte, int pHints) { - // TODO: Consider: - /* - if (pImage.getType() == BufferedImage.TYPE_BYTE_INDEXED - || pImage.getType() == BufferedImage.TYPE_BYTE_BINARY) { - pImage = ImageUtil.toBufferedImage(pImage, BufferedImage.TYPE_INT_ARGB); - } - */ - - // Get dimensions - final int width = pImage.getWidth(); - final int height = pImage.getHeight(); - - // 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 - solid = createSolid(pImage, pMatte); - } - - BufferedImage indexed; - - // Support TYPE_BYTE_BINARY, but only for 2 bit images, as the default - // dither does not work with TYPE_BYTE_BINARY it seems... - if (pColors.getMapSize() > 2) { - indexed = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, pColors); - } - else { - indexed = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY, pColors); - } - - // Apply dither if requested - switch (pHints & DITHER_MASK) { - case DITHER_DIFFUSION: - case DITHER_DIFFUSION_ALTSCANS: - // Create a DiffusionDither to apply dither to indexed - DiffusionDither dither = new DiffusionDither(pColors); - - if ((pHints & DITHER_MASK) == DITHER_DIFFUSION_ALTSCANS) { - dither.setAlternateScans(true); - } - - dither.filter(solid, indexed); - - break; - case DITHER_NONE: - // Just copy pixels, without dither - // NOTE: This seems to be slower than the method below, using - // Graphics2D.drawImage, and VALUE_DITHER_DISABLE, - // however you possibly end up getting a dithered image anyway, - // therefore, do it slower and produce correct result. :-) - CopyDither copy = new CopyDither(pColors); - copy.filter(solid, indexed); - - break; - case DITHER_DEFAULT: - // This is the default - default: - // Render image data onto indexed image, using default - // (probably we get dither, but it depends on the GFX engine). - Graphics2D g2d = indexed.createGraphics(); - try { - RenderingHints hints = new RenderingHints(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); - - g2d.setRenderingHints(hints); - g2d.drawImage(solid, 0, 0, null); - } - finally { - g2d.dispose(); - } - - break; - } - - // Transparency support, this approach seems lame, but it's the only - // solution I've found until now (that actually works). - if (transparency) { - // Re-apply the alpha-channel of the original image - applyAlpha(indexed, pImage); - } - - // Return the indexed BufferedImage - return indexed; - } - - /** - * Converts the input image (must be {@code TYPE_INT_RGB} or - * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive - * palette with the given number of colors. - * Dithering, transparency and color selection is controlled with the - * {@code pHints}parameter. - *

- * The image returned is a new image, the input image is not modified. - * - * @param pImage the BufferedImage to index - * @param pNumberOfColors the number of colors for the image - * @param pHints hints that control output quality and speed. - * @return the indexed BufferedImage. The image will be of type - * {@code BufferedImage.TYPE_BYTE_INDEXED} or - * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an - * {@code IndexColorModel}. - * @see #DITHER_DIFFUSION - * @see #DITHER_NONE - * @see #COLOR_SELECTION_FAST - * @see #COLOR_SELECTION_QUALITY - * @see #TRANSPARENCY_OPAQUE - * @see #TRANSPARENCY_BITMASK - * @see BufferedImage#TYPE_BYTE_INDEXED - * @see BufferedImage#TYPE_BYTE_BINARY - * @see IndexColorModel - */ - public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, int pHints) { - return getIndexedImage(pImage, pNumberOfColors, null, pHints); - } - - /** - * Converts the input image (must be {@code TYPE_INT_RGB} or - * {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied - * {@code IndexColorModel}'s palette. - * Dithering, transparency and color selection is controlled with the - * {@code pHints}parameter. - *

- * The image returned is a new image, the input image is not modified. - * - * @param pImage the BufferedImage to index - * @param pColors an {@code IndexColorModel} containing the color information - * @param pHints RenderingHints that control output quality and speed. - * @return the indexed BufferedImage. The image will be of type - * {@code BufferedImage.TYPE_BYTE_INDEXED} or - * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an - * {@code IndexColorModel}. - * @see #DITHER_DIFFUSION - * @see #DITHER_NONE - * @see #COLOR_SELECTION_FAST - * @see #COLOR_SELECTION_QUALITY - * @see #TRANSPARENCY_OPAQUE - * @see #TRANSPARENCY_BITMASK - * @see BufferedImage#TYPE_BYTE_INDEXED - * @see BufferedImage#TYPE_BYTE_BINARY - * @see IndexColorModel - */ - public static BufferedImage getIndexedImage(BufferedImage pImage, IndexColorModel pColors, int pHints) { - return getIndexedImage(pImage, pColors, null, pHints); - } - - /** - * Converts the input image (must be {@code TYPE_INT_RGB} or - * {@code TYPE_INT_ARGB}) to an indexed image. If the palette image - * uses an {@code IndexColorModel}, this will be used. Otherwise, generating an - * adaptive palette (8 bit) from the given palette image. - * Dithering, transparency and color selection is controlled with the - * {@code pHints}parameter. - *

- * The image returned is a new image, the input image is not modified. - * - * @param pImage the BufferedImage to index - * @param pPalette the Image to read color information from - * @param pHints hints that control output quality and speed. - * @return the indexed BufferedImage. The image will be of type - * {@code BufferedImage.TYPE_BYTE_INDEXED} or - * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an - * {@code IndexColorModel}. - * @see #DITHER_DIFFUSION - * @see #DITHER_NONE - * @see #COLOR_SELECTION_FAST - * @see #COLOR_SELECTION_QUALITY - * @see #TRANSPARENCY_OPAQUE - * @see #TRANSPARENCY_BITMASK - * @see BufferedImage#TYPE_BYTE_INDEXED - * @see BufferedImage#TYPE_BYTE_BINARY - * @see IndexColorModel - */ - public static BufferedImage getIndexedImage(BufferedImage pImage, Image pPalette, int pHints) { - return getIndexedImage(pImage, pPalette, null, pHints); - } - - /** - * Creates a copy of the given image, with a solid background - * - * @param pOriginal the original image - * @param pBackground the background color - * @return a new {@code BufferedImage} - */ - private static BufferedImage createSolid(BufferedImage pOriginal, Color pBackground) { - // Create a temporary image of same dimension and type - BufferedImage solid = new BufferedImage(pOriginal.getColorModel(), pOriginal.copyData(null), pOriginal.isAlphaPremultiplied(), null); - Graphics2D g = solid.createGraphics(); - - try { - // Clear in background color - g.setColor(pBackground); - g.setComposite(AlphaComposite.DstOver);// Paint "underneath" - g.fillRect(0, 0, pOriginal.getWidth(), pOriginal.getHeight()); - } - finally { - g.dispose(); - } - - return solid; - } - - /** - * Applies the alpha-component of the alpha image to the given image. - * The given image is modified in place. - * - * @param pImage the image to apply alpha to - * @param pAlpha the image containing the alpha - */ - private static void applyAlpha(BufferedImage pImage, BufferedImage pAlpha) { - // 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 - } - } - } - } - - /* - * This class is also a command-line utility. - */ - public static void main(String pArgs[]) { - // Defaults - int argIdx = 0; - int speedTest = -1; - boolean overWrite = false; - boolean monochrome = false; - boolean gray = false; - int numColors = 256; - String dither = null; - String quality = null; - String format = null; - Color background = null; - boolean transparency = false; - String paletteFileName = null; - boolean errArgs = false; - - // Parse args - while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { - if ((pArgs[argIdx].charAt(1) == 's') || pArgs[argIdx].equals("--speedtest")) { - argIdx++; - - // Get number of iterations - if ((pArgs.length > argIdx) && (pArgs[argIdx].charAt(0) != '-')) { - try { - speedTest = Integer.parseInt(pArgs[argIdx++]); - } - catch (NumberFormatException nfe) { - errArgs = true; - break; - } - } - else { - - // Default to 10 iterations - speedTest = 10; - } - } - 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")) { - argIdx++; - - try { - numColors = Integer.parseInt(pArgs[argIdx++]); - } - catch (NumberFormatException nfe) { - errArgs = true; - break; - } - } - 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")) { - argIdx++; - numColors = 2; - monochrome = true; - } - 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")) { - argIdx++; - paletteFileName = pArgs[argIdx++]; - } - 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")) { - argIdx++; - try { - background = StringUtil.toColor(pArgs[argIdx++]); - } - catch (Exception e) { - errArgs = true; - break; - } - } - 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")) { - argIdx++; - format = StringUtil.toLowerCase(pArgs[argIdx++]); - } - else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--help")) { - argIdx++; - - // Setting errArgs to true, to print usage - errArgs = true; - } - else { - System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); - } - } - if (errArgs || (pArgs.length < (argIdx + 1))) { - System.err.println("Usage: IndexImage [--help|-h] [--speedtest|-s ] [--bgcolor|-b ] [--colors|-c | --grayscale|g | --monochrome|-m | --palette|-p ] [--dither|-d (default|diffusion|none)] [--quality|-q (default|high|low)] [--transparency|-t] [--outputformat|-f (gif|jpeg|png|wbmp|...)] [--overwrite|-w] []"); - System.err.print("Input format names: "); - String[] readers = ImageIO.getReaderFormatNames(); - - for (int i = 0; i < readers.length; i++) { - System.err.print(readers[i] + ((i + 1 < readers.length) - ? ", " - : "\n")); - } - - System.err.print("Output format names: "); - String[] writers = ImageIO.getWriterFormatNames(); - - for (int i = 0; i < writers.length; i++) { - System.err.print(writers[i] + ((i + 1 < writers.length) - ? ", " - : "\n")); - } - System.exit(5); - } - - // Read in image - File in = new File(pArgs[argIdx++]); - - if (!in.exists()) { - System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!"); - System.exit(5); - } - - // Read palette if needed - File paletteFile = null; - - if (paletteFileName != null) { - paletteFile = new File(paletteFileName); - if (!paletteFile.exists()) { - System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!"); - System.exit(5); - } - } - - // Make sure we can write - File out; - - if (argIdx < pArgs.length) { - out = new File(pArgs[argIdx/*++*/]); - - // Get format from file extension - if (format == null) { - format = FileUtil.getExtension(out); - } - } - else { - // Create new file in current dir, same name + format extension - String baseName = FileUtil.getBasename(in); - - // Use png as default format - if (format == null) { - format = "png"; - } - out = new File(baseName + '.' + format); - } - - if (!overWrite && out.exists()) { - System.err.println("The file \"" + out.getAbsolutePath() + "\" allready exists!"); - System.exit(5); - } - - // Do the image processing - BufferedImage image = null; - BufferedImage paletteImg = null; - - try { - image = ImageIO.read(in); - if (image == null) { - System.err.println("No reader for image: \"" + in.getAbsolutePath() + "\"!"); - System.exit(5); - } - if (paletteFile != null) { - paletteImg = ImageIO.read(paletteFile); - if (paletteImg == null) { - System.err.println("No reader for image: \"" + paletteFile.getAbsolutePath() + "\"!"); - System.exit(5); - } - } - } - catch (IOException ioe) { - ioe.printStackTrace(System.err); - System.exit(5); - } - - // Create hints - int hints = DITHER_DEFAULT; - - if ("DIFFUSION".equalsIgnoreCase(dither)) { - hints |= DITHER_DIFFUSION; - } - else if ("DIFFUSION_ALTSCANS".equalsIgnoreCase(dither)) { - hints |= DITHER_DIFFUSION_ALTSCANS; - } - else if ("NONE".equalsIgnoreCase(dither)) { - hints |= DITHER_NONE; - } - else { - - // Don't care, use default - } - if ("HIGH".equalsIgnoreCase(quality)) { - hints |= COLOR_SELECTION_QUALITY; - } - else if ("LOW".equalsIgnoreCase(quality)) { - hints |= COLOR_SELECTION_FAST; - } - else { - - // Don't care, use default - } - if (transparency) { - hints |= TRANSPARENCY_BITMASK; - } - - ////////////////////////////// - // Apply bg-color WORKAROUND! - // This needs to be done BEFORE palette creation to have desired effect.. - if ((background != null) && (paletteImg == null)) { - paletteImg = createSolid(image, background); - } - - /////////////////////////////// - // Index - long start = 0; - - if (speedTest > 0) { - // SPEED TESTING - System.out.println("Measuring speed!"); - start = System.currentTimeMillis(); - // END SPEED TESTING - } - - BufferedImage indexed; - IndexColorModel colors; - - if (monochrome) { - indexed = getIndexedImage(image, MonochromeColorModel.getInstance(), background, hints); - 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); - - // In casse of speedtest, this makes sense... - if (speedTest > 0) { - colors = getIndexColorModel(indexed, numColors, hints); - } - } - else if (paletteImg != null) { - // Get palette from image - indexed = getIndexedImage(ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_ARGB), - colors = getIndexColorModel(paletteImg, numColors, hints), background, hints); - } - else { - image = ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_ARGB); - indexed = getIndexedImage(image, colors = getIndexColorModel(image, numColors, hints), background, hints); - } - - if (speedTest > 0) { - // SPEED TESTING - System.out.println("Color selection + dither: " + (System.currentTimeMillis() - start) + " ms"); - // END SPEED TESTING - } - - // Write output (in given format) - try { - if (!ImageIO.write(indexed, format, out)) { - System.err.println("No writer for format: \"" + format + "\"!"); - } - } - catch (IOException ioe) { - ioe.printStackTrace(System.err); - } - - if (speedTest > 0) { - // SPEED TESTING - System.out.println("Measuring speed!"); - - // Warmup! - for (int i = 0; i < 10; i++) { - getIndexedImage(image, colors, background, hints); - } - - // Measure - long time = 0; - - for (int i = 0; i < speedTest; i++) { - start = System.currentTimeMillis(); - getIndexedImage(image, colors, background, hints); - 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 - } - } +/* + * 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 of the copyright holder 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 HOLDER 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. + */ +/* +****************************************************************************** +* +* ============================================================================ +* The Apache Software License, Version 1.1 +* ============================================================================ +* +* Copyright (C) 2000 The Apache Software Foundation. All rights reserved. +* +* Redistribution and use in source and binary forms, with or without modifica- +* tion, are permitted provided that the following conditions are met: +* +* 1. Redistributions of source code must retain the above copyright notice, +* this list of conditions and the following disclaimer. +* +* 2. Redistributions in binary form must reproduce the above copyright notice, +* this list of conditions and the following disclaimer in the documentation +* and/or other materials provided with the distribution. +* +* 3. The end-user documentation included with the redistribution, if any, must +* include the following acknowledgment: "This product includes software +* developed by the Apache Software Foundation (http://www.apache.org/)." +* Alternately, this acknowledgment may appear in the software itself, if +* and wherever such third-party acknowledgments normally appear. +* +* 4. The names "Batik" and "Apache Software Foundation" must not be used to +* endorse or promote products derived from this software without prior +* written permission. For written permission, please contact +* apache@apache.org. +* +* 5. Products derived from this software may not be called "Apache", nor may +* "Apache" appear in their name, without prior written permission of the +* Apache Software Foundation. +* +* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, +* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +* APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU- +* DING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +* OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +* This software consists of voluntary contributions made by many individuals +* on behalf of the Apache Software Foundation. For more information on the +* Apache Software Foundation, please see . +* +****************************************************************************** +* +*/ + +package com.twelvemonkeys.image; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.lang.StringUtil; + +import javax.imageio.ImageIO; +import java.awt.*; +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; + +/** + * This class implements an adaptive palette generator to reduce images + * to a variable number of colors. + * It can also render images into fixed color pallettes. + *

+ * Support for the default JVM (ordered/pattern) dither, Floyd-Steinberg like + * error-diffusion and no dither, controlled by the hints + * {@link #DITHER_DIFFUSION}, + * {@link #DITHER_NONE} and + * {@link #DITHER_DEFAULT}. + *

+ *

+ * Color selection speed/accuracy can be controlled using the hints + * {@link #COLOR_SELECTION_FAST}, + * {@link #COLOR_SELECTION_QUALITY} and + * {@link #COLOR_SELECTION_DEFAULT}. + *

+ *

+ * Transparency support can be controlled using the hints + * {@link #TRANSPARENCY_OPAQUE}, + * {@link #TRANSPARENCY_BITMASK} and + * {@link #TRANSPARENCY_TRANSLUCENT}. + *

+ *
+ *

+ *

+ * This product includes software developed by the Apache Software Foundation.
+ *
+ * This software  consists of voluntary contributions made  by many individuals
+ * on  behalf  of the Apache Software  Foundation. For more  information on the
+ * Apache Software Foundation, please see http://www.apache.org/
+ * 
+ *

+ * + * @author Thomas DeWeese + * @author Jun Inamori + * @author Harald Kuhr + * @version $Id: IndexImage.java#1 $ + * @see DiffusionDither + */ +class IndexImage { + + /** + * Dither mask + */ + protected final static int DITHER_MASK = 0xFF; + + /** + * Java default dither + */ + public final static int DITHER_DEFAULT = 0x00; + + /** + * No dither + */ + public final static int DITHER_NONE = 0x01; + + /** + * Error diffusion dither + */ + public final static int DITHER_DIFFUSION = 0x02; + + /** + * Error diffusion dither with alternating scans + */ + public final static int DITHER_DIFFUSION_ALTSCANS = 0x03; + + /** + * Color Selection mask + */ + protected final static int COLOR_SELECTION_MASK = 0xFF00; + + /** + * Default color selection + */ + public final static int COLOR_SELECTION_DEFAULT = 0x0000; + + /** + * Prioritize speed + */ + public final static int COLOR_SELECTION_FAST = 0x0100; + + /** + * Prioritize quality + */ + public final static int COLOR_SELECTION_QUALITY = 0x0200; + + /** + * Transparency mask + */ + protected final static int TRANSPARENCY_MASK = 0xFF0000; + + /** + * Default transparency (none) + */ + public final static int TRANSPARENCY_DEFAULT = 0x000000; + + /** + * Discard any alpha information + */ + public final static int TRANSPARENCY_OPAQUE = 0x010000; + + /** + * Convert alpha to bitmask + */ + public final static int TRANSPARENCY_BITMASK = 0x020000; + + /** + * Keep original alpha (not supported yet) + */ + protected final static int TRANSPARENCY_TRANSLUCENT = 0x030000; + + /** + * Used to track a color and the number of pixels of that colors + */ + private static class Counter { + + /** + * Field val + */ + public int val; + + /** + * Field count + */ + public int count = 1; + + /** + * Constructor Counter + * + * @param val the initial value + */ + public Counter(int val) { + this.val = val; + } + + /** + * Method add + * + * @param val the new value + * @return {@code true} if the value was added, otherwise {@code false} + */ + public boolean add(int val) { + // See if the value matches us... + if (this.val != val) { + return false; + } + + count++; + + return true; + } + } + + /** + * 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}; + int[] max = {255, 255, 255}; + boolean done = false; + List[] colors = null; + int count = 0; + static final int RED = 0; + static final int GRN = 1; + static final int BLU = 2; + + /** + * Define a new cube. + * + * @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) { + this.colors = colors; + this.count = count; + } + + /** + * If this returns true then the cube can not be subdivided any + * further + * + * @return true if cube can not be subdivided any further + */ + public boolean isDone() { + return done; + } + + /** + * Splits the cube into two parts. This cube is + * changed to be one half and the returned cube is the other half. + * This tries to pick the right channel to split on. + * + * @return the {@code Cube} containing the other half + */ + public Cube split() { + int dr = max[0] - min[0] + 1; + int dg = max[1] - min[1] + 1; + int db = max[2] - min[2] + 1; + int c0, c1, splitChannel; + + // Figure out which axis is the longest and split along + // that axis (this tries to keep cubes square-ish). + if (dr >= dg) { + c0 = GRN; + if (dr >= db) { + splitChannel = RED; + c1 = BLU; + } + else { + splitChannel = BLU; + c1 = RED; + } + } + else if (dg >= db) { + splitChannel = GRN; + c0 = RED; + c1 = BLU; + } + else { + splitChannel = BLU; + 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; + } + + /** + * Splits the image according to the parameters. It tries + * to find a location where half the pixels are on one side + * and half the pixels are on the other. + * + * @param splitChannel split channel + * @param c0 channel 0 + * @param c1 channel 1 + * @return the {@code Cube} containing the other half + */ + public Cube splitChannel(int splitChannel, int c0, int c1) { + if (min[splitChannel] == max[splitChannel]) { + return null; + } + int splitSh4 = (2 - splitChannel) * 4; + int c0Sh4 = (2 - c0) * 4; + int c1Sh4 = (2 - c1) * 4; + + // int splitSh8 = (2-splitChannel)*8; + // int c0Sh8 = (2-c0)*8; + // int c1Sh8 = (2-c1)*8; + // + int half = count / 2; + + // Each entry is the number of pixels that have that value + // in the split channel within the cube (so pixels + // that have that value in the split channel aren't counted + // if they are outside the cube in the other color channels. + int counts[] = new int[256]; + int tcount = 0; + + // System.out.println("Cube: [" + + // min[0] + "-" + max[0] + "] [" + + // min[1] + "-" + max[1] + "] [" + + // min[2] + "-" + max[2] + "]"); + int[] minIdx = {min[0] >> 4, min[1] >> 4, min[2] >> 4}; + int[] maxIdx = {max[0] >> 4, max[1] >> 4, max[2] >> 4}; + int minR = min[0], minG = min[1], minB = min[2]; + int maxR = max[0], maxG = max[1], maxB = max[2]; + int val; + int[] vals = {0, 0, 0}; + + for (int i = minIdx[splitChannel]; i <= maxIdx[splitChannel]; i++) { + int idx1 = i << splitSh4; + + for (int j = minIdx[c0]; j <= maxIdx[c0]; j++) { + int idx2 = idx1 | (j << c0Sh4); + + for (int k = minIdx[c1]; k <= maxIdx[c1]; k++) { + int idx = idx2 | (k << c1Sh4); + List v = colors[idx]; + + if (v == null) { + continue; + } + + for (Counter c : v) { + val = c.val; + vals[0] = (val & 0xFF0000) >> 16; + vals[1] = (val & 0xFF00) >> 8; + vals[2] = (val & 0xFF); + if (((vals[0] >= minR) && (vals[0] <= maxR)) && ((vals[1] >= minG) && (vals[1] <= maxG)) + && ((vals[2] >= minB) && (vals[2] <= maxB))) { + + // The val lies within this cube so count it. + counts[vals[splitChannel]] += c.count; + tcount += c.count; + } + } + } + } + + // We've found the half way point. Note that the + // rest of counts is not filled out. + if (tcount >= half) { + break; + } + } + tcount = 0; + int lastAdd = -1; + + // These indicate what the top value for the low cube and + // the low value of the high cube should be in the split channel + // (they may not be one off if there are 'dead' spots in the + // counts array.) + int splitLo = min[splitChannel], splitHi = max[splitChannel]; + + for (int i = min[splitChannel]; i <= max[splitChannel]; i++) { + 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; + } + continue; + } + if (tcount + c < half) { + lastAdd = i; + tcount += c; + 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) { + + // All pixels are at this value so make min/max + // reflect that. + this.max[splitChannel] = i; + return null;// no split to make. + } + else { + + // There are values about this one so + // split above. + splitLo = i; + splitHi = i + 1; + break; + } + } + splitLo = lastAdd; + splitHi = i; + } + 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; + splitHi = i; + break; + } + } + + // Include c in counts + tcount += c; + splitLo = i; + splitHi = i + 1; + } + break; + } + + // System.out.println("Split: " + splitChannel + "@" + // + splitLo + "-"+splitHi + + // " Count: " + tcount + " of " + count + + // " LA: " + lastAdd); + // Create the new cube and update everyone's bounds & counts. + Cube ret = new Cube(colors, tcount); + + this.count = this.count - tcount; + ret.min[splitChannel] = this.min[splitChannel]; + ret.max[splitChannel] = splitLo; + this.min[splitChannel] = splitHi; + ret.min[c0] = this.min[c0]; + ret.max[c0] = this.max[c0]; + ret.min[c1] = this.min[c1]; + ret.max[c1] = this.max[c1]; + + return ret; + } + + /** + * Returns the average color for this cube + * + * @return the average + */ + public int averageColor() { + 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]; + int[] minIdx = {minR >> 4, minG >> 4, minB >> 4}; + int[] maxIdx = {maxR >> 4, maxG >> 4, maxB >> 4}; + int val, ired, igrn, iblu; + float weight; + + for (int i = minIdx[0]; i <= maxIdx[0]; i++) { + int idx1 = i << 8; + + for (int j = minIdx[1]; j <= maxIdx[1]; j++) { + int idx2 = idx1 | (j << 4); + + for (int k = minIdx[2]; k <= maxIdx[2]; k++) { + int idx = idx2 | k; + List v = colors[idx]; + + if (v == null) { + continue; + } + + 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; + grn += ((float) igrn) * weight; + blu += ((float) iblu) * weight; + } + } + } + } + } + + // System.out.println("RGB: [" + red + ", " + + // grn + ", " + blu + "]"); + return (((int) (red + 0.5f)) << 16 | ((int) (grn + 0.5f)) << 8 | ((int) (blu + 0.5f))); + } + }// end Cube + + /** + * You cannot create this + */ + private IndexImage() { + } + + /** + * @param pImage the image to get {@code IndexColorModel} from + * @param pNumberOfColors the number of colors for the {@code IndexColorModel} + * @param pFast {@code true} if fast + * @return an {@code IndexColorModel} + * @see #getIndexColorModel(Image,int,int) + * + * @deprecated Use {@link #getIndexColorModel(Image,int,int)} instead! + * 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); + } + + /** + * Gets an {@code IndexColorModel} from the given image. If the image has an + * {@code IndexColorModel}, this will be returned. Otherwise, an {@code IndexColorModel} + * is created, using an adaptive palette. + * + * @param pImage the image to get {@code IndexColorModel} from + * @param pNumberOfColors the number of colors for the {@code IndexColorModel} + * @param pHints one of {@link #COLOR_SELECTION_FAST}, + * {@link #COLOR_SELECTION_QUALITY} or + * {@link #COLOR_SELECTION_DEFAULT}. + * @return The {@code IndexColorModel} from the given image, or a newly created + * {@code IndexColorModel} using an adaptive palette. + * @throws ImageConversionException if an exception occurred during color + * model extraction. + */ + public static IndexColorModel getIndexColorModel(Image pImage, int pNumberOfColors, int pHints) throws ImageConversionException { + IndexColorModel icm = null; + RenderedImage image = null; + + if (pImage instanceof RenderedImage) { + image = (RenderedImage) pImage; + ColorModel cm = image.getColorModel(); + + if (cm instanceof IndexColorModel) { + // Test if we have right number of colors + if (((IndexColorModel) cm).getMapSize() <= pNumberOfColors) { + //System.out.println("IndexColorModel from BufferedImage"); + icm = (IndexColorModel) cm;// Done + } + } + + // Else create from buffered image, hard way, see below + } + else { + // Create from image using BufferedImageFactory + BufferedImageFactory factory = new BufferedImageFactory(pImage); + ColorModel cm = factory.getColorModel(); + + if ((cm instanceof IndexColorModel) && ((IndexColorModel) cm).getMapSize() <= pNumberOfColors) { + //System.out.println("IndexColorModel from Image"); + icm = (IndexColorModel) cm;// Done + } + else { + // Else create from (buffered) image, hard way + image = factory.getBufferedImage(); + } + } + + // We now have at least a buffered image, create model from it + if (icm == null) { + icm = createIndexColorModel(ImageUtil.toBuffered(image), pNumberOfColors, pHints); + } + else if (!(icm instanceof InverseColorMapIndexColorModel)) { + // If possible, use faster code + icm = new InverseColorMapIndexColorModel(icm); + } + + return icm; + } + + /** + * Creates an {@code IndexColorModel} from the given image, using an adaptive + * palette. + * + * @param pImage the image to get {@code IndexColorModel} from + * @param pNumberOfColors the number of colors for the {@code IndexColorModel} + * @param pHints use fast mode if possible (might give slightly lower + * quality) + * @return a new {@code IndexColorModel} created from the given image + */ + private static IndexColorModel createIndexColorModel(BufferedImage pImage, int pNumberOfColors, int pHints) { + // TODO: Use ImageUtil.hasTransparentPixels(pImage, true) || + // -- haraldK, 20021024, experimental, try to use one transparent pixel + boolean useTransparency = isTransparent(pHints); + + if (useTransparency) { + pNumberOfColors--; + } + + //System.out.println("Transp: " + useTransparency + " colors: " + pNumberOfColors); + int width = pImage.getWidth(); + int height = pImage.getHeight(); + + // Using 4 bits from R, G & B. + @SuppressWarnings("unchecked") + List[] colors = new List[1 << 12];// [4096] + + // Speedup, doesn't decrease image quality much + int step = 1; + + if (isFast(pHints)) { + step += (width * height / 16384);// 128x128px + } + int sampleCount = 0; + int rgb; + + //for (int x = 0; x < width; x++) { + //for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + for (int y = x % step; y < height; y += step) { + // Count the number of color samples + sampleCount++; + + // Get ARGB pixel from image + rgb = (pImage.getRGB(x, y) & 0xFFFFFF); + + // Get index from high four bits of each component. + int index = (((rgb & 0xF00000) >>> 12) | ((rgb & 0x00F000) >>> 8) | ((rgb & 0x0000F0) >>> 4)); + + // Get the 'hash vector' for that key. + List v = colors[index]; + + if (v == null) { + // No colors in this bin yet so create vector and + // add color. + v = new ArrayList(); + v.add(new Counter(rgb)); + colors[index] = v; + } + else { + // Find our color in the bin or create a counter for it. + Iterator i = v.iterator(); + + while (true) { + if (i.hasNext()) { + // try adding our color to each counter... + if (((Counter) i.next()).add(rgb)) { + break; + } + } + else { + v.add(new Counter(rgb)); + break; + } + } + } + } + } + + // All colours found, reduce to pNumberOfColors + int numberOfCubes = 1; + int fCube = 0; + Cube[] cubes = new Cube[pNumberOfColors]; + + cubes[0] = new Cube(colors, sampleCount); + + //cubes[0] = new Cube(colors, width * height); + while (numberOfCubes < pNumberOfColors) { + while (cubes[fCube].isDone()) { + fCube++; + + if (fCube == numberOfCubes) { + break; + } + } + + if (fCube == numberOfCubes) { + break; + } + + Cube cube = cubes[fCube]; + Cube newCube = cube.split(); + + if (newCube != null) { + if (newCube.count > cube.count) { + Cube tmp = cube; + + cube = newCube; + newCube = tmp; + } + + int j = fCube; + int count = cube.count; + + for (int i = fCube + 1; i < numberOfCubes; i++) { + if (cubes[i].count < count) { + break; + } + cubes[j++] = cubes[i]; + } + + cubes[j++] = cube; + count = newCube.count; + + while (j < numberOfCubes) { + if (cubes[j].count < count) { + break; + } + j++; + } + + System.arraycopy(cubes, j, cubes, j + 1, numberOfCubes - j); + + cubes[j/*++*/] = newCube; + numberOfCubes++; + } + } + + // Create RGB arrays with correct number of colors + // If we have transparency, the last color will be the transparent one + byte[] r = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes]; + byte[] g = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes]; + byte[] b = new byte[useTransparency ? numberOfCubes + 1 : numberOfCubes]; + + for (int i = 0; i < numberOfCubes; i++) { + int val = cubes[i].averageColor(); + + r[i] = (byte) ((val >> 16) & 0xFF); + g[i] = (byte) ((val >> 8) & 0xFF); + b[i] = (byte) ((val) & 0xFF); + + //System.out.println("Color [" + i + "]: #" + + // (((val>>16)<16)?"0":"") + + // Integer.toHexString(val)); + } + + // For some reason using less than 8 bits causes a bug in the dither + // - transparency added to all totally black colors? + int numOfBits = 8; + + // -- haraldK, 20021024, as suggested by Thomas E. Deweese + // plus adding a transparent pixel + IndexColorModel icm; + if (useTransparency) { + icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b, r.length - 1); + } + else { + icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b); + } + return icm; + } + + /** + * Converts the input image (must be {@code TYPE_INT_RGB} or + * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive + * palette (8 bit) from the color data in the image, and uses default + * dither. + *

+ * The image returned is a new image, the input image is not modified. + *

+ * + * @param pImage the BufferedImage to index and get color information from. + * @return the indexed BufferedImage. The image will be of type + * {@code BufferedImage.TYPE_BYTE_INDEXED}, and use an + * {@code IndexColorModel}. + * @see BufferedImage#TYPE_BYTE_INDEXED + * @see IndexColorModel + */ + public static BufferedImage getIndexedImage(BufferedImage pImage) { + return getIndexedImage(pImage, 256, DITHER_DEFAULT); + } + + /** + * Tests if the hint {@code COLOR_SELECTION_QUALITY} is not + * set. + * + * @param pHints hints + * @return true if the hint {@code COLOR_SELECTION_QUALITY} + * is not set. + */ + private static boolean isFast(int pHints) { + return (pHints & COLOR_SELECTION_MASK) != COLOR_SELECTION_QUALITY; + } + + /** + * Tests if the hint {@code TRANSPARENCY_BITMASK} or + * {@code TRANSPARENCY_TRANSLUCENT} is set. + * + * @param pHints hints + * @return true if the hint {@code TRANSPARENCY_BITMASK} or + * {@code TRANSPARENCY_TRANSLUCENT} is set. + */ + static boolean isTransparent(int pHints) { + return (pHints & TRANSPARENCY_BITMASK) != 0 || (pHints & TRANSPARENCY_TRANSLUCENT) != 0; + } + + /** + * Converts the input image (must be {@code TYPE_INT_RGB} or + * {@code TYPE_INT_ARGB}) to an indexed image. If the palette image + * uses an {@code IndexColorModel}, this will be used. Otherwise, generating an + * adaptive palette (8 bit) from the given palette image. + * Dithering, transparency and color selection is controlled with the + * {@code pHints}parameter. + *

+ * The image returned is a new image, the input image is not modified. + *

+ * + * @param pImage the BufferedImage to index + * @param pPalette the Image to read color information from + * @param pMatte the background color, used where the original image was + * transparent + * @param pHints hints that control output quality and speed. + * @return the indexed BufferedImage. The image will be of type + * {@code BufferedImage.TYPE_BYTE_INDEXED} or + * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an + * {@code IndexColorModel}. + * @throws ImageConversionException if an exception occurred during color + * model extraction. + * @see #DITHER_DIFFUSION + * @see #DITHER_NONE + * @see #COLOR_SELECTION_FAST + * @see #COLOR_SELECTION_QUALITY + * @see #TRANSPARENCY_OPAQUE + * @see #TRANSPARENCY_BITMASK + * @see BufferedImage#TYPE_BYTE_INDEXED + * @see BufferedImage#TYPE_BYTE_BINARY + * @see IndexColorModel + */ + public static BufferedImage getIndexedImage(BufferedImage pImage, Image pPalette, Color pMatte, int pHints) + throws ImageConversionException { + return getIndexedImage(pImage, getIndexColorModel(pPalette, 256, pHints), pMatte, pHints); + } + + /** + * Converts the input image (must be {@code TYPE_INT_RGB} or + * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive + * palette with the given number of colors. + * Dithering, transparency and color selection is controlled with the + * {@code pHints} parameter. + *

+ * The image returned is a new image, the input image is not modified. + *

+ * + * @param pImage the BufferedImage to index + * @param pNumberOfColors the number of colors for the image + * @param pMatte the background color, used where the original image was + * transparent + * @param pHints hints that control output quality and speed. + * @return the indexed BufferedImage. The image will be of type + * {@code BufferedImage.TYPE_BYTE_INDEXED} or + * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an + * {@code IndexColorModel}. + * @see #DITHER_DIFFUSION + * @see #DITHER_NONE + * @see #COLOR_SELECTION_FAST + * @see #COLOR_SELECTION_QUALITY + * @see #TRANSPARENCY_OPAQUE + * @see #TRANSPARENCY_BITMASK + * @see BufferedImage#TYPE_BYTE_INDEXED + * @see BufferedImage#TYPE_BYTE_BINARY + * @see IndexColorModel + */ + public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, Color pMatte, int pHints) { + // NOTE: We need to apply matte before creating color model, otherwise we + // won't have colors for potential faded transitions + IndexColorModel icm; + + if (pMatte != null) { + icm = getIndexColorModel(createSolid(pImage, pMatte), pNumberOfColors, pHints); + } + else { + icm = getIndexColorModel(pImage, pNumberOfColors, pHints); + } + + // If we found less colors, then no need to dither + if ((pHints & DITHER_MASK) != DITHER_NONE && (icm.getMapSize() < pNumberOfColors)) { + pHints = (pHints & ~DITHER_MASK) | DITHER_NONE; + } + return getIndexedImage(pImage, icm, pMatte, pHints); + } + + /** + * Converts the input image (must be {@code TYPE_INT_RGB} or + * {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied + * {@code IndexColorModel}'s palette. + * Dithering, transparency and color selection is controlled with the + * {@code pHints} parameter. + *

+ * The image returned is a new image, the input image is not modified. + *

+ * + * @param pImage the BufferedImage to index + * @param pColors an {@code IndexColorModel} containing the color information + * @param pMatte the background color, used where the original image was + * transparent. Also note that any transparent antialias will be + * rendered against this color. + * @param pHints RenderingHints that control output quality and speed. + * @return the indexed BufferedImage. The image will be of type + * {@code BufferedImage.TYPE_BYTE_INDEXED} or + * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an + * {@code IndexColorModel}. + * @see #DITHER_DIFFUSION + * @see #DITHER_NONE + * @see #COLOR_SELECTION_FAST + * @see #COLOR_SELECTION_QUALITY + * @see #TRANSPARENCY_OPAQUE + * @see #TRANSPARENCY_BITMASK + * @see BufferedImage#TYPE_BYTE_INDEXED + * @see BufferedImage#TYPE_BYTE_BINARY + * @see IndexColorModel + */ + public static BufferedImage getIndexedImage(BufferedImage pImage, IndexColorModel pColors, Color pMatte, int pHints) { + // TODO: Consider: + /* + if (pImage.getType() == BufferedImage.TYPE_BYTE_INDEXED + || pImage.getType() == BufferedImage.TYPE_BYTE_BINARY) { + pImage = ImageUtil.toBufferedImage(pImage, BufferedImage.TYPE_INT_ARGB); + } + */ + + // Get dimensions + final int width = pImage.getWidth(); + final int height = pImage.getHeight(); + + // 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 + solid = createSolid(pImage, pMatte); + } + + BufferedImage indexed; + + // Support TYPE_BYTE_BINARY, but only for 2 bit images, as the default + // dither does not work with TYPE_BYTE_BINARY it seems... + if (pColors.getMapSize() > 2) { + indexed = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_INDEXED, pColors); + } + else { + indexed = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY, pColors); + } + + // Apply dither if requested + switch (pHints & DITHER_MASK) { + case DITHER_DIFFUSION: + case DITHER_DIFFUSION_ALTSCANS: + // Create a DiffusionDither to apply dither to indexed + DiffusionDither dither = new DiffusionDither(pColors); + + if ((pHints & DITHER_MASK) == DITHER_DIFFUSION_ALTSCANS) { + dither.setAlternateScans(true); + } + + dither.filter(solid, indexed); + + break; + case DITHER_NONE: + // Just copy pixels, without dither + // NOTE: This seems to be slower than the method below, using + // Graphics2D.drawImage, and VALUE_DITHER_DISABLE, + // however you possibly end up getting a dithered image anyway, + // therefore, do it slower and produce correct result. :-) + CopyDither copy = new CopyDither(pColors); + copy.filter(solid, indexed); + + break; + case DITHER_DEFAULT: + // This is the default + default: + // Render image data onto indexed image, using default + // (probably we get dither, but it depends on the GFX engine). + Graphics2D g2d = indexed.createGraphics(); + try { + RenderingHints hints = new RenderingHints(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); + + g2d.setRenderingHints(hints); + g2d.drawImage(solid, 0, 0, null); + } + finally { + g2d.dispose(); + } + + break; + } + + // Transparency support, this approach seems lame, but it's the only + // solution I've found until now (that actually works). + if (transparency) { + // Re-apply the alpha-channel of the original image + applyAlpha(indexed, pImage); + } + + // Return the indexed BufferedImage + return indexed; + } + + /** + * Converts the input image (must be {@code TYPE_INT_RGB} or + * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive + * palette with the given number of colors. + * Dithering, transparency and color selection is controlled with the + * {@code pHints}parameter. + *

+ * The image returned is a new image, the input image is not modified. + *

+ * + * @param pImage the BufferedImage to index + * @param pNumberOfColors the number of colors for the image + * @param pHints hints that control output quality and speed. + * @return the indexed BufferedImage. The image will be of type + * {@code BufferedImage.TYPE_BYTE_INDEXED} or + * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an + * {@code IndexColorModel}. + * @see #DITHER_DIFFUSION + * @see #DITHER_NONE + * @see #COLOR_SELECTION_FAST + * @see #COLOR_SELECTION_QUALITY + * @see #TRANSPARENCY_OPAQUE + * @see #TRANSPARENCY_BITMASK + * @see BufferedImage#TYPE_BYTE_INDEXED + * @see BufferedImage#TYPE_BYTE_BINARY + * @see IndexColorModel + */ + public static BufferedImage getIndexedImage(BufferedImage pImage, int pNumberOfColors, int pHints) { + return getIndexedImage(pImage, pNumberOfColors, null, pHints); + } + + /** + * Converts the input image (must be {@code TYPE_INT_RGB} or + * {@code TYPE_INT_ARGB}) to an indexed image. Using the supplied + * {@code IndexColorModel}'s palette. + * Dithering, transparency and color selection is controlled with the + * {@code pHints}parameter. + *

+ * The image returned is a new image, the input image is not modified. + *

+ * + * @param pImage the BufferedImage to index + * @param pColors an {@code IndexColorModel} containing the color information + * @param pHints RenderingHints that control output quality and speed. + * @return the indexed BufferedImage. The image will be of type + * {@code BufferedImage.TYPE_BYTE_INDEXED} or + * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an + * {@code IndexColorModel}. + * @see #DITHER_DIFFUSION + * @see #DITHER_NONE + * @see #COLOR_SELECTION_FAST + * @see #COLOR_SELECTION_QUALITY + * @see #TRANSPARENCY_OPAQUE + * @see #TRANSPARENCY_BITMASK + * @see BufferedImage#TYPE_BYTE_INDEXED + * @see BufferedImage#TYPE_BYTE_BINARY + * @see IndexColorModel + */ + public static BufferedImage getIndexedImage(BufferedImage pImage, IndexColorModel pColors, int pHints) { + return getIndexedImage(pImage, pColors, null, pHints); + } + + /** + * Converts the input image (must be {@code TYPE_INT_RGB} or + * {@code TYPE_INT_ARGB}) to an indexed image. If the palette image + * uses an {@code IndexColorModel}, this will be used. Otherwise, generating an + * adaptive palette (8 bit) from the given palette image. + * Dithering, transparency and color selection is controlled with the + * {@code pHints}parameter. + *

+ * The image returned is a new image, the input image is not modified. + *

+ * + * @param pImage the BufferedImage to index + * @param pPalette the Image to read color information from + * @param pHints hints that control output quality and speed. + * @return the indexed BufferedImage. The image will be of type + * {@code BufferedImage.TYPE_BYTE_INDEXED} or + * {@code BufferedImage.TYPE_BYTE_BINARY}, and use an + * {@code IndexColorModel}. + * @see #DITHER_DIFFUSION + * @see #DITHER_NONE + * @see #COLOR_SELECTION_FAST + * @see #COLOR_SELECTION_QUALITY + * @see #TRANSPARENCY_OPAQUE + * @see #TRANSPARENCY_BITMASK + * @see BufferedImage#TYPE_BYTE_INDEXED + * @see BufferedImage#TYPE_BYTE_BINARY + * @see IndexColorModel + */ + public static BufferedImage getIndexedImage(BufferedImage pImage, Image pPalette, int pHints) { + return getIndexedImage(pImage, pPalette, null, pHints); + } + + /** + * Creates a copy of the given image, with a solid background + * + * @param pOriginal the original image + * @param pBackground the background color + * @return a new {@code BufferedImage} + */ + private static BufferedImage createSolid(BufferedImage pOriginal, Color pBackground) { + // Create a temporary image of same dimension and type + BufferedImage solid = new BufferedImage(pOriginal.getColorModel(), pOriginal.copyData(null), pOriginal.isAlphaPremultiplied(), null); + Graphics2D g = solid.createGraphics(); + + try { + // Clear in background color + g.setColor(pBackground); + g.setComposite(AlphaComposite.DstOver);// Paint "underneath" + g.fillRect(0, 0, pOriginal.getWidth(), pOriginal.getHeight()); + } + finally { + g.dispose(); + } + + return solid; + } + + /** + * Applies the alpha-component of the alpha image to the given image. + * The given image is modified in place. + * + * @param pImage the image to apply alpha to + * @param pAlpha the image containing the alpha + */ + private static void applyAlpha(BufferedImage pImage, BufferedImage pAlpha) { + // 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 + } + } + } + } + + /* + * This class is also a command-line utility. + */ + public static void main(String pArgs[]) { + // Defaults + int argIdx = 0; + int speedTest = -1; + boolean overWrite = false; + boolean monochrome = false; + boolean gray = false; + int numColors = 256; + String dither = null; + String quality = null; + String format = null; + Color background = null; + boolean transparency = false; + String paletteFileName = null; + boolean errArgs = false; + + // Parse args + while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { + if ((pArgs[argIdx].charAt(1) == 's') || pArgs[argIdx].equals("--speedtest")) { + argIdx++; + + // Get number of iterations + if ((pArgs.length > argIdx) && (pArgs[argIdx].charAt(0) != '-')) { + try { + speedTest = Integer.parseInt(pArgs[argIdx++]); + } + catch (NumberFormatException nfe) { + errArgs = true; + break; + } + } + else { + + // Default to 10 iterations + speedTest = 10; + } + } + 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")) { + argIdx++; + + try { + numColors = Integer.parseInt(pArgs[argIdx++]); + } + catch (NumberFormatException nfe) { + errArgs = true; + break; + } + } + 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")) { + argIdx++; + numColors = 2; + monochrome = true; + } + 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")) { + argIdx++; + paletteFileName = pArgs[argIdx++]; + } + 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")) { + argIdx++; + try { + background = StringUtil.toColor(pArgs[argIdx++]); + } + catch (Exception e) { + errArgs = true; + break; + } + } + 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")) { + argIdx++; + format = StringUtil.toLowerCase(pArgs[argIdx++]); + } + else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--help")) { + argIdx++; + + // Setting errArgs to true, to print usage + errArgs = true; + } + else { + System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); + } + } + if (errArgs || (pArgs.length < (argIdx + 1))) { + System.err.println("Usage: IndexImage [--help|-h] [--speedtest|-s ] [--bgcolor|-b ] [--colors|-c | --grayscale|g | --monochrome|-m | --palette|-p ] [--dither|-d (default|diffusion|none)] [--quality|-q (default|high|low)] [--transparency|-t] [--outputformat|-f (gif|jpeg|png|wbmp|...)] [--overwrite|-w] []"); + System.err.print("Input format names: "); + String[] readers = ImageIO.getReaderFormatNames(); + + for (int i = 0; i < readers.length; i++) { + System.err.print(readers[i] + ((i + 1 < readers.length) + ? ", " + : "\n")); + } + + System.err.print("Output format names: "); + String[] writers = ImageIO.getWriterFormatNames(); + + for (int i = 0; i < writers.length; i++) { + System.err.print(writers[i] + ((i + 1 < writers.length) + ? ", " + : "\n")); + } + System.exit(5); + } + + // Read in image + File in = new File(pArgs[argIdx++]); + + if (!in.exists()) { + System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!"); + System.exit(5); + } + + // Read palette if needed + File paletteFile = null; + + if (paletteFileName != null) { + paletteFile = new File(paletteFileName); + if (!paletteFile.exists()) { + System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!"); + System.exit(5); + } + } + + // Make sure we can write + File out; + + if (argIdx < pArgs.length) { + out = new File(pArgs[argIdx/*++*/]); + + // Get format from file extension + if (format == null) { + format = FileUtil.getExtension(out); + } + } + else { + // Create new file in current dir, same name + format extension + String baseName = FileUtil.getBasename(in); + + // Use png as default format + if (format == null) { + format = "png"; + } + out = new File(baseName + '.' + format); + } + + if (!overWrite && out.exists()) { + System.err.println("The file \"" + out.getAbsolutePath() + "\" allready exists!"); + System.exit(5); + } + + // Do the image processing + BufferedImage image = null; + BufferedImage paletteImg = null; + + try { + image = ImageIO.read(in); + if (image == null) { + System.err.println("No reader for image: \"" + in.getAbsolutePath() + "\"!"); + System.exit(5); + } + if (paletteFile != null) { + paletteImg = ImageIO.read(paletteFile); + if (paletteImg == null) { + System.err.println("No reader for image: \"" + paletteFile.getAbsolutePath() + "\"!"); + System.exit(5); + } + } + } + catch (IOException ioe) { + ioe.printStackTrace(System.err); + System.exit(5); + } + + // Create hints + int hints = DITHER_DEFAULT; + + if ("DIFFUSION".equalsIgnoreCase(dither)) { + hints |= DITHER_DIFFUSION; + } + else if ("DIFFUSION_ALTSCANS".equalsIgnoreCase(dither)) { + hints |= DITHER_DIFFUSION_ALTSCANS; + } + else if ("NONE".equalsIgnoreCase(dither)) { + hints |= DITHER_NONE; + } + else { + + // Don't care, use default + } + if ("HIGH".equalsIgnoreCase(quality)) { + hints |= COLOR_SELECTION_QUALITY; + } + else if ("LOW".equalsIgnoreCase(quality)) { + hints |= COLOR_SELECTION_FAST; + } + else { + + // Don't care, use default + } + if (transparency) { + hints |= TRANSPARENCY_BITMASK; + } + + ////////////////////////////// + // Apply bg-color WORKAROUND! + // This needs to be done BEFORE palette creation to have desired effect.. + if ((background != null) && (paletteImg == null)) { + paletteImg = createSolid(image, background); + } + + /////////////////////////////// + // Index + long start = 0; + + if (speedTest > 0) { + // SPEED TESTING + System.out.println("Measuring speed!"); + start = System.currentTimeMillis(); + // END SPEED TESTING + } + + BufferedImage indexed; + IndexColorModel colors; + + if (monochrome) { + indexed = getIndexedImage(image, MonochromeColorModel.getInstance(), background, hints); + 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); + + // In casse of speedtest, this makes sense... + if (speedTest > 0) { + colors = getIndexColorModel(indexed, numColors, hints); + } + } + else if (paletteImg != null) { + // Get palette from image + indexed = getIndexedImage(ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_ARGB), + colors = getIndexColorModel(paletteImg, numColors, hints), background, hints); + } + else { + image = ImageUtil.toBuffered(image, BufferedImage.TYPE_INT_ARGB); + indexed = getIndexedImage(image, colors = getIndexColorModel(image, numColors, hints), background, hints); + } + + if (speedTest > 0) { + // SPEED TESTING + System.out.println("Color selection + dither: " + (System.currentTimeMillis() - start) + " ms"); + // END SPEED TESTING + } + + // Write output (in given format) + try { + if (!ImageIO.write(indexed, format, out)) { + System.err.println("No writer for format: \"" + format + "\"!"); + } + } + catch (IOException ioe) { + ioe.printStackTrace(System.err); + } + + if (speedTest > 0) { + // SPEED TESTING + System.out.println("Measuring speed!"); + + // Warmup! + for (int i = 0; i < 10; i++) { + getIndexedImage(image, colors, background, hints); + } + + // Measure + long time = 0; + + for (int i = 0; i < speedTest; i++) { + start = System.currentTimeMillis(); + getIndexedImage(image, colors, background, hints); + 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 + } + } } \ No newline at end of file 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 6b5b1d28..ec1fb1be 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 @@ -1,213 +1,214 @@ -/* - * 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 of the copyright holder 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 HOLDER 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; - -/** - * Inverse Colormap to provide efficient lookup of any given input color - * to the closest match to the given color map. - *

- * Based on "Efficient Inverse Color Map Computation" by Spencer W. Thomas - * in "Graphics Gems Volume II" - * - * @author Harald Kuhr - * @author Robin Luiten (Java port) - * @author Spencer W. Thomas (original c version). - * - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMap.java#1 $ - */ -class InverseColorMap { - /** - * Number of high bits of each color channel to use to lookup near match - */ - final static int QUANTBITS = 5; - - /** - * Truncated bits of each color channel - */ - final static int TRUNCBITS = 8 - QUANTBITS; - - /** - * BITMASK representing the bits for blue in the color lookup - */ - final static int QUANTMASK_BLUE = (1 << 5) - 1; - - /** - * BITMASK representing the bits for green in the color lookup - */ - final static int QUANTMASK_GREEN = (QUANTMASK_BLUE << QUANTBITS); - - /** - * BITMASK representing the bits for red in the color lookup - */ - final static int QUANTMASK_RED = (QUANTMASK_GREEN << QUANTBITS); - - /** - * Maximum value a quantised color channel can have - */ - final static int MAXQUANTVAL = 1 << 5; - - 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. - */ - InverseColorMap(byte[] pRGBColorMap) { - this(pRGBColorMap, -1); - } - - /** - * @param pRGBColorMap the rgb color map to create inverse color map for. - */ - // HaraldK 20040801: Added support for int[] - InverseColorMap(int[] pRGBColorMap) { - this(pRGBColorMap, -1); - } - - /** - * @param pRGBColorMap the rgb color map to create inverse color map for. - * @param pTransparent the index of the transparent pixel in the map - */ - InverseColorMap(byte[] pRGBColorMap, int pTransparent) { - rgbMapByte = pRGBColorMap; - numColors = rgbMapByte.length / 4; - transparentIndex = pTransparent; - - inverseRGB = new byte[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]; - initIRGB(new int[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]); - } - - /** - * @param pRGBColorMap the rgb color map to create inverse color map for. - * @param pTransparent the index of the transparent pixel in the map - */ - InverseColorMap(int[] pRGBColorMap, int pTransparent) { - rgbMapInt = pRGBColorMap; - numColors = rgbMapInt.length; - transparentIndex = pTransparent; - - inverseRGB = new byte[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]; - initIRGB(new int[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]); - } - - - /** - * Simple inverse color table creation method. - * @param pTemp temp array - */ - void initIRGB(int[] pTemp) { - final int x = (1 << TRUNCBITS); // 8 the size of 1 Dimension of each quantized cell - 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 < numColors; ++i) { - if (i == transparentIndex) { - // Skip the transparent pixel - continue; - } - - int red, r, rdist, rinc, rxx; - int green, g, gdist, ginc, gxx; - int blue, b, bdist, binc, bxx; - - // HaraldK 20040801: Added support for int[] - if (rgbMapByte != null) { - red = rgbMapByte[i * 4] & 0xFF; - green = rgbMapByte[i * 4 + 1] & 0xFF; - blue = rgbMapByte[i * 4 + 2] & 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"); - } - - rdist = red - x / 2; // distance of red to center of current cell - gdist = green - x / 2; // green - bdist = blue - x / 2; // blue - rdist = rdist * rdist + gdist * gdist + bdist * bdist; - - rinc = 2 * (xsqr - (red << TRUNCBITS)); - ginc = 2 * (xsqr - (green << TRUNCBITS)); - binc = 2 * (xsqr - (blue << TRUNCBITS)); - - int rgbI = 0; - for (r = 0, rxx = rinc; r < MAXQUANTVAL; rdist += rxx, ++r, rxx += xsqr2) { - for (g = 0, gdist = rdist, gxx = ginc; g < MAXQUANTVAL; gdist += gxx, ++g, gxx += xsqr2) { - for (b = 0, bdist = gdist, bxx = binc; b < MAXQUANTVAL; bdist += bxx, ++b, ++rgbI, bxx += xsqr2) { - if (i == 0 || pTemp[rgbI] > bdist) { - pTemp[rgbI] = bdist; - inverseRGB[rgbI] = (byte) i; - } - } - } - } - } - } - - /** - * Gets the index of the nearest color to from the color map. - * - * @param pColor the color to get the nearest color to from color map - * color must be of format {@code 0x00RRGGBB} - standard default RGB - * @return index of color which closest matches input color by using the - * created inverse color map. - */ - public final int getIndexNearest(int pColor) { - return inverseRGB[((pColor >> (3 * TRUNCBITS)) & QUANTMASK_RED) + - ((pColor >> (2 * TRUNCBITS)) & QUANTMASK_GREEN) + - ((pColor >> (/* 1 * */ TRUNCBITS)) & QUANTMASK_BLUE)] & 0xFF; - } - - /** - * Gets the index of the nearest color to from the color map. - * - * @param pRed red component of the color to get the nearest color to from color map - * @param pGreen green component of the color to get the nearest color to from color map - * @param pBlue blue component of the color to get the nearest color to from color map - * @return index of color which closest matches input color by using the - * created inverse color map. - */ - public final int getIndexNearest(int pRed, int pGreen, int pBlue) { - // NOTE: the third line in expression for blue is shifting DOWN not UP. - return inverseRGB[((pRed << (2 * QUANTBITS - TRUNCBITS)) & QUANTMASK_RED) + - ((pGreen << (/* 1 * */ QUANTBITS - TRUNCBITS)) & QUANTMASK_GREEN) + - ((pBlue >> (TRUNCBITS)) & QUANTMASK_BLUE)] & 0xFF; - } -} - +/* + * 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 of the copyright holder 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 HOLDER 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; + +/** + * Inverse Colormap to provide efficient lookup of any given input color + * to the closest match to the given color map. + *

+ * Based on "Efficient Inverse Color Map Computation" by Spencer W. Thomas + * in "Graphics Gems Volume II". + *

+ * + * @author Harald Kuhr + * @author Robin Luiten (Java port) + * @author Spencer W. Thomas (original c version). + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMap.java#1 $ + */ +class InverseColorMap { + /** + * Number of high bits of each color channel to use to lookup near match + */ + final static int QUANTBITS = 5; + + /** + * Truncated bits of each color channel + */ + final static int TRUNCBITS = 8 - QUANTBITS; + + /** + * BITMASK representing the bits for blue in the color lookup + */ + final static int QUANTMASK_BLUE = (1 << 5) - 1; + + /** + * BITMASK representing the bits for green in the color lookup + */ + final static int QUANTMASK_GREEN = (QUANTMASK_BLUE << QUANTBITS); + + /** + * BITMASK representing the bits for red in the color lookup + */ + final static int QUANTMASK_RED = (QUANTMASK_GREEN << QUANTBITS); + + /** + * Maximum value a quantised color channel can have + */ + final static int MAXQUANTVAL = 1 << 5; + + 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. + */ + InverseColorMap(byte[] pRGBColorMap) { + this(pRGBColorMap, -1); + } + + /** + * @param pRGBColorMap the rgb color map to create inverse color map for. + */ + // HaraldK 20040801: Added support for int[] + InverseColorMap(int[] pRGBColorMap) { + this(pRGBColorMap, -1); + } + + /** + * @param pRGBColorMap the rgb color map to create inverse color map for. + * @param pTransparent the index of the transparent pixel in the map + */ + InverseColorMap(byte[] pRGBColorMap, int pTransparent) { + rgbMapByte = pRGBColorMap; + numColors = rgbMapByte.length / 4; + transparentIndex = pTransparent; + + inverseRGB = new byte[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]; + initIRGB(new int[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]); + } + + /** + * @param pRGBColorMap the rgb color map to create inverse color map for. + * @param pTransparent the index of the transparent pixel in the map + */ + InverseColorMap(int[] pRGBColorMap, int pTransparent) { + rgbMapInt = pRGBColorMap; + numColors = rgbMapInt.length; + transparentIndex = pTransparent; + + inverseRGB = new byte[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]; + initIRGB(new int[MAXQUANTVAL * MAXQUANTVAL * MAXQUANTVAL]); + } + + + /** + * Simple inverse color table creation method. + * @param pTemp temp array + */ + void initIRGB(int[] pTemp) { + final int x = (1 << TRUNCBITS); // 8 the size of 1 Dimension of each quantized cell + 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 < numColors; ++i) { + if (i == transparentIndex) { + // Skip the transparent pixel + continue; + } + + int red, r, rdist, rinc, rxx; + int green, g, gdist, ginc, gxx; + int blue, b, bdist, binc, bxx; + + // HaraldK 20040801: Added support for int[] + if (rgbMapByte != null) { + red = rgbMapByte[i * 4] & 0xFF; + green = rgbMapByte[i * 4 + 1] & 0xFF; + blue = rgbMapByte[i * 4 + 2] & 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"); + } + + rdist = red - x / 2; // distance of red to center of current cell + gdist = green - x / 2; // green + bdist = blue - x / 2; // blue + rdist = rdist * rdist + gdist * gdist + bdist * bdist; + + rinc = 2 * (xsqr - (red << TRUNCBITS)); + ginc = 2 * (xsqr - (green << TRUNCBITS)); + binc = 2 * (xsqr - (blue << TRUNCBITS)); + + int rgbI = 0; + for (r = 0, rxx = rinc; r < MAXQUANTVAL; rdist += rxx, ++r, rxx += xsqr2) { + for (g = 0, gdist = rdist, gxx = ginc; g < MAXQUANTVAL; gdist += gxx, ++g, gxx += xsqr2) { + for (b = 0, bdist = gdist, bxx = binc; b < MAXQUANTVAL; bdist += bxx, ++b, ++rgbI, bxx += xsqr2) { + if (i == 0 || pTemp[rgbI] > bdist) { + pTemp[rgbI] = bdist; + inverseRGB[rgbI] = (byte) i; + } + } + } + } + } + } + + /** + * Gets the index of the nearest color to from the color map. + * + * @param pColor the color to get the nearest color to from color map + * color must be of format {@code 0x00RRGGBB} - standard default RGB + * @return index of color which closest matches input color by using the + * created inverse color map. + */ + public final int getIndexNearest(int pColor) { + return inverseRGB[((pColor >> (3 * TRUNCBITS)) & QUANTMASK_RED) + + ((pColor >> (2 * TRUNCBITS)) & QUANTMASK_GREEN) + + ((pColor >> (/* 1 * */ TRUNCBITS)) & QUANTMASK_BLUE)] & 0xFF; + } + + /** + * Gets the index of the nearest color to from the color map. + * + * @param pRed red component of the color to get the nearest color to from color map + * @param pGreen green component of the color to get the nearest color to from color map + * @param pBlue blue component of the color to get the nearest color to from color map + * @return index of color which closest matches input color by using the + * created inverse color map. + */ + public final int getIndexNearest(int pRed, int pGreen, int pBlue) { + // NOTE: the third line in expression for blue is shifting DOWN not UP. + 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/MagickAccelerator.java b/common/common-image/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java index 786fc7fd..ed0753e9 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 @@ -1,187 +1,187 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.SystemUtil; -import magick.MagickImage; - -import java.awt.image.BufferedImage; -import java.awt.image.BufferedImageOp; - -/** - * This class accelerates certain graphics operations, using - * JMagick and ImageMagick, if available. - * If those libraries are not installed, this class silently does nothing. - *

- * Set the system property {@code "com.twelvemonkeys.image.accel"} to - * {@code false}, to disable, even if JMagick is installed. - * Set the system property {@code "com.twelvemonkeys.image.magick.debug"} to - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java#3 $ - */ -final class MagickAccelerator { - - private static final boolean DEBUG = Magick.DEBUG; - private static final boolean USE_MAGICK = useMagick(); - - private static final int RESAMPLE_OP = 0; - - private static Class[] nativeOp = new Class[1]; - - static { - try { - nativeOp[RESAMPLE_OP] = Class.forName("com.twelvemonkeys.image.ResampleOp"); - } - catch (ClassNotFoundException e) { - System.err.println("Could not find class: " + e); - } - } - - private static boolean useMagick() { - try { - boolean available = SystemUtil.isClassAvailable("magick.MagickImage"); - - if (DEBUG && !available) { - System.err.print("ImageMagick bindings not available."); - } - - boolean useMagick = - available && !"FALSE".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.image.accel")); - - if (DEBUG) { - System.err.println( - useMagick - ? "Will use ImageMagick bindings to accelerate image resampling operations." - : "Will not use ImageMagick to accelerate image resampling operations." - ); - } - - return useMagick; - } - catch (Throwable t) { - // Most probably in case of a SecurityManager - System.err.println("Could not enable ImageMagick bindings: " + t); - return false; - } - } - - private static int getNativeOpIndex(Class pOpClass) { - for (int i = 0; i < nativeOp.length; i++) { - if (pOpClass == nativeOp[i]) { - return i; - } - } - - return -1; - } - - public static BufferedImage filter(BufferedImageOp pOperation, BufferedImage pInput, BufferedImage pOutput) { - if (!USE_MAGICK) { - return null; - } - - BufferedImage result = null; - switch (getNativeOpIndex(pOperation.getClass())) { - case RESAMPLE_OP: - ResampleOp resample = (ResampleOp) pOperation; - 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 - if (pOutput != null) { - //pOutput.setData(result.getRaster()); // Fast, but less compatible - // NOTE: For some reason, this is sometimes super-slow...? - ImageUtil.drawOnto(pOutput, result); - result = pOutput; - } - - break; - - default: - // Simply fall through, allowing acceleration to be added later - break; - - } - - return result; - } - - private static BufferedImage resampleMagick(BufferedImage pSrc, int pWidth, int pHeight, int pFilterType) { - // Convert to Magick, scale and convert back - MagickImage image = null; - MagickImage scaled = null; - try { - image = MagickUtil.toMagick(pSrc); - - long start = 0; - if (DEBUG) { - start = System.currentTimeMillis(); - } - - // NOTE: setFilter affects zoomImage, NOT scaleImage - image.setFilter(pFilterType); - scaled = image.zoomImage(pWidth, pHeight); - //scaled = image.scaleImage(pWidth, pHeight); // AREA_AVERAGING - - if (DEBUG) { - long time = System.currentTimeMillis() - start; - System.out.println("Filtered: " + time + " ms"); - } - - return MagickUtil.toBuffered(scaled); - } - //catch (MagickException e) { - catch (Exception e) { - // NOTE: Stupid workaround: If MagickException is caught, a - // NoClassDefFoundError is thrown, when MagickException class is - // unavailable... - if (e instanceof RuntimeException) { - throw (RuntimeException) e; - } - - throw new ImageConversionException(e.getMessage(), e); - } - finally { - // NOTE: ImageMagick might be unstable after a while, if image data - // is not deallocated. The GC/finalize method handles this, but in - // special circumstances, it's not triggered often enough. - if (image != null) { - image.destroyImages(); - } - if (scaled != null) { - scaled.destroyImages(); - } - } - } +/* + * 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 of the copyright holder 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 HOLDER 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.SystemUtil; +import magick.MagickImage; + +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; + +/** + * This class accelerates certain graphics operations, using + * JMagick and ImageMagick, if available. + * If those libraries are not installed, this class silently does nothing. + *

+ * Set the system property {@code "com.twelvemonkeys.image.accel"} to + * {@code false}, to disable, even if JMagick is installed. + * Set the system property {@code "com.twelvemonkeys.image.magick.debug"} to + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java#3 $ + */ +final class MagickAccelerator { + + private static final boolean DEBUG = Magick.DEBUG; + private static final boolean USE_MAGICK = useMagick(); + + private static final int RESAMPLE_OP = 0; + + private static Class[] nativeOp = new Class[1]; + + static { + try { + nativeOp[RESAMPLE_OP] = Class.forName("com.twelvemonkeys.image.ResampleOp"); + } + catch (ClassNotFoundException e) { + System.err.println("Could not find class: " + e); + } + } + + private static boolean useMagick() { + try { + boolean available = SystemUtil.isClassAvailable("magick.MagickImage"); + + if (DEBUG && !available) { + System.err.print("ImageMagick bindings not available."); + } + + boolean useMagick = + available && !"FALSE".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.image.accel")); + + if (DEBUG) { + System.err.println( + useMagick + ? "Will use ImageMagick bindings to accelerate image resampling operations." + : "Will not use ImageMagick to accelerate image resampling operations." + ); + } + + return useMagick; + } + catch (Throwable t) { + // Most probably in case of a SecurityManager + System.err.println("Could not enable ImageMagick bindings: " + t); + return false; + } + } + + private static int getNativeOpIndex(Class pOpClass) { + for (int i = 0; i < nativeOp.length; i++) { + if (pOpClass == nativeOp[i]) { + return i; + } + } + + return -1; + } + + public static BufferedImage filter(BufferedImageOp pOperation, BufferedImage pInput, BufferedImage pOutput) { + if (!USE_MAGICK) { + return null; + } + + BufferedImage result = null; + switch (getNativeOpIndex(pOperation.getClass())) { + case RESAMPLE_OP: + ResampleOp resample = (ResampleOp) pOperation; + 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 + if (pOutput != null) { + //pOutput.setData(result.getRaster()); // Fast, but less compatible + // NOTE: For some reason, this is sometimes super-slow...? + ImageUtil.drawOnto(pOutput, result); + result = pOutput; + } + + break; + + default: + // Simply fall through, allowing acceleration to be added later + break; + + } + + return result; + } + + private static BufferedImage resampleMagick(BufferedImage pSrc, int pWidth, int pHeight, int pFilterType) { + // Convert to Magick, scale and convert back + MagickImage image = null; + MagickImage scaled = null; + try { + image = MagickUtil.toMagick(pSrc); + + long start = 0; + if (DEBUG) { + start = System.currentTimeMillis(); + } + + // NOTE: setFilter affects zoomImage, NOT scaleImage + image.setFilter(pFilterType); + scaled = image.zoomImage(pWidth, pHeight); + //scaled = image.scaleImage(pWidth, pHeight); // AREA_AVERAGING + + if (DEBUG) { + long time = System.currentTimeMillis() - start; + System.out.println("Filtered: " + time + " ms"); + } + + return MagickUtil.toBuffered(scaled); + } + //catch (MagickException e) { + catch (Exception e) { + // NOTE: Stupid workaround: If MagickException is caught, a + // NoClassDefFoundError is thrown, when MagickException class is + // unavailable... + if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } + + throw new ImageConversionException(e.getMessage(), e); + } + finally { + // NOTE: ImageMagick might be unstable after a while, if image data + // is not deallocated. The GC/finalize method handles this, but in + // special circumstances, it's not triggered often enough. + if (image != null) { + image.destroyImages(); + } + if (scaled != null) { + scaled.destroyImages(); + } + } + } } \ No newline at end of file diff --git a/common/common-image/src/main/java/com/twelvemonkeys/image/MagickUtil.java b/common/common-image/src/main/java/com/twelvemonkeys/image/MagickUtil.java index 154e1e0a..a5a316ea 100755 --- a/common/common-image/src/main/java/com/twelvemonkeys/image/MagickUtil.java +++ b/common/common-image/src/main/java/com/twelvemonkeys/image/MagickUtil.java @@ -1,616 +1,621 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 magick.ImageType; -import magick.MagickException; -import magick.MagickImage; -import magick.PixelPacket; - -import java.awt.*; -import java.awt.color.ColorSpace; -import java.awt.color.ICC_ColorSpace; -import java.awt.color.ICC_Profile; -import java.awt.image.*; - -/** - * Utility for converting JMagick {@code MagickImage}s to standard Java - * {@code BufferedImage}s and back. - *

- * NOTE: This class is considered an implementation detail and not part of - * the public API. This class is subject to change without further notice. - * You have been warned. :-) - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickUtil.java#4 $ - */ -public final class MagickUtil { - // IMPORTANT NOTE: Disaster happens if any of these constants are used outside this class - // because you then have a dependency on MagickException (this is due to Java class loading - // and initialization magic). - // Do not use outside this class. If the constants need to be shared, move to Magick or ImageUtil. - - /** Color Model usesd for bilevel (B/W) */ - private static final IndexColorModel CM_MONOCHROME = MonochromeColorModel.getInstance(); - - /** Color Model usesd for raw ABGR */ - private static final ColorModel CM_COLOR_ALPHA = - new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8, 8}, - true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); - - /** Color Model usesd for raw BGR */ - private static final ColorModel CM_COLOR_OPAQUE = - new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8}, - false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); - - /** Color Model usesd for raw RGB */ - //private static final ColorModel CM_COLOR_RGB = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, 0x000000ff, 0x0); - - /** Color Model usesd for raw GRAY + ALPHA */ - private static final ColorModel CM_GRAY_ALPHA = - new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), - true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); - - /** Color Model usesd for raw GRAY */ - private static final ColorModel CM_GRAY_OPAQUE = - new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), - false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); - - /** Band offsets for raw ABGR */ - private static final int[] BAND_OFF_TRANS = new int[] {3, 2, 1, 0}; - - /** Band offsets for raw BGR */ - private static final int[] BAND_OFF_OPAQUE = new int[] {2, 1, 0}; - - /** The point at {@code 0, 0} */ - private static final Point LOCATION_UPPER_LEFT = new Point(0, 0); - - private static final boolean DEBUG = Magick.DEBUG; - - // Only static members and methods - private MagickUtil() {} - - /** - * Converts a {@code MagickImage} to a {@code BufferedImage}. - *

- * The conversion depends on {@code pImage}'s {@code ImageType}: - *

- *
{@code ImageType.BilevelType}
- *
{@code BufferedImage} of type {@code TYPE_BYTE_BINARY}
- * - *
{@code ImageType.GrayscaleType}
- *
{@code BufferedImage} of type {@code TYPE_BYTE_GRAY}
- *
{@code ImageType.GrayscaleMatteType}
- *
{@code BufferedImage} of type {@code TYPE_USHORT_GRAY}
- * - *
{@code ImageType.PaletteType}
- *
{@code BufferedImage} of type {@code TYPE_BYTE_BINARY} (for images - * with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}
- *
{@code ImageType.PaletteMatteType}
- *
{@code BufferedImage} of type {@code TYPE_BYTE_BINARY} (for images - * with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}
- * - *
{@code ImageType.TrueColorType}
- *
{@code BufferedImage} of type {@code TYPE_3BYTE_BGR}
- *
{@code ImageType.TrueColorPaletteType}
- *
{@code BufferedImage} of type {@code TYPE_4BYTE_ABGR}
- * - * @param pImage the original {@code MagickImage} - * @return a new {@code BufferedImage} - * - * @throws IllegalArgumentException if {@code pImage} is {@code null} - * or if the {@code ImageType} is not one mentioned above. - * @throws MagickException if an exception occurs during conversion - * - * @see BufferedImage - */ - public static BufferedImage toBuffered(MagickImage pImage) throws MagickException { - if (pImage == null) { - throw new IllegalArgumentException("image == null"); - } - - long start = 0L; - if (DEBUG) { - start = System.currentTimeMillis(); - } - - BufferedImage image = null; - try { - switch (pImage.getImageType()) { - case ImageType.BilevelType: - image = bilevelToBuffered(pImage); - break; - case ImageType.GrayscaleType: - image = grayToBuffered(pImage, false); - break; - case ImageType.GrayscaleMatteType: - image = grayToBuffered(pImage, true); - break; - case ImageType.PaletteType: - image = paletteToBuffered(pImage, false); - break; - case ImageType.PaletteMatteType: - image = paletteToBuffered(pImage, true); - break; - case ImageType.TrueColorType: - image = rgbToBuffered(pImage, false); - break; - case ImageType.TrueColorMatteType: - image = rgbToBuffered(pImage, true); - break; - case ImageType.ColorSeparationType: - image = cmykToBuffered(pImage, false); - break; - case ImageType.ColorSeparationMatteType: - image = cmykToBuffered(pImage, true); - break; - case ImageType.OptimizeType: - default: - throw new IllegalArgumentException("Unknown JMagick image type: " + pImage.getImageType()); - } - - } - finally { - if (DEBUG) { - long time = System.currentTimeMillis() - start; - System.out.println("Converted JMagick image type: " + pImage.getImageType() + " to BufferedImage: " + image); - System.out.println("Conversion to BufferedImage: " + time + " ms"); - } - } - - return image; - } - - /** - * Converts a {@code BufferedImage} to a {@code MagickImage}. - *

- * The conversion depends on {@code pImage}'s {@code ColorModel}: - *

- *
{@code IndexColorModel} with 1 bit b/w
- *
{@code MagickImage} of type {@code ImageType.BilevelType}
- *
{@code IndexColorModel} > 1 bit,
- *
{@code MagickImage} of type {@code ImageType.PaletteType} - * or {@code MagickImage} of type {@code ImageType.PaletteMatteType} - * depending on ColorModel.getAlpha()
- * - *
{@code ColorModel.getColorSpace().getType() == ColorSpace.TYPE_GRAY}
- *
{@code MagickImage} of type {@code ImageType.GrayscaleType} - * or {@code MagickImage} of type {@code ImageType.GrayscaleMatteType} - * depending on ColorModel.getAlpha()
- * - *
{@code ColorModel.getColorSpace().getType() == ColorSpace.TYPE_RGB}
- *
{@code MagickImage} of type {@code ImageType.TrueColorType} - * or {@code MagickImage} of type {@code ImageType.TrueColorPaletteType}
- * - * @param pImage the original {@code BufferedImage} - * @return a new {@code MagickImage} - * - * @throws IllegalArgumentException if {@code pImage} is {@code null} - * or if the {@code ColorModel} is not one mentioned above. - * @throws MagickException if an exception occurs during conversion - * - * @see BufferedImage - */ - public static MagickImage toMagick(BufferedImage pImage) throws MagickException { - if (pImage == null) { - throw new IllegalArgumentException("image == null"); - } - - long start = 0L; - if (DEBUG) { - start = System.currentTimeMillis(); - } - - try { - ColorModel cm = pImage.getColorModel(); - if (cm instanceof IndexColorModel) { - // Handles both BilevelType, PaletteType and PaletteMatteType - return indexedToMagick(pImage, (IndexColorModel) cm, cm.hasAlpha()); - } - - switch (cm.getColorSpace().getType()) { - case ColorSpace.TYPE_GRAY: - // Handles GrayType and GrayMatteType - return grayToMagick(pImage, cm.hasAlpha()); - case ColorSpace.TYPE_RGB: - // Handles TrueColorType and TrueColorMatteType - return rgbToMagic(pImage, cm.hasAlpha()); - case ColorSpace.TYPE_CMY: - case ColorSpace.TYPE_CMYK: - case ColorSpace.TYPE_HLS: - case ColorSpace.TYPE_HSV: - // Other types not supported yet - default: - throw new IllegalArgumentException("Unknown buffered image type: " + pImage); - } - } - finally { - if (DEBUG) { - long time = System.currentTimeMillis() - start; - System.out.println("Conversion to MagickImage: " + time + " ms"); - } - } - } - - private static MagickImage rgbToMagic(BufferedImage pImage, boolean pAlpha) throws MagickException { - MagickImage image = new MagickImage(); - - BufferedImage buffered = ImageUtil.toBuffered(pImage, pAlpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); - - // Need to get data of sub raster, not the full data array, this is - // just a convenient way - Raster raster; - if (buffered.getRaster().getParent() != null) { - raster = buffered.getData(new Rectangle(buffered.getWidth(), buffered.getHeight())); - } - else { - raster = buffered.getRaster(); - } - - image.constituteImage(buffered.getWidth(), buffered.getHeight(), pAlpha ? "ABGR" : "BGR", - ((DataBufferByte) raster.getDataBuffer()).getData()); - - return image; - } - - private static MagickImage grayToMagick(BufferedImage pImage, boolean pAlpha) throws MagickException { - MagickImage image = new MagickImage(); - - // TODO: Make a fix for TYPE_USHORT_GRAY - // The code below does not seem to work (JMagick issues?)... - /* - if (pImage.getType() == BufferedImage.TYPE_USHORT_GRAY) { - short[] data = ((DataBufferUShort) pImage.getRaster().getDataBuffer()).getData(); - int[] intData = new int[data.length]; - for (int i = 0; i < data.length; i++) { - intData[i] = (data[i] & 0xffff) * 0xffff; - } - image.constituteImage(pImage.getWidth(), pImage.getHeight(), "I", intData); - - System.out.println("storageClass: " + image.getStorageClass()); - System.out.println("depth: " + image.getDepth()); - System.out.println("imageType: " + image.getImageType()); - } - else { - */ - BufferedImage buffered = ImageUtil.toBuffered(pImage, pAlpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_BYTE_GRAY); - - // Need to get data of sub raster, not the full data array, this is - // just a convenient way - Raster raster; - if (buffered.getRaster().getParent() != null) { - raster = buffered.getData(new Rectangle(buffered.getWidth(), buffered.getHeight())); - } - else { - raster = buffered.getRaster(); - } - - image.constituteImage(buffered.getWidth(), buffered.getHeight(), pAlpha ? "ABGR" : "I", ((DataBufferByte) raster.getDataBuffer()).getData()); - //} - - return image; - } - - private static MagickImage indexedToMagick(BufferedImage pImage, IndexColorModel pColorModel, boolean pAlpha) throws MagickException { - MagickImage image = rgbToMagic(pImage, pAlpha); - - int mapSize = pColorModel.getMapSize(); - image.setNumberColors(mapSize); - - return image; - } - - /* - public static MagickImage toMagick(BufferedImage pImage) throws MagickException { - if (pImage == null) { - throw new IllegalArgumentException("image == null"); - } - - final int width = pImage.getWidth(); - final int height = pImage.getHeight(); - - // int ARGB -> byte RGBA conversion - // NOTE: This is ImageMagick Q16 compatible raw RGBA format with 16 bits/sample... - // For a Q8 build, we could probably go with half the space... - // NOTE: This is close to insanity, as it wastes extreme ammounts of memory - final int[] argb = new int[width]; - final byte[] raw16 = new byte[width * height * 8]; - for (int y = 0; y < height; y++) { - // Fetch one line of ARGB data - pImage.getRGB(0, y, width, 1, argb, 0, width); - - for (int x = 0; x < width; x++) { - int pixel = (x + (y * width)) * 8; - raw16[pixel ] = (byte) ((argb[x] >> 16) & 0xff); // R - raw16[pixel + 2] = (byte) ((argb[x] >> 8) & 0xff); // G - raw16[pixel + 4] = (byte) ((argb[x] ) & 0xff); // B - raw16[pixel + 6] = (byte) ((argb[x] >> 24) & 0xff); // A - } - } - - // Create magick image - ImageInfo info = new ImageInfo(); - info.setMagick("RGBA"); // Raw RGBA samples - info.setSize(width + "x" + height); // String?!? - - MagickImage image = new MagickImage(info); - image.setImageAttribute("depth", "8"); - - // Set pixel data in 16 bit raw RGBA format - image.blobToImage(info, raw16); - - return image; - } - */ - - /** - * Converts a bi-level {@code MagickImage} to a {@code BufferedImage}, of - * type {@code TYPE_BYTE_BINARY}. - * - * @param pImage the original {@code MagickImage} - * @return a new {@code BufferedImage} - * - * @throws MagickException if an exception occurs during conversion - * - * @see BufferedImage - */ - private static BufferedImage bilevelToBuffered(MagickImage pImage) throws MagickException { - // As there is no way to get the binary representation of the image, - // convert to gray, and the create a binary image from it - BufferedImage temp = grayToBuffered(pImage, false); - - BufferedImage image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_BINARY, CM_MONOCHROME); - - ImageUtil.drawOnto(image, temp); - - return image; - } - - /** - * Converts a gray {@code MagickImage} to a {@code BufferedImage}, of - * type {@code TYPE_USHORT_GRAY} or {@code TYPE_BYTE_GRAY}. - * - * @param pImage the original {@code MagickImage} - * @param pAlpha keep alpha channel - * @return a new {@code BufferedImage} - * - * @throws MagickException if an exception occurs during conversion - * - * @see BufferedImage - */ - private static BufferedImage grayToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { - Dimension size = pImage.getDimension(); - int length = size.width * size.height; - int bands = pAlpha ? 2 : 1; - byte[] pixels = new byte[length * bands]; - - // TODO: Make a fix for 16 bit TYPE_USHORT_GRAY?! - // Note: The ordering AI or I corresponds to BufferedImage - // TYPE_CUSTOM and TYPE_BYTE_GRAY respectively - pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "AI" : "I", pixels); - - // Init databuffer with array, to avoid allocation of empty array - DataBuffer buffer = new DataBufferByte(pixels, pixels.length); - - int[] bandOffsets = pAlpha ? new int[] {1, 0} : new int[] {0}; - - WritableRaster raster = - Raster.createInterleavedRaster(buffer, size.width, size.height, - size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT); - - return new BufferedImage(pAlpha ? CM_GRAY_ALPHA : CM_GRAY_OPAQUE, raster, pAlpha, null); - } - - /** - * Converts a palette-based {@code MagickImage} to a - * {@code BufferedImage}, of type {@code TYPE_BYTE_BINARY} (for images - * with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}. - * - * @param pImage the original {@code MagickImage} - * @param pAlpha keep alpha channel - * @return a new {@code BufferedImage} - * - * @throws MagickException if an exception occurs during conversion - * - * @see BufferedImage - */ - private static BufferedImage paletteToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { - // Create indexcolormodel for the image - IndexColorModel cm; - - try { - cm = createIndexColorModel(pImage.getColormap(), pAlpha); - } - catch (MagickException e) { - // NOTE: Some MagickImages incorrecly (?) reports to be paletteType, - // but does not have a colormap, this is a workaround. - return rgbToBuffered(pImage, pAlpha); - } - - // As there is no way to get the indexes of an indexed image, convert to - // RGB, and the create an indexed image from it - BufferedImage temp = rgbToBuffered(pImage, pAlpha); - - BufferedImage image; - if (cm.getMapSize() <= 16) { - image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_BINARY, cm); - } - else { - image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, cm); - } - - // Create transparent background for images containing alpha - if (pAlpha) { - Graphics2D g = image.createGraphics(); - try { - g.setComposite(AlphaComposite.Clear); - g.fillRect(0, 0, temp.getWidth(), temp.getHeight()); - } - finally { - g.dispose(); - } - } - - // NOTE: This is (surprisingly) much faster than using g2d.drawImage().. - // (Tests shows 20-30ms, vs. 600-700ms on the same image) - BufferedImageOp op = new CopyDither(cm); - op.filter(temp, image); - - return image; - } - - /** - * Creates an {@code IndexColorModel} from an array of - * {@code PixelPacket}s. - * - * @param pColormap the original colormap as a {@code PixelPacket} array - * @param pAlpha keep alpha channel - * - * @return a new {@code IndexColorModel} - */ - public static IndexColorModel createIndexColorModel(PixelPacket[] pColormap, boolean pAlpha) { - int[] colors = new int[pColormap.length]; - - // TODO: Verify if this is correct for alpha...? - int trans = pAlpha ? colors.length - 1 : -1; - - //for (int i = 0; i < pColormap.length; i++) { - for (int i = pColormap.length - 1; i != 0; i--) { - PixelPacket color = pColormap[i]; - if (pAlpha) { - colors[i] = (0xff - (color.getOpacity() & 0xff)) << 24 | - (color.getRed() & 0xff) << 16 | - (color.getGreen() & 0xff) << 8 | - (color.getBlue() & 0xff); - } - else { - colors[i] = (color.getRed() & 0xff) << 16 | - (color.getGreen() & 0xff) << 8 | - (color.getBlue() & 0xff); - } - } - - return new InverseColorMapIndexColorModel(8, colors.length, colors, 0, pAlpha, trans, DataBuffer.TYPE_BYTE); - } - - /** - * Converts an (A)RGB {@code MagickImage} to a {@code BufferedImage}, of - * type {@code TYPE_4BYTE_ABGR} or {@code TYPE_3BYTE_BGR}. - * - * @param pImage the original {@code MagickImage} - * @param pAlpha keep alpha channel - * @return a new {@code BufferedImage} - * - * @throws MagickException if an exception occurs during conversion - * - * @see BufferedImage - */ - private static BufferedImage rgbToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { - Dimension size = pImage.getDimension(); - int length = size.width * size.height; - int bands = pAlpha ? 4 : 3; - byte[] pixels = new byte[length * bands]; - - // TODO: If we do multiple dispatches (one per line, typically), we could provide listener - // feedback. But it's currently a lot slower than fetching all the pixels in one go. - - // Note: The ordering ABGR or BGR corresponds to BufferedImage - // TYPE_4BYTE_ABGR and TYPE_3BYTE_BGR respectively - pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "ABGR" : "BGR", pixels); - - // Init databuffer with array, to avoid allocation of empty array - DataBuffer buffer = new DataBufferByte(pixels, pixels.length); - - int[] bandOffsets = pAlpha ? BAND_OFF_TRANS : BAND_OFF_OPAQUE; - - WritableRaster raster = - Raster.createInterleavedRaster(buffer, size.width, size.height, - size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT); - - return new BufferedImage(pAlpha ? CM_COLOR_ALPHA : CM_COLOR_OPAQUE, raster, pAlpha, null); - } - - - - - /** - * Converts an {@code MagickImage} to a {@code BufferedImage} which holds an CMYK ICC profile - * - * @param pImage the original {@code MagickImage} - * @param pAlpha keep alpha channel - * @return a new {@code BufferedImage} - * - * @throws MagickException if an exception occurs during conversion - * - * @see BufferedImage - */ - private static BufferedImage cmykToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { - Dimension size = pImage.getDimension(); - int length = size.width * size.height; - - // Retreive the ICC profile - ICC_Profile profile = ICC_Profile.getInstance(pImage.getColorProfile().getInfo()); - ColorSpace cs = new ICC_ColorSpace(profile); - - int bands = cs.getNumComponents() + (pAlpha ? 1 : 0); - - int[] bits = new int[bands]; - for (int i = 0; i < bands; i++) { - bits[i] = 8; - } - - ColorModel cm = pAlpha ? - new ComponentColorModel(cs, bits, true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE) : - new ComponentColorModel(cs, bits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); - - byte[] pixels = new byte[length * bands]; - - // TODO: If we do multiple dispatches (one per line, typically), we could provide listener - // feedback. But it's currently a lot slower than fetching all the pixels in one go. - // TODO: handle more generic cases if profile is not CMYK - // TODO: Test "ACMYK" - pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "ACMYK" : "CMYK", pixels); - - // Init databuffer with array, to avoid allocation of empty array - DataBuffer buffer = new DataBufferByte(pixels, pixels.length); - - // TODO: build array from bands variable, here it just works for CMYK - // The values has not been tested with an alpha picture actually... - int[] bandOffsets = pAlpha ? new int[] {0, 1, 2, 3, 4} : new int[] {0, 1, 2, 3}; - - WritableRaster raster = - Raster.createInterleavedRaster(buffer, size.width, size.height, - size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT); - - return new BufferedImage(cm, raster, pAlpha, 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 of the copyright holder 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 HOLDER 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 magick.ImageType; +import magick.MagickException; +import magick.MagickImage; +import magick.PixelPacket; + +import java.awt.*; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.awt.image.*; + +/** + * Utility for converting JMagick {@code MagickImage}s to standard Java + * {@code BufferedImage}s and back. + *

+ * NOTE: This class is considered an implementation detail and not part of + * the public API. This class is subject to change without further notice. + * You have been warned. :-) + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickUtil.java#4 $ + */ +public final class MagickUtil { + // IMPORTANT NOTE: Disaster happens if any of these constants are used outside this class + // because you then have a dependency on MagickException (this is due to Java class loading + // and initialization magic). + // Do not use outside this class. If the constants need to be shared, move to Magick or ImageUtil. + + /** Color Model usesd for bilevel (B/W) */ + private static final IndexColorModel CM_MONOCHROME = MonochromeColorModel.getInstance(); + + /** Color Model usesd for raw ABGR */ + private static final ColorModel CM_COLOR_ALPHA = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8, 8}, + true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); + + /** Color Model usesd for raw BGR */ + private static final ColorModel CM_COLOR_OPAQUE = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_sRGB), new int[] {8, 8, 8}, + false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); + + /** Color Model usesd for raw RGB */ + //private static final ColorModel CM_COLOR_RGB = new DirectColorModel(24, 0x00ff0000, 0x0000ff00, 0x000000ff, 0x0); + + /** Color Model usesd for raw GRAY + ALPHA */ + private static final ColorModel CM_GRAY_ALPHA = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE); + + /** Color Model usesd for raw GRAY */ + private static final ColorModel CM_GRAY_OPAQUE = + new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), + false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); + + /** Band offsets for raw ABGR */ + private static final int[] BAND_OFF_TRANS = new int[] {3, 2, 1, 0}; + + /** Band offsets for raw BGR */ + private static final int[] BAND_OFF_OPAQUE = new int[] {2, 1, 0}; + + /** The point at {@code 0, 0} */ + private static final Point LOCATION_UPPER_LEFT = new Point(0, 0); + + private static final boolean DEBUG = Magick.DEBUG; + + // Only static members and methods + private MagickUtil() {} + + /** + * Converts a {@code MagickImage} to a {@code BufferedImage}. + *

+ * The conversion depends on {@code pImage}'s {@code ImageType}: + *

+ *
+ *
{@code ImageType.BilevelType}
+ *
{@code BufferedImage} of type {@code TYPE_BYTE_BINARY}
+ * + *
{@code ImageType.GrayscaleType}
+ *
{@code BufferedImage} of type {@code TYPE_BYTE_GRAY}
+ *
{@code ImageType.GrayscaleMatteType}
+ *
{@code BufferedImage} of type {@code TYPE_USHORT_GRAY}
+ * + *
{@code ImageType.PaletteType}
+ *
{@code BufferedImage} of type {@code TYPE_BYTE_BINARY} (for images + * with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}
+ *
{@code ImageType.PaletteMatteType}
+ *
{@code BufferedImage} of type {@code TYPE_BYTE_BINARY} (for images + * with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}
+ * + *
{@code ImageType.TrueColorType}
+ *
{@code BufferedImage} of type {@code TYPE_3BYTE_BGR}
+ *
{@code ImageType.TrueColorPaletteType}
+ *
{@code BufferedImage} of type {@code TYPE_4BYTE_ABGR}
+ *
+ * + * @param pImage the original {@code MagickImage} + * @return a new {@code BufferedImage} + * + * @throws IllegalArgumentException if {@code pImage} is {@code null} + * or if the {@code ImageType} is not one mentioned above. + * @throws MagickException if an exception occurs during conversion + * + * @see BufferedImage + */ + public static BufferedImage toBuffered(MagickImage pImage) throws MagickException { + if (pImage == null) { + throw new IllegalArgumentException("image == null"); + } + + long start = 0L; + if (DEBUG) { + start = System.currentTimeMillis(); + } + + BufferedImage image = null; + try { + switch (pImage.getImageType()) { + case ImageType.BilevelType: + image = bilevelToBuffered(pImage); + break; + case ImageType.GrayscaleType: + image = grayToBuffered(pImage, false); + break; + case ImageType.GrayscaleMatteType: + image = grayToBuffered(pImage, true); + break; + case ImageType.PaletteType: + image = paletteToBuffered(pImage, false); + break; + case ImageType.PaletteMatteType: + image = paletteToBuffered(pImage, true); + break; + case ImageType.TrueColorType: + image = rgbToBuffered(pImage, false); + break; + case ImageType.TrueColorMatteType: + image = rgbToBuffered(pImage, true); + break; + case ImageType.ColorSeparationType: + image = cmykToBuffered(pImage, false); + break; + case ImageType.ColorSeparationMatteType: + image = cmykToBuffered(pImage, true); + break; + case ImageType.OptimizeType: + default: + throw new IllegalArgumentException("Unknown JMagick image type: " + pImage.getImageType()); + } + + } + finally { + if (DEBUG) { + long time = System.currentTimeMillis() - start; + System.out.println("Converted JMagick image type: " + pImage.getImageType() + " to BufferedImage: " + image); + System.out.println("Conversion to BufferedImage: " + time + " ms"); + } + } + + return image; + } + + /** + * Converts a {@code BufferedImage} to a {@code MagickImage}. + *

+ * The conversion depends on {@code pImage}'s {@code ColorModel}: + *

+ *
+ *
{@code IndexColorModel} with 1 bit b/w
+ *
{@code MagickImage} of type {@code ImageType.BilevelType}
+ *
{@code IndexColorModel} > 1 bit,
+ *
{@code MagickImage} of type {@code ImageType.PaletteType} + * or {@code MagickImage} of type {@code ImageType.PaletteMatteType} + * depending on ColorModel.getAlpha()
+ * + *
{@code ColorModel.getColorSpace().getType() == ColorSpace.TYPE_GRAY}
+ *
{@code MagickImage} of type {@code ImageType.GrayscaleType} + * or {@code MagickImage} of type {@code ImageType.GrayscaleMatteType} + * depending on ColorModel.getAlpha()
+ * + *
{@code ColorModel.getColorSpace().getType() == ColorSpace.TYPE_RGB}
+ *
{@code MagickImage} of type {@code ImageType.TrueColorType} + * or {@code MagickImage} of type {@code ImageType.TrueColorPaletteType}
+ *
+ * + * @param pImage the original {@code BufferedImage} + * @return a new {@code MagickImage} + * + * @throws IllegalArgumentException if {@code pImage} is {@code null} + * or if the {@code ColorModel} is not one mentioned above. + * @throws MagickException if an exception occurs during conversion + * + * @see BufferedImage + */ + public static MagickImage toMagick(BufferedImage pImage) throws MagickException { + if (pImage == null) { + throw new IllegalArgumentException("image == null"); + } + + long start = 0L; + if (DEBUG) { + start = System.currentTimeMillis(); + } + + try { + ColorModel cm = pImage.getColorModel(); + if (cm instanceof IndexColorModel) { + // Handles both BilevelType, PaletteType and PaletteMatteType + return indexedToMagick(pImage, (IndexColorModel) cm, cm.hasAlpha()); + } + + switch (cm.getColorSpace().getType()) { + case ColorSpace.TYPE_GRAY: + // Handles GrayType and GrayMatteType + return grayToMagick(pImage, cm.hasAlpha()); + case ColorSpace.TYPE_RGB: + // Handles TrueColorType and TrueColorMatteType + return rgbToMagic(pImage, cm.hasAlpha()); + case ColorSpace.TYPE_CMY: + case ColorSpace.TYPE_CMYK: + case ColorSpace.TYPE_HLS: + case ColorSpace.TYPE_HSV: + // Other types not supported yet + default: + throw new IllegalArgumentException("Unknown buffered image type: " + pImage); + } + } + finally { + if (DEBUG) { + long time = System.currentTimeMillis() - start; + System.out.println("Conversion to MagickImage: " + time + " ms"); + } + } + } + + private static MagickImage rgbToMagic(BufferedImage pImage, boolean pAlpha) throws MagickException { + MagickImage image = new MagickImage(); + + BufferedImage buffered = ImageUtil.toBuffered(pImage, pAlpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); + + // Need to get data of sub raster, not the full data array, this is + // just a convenient way + Raster raster; + if (buffered.getRaster().getParent() != null) { + raster = buffered.getData(new Rectangle(buffered.getWidth(), buffered.getHeight())); + } + else { + raster = buffered.getRaster(); + } + + image.constituteImage(buffered.getWidth(), buffered.getHeight(), pAlpha ? "ABGR" : "BGR", + ((DataBufferByte) raster.getDataBuffer()).getData()); + + return image; + } + + private static MagickImage grayToMagick(BufferedImage pImage, boolean pAlpha) throws MagickException { + MagickImage image = new MagickImage(); + + // TODO: Make a fix for TYPE_USHORT_GRAY + // The code below does not seem to work (JMagick issues?)... + /* + if (pImage.getType() == BufferedImage.TYPE_USHORT_GRAY) { + short[] data = ((DataBufferUShort) pImage.getRaster().getDataBuffer()).getData(); + int[] intData = new int[data.length]; + for (int i = 0; i < data.length; i++) { + intData[i] = (data[i] & 0xffff) * 0xffff; + } + image.constituteImage(pImage.getWidth(), pImage.getHeight(), "I", intData); + + System.out.println("storageClass: " + image.getStorageClass()); + System.out.println("depth: " + image.getDepth()); + System.out.println("imageType: " + image.getImageType()); + } + else { + */ + BufferedImage buffered = ImageUtil.toBuffered(pImage, pAlpha ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_BYTE_GRAY); + + // Need to get data of sub raster, not the full data array, this is + // just a convenient way + Raster raster; + if (buffered.getRaster().getParent() != null) { + raster = buffered.getData(new Rectangle(buffered.getWidth(), buffered.getHeight())); + } + else { + raster = buffered.getRaster(); + } + + image.constituteImage(buffered.getWidth(), buffered.getHeight(), pAlpha ? "ABGR" : "I", ((DataBufferByte) raster.getDataBuffer()).getData()); + //} + + return image; + } + + private static MagickImage indexedToMagick(BufferedImage pImage, IndexColorModel pColorModel, boolean pAlpha) throws MagickException { + MagickImage image = rgbToMagic(pImage, pAlpha); + + int mapSize = pColorModel.getMapSize(); + image.setNumberColors(mapSize); + + return image; + } + + /* + public static MagickImage toMagick(BufferedImage pImage) throws MagickException { + if (pImage == null) { + throw new IllegalArgumentException("image == null"); + } + + final int width = pImage.getWidth(); + final int height = pImage.getHeight(); + + // int ARGB -> byte RGBA conversion + // NOTE: This is ImageMagick Q16 compatible raw RGBA format with 16 bits/sample... + // For a Q8 build, we could probably go with half the space... + // NOTE: This is close to insanity, as it wastes extreme ammounts of memory + final int[] argb = new int[width]; + final byte[] raw16 = new byte[width * height * 8]; + for (int y = 0; y < height; y++) { + // Fetch one line of ARGB data + pImage.getRGB(0, y, width, 1, argb, 0, width); + + for (int x = 0; x < width; x++) { + int pixel = (x + (y * width)) * 8; + raw16[pixel ] = (byte) ((argb[x] >> 16) & 0xff); // R + raw16[pixel + 2] = (byte) ((argb[x] >> 8) & 0xff); // G + raw16[pixel + 4] = (byte) ((argb[x] ) & 0xff); // B + raw16[pixel + 6] = (byte) ((argb[x] >> 24) & 0xff); // A + } + } + + // Create magick image + ImageInfo info = new ImageInfo(); + info.setMagick("RGBA"); // Raw RGBA samples + info.setSize(width + "x" + height); // String?!? + + MagickImage image = new MagickImage(info); + image.setImageAttribute("depth", "8"); + + // Set pixel data in 16 bit raw RGBA format + image.blobToImage(info, raw16); + + return image; + } + */ + + /** + * Converts a bi-level {@code MagickImage} to a {@code BufferedImage}, of + * type {@code TYPE_BYTE_BINARY}. + * + * @param pImage the original {@code MagickImage} + * @return a new {@code BufferedImage} + * + * @throws MagickException if an exception occurs during conversion + * + * @see BufferedImage + */ + private static BufferedImage bilevelToBuffered(MagickImage pImage) throws MagickException { + // As there is no way to get the binary representation of the image, + // convert to gray, and the create a binary image from it + BufferedImage temp = grayToBuffered(pImage, false); + + BufferedImage image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_BINARY, CM_MONOCHROME); + + ImageUtil.drawOnto(image, temp); + + return image; + } + + /** + * Converts a gray {@code MagickImage} to a {@code BufferedImage}, of + * type {@code TYPE_USHORT_GRAY} or {@code TYPE_BYTE_GRAY}. + * + * @param pImage the original {@code MagickImage} + * @param pAlpha keep alpha channel + * @return a new {@code BufferedImage} + * + * @throws MagickException if an exception occurs during conversion + * + * @see BufferedImage + */ + private static BufferedImage grayToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { + Dimension size = pImage.getDimension(); + int length = size.width * size.height; + int bands = pAlpha ? 2 : 1; + byte[] pixels = new byte[length * bands]; + + // TODO: Make a fix for 16 bit TYPE_USHORT_GRAY?! + // Note: The ordering AI or I corresponds to BufferedImage + // TYPE_CUSTOM and TYPE_BYTE_GRAY respectively + pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "AI" : "I", pixels); + + // Init databuffer with array, to avoid allocation of empty array + DataBuffer buffer = new DataBufferByte(pixels, pixels.length); + + int[] bandOffsets = pAlpha ? new int[] {1, 0} : new int[] {0}; + + WritableRaster raster = + Raster.createInterleavedRaster(buffer, size.width, size.height, + size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT); + + return new BufferedImage(pAlpha ? CM_GRAY_ALPHA : CM_GRAY_OPAQUE, raster, pAlpha, null); + } + + /** + * Converts a palette-based {@code MagickImage} to a + * {@code BufferedImage}, of type {@code TYPE_BYTE_BINARY} (for images + * with a palette of <= 16 colors) or {@code TYPE_BYTE_INDEXED}. + * + * @param pImage the original {@code MagickImage} + * @param pAlpha keep alpha channel + * @return a new {@code BufferedImage} + * + * @throws MagickException if an exception occurs during conversion + * + * @see BufferedImage + */ + private static BufferedImage paletteToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { + // Create indexcolormodel for the image + IndexColorModel cm; + + try { + cm = createIndexColorModel(pImage.getColormap(), pAlpha); + } + catch (MagickException e) { + // NOTE: Some MagickImages incorrecly (?) reports to be paletteType, + // but does not have a colormap, this is a workaround. + return rgbToBuffered(pImage, pAlpha); + } + + // As there is no way to get the indexes of an indexed image, convert to + // RGB, and the create an indexed image from it + BufferedImage temp = rgbToBuffered(pImage, pAlpha); + + BufferedImage image; + if (cm.getMapSize() <= 16) { + image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_BINARY, cm); + } + else { + image = new BufferedImage(temp.getWidth(), temp.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, cm); + } + + // Create transparent background for images containing alpha + if (pAlpha) { + Graphics2D g = image.createGraphics(); + try { + g.setComposite(AlphaComposite.Clear); + g.fillRect(0, 0, temp.getWidth(), temp.getHeight()); + } + finally { + g.dispose(); + } + } + + // NOTE: This is (surprisingly) much faster than using g2d.drawImage().. + // (Tests shows 20-30ms, vs. 600-700ms on the same image) + BufferedImageOp op = new CopyDither(cm); + op.filter(temp, image); + + return image; + } + + /** + * Creates an {@code IndexColorModel} from an array of + * {@code PixelPacket}s. + * + * @param pColormap the original colormap as a {@code PixelPacket} array + * @param pAlpha keep alpha channel + * + * @return a new {@code IndexColorModel} + */ + public static IndexColorModel createIndexColorModel(PixelPacket[] pColormap, boolean pAlpha) { + int[] colors = new int[pColormap.length]; + + // TODO: Verify if this is correct for alpha...? + int trans = pAlpha ? colors.length - 1 : -1; + + //for (int i = 0; i < pColormap.length; i++) { + for (int i = pColormap.length - 1; i != 0; i--) { + PixelPacket color = pColormap[i]; + if (pAlpha) { + colors[i] = (0xff - (color.getOpacity() & 0xff)) << 24 | + (color.getRed() & 0xff) << 16 | + (color.getGreen() & 0xff) << 8 | + (color.getBlue() & 0xff); + } + else { + colors[i] = (color.getRed() & 0xff) << 16 | + (color.getGreen() & 0xff) << 8 | + (color.getBlue() & 0xff); + } + } + + return new InverseColorMapIndexColorModel(8, colors.length, colors, 0, pAlpha, trans, DataBuffer.TYPE_BYTE); + } + + /** + * Converts an (A)RGB {@code MagickImage} to a {@code BufferedImage}, of + * type {@code TYPE_4BYTE_ABGR} or {@code TYPE_3BYTE_BGR}. + * + * @param pImage the original {@code MagickImage} + * @param pAlpha keep alpha channel + * @return a new {@code BufferedImage} + * + * @throws MagickException if an exception occurs during conversion + * + * @see BufferedImage + */ + private static BufferedImage rgbToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { + Dimension size = pImage.getDimension(); + int length = size.width * size.height; + int bands = pAlpha ? 4 : 3; + byte[] pixels = new byte[length * bands]; + + // TODO: If we do multiple dispatches (one per line, typically), we could provide listener + // feedback. But it's currently a lot slower than fetching all the pixels in one go. + + // Note: The ordering ABGR or BGR corresponds to BufferedImage + // TYPE_4BYTE_ABGR and TYPE_3BYTE_BGR respectively + pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "ABGR" : "BGR", pixels); + + // Init databuffer with array, to avoid allocation of empty array + DataBuffer buffer = new DataBufferByte(pixels, pixels.length); + + int[] bandOffsets = pAlpha ? BAND_OFF_TRANS : BAND_OFF_OPAQUE; + + WritableRaster raster = + Raster.createInterleavedRaster(buffer, size.width, size.height, + size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT); + + return new BufferedImage(pAlpha ? CM_COLOR_ALPHA : CM_COLOR_OPAQUE, raster, pAlpha, null); + } + + + + + /** + * Converts an {@code MagickImage} to a {@code BufferedImage} which holds an CMYK ICC profile + * + * @param pImage the original {@code MagickImage} + * @param pAlpha keep alpha channel + * @return a new {@code BufferedImage} + * + * @throws MagickException if an exception occurs during conversion + * + * @see BufferedImage + */ + private static BufferedImage cmykToBuffered(MagickImage pImage, boolean pAlpha) throws MagickException { + Dimension size = pImage.getDimension(); + int length = size.width * size.height; + + // Retreive the ICC profile + ICC_Profile profile = ICC_Profile.getInstance(pImage.getColorProfile().getInfo()); + ColorSpace cs = new ICC_ColorSpace(profile); + + int bands = cs.getNumComponents() + (pAlpha ? 1 : 0); + + int[] bits = new int[bands]; + for (int i = 0; i < bands; i++) { + bits[i] = 8; + } + + ColorModel cm = pAlpha ? + new ComponentColorModel(cs, bits, true, true, Transparency.TRANSLUCENT, DataBuffer.TYPE_BYTE) : + new ComponentColorModel(cs, bits, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); + + byte[] pixels = new byte[length * bands]; + + // TODO: If we do multiple dispatches (one per line, typically), we could provide listener + // feedback. But it's currently a lot slower than fetching all the pixels in one go. + // TODO: handle more generic cases if profile is not CMYK + // TODO: Test "ACMYK" + pImage.dispatchImage(0, 0, size.width, size.height, pAlpha ? "ACMYK" : "CMYK", pixels); + + // Init databuffer with array, to avoid allocation of empty array + DataBuffer buffer = new DataBufferByte(pixels, pixels.length); + + // TODO: build array from bands variable, here it just works for CMYK + // The values has not been tested with an alpha picture actually... + int[] bandOffsets = pAlpha ? new int[] {0, 1, 2, 3, 4} : new int[] {0, 1, 2, 3}; + + WritableRaster raster = + Raster.createInterleavedRaster(buffer, size.width, size.height, + size.width * bands, bands, bandOffsets, LOCATION_UPPER_LEFT); + + return new BufferedImage(cm, raster, pAlpha, null); + + } +} 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 155f0728..d61d220d 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 @@ -1,1631 +1,1639 @@ -/* - * 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 of the copyright holder 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 HOLDER 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. - */ -/* - ******************************************************************************* - * - * Based on example code found in Graphics Gems III, Filtered Image Rescaling - * (filter_rcg.c), available from http://www.acm.org/tog/GraphicsGems/. - * - * Public Domain 1991 by Dale Schumacher. Mods by Ray Gardener - * - * Original by Dale Schumacher (fzoom) - * - * Additional changes by Ray Gardener, Daylon Graphics Ltd. - * December 4, 1999 - * - ******************************************************************************* - * - * Aditional changes inspired by ImageMagick's resize.c. - * - ******************************************************************************* - * - * Java port and additional changes/bugfixes by Harald Kuhr, Twelvemonkeys. - * February 20, 2006 - * - ******************************************************************************* - */ - -package com.twelvemonkeys.image; - -import java.awt.*; -import java.awt.geom.AffineTransform; -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. - * Several different interpolation algorithms may be specifed in the - * constructor, either using the - * filter type constants, or one of the - * {@code RendereingHints}. - *

- * For fastest results, use {@link #FILTER_POINT} or {@link #FILTER_BOX}. - * In most cases, {@link #FILTER_TRIANGLE} will produce acceptable results, while - * being relatively fast. - * For higher quality output, use more sophisticated interpolation algorithms, - * like {@link #FILTER_MITCHELL} or {@link #FILTER_LANCZOS}. - *

- * Example: - *

- * BufferedImage image;
- * 

- * //... - *

- * ResampleOp resampler = new ResampleOp(100, 100, ResampleOp.FILTER_TRIANGLE); - * BufferedImage thumbnail = resampler.filter(image, null); - *

- *

- * If your imput image is very large, it's possible to first resample using the - * very fast {@code FILTER_POINT} algorithm, then resample to the wanted size, - * using a higher quality algorithm: - *

- * BufferedImage verylLarge;
- * 

- * //... - *

- * int w = 300; - * int h = 200; - *

- * BufferedImage temp = new ResampleOp(w * 2, h * 2, FILTER_POINT).filter(verylLarge, null); - *

- * BufferedImage scaled = new ResampleOp(w, h).filter(temp, null); - *

- *

- * For maximum performance, this class will use native code, through - * JMagick, when available. - * Otherwise, the class will silently fall back to pure Java mode. - * Native code may be disabled globally, by setting the system property - * {@code com.twelvemonkeys.image.accel} to {@code false}. - * To allow debug of the native code, set the system property - * {@code com.twelvemonkeys.image.magick.debug} to {@code true}. - *

- * This {@code BufferedImageOp} is based on C example code found in - * Graphics Gems III, - * Filtered Image Rescaling, by Dale Schumacher (with additional improvments by - * Ray Gardener). - * Additional changes are inspired by - * ImageMagick and - * Marco Schmidt's Java Imaging Utilities - * (which are also adaptions of the same original code from Graphics Gems III). - *

- * For a description of the various interpolation algorithms, see - * General Filtered Image Rescaling in Graphics Gems III, - * Academic Press, 1994. - * - * @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/ResampleOp.java#1 $ - * @see #ResampleOp(int,int,int) - * @see #ResampleOp(int,int,java.awt.RenderingHints) - * @see BufferedImage - * @see RenderingHints - * @see AffineTransformOp - */ -// TODO: Consider using AffineTransformOp for more operations!? -public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { - - // NOTE: These MUST correspond to ImageMagick filter types, for the - // MagickAccelerator to work consistently (see magick.FilterType). - - /** - * Undefined interpolation, filter method will use default filter. - */ - public final static int FILTER_UNDEFINED = 0; - /** - * Point interpolation (also known as "nearest neighbour"). - * Very fast, but low quality - * (similar to {@link RenderingHints#VALUE_INTERPOLATION_NEAREST_NEIGHBOR} - * and {@link Image#SCALE_REPLICATE}). - */ - public final static int FILTER_POINT = 1; - /** - * Box interpolation. Fast, but low quality. - */ - public final static int FILTER_BOX = 2; - /** - * Triangle interpolation (also known as "linear" or "bilinear"). - * Quite fast, with acceptable quality - * (similar to {@link RenderingHints#VALUE_INTERPOLATION_BILINEAR} and - * {@link Image#SCALE_AREA_AVERAGING}). - */ - public final static int FILTER_TRIANGLE = 3; - /** - * Hermite interpolation. - */ - public final static int FILTER_HERMITE = 4; - /** - * Hanning interpolation. - */ - public final static int FILTER_HANNING = 5; - /** - * Hamming interpolation. - */ - public final static int FILTER_HAMMING = 6; - /** - * Blackman interpolation.. - */ - public final static int FILTER_BLACKMAN = 7; - /** - * Gaussian interpolation. - */ - public final static int FILTER_GAUSSIAN = 8; - /** - * Quadratic interpolation. - */ - public final static int FILTER_QUADRATIC = 9; - /** - * Cubic interpolation. - */ - public final static int FILTER_CUBIC = 10; - /** - * Catrom interpolation. - */ - public final static int FILTER_CATROM = 11; - /** - * Mitchell interpolation. High quality. - */ - 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 - /** - * Blackman-Bessel interpolation. High quality. - */ - public final static int FILTER_BLACKMAN_BESSEL = 14; - /** - * Blackman-Sinc interpolation. High quality. - */ - public final static int FILTER_BLACKMAN_SINC = 15; - - /** - * RenderingHints.Key specifying resampling interpolation algorithm. - */ - public final static RenderingHints.Key KEY_RESAMPLE_INTERPOLATION = new Key("ResampleInterpolation"); - - /** - * @see #FILTER_POINT - */ - public final static Object VALUE_INTERPOLATION_POINT = - new Value(KEY_RESAMPLE_INTERPOLATION, "Point", FILTER_POINT); - /** - * @see #FILTER_BOX - */ - public final static Object VALUE_INTERPOLATION_BOX = - new Value(KEY_RESAMPLE_INTERPOLATION, "Box", FILTER_BOX); - /** - * @see #FILTER_TRIANGLE - */ - public final static Object VALUE_INTERPOLATION_TRIANGLE = - new Value(KEY_RESAMPLE_INTERPOLATION, "Triangle", FILTER_TRIANGLE); - /** - * @see #FILTER_HERMITE - */ - public final static Object VALUE_INTERPOLATION_HERMITE = - new Value(KEY_RESAMPLE_INTERPOLATION, "Hermite", FILTER_HERMITE); - /** - * @see #FILTER_HANNING - */ - public final static Object VALUE_INTERPOLATION_HANNING = - new Value(KEY_RESAMPLE_INTERPOLATION, "Hanning", FILTER_HANNING); - /** - * @see #FILTER_HAMMING - */ - public final static Object VALUE_INTERPOLATION_HAMMING = - new Value(KEY_RESAMPLE_INTERPOLATION, "Hamming", FILTER_HAMMING); - /** - * @see #FILTER_BLACKMAN - */ - public final static Object VALUE_INTERPOLATION_BLACKMAN = - new Value(KEY_RESAMPLE_INTERPOLATION, "Blackman", FILTER_BLACKMAN); - /** - * @see #FILTER_GAUSSIAN - */ - public final static Object VALUE_INTERPOLATION_GAUSSIAN = - new Value(KEY_RESAMPLE_INTERPOLATION, "Gaussian", FILTER_GAUSSIAN); - /** - * @see #FILTER_QUADRATIC - */ - public final static Object VALUE_INTERPOLATION_QUADRATIC = - new Value(KEY_RESAMPLE_INTERPOLATION, "Quadratic", FILTER_QUADRATIC); - /** - * @see #FILTER_CUBIC - */ - public final static Object VALUE_INTERPOLATION_CUBIC = - new Value(KEY_RESAMPLE_INTERPOLATION, "Cubic", FILTER_CUBIC); - /** - * @see #FILTER_CATROM - */ - public final static Object VALUE_INTERPOLATION_CATROM = - new Value(KEY_RESAMPLE_INTERPOLATION, "Catrom", FILTER_CATROM); - /** - * @see #FILTER_MITCHELL - */ - public final static Object VALUE_INTERPOLATION_MITCHELL = - new Value(KEY_RESAMPLE_INTERPOLATION, "Mitchell", FILTER_MITCHELL); - /** - * @see #FILTER_LANCZOS - */ - public final static Object VALUE_INTERPOLATION_LANCZOS = - new Value(KEY_RESAMPLE_INTERPOLATION, "Lanczos", FILTER_LANCZOS); - /** - * @see #FILTER_BLACKMAN_BESSEL - */ - public final static Object VALUE_INTERPOLATION_BLACKMAN_BESSEL = - new Value(KEY_RESAMPLE_INTERPOLATION, "Blackman-Bessel", FILTER_BLACKMAN_BESSEL); - /** - * @see #FILTER_BLACKMAN_SINC - */ - public final static Object VALUE_INTERPOLATION_BLACKMAN_SINC = - new Value(KEY_RESAMPLE_INTERPOLATION, "Blackman-Sinc", FILTER_BLACKMAN_SINC); - - // Member variables - // Package access, to allow access from MagickAccelerator - int width; - int height; - - int filterType; - - /** - * RendereingHints.Key implementation, works only with Value values. - */ - // TODO: Move to abstract class AbstractBufferedImageOp? - static class Key extends RenderingHints.Key { - static int sIndex = 10000; - - private final String name; - - public Key(final String pName) { - super(sIndex++); - name = pName; - } - - public boolean isCompatibleValue(Object pValue) { - return pValue instanceof Value && ((Value) pValue).isCompatibleKey(this); - } - - public String toString() { - return name; - } - } - - /** - * RenderingHints value implementation, works with Key keys. - */ - // TODO: Extract abstract Value class, and move to AbstractBufferedImageOp - static final class Value { - 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) { - key = pKey; - name = pName; - type = validateFilterType(pType); - } - - public boolean isCompatibleKey(Key pKey) { - return pKey == key; - } - - public int getFilterType() { - return type; - } - - public String toString() { - return name; - } - } - - /** - * Creates a {@code ResampleOp} that will resample input images to the - * given width and height, using the default interpolation filter. - * - * @param width width of the re-sampled image - * @param height height of the re-sampled image - */ - public ResampleOp(int width, int height) { - this(width, height, FILTER_UNDEFINED); - } - - /** - * Creates a {@code ResampleOp} that will resample input images to the - * given width and height, using the interpolation filter specified by - * the given hints. - * If using {@code RenderingHints}, the hints are mapped as follows: - *

    - *
  • {@code KEY_RESAMPLE_INTERPOLATION} takes precedence over any - * standard {@code java.awt} hints, and dictates interpolation - * directly, see - * {@code RenderingHints} constants.
  • - *

    - *

  • {@code KEY_INTERPOLATION} takes precedence over other hints. - *
      - *
    • {@link RenderingHints#VALUE_INTERPOLATION_NEAREST_NEIGHBOR} specifies - * {@code FILTER_POINT}
    • - *
    • {@link RenderingHints#VALUE_INTERPOLATION_BILINEAR} specifies - * {@code FILTER_TRIANGLE}
    • - *
    • {@link RenderingHints#VALUE_INTERPOLATION_BICUBIC} specifies - * {@code FILTER_QUADRATIC}
    • - *
    - *
  • - *

    - *

  • {@code KEY_RENDERING} or {@code KEY_COLOR_RENDERING} - *
      - *
    • {@link RenderingHints#VALUE_RENDER_SPEED} specifies - * {@code FILTER_POINT}
    • - *
    • {@link RenderingHints#VALUE_RENDER_QUALITY} specifies - * {@code FILTER_MITCHELL}
    • - *
    - *
  • - *
- * Other hints have no effect on this filter. - * - * @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 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 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 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"); - } - - this.width = width; - this.height = height; - - this.filterType = validateFilterType(filterType); - } - - private static int validateFilterType(int pFilterType) { - switch (pFilterType) { - case FILTER_UNDEFINED: - case FILTER_POINT: - case FILTER_BOX: - case FILTER_TRIANGLE: - case FILTER_HERMITE: - case FILTER_HANNING: - case FILTER_HAMMING: - case FILTER_BLACKMAN: - case FILTER_GAUSSIAN: - case FILTER_QUADRATIC: - case FILTER_CUBIC: - case FILTER_CATROM: - case FILTER_MITCHELL: - case FILTER_LANCZOS: - case FILTER_BLACKMAN_BESSEL: - case FILTER_BLACKMAN_SINC: - return pFilterType; - default: - throw new IllegalArgumentException("Unknown filter type: " + pFilterType); - } - } - - /** - * Gets the filter type specified by the given hints. - * - * @param pHints rendering hints - * @return a filter type constant - */ - private static int getFilterType(RenderingHints pHints) { - if (pHints == null) { - return FILTER_UNDEFINED; - } - - if (pHints.containsKey(KEY_RESAMPLE_INTERPOLATION)) { - Object value = pHints.get(KEY_RESAMPLE_INTERPOLATION); - // NOTE: Workaround for a bug in RenderingHints constructor (Bug id# 5084832) - if (!KEY_RESAMPLE_INTERPOLATION.isCompatibleValue(value)) { - throw new IllegalArgumentException(value + " incompatible with key " + KEY_RESAMPLE_INTERPOLATION); - } - return value != null ? ((Value) value).getFilterType() : FILTER_UNDEFINED; - } - 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 prioritize speed - return FILTER_POINT; - } - 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))) { - return FILTER_QUADRATIC;// No idea if this is correct..? - } - 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; - } - - // NOTE: Other hints are ignored - return FILTER_UNDEFINED; - } - - /** - * Re-samples (scales) the image to the size, and using the algorithm - * specified in the constructor. - * - * @param input The {@code BufferedImage} to be filtered - * @param output The {@code BufferedImage} in which to store the resampled - * image - * @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 input, final BufferedImage output) { - if (input == null) { - throw new NullPointerException("Input == null"); - } - 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 accelerated) - switch (filterType) { - case FILTER_POINT: - if (input.getType() != BufferedImage.TYPE_CUSTOM) { - return fastResample(input, output, width, height, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); - } - // 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) { - return fastResample(input, output, width, height, AffineTransformOp.TYPE_BICUBIC); - } - // Else fall through - default: - filter = createFilter(filterType); - // NOTE: Workaround for filter throwing exceptions when input or output is less than support... - 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, input, output); - if (result != null) { - return result; - } - - // Otherwise, continue in pure Java mode - - // TODO: What if output != null and wrong size? Create new? Render on only a part? Document? - - // If filter type != POINT or BOX and input has IndexColorModel, convert - // to true color, with alpha reflecting that of the original color model. - BufferedImage temp; - ColorModel cm; - 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 { - 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 = output != null && temp.getType() != BufferedImage.TYPE_CUSTOM ? /*output*/ ImageUtil.toBuffered(output, temp.getType()) : createCompatibleDestImage(temp, null); - - resample(temp, result, filter); - - // 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; - } - - /* - private static BufferedImage pointResample(final BufferedImage pInput, final BufferedImage pOutput, final int pWidth, final int pHeight) { - double xScale = pWidth / (double) pInput.getWidth(); - double yScale = pHeight / (double) pInput.getHeight(); - - // NOTE: This is extremely fast, native, possibly H/W accelerated code - AffineTransform transform = AffineTransform.getScaleInstance(xScale, yScale); - AffineTransformOp scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); - return scale.filter(pInput, pOutput); - } - */ - - /* - // TODO: This idea from Chet and Romain is actually not too bad.. - // It reuses the image/raster/graphics... - // However, they don't end with a halve operation.. - private static BufferedImage getFasterScaledInstance(BufferedImage img, - int targetWidth, int targetHeight, Object hint, - boolean progressiveBilinear) { - int type = (img.getTransparency() == Transparency.OPAQUE) ? - BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; - BufferedImage ret = img; - BufferedImage scratchImage = null; - Graphics2D g2 = null; - int w, h; - int prevW = ret.getWidth(); - int prevH = ret.getHeight(); - boolean isTranslucent = img.getTransparency() != Transparency.OPAQUE; - - if (progressiveBilinear) { - // Use multi-step technique: start with original size, then - // scale down in multiple passes with drawImage() - // until the target size is reached - w = img.getWidth(); - h = img.getHeight(); - } else { - // Use one-step technique: scale directly from original - // size to target size with a single drawImage() call - w = targetWidth; - h = targetHeight; - } - - do { - if (progressiveBilinear && w > targetWidth) { - w /= 2; - if (w < targetWidth) { - w = targetWidth; - } - } - - if (progressiveBilinear && h > targetHeight) { - h /= 2; - if (h < targetHeight) { - h = targetHeight; - } - } - - if (scratchImage == null || isTranslucent) { - // Use a single scratch buffer for all iterations - // and then copy to the final, correctly-sized image - // before returning - scratchImage = new BufferedImage(w, h, type); - g2 = scratchImage.createGraphics(); - } - g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); - g2.drawImage(ret, 0, 0, w, h, 0, 0, prevW, prevH, null); - prevW = w; - prevH = h; - - ret = scratchImage; - } while (w != targetWidth || h != targetHeight); - - if (g2 != null) { - g2.dispose(); - } - - // If we used a scratch buffer that is larger than our target size, - // create an image of the right size and copy the results into it - if (targetWidth != ret.getWidth() || targetHeight != ret.getHeight()) { - scratchImage = new BufferedImage(targetWidth, targetHeight, type); - g2 = scratchImage.createGraphics(); - g2.drawImage(ret, 0, 0, null); - g2.dispose(); - ret = scratchImage; - } - - return ret; - } - */ - - 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; - - AffineTransform transform; - AffineTransformOp scale; - - if (type > AffineTransformOp.TYPE_NEAREST_NEIGHBOR) { - // Initially scale so all remaining operations will halve the image - if (width < input.getWidth() || height < input.getHeight()) { - int w = width; - int h = height; - while (w < input.getWidth() / 2) { - w *= 2; - } - while (h < input.getHeight() / 2) { - h *= 2; - } - - xScale = w / (double) input.getWidth(); - yScale = h / (double) input.getHeight(); - - //System.out.println("First scale by x=" + xScale + ", y=" + yScale); - - transform = AffineTransform.getScaleInstance(xScale, yScale); - scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); - temp = scale.filter(temp, null); - } - } - - scale = null; // NOTE: This resets! - - xScale = width / (double) temp.getWidth(); - yScale = height / (double) temp.getHeight(); - - 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... - while (xScale < 0.5 || yScale < 0.5) { - if (xScale >= 0.5) { - //System.out.println("Halving by y=" + (yScale * 2.0)); - transform = AffineTransform.getScaleInstance(1.0, 0.5); - scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); - - yScale *= 2.0; - } - else if (yScale >= 0.5) { - //System.out.println("Halving by x=" + (xScale * 2.0)); - transform = AffineTransform.getScaleInstance(0.5, 1.0); - scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); - - xScale *= 2.0; - } - else { - //System.out.println("Halving by x=" + (xScale * 2.0) + ", y=" + (yScale * 2.0)); - xScale *= 2.0; - yScale *= 2.0; - } - - if (scale == null) { - transform = AffineTransform.getScaleInstance(0.5, 0.5); - scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); - } - - 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, type); - - return scale.filter(temp, output); - } - - /** - * Returns the current filter type constant. - * - * @return the current filter type constant. - * @see filter type constants - */ - public int getFilterType() { - return filterType; - } - - private static InterpolationFilter createFilter(int pFilterType) { - // TODO: Select correct filter based on scale up or down, if undefined! - if (pFilterType == FILTER_UNDEFINED) { - pFilterType = FILTER_LANCZOS; - } - - switch (pFilterType) { - case FILTER_POINT: - return new PointFilter(); - case FILTER_BOX: - return new BoxFilter(); - case FILTER_TRIANGLE: - return new TriangleFilter(); - case FILTER_HERMITE: - return new HermiteFilter(); - case FILTER_HANNING: - return new HanningFilter(); - case FILTER_HAMMING: - return new HammingFilter(); - case FILTER_BLACKMAN: - return new BlacmanFilter(); - case FILTER_GAUSSIAN: - return new GaussianFilter(); - case FILTER_QUADRATIC: - return new QuadraticFilter(); - case FILTER_CUBIC: - return new CubicFilter(); - case FILTER_CATROM: - return new CatromFilter(); - case FILTER_MITCHELL: - return new MitchellFilter(); - case FILTER_LANCZOS: - return new LanczosFilter(); - case FILTER_BLACKMAN_BESSEL: - return new BlackmanBesselFilter(); - case FILTER_BLACKMAN_SINC: - return new BlackmanSincFilter(); - default: - throw new IllegalStateException("Unknown filter type: " + pFilterType); - } - } - - public final BufferedImage createCompatibleDestImage(final BufferedImage pInput, final ColorModel pModel) { - if (pInput == null) { - throw new NullPointerException("pInput == null"); - } - - ColorModel cm = pModel != null ? pModel : pInput.getColorModel(); - - // TODO: Might not work with all colormodels.. - // 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, width, height), - cm.isAlphaPremultiplied(), null); - } - - public RenderingHints getRenderingHints() { - Object value; - switch (filterType) { - case FILTER_UNDEFINED: - return null; - case FILTER_POINT: - value = VALUE_INTERPOLATION_POINT; - break; - case FILTER_BOX: - value = VALUE_INTERPOLATION_BOX; - break; - case FILTER_TRIANGLE: - value = VALUE_INTERPOLATION_TRIANGLE; - break; - case FILTER_HERMITE: - value = VALUE_INTERPOLATION_HERMITE; - break; - case FILTER_HANNING: - value = VALUE_INTERPOLATION_HANNING; - break; - case FILTER_HAMMING: - value = VALUE_INTERPOLATION_HAMMING; - break; - case FILTER_BLACKMAN: - value = VALUE_INTERPOLATION_BLACKMAN; - break; - case FILTER_GAUSSIAN: - value = VALUE_INTERPOLATION_GAUSSIAN; - break; - case FILTER_QUADRATIC: - value = VALUE_INTERPOLATION_QUADRATIC; - break; - case FILTER_CUBIC: - value = VALUE_INTERPOLATION_CUBIC; - break; - case FILTER_CATROM: - value = VALUE_INTERPOLATION_CATROM; - break; - case FILTER_MITCHELL: - value = VALUE_INTERPOLATION_MITCHELL; - break; - case FILTER_LANCZOS: - value = VALUE_INTERPOLATION_LANCZOS; - break; - case FILTER_BLACKMAN_BESSEL: - value = VALUE_INTERPOLATION_BLACKMAN_BESSEL; - break; - case FILTER_BLACKMAN_SINC: - value = VALUE_INTERPOLATION_BLACKMAN_SINC; - break; - default: - throw new IllegalStateException("Unknown filter type: " + filterType); - } - - return new RenderingHints(KEY_RESAMPLE_INTERPOLATION, value); - } - - public Rectangle2D getBounds2D(BufferedImage src) { - return new Rectangle(width, height); - } - - public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { - // TODO: This is wrong... - // How can I possible know how much one point is scaled, without first knowing the ration?! - // TODO: Maybe set all points outside of bounds, inside? - // TODO: Assume input image of Integer.MAX_VAL x Integer.MAX_VAL?! ;-) - if (dstPt == null) { - if (srcPt instanceof Point2D.Double) { - dstPt = new Point2D.Double(); - } - else { - dstPt = new Point2D.Float(); - } - dstPt.setLocation(srcPt); - } - return dstPt; - } - - /* -- Java port of filter_rcg.c below... -- */ - - /* - * filter function definitions - */ - - interface InterpolationFilter { - double filter(double t); - - double support(); - } - - static class HermiteFilter implements InterpolationFilter { - public final double filter(double t) { - /* f(t) = 2|t|^3 - 3|t|^2 + 1, -1 <= t <= 1 */ - if (t < 0.0) { - t = -t; - } - if (t < 1.0) { - return (2.0 * t - 3.0) * t * t + 1.0; - } - return 0.0; - } - - public final double support() { - return 1.0; - } - } - - static class PointFilter extends BoxFilter { - public PointFilter() { - super(0.0); - } - } - - static class BoxFilter implements InterpolationFilter { - private final double mSupport; - - public BoxFilter() { - mSupport = 0.5; - } - - protected BoxFilter(double pSupport) { - mSupport = pSupport; - } - - public final double filter(final double t) { - //if ((t > -0.5) && (t <= 0.5)) { - if ((t >= -0.5) && (t < 0.5)) {// ImageMagick resample.c - return 1.0; - } - return 0.0; - } - - public final double support() { - return mSupport; - } - } - - static class TriangleFilter implements InterpolationFilter { - public final double filter(double t) { - if (t < 0.0) { - t = -t; - } - if (t < 1.0) { - return 1.0 - t; - } - return 0.0; - } - - public final double support() { - return 1.0; - } - } - - static class QuadraticFilter implements InterpolationFilter { - // AKA Bell - public final double filter(double t)/* box (*) box (*) box */ { - if (t < 0) { - t = -t; - } - if (t < .5) { - return .75 - (t * t); - } - if (t < 1.5) { - t = (t - 1.5); - return .5 * (t * t); - } - return 0.0; - } - - public final double support() { - return 1.5; - } - } - - static class CubicFilter implements InterpolationFilter { - // AKA B-Spline - public final double filter(double t)/* box (*) box (*) box (*) box */ { - final double tt; - - if (t < 0) { - t = -t; - } - if (t < 1) { - tt = t * t; - return (.5 * tt * t) - tt + (2.0 / 3.0); - } - else if (t < 2) { - t = 2 - t; - return (1.0 / 6.0) * (t * t * t); - } - return 0.0; - } - - public final double support() { - return 2.0; - } - } - - private static double sinc(double x) { - x *= Math.PI; - if (x != 0.0) { - return Math.sin(x) / x; - } - return 1.0; - } - - static class LanczosFilter implements InterpolationFilter { - // AKA Lanczos3 - public final double filter(double t) { - if (t < 0) { - t = -t; - } - if (t < 3.0) { - return sinc(t) * sinc(t / 3.0); - } - return 0.0; - } - - public final double support() { - return 3.0; - } - } - - private final static double B = 1.0 / 3.0; - private final static double C = 1.0 / 3.0; - private final static double P0 = (6.0 - 2.0 * B) / 6.0; - private final static double P2 = (-18.0 + 12.0 * B + 6.0 * C) / 6.0; - private final static double P3 = (12.0 - 9.0 * B - 6.0 * C) / 6.0; - private final static double Q0 = (8.0 * B + 24.0 * C) / 6.0; - private final static double Q1 = (-12.0 * B - 48.0 * C) / 6.0; - private final static double Q2 = (6.0 * B + 30.0 * C) / 6.0; - private final static double Q3 = (-1.0 * B - 6.0 * C) / 6.0; - - static class MitchellFilter implements InterpolationFilter { - public final double filter(double t) { - if (t < -2.0) { - return 0.0; - } - if (t < -1.0) { - return Q0 - t * (Q1 - t * (Q2 - t * Q3)); - } - if (t < 0.0) { - return P0 + t * t * (P2 - t * P3); - } - if (t < 1.0) { - return P0 + t * t * (P2 + t * P3); - } - if (t < 2.0) { - return Q0 + t * (Q1 + t * (Q2 + t * Q3)); - } - return 0.0; - } - - public final double support() { - return 2.0; - } - } - - private static double j1(final double t) { - final double[] pOne = { - 0.581199354001606143928050809e+21, - -0.6672106568924916298020941484e+20, - 0.2316433580634002297931815435e+19, - -0.3588817569910106050743641413e+17, - 0.2908795263834775409737601689e+15, - -0.1322983480332126453125473247e+13, - 0.3413234182301700539091292655e+10, - -0.4695753530642995859767162166e+7, - 0.270112271089232341485679099e+4 - }; - final double[] qOne = { - 0.11623987080032122878585294e+22, - 0.1185770712190320999837113348e+20, - 0.6092061398917521746105196863e+17, - 0.2081661221307607351240184229e+15, - 0.5243710262167649715406728642e+12, - 0.1013863514358673989967045588e+10, - 0.1501793594998585505921097578e+7, - 0.1606931573481487801970916749e+4, - 0.1e+1 - }; - - double p = pOne[8]; - double q = qOne[8]; - for (int i = 7; i >= 0; i--) { - p = p * t * t + pOne[i]; - q = q * t * t + qOne[i]; - } - return p / q; - } - - private static double p1(final double t) { - final double[] pOne = { - 0.352246649133679798341724373e+5, - 0.62758845247161281269005675e+5, - 0.313539631109159574238669888e+5, - 0.49854832060594338434500455e+4, - 0.2111529182853962382105718e+3, - 0.12571716929145341558495e+1 - }; - final double[] qOne = { - 0.352246649133679798068390431e+5, - 0.626943469593560511888833731e+5, - 0.312404063819041039923015703e+5, - 0.4930396490181088979386097e+4, - 0.2030775189134759322293574e+3, - 0.1e+1 - }; - - double p = pOne[5]; - double q = qOne[5]; - for (int i = 4; i >= 0; i--) { - p = p * (8.0 / t) * (8.0 / t) + pOne[i]; - q = q * (8.0 / t) * (8.0 / t) + qOne[i]; - } - return p / q; - } - - private static double q1(final double t) { - final double[] pOne = { - 0.3511751914303552822533318e+3, - 0.7210391804904475039280863e+3, - 0.4259873011654442389886993e+3, - 0.831898957673850827325226e+2, - 0.45681716295512267064405e+1, - 0.3532840052740123642735e-1 - }; - final double[] qOne = { - 0.74917374171809127714519505e+4, - 0.154141773392650970499848051e+5, - 0.91522317015169922705904727e+4, - 0.18111867005523513506724158e+4, - 0.1038187585462133728776636e+3, - 0.1e+1 - }; - - double p = pOne[5]; - double q = qOne[5]; - for (int i = 4; i >= 0; i--) { - p = p * (8.0 / t) * (8.0 / t) + pOne[i]; - q = q * (8.0 / t) * (8.0 / t) + qOne[i]; - } - return p / q; - } - - static double besselOrderOne(double t) { - double p, q; - - if (t == 0.0) { - return 0.0; - } - p = t; - if (t < 0.0) { - t = -t; - } - if (t < 8.0) { - return p * j1(t); - } - q = Math.sqrt(2.0 / (Math.PI * t)) * (p1(t) * (1.0 / Math.sqrt(2.0) * (Math.sin(t) - Math.cos(t))) - 8.0 / t * q1(t) * - (-1.0 / Math.sqrt(2.0) * (Math.sin(t) + Math.cos(t)))); - if (p < 0.0) { - q = -q; - } - return q; - } - - private static double bessel(final double t) { - if (t == 0.0) { - return Math.PI / 4.0; - } - return besselOrderOne(Math.PI * t) / (2.0 * t); - } - - private static double blackman(final double t) { - return 0.42 + 0.50 * Math.cos(Math.PI * t) + 0.08 * Math.cos(2.0 * Math.PI * t); - } - - static class BlacmanFilter implements InterpolationFilter { - public final double filter(final double t) { - return blackman(t); - } - - public final double support() { - return 1.0; - } - } - - static class CatromFilter implements InterpolationFilter { - public final double filter(double t) { - if (t < 0) { - t = -t; - } - if (t < 1.0) { - return 0.5 * (2.0 + t * t * (-5.0 + t * 3.0)); - } - if (t < 2.0) { - return 0.5 * (4.0 + t * (-8.0 + t * (5.0 - t))); - } - return 0.0; - } - - public final double support() { - return 2.0; - } - } - - static class GaussianFilter implements InterpolationFilter { - public final double filter(final double t) { - return Math.exp(-2.0 * t * t) * Math.sqrt(2.0 / Math.PI); - } - - public final double support() { - return 1.25; - } - } - - static class HanningFilter implements InterpolationFilter { - public final double filter(final double t) { - return 0.5 + 0.5 * Math.cos(Math.PI * t); - } - - public final double support() { - return 1.0; - } - } - - static class HammingFilter implements InterpolationFilter { - public final double filter(final double t) { - return 0.54 + 0.46 * Math.cos(Math.PI * t); - } - - public final double support() { - return 1.0; - } - } - - static class BlackmanBesselFilter implements InterpolationFilter { - public final double filter(final double t) { - return blackman(t / support()) * bessel(t); - } - - public final double support() { - return 3.2383; - } - } - - static class BlackmanSincFilter implements InterpolationFilter { - public final double filter(final double t) { - return blackman(t / support()) * sinc(t); - } - - public final double support() { - return 4.0; - } - } - - /* - * image rescaling routine - */ - class Contributor { - int pixel; - double weight; - } - - class ContributorList { - int n;/* number of contributors (may be < p.length) */ - Contributor[] p;/* pointer to list of contributions */ - } - - /* - round() - - Round an FP value to its closest int representation. - General routine; ideally belongs in general math lib file. - */ - - static int round(double d) { - // NOTE: This code seems to be faster than Math.round(double)... - // Version that uses no function calls at all. - int n = (int) d; - double diff = d - (double) n; - if (diff < 0) { - diff = -diff; - } - if (diff >= 0.5) { - if (d < 0) { - n--; - } - else { - n++; - } - } - return n; - }/* round */ - - /* - calcXContrib() - - Calculates the filter weights for a single target column. - contribX->p must be freed afterwards. - - Returns -1 if error, 0 otherwise. - */ - private ContributorList calcXContrib(double xscale, double fwidth, int srcwidth, InterpolationFilter pFilter, int i) { - // TODO: What to do when fwidth > srcwidyj or dstwidth - - double width; - double fscale; - double center; - double weight; - - ContributorList contribX = new ContributorList(); - - if (xscale < 1.0) { - /* Shrinking image */ - width = fwidth / xscale; - fscale = 1.0 / xscale; - - if (width <= .5) { - // Reduce to point sampling. - width = .5 + 1.0e-6; - fscale = 1.0; - } - - //contribX.n = 0; - contribX.p = new Contributor[(int) (width * 2.0 + 1.0 + 0.5)]; - - center = (double) i / xscale; - int left = (int) Math.ceil(center - width);// Note: Assumes width <= .5 - int right = (int) Math.floor(center + width); - - double density = 0.0; - - for (int j = left; j <= right; j++) { - weight = center - (double) j; - weight = pFilter.filter(weight / fscale) / fscale; - int n; - if (j < 0) { - n = -j; - } - else if (j >= srcwidth) { - n = (srcwidth - j) + srcwidth - 1; - } - else { - n = j; - } - - /**/ - if (n >= srcwidth) { - n = n % srcwidth; - } - else if (n < 0) { - n = srcwidth - 1; - } - /**/ - - int k = contribX.n++; - contribX.p[k] = new Contributor(); - contribX.p[k].pixel = n; - contribX.p[k].weight = weight; - - density += weight; - - } - - if ((density != 0.0) && (density != 1.0)) { - //Normalize. - density = 1.0 / density; - for (int k = 0; k < contribX.n; k++) { - contribX.p[k].weight *= density; - } - } - } - else { - /* Expanding image */ - //contribX.n = 0; - contribX.p = new Contributor[(int) (fwidth * 2.0 + 1.0 + 0.5)]; - - center = (double) i / xscale; - int left = (int) Math.ceil(center - fwidth); - int right = (int) Math.floor(center + fwidth); - - for (int j = left; j <= right; j++) { - weight = center - (double) j; - weight = pFilter.filter(weight); - - int n; - if (j < 0) { - n = -j; - } - else if (j >= srcwidth) { - n = (srcwidth - j) + srcwidth - 1; - } - else { - n = j; - } - - /**/ - if (n >= srcwidth) { - n = n % srcwidth; - } - else if (n < 0) { - n = srcwidth - 1; - } - /**/ - - int k = contribX.n++; - contribX.p[k] = new Contributor(); - contribX.p[k].pixel = n; - contribX.p[k].weight = weight; - } - } - return contribX; - }/* calcXContrib */ - - /* - resample() - - Resizes bitmaps while resampling them. - */ - private BufferedImage resample(BufferedImage pSource, BufferedImage pDest, InterpolationFilter pFilter) { - final int dstWidth = pDest.getWidth(); - final int dstHeight = pDest.getHeight(); - - final int srcWidth = pSource.getWidth(); - final int srcHeight = pSource.getHeight(); - - /* create intermediate column to hold horizontal dst column zoom */ - final ColorModel cm = pSource.getColorModel(); -// 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; - - ContributorList[] contribY = new ContributorList[dstHeight]; - for (int i = 0; i < contribY.length; i++) { - contribY[i] = new ContributorList(); - } - - // TODO: What to do when fwidth > srcHeight or dstHeight - double fwidth = pFilter.support(); - if (yscale < 1.0) { - double width = fwidth / yscale; - double fscale = 1.0 / yscale; - - if (width <= .5) { - // Reduce to point sampling. - width = .5 + 1.0e-6; - fscale = 1.0; - } - - for (int i = 0; i < dstHeight; i++) { - //contribY[i].n = 0; - contribY[i].p = new Contributor[(int) (width * 2.0 + 1 + 0.5)]; - - double center = (double) i / yscale; - int left = (int) Math.ceil(center - width); - int right = (int) Math.floor(center + width); - - double density = 0.0; - - for (int j = left; j <= right; j++) { - double weight = center - (double) j; - weight = pFilter.filter(weight / fscale) / fscale; - int n; - if (j < 0) { - n = -j; - } - else if (j >= srcHeight) { - n = (srcHeight - j) + srcHeight - 1; - } - else { - n = j; - } - - /**/ - if (n >= srcHeight) { - n = n % srcHeight; - } - else if (n < 0) { - n = srcHeight - 1; - } - /**/ - - int k = contribY[i].n++; - contribY[i].p[k] = new Contributor(); - contribY[i].p[k].pixel = n; - contribY[i].p[k].weight = weight; - - density += weight; - } - - if ((density != 0.0) && (density != 1.0)) { - //Normalize. - density = 1.0 / density; - for (int k = 0; k < contribY[i].n; k++) { - contribY[i].p[k].weight *= density; - } - } - } - } - else { - for (int i = 0; i < dstHeight; ++i) { - //contribY[i].n = 0; - contribY[i].p = new Contributor[(int) (fwidth * 2 + 1 + 0.5)]; - - double center = (double) i / yscale; - double left = Math.ceil(center - fwidth); - double right = Math.floor(center + fwidth); - for (int j = (int) left; j <= right; ++j) { - double weight = center - (double) j; - weight = pFilter.filter(weight); - int n; - if (j < 0) { - n = -j; - } - else if (j >= srcHeight) { - n = (srcHeight - j) + srcHeight - 1; - } - else { - n = j; - } - - /**/ - if (n >= srcHeight) { - n = n % srcHeight; - } - else if (n < 0) { - n = srcHeight - 1; - } - /**/ - - int k = contribY[i].n++; - contribY[i].p[k] = new Contributor(); - contribY[i].p[k].pixel = n; - contribY[i].p[k].weight = weight; - } - } - } - - final Raster raster = pSource.getRaster(); - 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 TODO?) - final int numChannels = raster.getNumBands(); - final int[] channelMax = new int[numChannels]; - for (int k = 0; k < numChannels; k++) { - channelMax[k] = (1 << pSource.getColorModel().getComponentSize(k)) - 1; - } - - for (int xx = 0; xx < dstWidth; xx++) { - ContributorList contribX = calcXContrib(xscale, fwidth, srcWidth, pFilter, xx); - /* Apply horiz filter to make dst column in tmp. */ - for (int k = 0; k < srcHeight; k++) { - for (int channel = 0; channel < numChannels; channel++) { - - double weight = 0.0; - boolean bPelDelta = false; - // TODO: This line throws index out of bounds, if the image - // is smaller than filter.support() - double pel = raster.getSample(contribX.p[0].pixel, k, channel); - for (int j = 0; j < contribX.n; j++) { - double pel2 = j == 0 ? pel : raster.getSample(contribX.p[j].pixel, k, channel); - if (pel2 != pel) { - bPelDelta = true; - } - weight += pel2 * contribX.p[j].weight; - } - weight = bPelDelta ? round(weight) : pel; - - if (weight < 0) { - weight = 0; - } - else if (weight > channelMax[channel]) { - weight = channelMax[channel]; - } - - work.setSample(0, k, channel, weight); - - } - }/* next row in temp column */ - - /* The temp column has been built. Now stretch it vertically into dst column. */ - for (int i = 0; i < dstHeight; i++) { - for (int channel = 0; channel < numChannels; channel++) { - - double weight = 0.0; - boolean bPelDelta = false; - double pel = work.getSample(0, contribY[i].p[0].pixel, channel); - - for (int j = 0; j < contribY[i].n; j++) { - // TODO: This line throws index out of bounds, if the image - // is smaller than filter.support() - double pel2 = j == 0 ? pel : work.getSample(0, contribY[i].p[j].pixel, channel); - if (pel2 != pel) { - bPelDelta = true; - } - weight += pel2 * contribY[i].p[j].weight; - } - weight = bPelDelta ? round(weight) : pel; - if (weight < 0) { - weight = 0; - } - else if (weight > channelMax[channel]) { - weight = channelMax[channel]; - } - - out.setSample(xx, i, channel, weight); - } - }/* next dst row */ - }/* next dst column */ - return pDest; - }/* resample */ +/* + * 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 of the copyright holder 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 HOLDER 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. + */ +/* + ******************************************************************************* + * + * Based on example code found in Graphics Gems III, Filtered Image Rescaling + * (filter_rcg.c), available from http://www.acm.org/tog/GraphicsGems/. + * + * Public Domain 1991 by Dale Schumacher. Mods by Ray Gardener + * + * Original by Dale Schumacher (fzoom) + * + * Additional changes by Ray Gardener, Daylon Graphics Ltd. + * December 4, 1999 + * + ******************************************************************************* + * + * Aditional changes inspired by ImageMagick's resize.c. + * + ******************************************************************************* + * + * Java port and additional changes/bugfixes by Harald Kuhr, Twelvemonkeys. + * February 20, 2006 + * + ******************************************************************************* + */ + +package com.twelvemonkeys.image; + +import java.awt.*; +import java.awt.geom.AffineTransform; +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. + * Several different interpolation algorithms may be specifed in the + * constructor, either using the + * filter type constants, or one of the + * {@code RendereingHints}. + *

+ * For fastest results, use {@link #FILTER_POINT} or {@link #FILTER_BOX}. + * In most cases, {@link #FILTER_TRIANGLE} will produce acceptable results, while + * being relatively fast. + * For higher quality output, use more sophisticated interpolation algorithms, + * like {@link #FILTER_MITCHELL} or {@link #FILTER_LANCZOS}. + *

+ *

+ * Example: + *

+ *
+ * BufferedImage image;
+ *
+ * //...
+ *
+ * ResampleOp resampler = new ResampleOp(100, 100, ResampleOp.FILTER_TRIANGLE);
+ * BufferedImage thumbnail = resampler.filter(image, null);
+ * 
+ *

+ * If your input image is very large, it's possible to first resample using the + * very fast {@code FILTER_POINT} algorithm, then resample to the wanted size, + * using a higher quality algorithm: + *

+ *
+ * BufferedImage verylLarge;
+ *
+ * //...
+ *
+ * int w = 300;
+ * int h = 200;
+ *
+ * BufferedImage temp = new ResampleOp(w * 2, h * 2, FILTER_POINT).filter(verylLarge, null);
+ *
+ * BufferedImage scaled = new ResampleOp(w, h).filter(temp, null);
+ * 
+ *

+ * For maximum performance, this class will use native code, through + * JMagick, when available. + * Otherwise, the class will silently fall back to pure Java mode. + * Native code may be disabled globally, by setting the system property + * {@code com.twelvemonkeys.image.accel} to {@code false}. + * To allow debug of the native code, set the system property + * {@code com.twelvemonkeys.image.magick.debug} to {@code true}. + *

+ *

+ * This {@code BufferedImageOp} is based on C example code found in + * Graphics Gems III, + * Filtered Image Rescaling, by Dale Schumacher (with additional improvments by + * Ray Gardener). + * Additional changes are inspired by + * ImageMagick and + * Marco Schmidt's Java Imaging Utilities + * (which are also adaptions of the same original code from Graphics Gems III). + *

+ *

+ * For a description of the various interpolation algorithms, see + * General Filtered Image Rescaling in Graphics Gems III, + * Academic Press, 1994. + *

+ * + * @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/ResampleOp.java#1 $ + * @see #ResampleOp(int,int,int) + * @see #ResampleOp(int,int,java.awt.RenderingHints) + * @see BufferedImage + * @see RenderingHints + * @see AffineTransformOp + */ +// TODO: Consider using AffineTransformOp for more operations!? +public class ResampleOp implements BufferedImageOp/* TODO: RasterOp */ { + + // NOTE: These MUST correspond to ImageMagick filter types, for the + // MagickAccelerator to work consistently (see magick.FilterType). + + /** + * Undefined interpolation, filter method will use default filter. + */ + public final static int FILTER_UNDEFINED = 0; + /** + * Point interpolation (also known as "nearest neighbour"). + * Very fast, but low quality + * (similar to {@link RenderingHints#VALUE_INTERPOLATION_NEAREST_NEIGHBOR} + * and {@link Image#SCALE_REPLICATE}). + */ + public final static int FILTER_POINT = 1; + /** + * Box interpolation. Fast, but low quality. + */ + public final static int FILTER_BOX = 2; + /** + * Triangle interpolation (also known as "linear" or "bilinear"). + * Quite fast, with acceptable quality + * (similar to {@link RenderingHints#VALUE_INTERPOLATION_BILINEAR} and + * {@link Image#SCALE_AREA_AVERAGING}). + */ + public final static int FILTER_TRIANGLE = 3; + /** + * Hermite interpolation. + */ + public final static int FILTER_HERMITE = 4; + /** + * Hanning interpolation. + */ + public final static int FILTER_HANNING = 5; + /** + * Hamming interpolation. + */ + public final static int FILTER_HAMMING = 6; + /** + * Blackman interpolation.. + */ + public final static int FILTER_BLACKMAN = 7; + /** + * Gaussian interpolation. + */ + public final static int FILTER_GAUSSIAN = 8; + /** + * Quadratic interpolation. + */ + public final static int FILTER_QUADRATIC = 9; + /** + * Cubic interpolation. + */ + public final static int FILTER_CUBIC = 10; + /** + * Catrom interpolation. + */ + public final static int FILTER_CATROM = 11; + /** + * Mitchell interpolation. High quality. + */ + 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 + /** + * Blackman-Bessel interpolation. High quality. + */ + public final static int FILTER_BLACKMAN_BESSEL = 14; + /** + * Blackman-Sinc interpolation. High quality. + */ + public final static int FILTER_BLACKMAN_SINC = 15; + + /** + * RenderingHints.Key specifying resampling interpolation algorithm. + */ + public final static RenderingHints.Key KEY_RESAMPLE_INTERPOLATION = new Key("ResampleInterpolation"); + + /** + * @see #FILTER_POINT + */ + public final static Object VALUE_INTERPOLATION_POINT = + new Value(KEY_RESAMPLE_INTERPOLATION, "Point", FILTER_POINT); + /** + * @see #FILTER_BOX + */ + public final static Object VALUE_INTERPOLATION_BOX = + new Value(KEY_RESAMPLE_INTERPOLATION, "Box", FILTER_BOX); + /** + * @see #FILTER_TRIANGLE + */ + public final static Object VALUE_INTERPOLATION_TRIANGLE = + new Value(KEY_RESAMPLE_INTERPOLATION, "Triangle", FILTER_TRIANGLE); + /** + * @see #FILTER_HERMITE + */ + public final static Object VALUE_INTERPOLATION_HERMITE = + new Value(KEY_RESAMPLE_INTERPOLATION, "Hermite", FILTER_HERMITE); + /** + * @see #FILTER_HANNING + */ + public final static Object VALUE_INTERPOLATION_HANNING = + new Value(KEY_RESAMPLE_INTERPOLATION, "Hanning", FILTER_HANNING); + /** + * @see #FILTER_HAMMING + */ + public final static Object VALUE_INTERPOLATION_HAMMING = + new Value(KEY_RESAMPLE_INTERPOLATION, "Hamming", FILTER_HAMMING); + /** + * @see #FILTER_BLACKMAN + */ + public final static Object VALUE_INTERPOLATION_BLACKMAN = + new Value(KEY_RESAMPLE_INTERPOLATION, "Blackman", FILTER_BLACKMAN); + /** + * @see #FILTER_GAUSSIAN + */ + public final static Object VALUE_INTERPOLATION_GAUSSIAN = + new Value(KEY_RESAMPLE_INTERPOLATION, "Gaussian", FILTER_GAUSSIAN); + /** + * @see #FILTER_QUADRATIC + */ + public final static Object VALUE_INTERPOLATION_QUADRATIC = + new Value(KEY_RESAMPLE_INTERPOLATION, "Quadratic", FILTER_QUADRATIC); + /** + * @see #FILTER_CUBIC + */ + public final static Object VALUE_INTERPOLATION_CUBIC = + new Value(KEY_RESAMPLE_INTERPOLATION, "Cubic", FILTER_CUBIC); + /** + * @see #FILTER_CATROM + */ + public final static Object VALUE_INTERPOLATION_CATROM = + new Value(KEY_RESAMPLE_INTERPOLATION, "Catrom", FILTER_CATROM); + /** + * @see #FILTER_MITCHELL + */ + public final static Object VALUE_INTERPOLATION_MITCHELL = + new Value(KEY_RESAMPLE_INTERPOLATION, "Mitchell", FILTER_MITCHELL); + /** + * @see #FILTER_LANCZOS + */ + public final static Object VALUE_INTERPOLATION_LANCZOS = + new Value(KEY_RESAMPLE_INTERPOLATION, "Lanczos", FILTER_LANCZOS); + /** + * @see #FILTER_BLACKMAN_BESSEL + */ + public final static Object VALUE_INTERPOLATION_BLACKMAN_BESSEL = + new Value(KEY_RESAMPLE_INTERPOLATION, "Blackman-Bessel", FILTER_BLACKMAN_BESSEL); + /** + * @see #FILTER_BLACKMAN_SINC + */ + public final static Object VALUE_INTERPOLATION_BLACKMAN_SINC = + new Value(KEY_RESAMPLE_INTERPOLATION, "Blackman-Sinc", FILTER_BLACKMAN_SINC); + + // Member variables + // Package access, to allow access from MagickAccelerator + int width; + int height; + + int filterType; + + /** + * RendereingHints.Key implementation, works only with Value values. + */ + // TODO: Move to abstract class AbstractBufferedImageOp? + static class Key extends RenderingHints.Key { + static int sIndex = 10000; + + private final String name; + + public Key(final String pName) { + super(sIndex++); + name = pName; + } + + public boolean isCompatibleValue(Object pValue) { + return pValue instanceof Value && ((Value) pValue).isCompatibleKey(this); + } + + public String toString() { + return name; + } + } + + /** + * RenderingHints value implementation, works with Key keys. + */ + // TODO: Extract abstract Value class, and move to AbstractBufferedImageOp + static final class Value { + 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) { + key = pKey; + name = pName; + type = validateFilterType(pType); + } + + public boolean isCompatibleKey(Key pKey) { + return pKey == key; + } + + public int getFilterType() { + return type; + } + + public String toString() { + return name; + } + } + + /** + * Creates a {@code ResampleOp} that will resample input images to the + * given width and height, using the default interpolation filter. + * + * @param width width of the re-sampled image + * @param height height of the re-sampled image + */ + public ResampleOp(int width, int height) { + this(width, height, FILTER_UNDEFINED); + } + + /** + * Creates a {@code ResampleOp} that will resample input images to the + * given width and height, using the interpolation filter specified by + * the given hints. + *

+ * If using {@code RenderingHints}, the hints are mapped as follows: + *

+ *
    + *
  • {@code KEY_RESAMPLE_INTERPOLATION} takes precedence over any + * standard {@code java.awt} hints, and dictates interpolation + * directly, see + * {@code RenderingHints} constants.
  • + *
  • {@code KEY_INTERPOLATION} takes precedence over other hints. + *
      + *
    • {@link RenderingHints#VALUE_INTERPOLATION_NEAREST_NEIGHBOR} specifies + * {@code FILTER_POINT}
    • + *
    • {@link RenderingHints#VALUE_INTERPOLATION_BILINEAR} specifies + * {@code FILTER_TRIANGLE}
    • + *
    • {@link RenderingHints#VALUE_INTERPOLATION_BICUBIC} specifies + * {@code FILTER_QUADRATIC}
    • + *
    + *
  • + *
  • {@code KEY_RENDERING} or {@code KEY_COLOR_RENDERING} + *
      + *
    • {@link RenderingHints#VALUE_RENDER_SPEED} specifies + * {@code FILTER_POINT}
    • + *
    • {@link RenderingHints#VALUE_RENDER_QUALITY} specifies + * {@code FILTER_MITCHELL}
    • + *
    + *
  • + *
+ *

+ * Other hints have no effect on this filter. + *

+ * + * @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 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 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 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"); + } + + this.width = width; + this.height = height; + + this.filterType = validateFilterType(filterType); + } + + private static int validateFilterType(int pFilterType) { + switch (pFilterType) { + case FILTER_UNDEFINED: + case FILTER_POINT: + case FILTER_BOX: + case FILTER_TRIANGLE: + case FILTER_HERMITE: + case FILTER_HANNING: + case FILTER_HAMMING: + case FILTER_BLACKMAN: + case FILTER_GAUSSIAN: + case FILTER_QUADRATIC: + case FILTER_CUBIC: + case FILTER_CATROM: + case FILTER_MITCHELL: + case FILTER_LANCZOS: + case FILTER_BLACKMAN_BESSEL: + case FILTER_BLACKMAN_SINC: + return pFilterType; + default: + throw new IllegalArgumentException("Unknown filter type: " + pFilterType); + } + } + + /** + * Gets the filter type specified by the given hints. + * + * @param pHints rendering hints + * @return a filter type constant + */ + private static int getFilterType(RenderingHints pHints) { + if (pHints == null) { + return FILTER_UNDEFINED; + } + + if (pHints.containsKey(KEY_RESAMPLE_INTERPOLATION)) { + Object value = pHints.get(KEY_RESAMPLE_INTERPOLATION); + // NOTE: Workaround for a bug in RenderingHints constructor (Bug id# 5084832) + if (!KEY_RESAMPLE_INTERPOLATION.isCompatibleValue(value)) { + throw new IllegalArgumentException(value + " incompatible with key " + KEY_RESAMPLE_INTERPOLATION); + } + return value != null ? ((Value) value).getFilterType() : FILTER_UNDEFINED; + } + 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 prioritize speed + return FILTER_POINT; + } + 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))) { + return FILTER_QUADRATIC;// No idea if this is correct..? + } + 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; + } + + // NOTE: Other hints are ignored + return FILTER_UNDEFINED; + } + + /** + * Re-samples (scales) the image to the size, and using the algorithm + * specified in the constructor. + * + * @param input The {@code BufferedImage} to be filtered + * @param output The {@code BufferedImage} in which to store the resampled + * image + * @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 input, final BufferedImage output) { + if (input == null) { + throw new NullPointerException("Input == null"); + } + 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 accelerated) + switch (filterType) { + case FILTER_POINT: + if (input.getType() != BufferedImage.TYPE_CUSTOM) { + return fastResample(input, output, width, height, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + } + // 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) { + return fastResample(input, output, width, height, AffineTransformOp.TYPE_BICUBIC); + } + // Else fall through + default: + filter = createFilter(filterType); + // NOTE: Workaround for filter throwing exceptions when input or output is less than support... + 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, input, output); + if (result != null) { + return result; + } + + // Otherwise, continue in pure Java mode + + // TODO: What if output != null and wrong size? Create new? Render on only a part? Document? + + // If filter type != POINT or BOX and input has IndexColorModel, convert + // to true color, with alpha reflecting that of the original color model. + BufferedImage temp; + ColorModel cm; + 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 { + 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 = output != null && temp.getType() != BufferedImage.TYPE_CUSTOM ? /*output*/ ImageUtil.toBuffered(output, temp.getType()) : createCompatibleDestImage(temp, null); + + resample(temp, result, filter); + + // 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; + } + + /* + private static BufferedImage pointResample(final BufferedImage pInput, final BufferedImage pOutput, final int pWidth, final int pHeight) { + double xScale = pWidth / (double) pInput.getWidth(); + double yScale = pHeight / (double) pInput.getHeight(); + + // NOTE: This is extremely fast, native, possibly H/W accelerated code + AffineTransform transform = AffineTransform.getScaleInstance(xScale, yScale); + AffineTransformOp scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + return scale.filter(pInput, pOutput); + } + */ + + /* + // TODO: This idea from Chet and Romain is actually not too bad.. + // It reuses the image/raster/graphics... + // However, they don't end with a halve operation.. + private static BufferedImage getFasterScaledInstance(BufferedImage img, + int targetWidth, int targetHeight, Object hint, + boolean progressiveBilinear) { + int type = (img.getTransparency() == Transparency.OPAQUE) ? + BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB; + BufferedImage ret = img; + BufferedImage scratchImage = null; + Graphics2D g2 = null; + int w, h; + int prevW = ret.getWidth(); + int prevH = ret.getHeight(); + boolean isTranslucent = img.getTransparency() != Transparency.OPAQUE; + + if (progressiveBilinear) { + // Use multi-step technique: start with original size, then + // scale down in multiple passes with drawImage() + // until the target size is reached + w = img.getWidth(); + h = img.getHeight(); + } else { + // Use one-step technique: scale directly from original + // size to target size with a single drawImage() call + w = targetWidth; + h = targetHeight; + } + + do { + if (progressiveBilinear && w > targetWidth) { + w /= 2; + if (w < targetWidth) { + w = targetWidth; + } + } + + if (progressiveBilinear && h > targetHeight) { + h /= 2; + if (h < targetHeight) { + h = targetHeight; + } + } + + if (scratchImage == null || isTranslucent) { + // Use a single scratch buffer for all iterations + // and then copy to the final, correctly-sized image + // before returning + scratchImage = new BufferedImage(w, h, type); + g2 = scratchImage.createGraphics(); + } + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, hint); + g2.drawImage(ret, 0, 0, w, h, 0, 0, prevW, prevH, null); + prevW = w; + prevH = h; + + ret = scratchImage; + } while (w != targetWidth || h != targetHeight); + + if (g2 != null) { + g2.dispose(); + } + + // If we used a scratch buffer that is larger than our target size, + // create an image of the right size and copy the results into it + if (targetWidth != ret.getWidth() || targetHeight != ret.getHeight()) { + scratchImage = new BufferedImage(targetWidth, targetHeight, type); + g2 = scratchImage.createGraphics(); + g2.drawImage(ret, 0, 0, null); + g2.dispose(); + ret = scratchImage; + } + + return ret; + } + */ + + 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; + + AffineTransform transform; + AffineTransformOp scale; + + if (type > AffineTransformOp.TYPE_NEAREST_NEIGHBOR) { + // Initially scale so all remaining operations will halve the image + if (width < input.getWidth() || height < input.getHeight()) { + int w = width; + int h = height; + while (w < input.getWidth() / 2) { + w *= 2; + } + while (h < input.getHeight() / 2) { + h *= 2; + } + + xScale = w / (double) input.getWidth(); + yScale = h / (double) input.getHeight(); + + //System.out.println("First scale by x=" + xScale + ", y=" + yScale); + + transform = AffineTransform.getScaleInstance(xScale, yScale); + scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); + temp = scale.filter(temp, null); + } + } + + scale = null; // NOTE: This resets! + + xScale = width / (double) temp.getWidth(); + yScale = height / (double) temp.getHeight(); + + 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... + while (xScale < 0.5 || yScale < 0.5) { + if (xScale >= 0.5) { + //System.out.println("Halving by y=" + (yScale * 2.0)); + transform = AffineTransform.getScaleInstance(1.0, 0.5); + scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); + + yScale *= 2.0; + } + else if (yScale >= 0.5) { + //System.out.println("Halving by x=" + (xScale * 2.0)); + transform = AffineTransform.getScaleInstance(0.5, 1.0); + scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); + + xScale *= 2.0; + } + else { + //System.out.println("Halving by x=" + (xScale * 2.0) + ", y=" + (yScale * 2.0)); + xScale *= 2.0; + yScale *= 2.0; + } + + if (scale == null) { + transform = AffineTransform.getScaleInstance(0.5, 0.5); + scale = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); + } + + 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, type); + + return scale.filter(temp, output); + } + + /** + * Returns the current filter type constant. + * + * @return the current filter type constant. + * @see filter type constants + */ + public int getFilterType() { + return filterType; + } + + private static InterpolationFilter createFilter(int pFilterType) { + // TODO: Select correct filter based on scale up or down, if undefined! + if (pFilterType == FILTER_UNDEFINED) { + pFilterType = FILTER_LANCZOS; + } + + switch (pFilterType) { + case FILTER_POINT: + return new PointFilter(); + case FILTER_BOX: + return new BoxFilter(); + case FILTER_TRIANGLE: + return new TriangleFilter(); + case FILTER_HERMITE: + return new HermiteFilter(); + case FILTER_HANNING: + return new HanningFilter(); + case FILTER_HAMMING: + return new HammingFilter(); + case FILTER_BLACKMAN: + return new BlacmanFilter(); + case FILTER_GAUSSIAN: + return new GaussianFilter(); + case FILTER_QUADRATIC: + return new QuadraticFilter(); + case FILTER_CUBIC: + return new CubicFilter(); + case FILTER_CATROM: + return new CatromFilter(); + case FILTER_MITCHELL: + return new MitchellFilter(); + case FILTER_LANCZOS: + return new LanczosFilter(); + case FILTER_BLACKMAN_BESSEL: + return new BlackmanBesselFilter(); + case FILTER_BLACKMAN_SINC: + return new BlackmanSincFilter(); + default: + throw new IllegalStateException("Unknown filter type: " + pFilterType); + } + } + + public final BufferedImage createCompatibleDestImage(final BufferedImage pInput, final ColorModel pModel) { + if (pInput == null) { + throw new NullPointerException("pInput == null"); + } + + ColorModel cm = pModel != null ? pModel : pInput.getColorModel(); + + // TODO: Might not work with all colormodels.. + // 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, width, height), + cm.isAlphaPremultiplied(), null); + } + + public RenderingHints getRenderingHints() { + Object value; + switch (filterType) { + case FILTER_UNDEFINED: + return null; + case FILTER_POINT: + value = VALUE_INTERPOLATION_POINT; + break; + case FILTER_BOX: + value = VALUE_INTERPOLATION_BOX; + break; + case FILTER_TRIANGLE: + value = VALUE_INTERPOLATION_TRIANGLE; + break; + case FILTER_HERMITE: + value = VALUE_INTERPOLATION_HERMITE; + break; + case FILTER_HANNING: + value = VALUE_INTERPOLATION_HANNING; + break; + case FILTER_HAMMING: + value = VALUE_INTERPOLATION_HAMMING; + break; + case FILTER_BLACKMAN: + value = VALUE_INTERPOLATION_BLACKMAN; + break; + case FILTER_GAUSSIAN: + value = VALUE_INTERPOLATION_GAUSSIAN; + break; + case FILTER_QUADRATIC: + value = VALUE_INTERPOLATION_QUADRATIC; + break; + case FILTER_CUBIC: + value = VALUE_INTERPOLATION_CUBIC; + break; + case FILTER_CATROM: + value = VALUE_INTERPOLATION_CATROM; + break; + case FILTER_MITCHELL: + value = VALUE_INTERPOLATION_MITCHELL; + break; + case FILTER_LANCZOS: + value = VALUE_INTERPOLATION_LANCZOS; + break; + case FILTER_BLACKMAN_BESSEL: + value = VALUE_INTERPOLATION_BLACKMAN_BESSEL; + break; + case FILTER_BLACKMAN_SINC: + value = VALUE_INTERPOLATION_BLACKMAN_SINC; + break; + default: + throw new IllegalStateException("Unknown filter type: " + filterType); + } + + return new RenderingHints(KEY_RESAMPLE_INTERPOLATION, value); + } + + public Rectangle2D getBounds2D(BufferedImage src) { + return new Rectangle(width, height); + } + + public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { + // TODO: This is wrong... + // How can I possible know how much one point is scaled, without first knowing the ration?! + // TODO: Maybe set all points outside of bounds, inside? + // TODO: Assume input image of Integer.MAX_VAL x Integer.MAX_VAL?! ;-) + if (dstPt == null) { + if (srcPt instanceof Point2D.Double) { + dstPt = new Point2D.Double(); + } + else { + dstPt = new Point2D.Float(); + } + dstPt.setLocation(srcPt); + } + return dstPt; + } + + /* -- Java port of filter_rcg.c below... -- */ + + /* + * filter function definitions + */ + + interface InterpolationFilter { + double filter(double t); + + double support(); + } + + static class HermiteFilter implements InterpolationFilter { + public final double filter(double t) { + /* f(t) = 2|t|^3 - 3|t|^2 + 1, -1 <= t <= 1 */ + if (t < 0.0) { + t = -t; + } + if (t < 1.0) { + return (2.0 * t - 3.0) * t * t + 1.0; + } + return 0.0; + } + + public final double support() { + return 1.0; + } + } + + static class PointFilter extends BoxFilter { + public PointFilter() { + super(0.0); + } + } + + static class BoxFilter implements InterpolationFilter { + private final double mSupport; + + public BoxFilter() { + mSupport = 0.5; + } + + protected BoxFilter(double pSupport) { + mSupport = pSupport; + } + + public final double filter(final double t) { + //if ((t > -0.5) && (t <= 0.5)) { + if ((t >= -0.5) && (t < 0.5)) {// ImageMagick resample.c + return 1.0; + } + return 0.0; + } + + public final double support() { + return mSupport; + } + } + + static class TriangleFilter implements InterpolationFilter { + public final double filter(double t) { + if (t < 0.0) { + t = -t; + } + if (t < 1.0) { + return 1.0 - t; + } + return 0.0; + } + + public final double support() { + return 1.0; + } + } + + static class QuadraticFilter implements InterpolationFilter { + // AKA Bell + public final double filter(double t)/* box (*) box (*) box */ { + if (t < 0) { + t = -t; + } + if (t < .5) { + return .75 - (t * t); + } + if (t < 1.5) { + t = (t - 1.5); + return .5 * (t * t); + } + return 0.0; + } + + public final double support() { + return 1.5; + } + } + + static class CubicFilter implements InterpolationFilter { + // AKA B-Spline + public final double filter(double t)/* box (*) box (*) box (*) box */ { + final double tt; + + if (t < 0) { + t = -t; + } + if (t < 1) { + tt = t * t; + return (.5 * tt * t) - tt + (2.0 / 3.0); + } + else if (t < 2) { + t = 2 - t; + return (1.0 / 6.0) * (t * t * t); + } + return 0.0; + } + + public final double support() { + return 2.0; + } + } + + private static double sinc(double x) { + x *= Math.PI; + if (x != 0.0) { + return Math.sin(x) / x; + } + return 1.0; + } + + static class LanczosFilter implements InterpolationFilter { + // AKA Lanczos3 + public final double filter(double t) { + if (t < 0) { + t = -t; + } + if (t < 3.0) { + return sinc(t) * sinc(t / 3.0); + } + return 0.0; + } + + public final double support() { + return 3.0; + } + } + + private final static double B = 1.0 / 3.0; + private final static double C = 1.0 / 3.0; + private final static double P0 = (6.0 - 2.0 * B) / 6.0; + private final static double P2 = (-18.0 + 12.0 * B + 6.0 * C) / 6.0; + private final static double P3 = (12.0 - 9.0 * B - 6.0 * C) / 6.0; + private final static double Q0 = (8.0 * B + 24.0 * C) / 6.0; + private final static double Q1 = (-12.0 * B - 48.0 * C) / 6.0; + private final static double Q2 = (6.0 * B + 30.0 * C) / 6.0; + private final static double Q3 = (-1.0 * B - 6.0 * C) / 6.0; + + static class MitchellFilter implements InterpolationFilter { + public final double filter(double t) { + if (t < -2.0) { + return 0.0; + } + if (t < -1.0) { + return Q0 - t * (Q1 - t * (Q2 - t * Q3)); + } + if (t < 0.0) { + return P0 + t * t * (P2 - t * P3); + } + if (t < 1.0) { + return P0 + t * t * (P2 + t * P3); + } + if (t < 2.0) { + return Q0 + t * (Q1 + t * (Q2 + t * Q3)); + } + return 0.0; + } + + public final double support() { + return 2.0; + } + } + + private static double j1(final double t) { + final double[] pOne = { + 0.581199354001606143928050809e+21, + -0.6672106568924916298020941484e+20, + 0.2316433580634002297931815435e+19, + -0.3588817569910106050743641413e+17, + 0.2908795263834775409737601689e+15, + -0.1322983480332126453125473247e+13, + 0.3413234182301700539091292655e+10, + -0.4695753530642995859767162166e+7, + 0.270112271089232341485679099e+4 + }; + final double[] qOne = { + 0.11623987080032122878585294e+22, + 0.1185770712190320999837113348e+20, + 0.6092061398917521746105196863e+17, + 0.2081661221307607351240184229e+15, + 0.5243710262167649715406728642e+12, + 0.1013863514358673989967045588e+10, + 0.1501793594998585505921097578e+7, + 0.1606931573481487801970916749e+4, + 0.1e+1 + }; + + double p = pOne[8]; + double q = qOne[8]; + for (int i = 7; i >= 0; i--) { + p = p * t * t + pOne[i]; + q = q * t * t + qOne[i]; + } + return p / q; + } + + private static double p1(final double t) { + final double[] pOne = { + 0.352246649133679798341724373e+5, + 0.62758845247161281269005675e+5, + 0.313539631109159574238669888e+5, + 0.49854832060594338434500455e+4, + 0.2111529182853962382105718e+3, + 0.12571716929145341558495e+1 + }; + final double[] qOne = { + 0.352246649133679798068390431e+5, + 0.626943469593560511888833731e+5, + 0.312404063819041039923015703e+5, + 0.4930396490181088979386097e+4, + 0.2030775189134759322293574e+3, + 0.1e+1 + }; + + double p = pOne[5]; + double q = qOne[5]; + for (int i = 4; i >= 0; i--) { + p = p * (8.0 / t) * (8.0 / t) + pOne[i]; + q = q * (8.0 / t) * (8.0 / t) + qOne[i]; + } + return p / q; + } + + private static double q1(final double t) { + final double[] pOne = { + 0.3511751914303552822533318e+3, + 0.7210391804904475039280863e+3, + 0.4259873011654442389886993e+3, + 0.831898957673850827325226e+2, + 0.45681716295512267064405e+1, + 0.3532840052740123642735e-1 + }; + final double[] qOne = { + 0.74917374171809127714519505e+4, + 0.154141773392650970499848051e+5, + 0.91522317015169922705904727e+4, + 0.18111867005523513506724158e+4, + 0.1038187585462133728776636e+3, + 0.1e+1 + }; + + double p = pOne[5]; + double q = qOne[5]; + for (int i = 4; i >= 0; i--) { + p = p * (8.0 / t) * (8.0 / t) + pOne[i]; + q = q * (8.0 / t) * (8.0 / t) + qOne[i]; + } + return p / q; + } + + static double besselOrderOne(double t) { + double p, q; + + if (t == 0.0) { + return 0.0; + } + p = t; + if (t < 0.0) { + t = -t; + } + if (t < 8.0) { + return p * j1(t); + } + q = Math.sqrt(2.0 / (Math.PI * t)) * (p1(t) * (1.0 / Math.sqrt(2.0) * (Math.sin(t) - Math.cos(t))) - 8.0 / t * q1(t) * + (-1.0 / Math.sqrt(2.0) * (Math.sin(t) + Math.cos(t)))); + if (p < 0.0) { + q = -q; + } + return q; + } + + private static double bessel(final double t) { + if (t == 0.0) { + return Math.PI / 4.0; + } + return besselOrderOne(Math.PI * t) / (2.0 * t); + } + + private static double blackman(final double t) { + return 0.42 + 0.50 * Math.cos(Math.PI * t) + 0.08 * Math.cos(2.0 * Math.PI * t); + } + + static class BlacmanFilter implements InterpolationFilter { + public final double filter(final double t) { + return blackman(t); + } + + public final double support() { + return 1.0; + } + } + + static class CatromFilter implements InterpolationFilter { + public final double filter(double t) { + if (t < 0) { + t = -t; + } + if (t < 1.0) { + return 0.5 * (2.0 + t * t * (-5.0 + t * 3.0)); + } + if (t < 2.0) { + return 0.5 * (4.0 + t * (-8.0 + t * (5.0 - t))); + } + return 0.0; + } + + public final double support() { + return 2.0; + } + } + + static class GaussianFilter implements InterpolationFilter { + public final double filter(final double t) { + return Math.exp(-2.0 * t * t) * Math.sqrt(2.0 / Math.PI); + } + + public final double support() { + return 1.25; + } + } + + static class HanningFilter implements InterpolationFilter { + public final double filter(final double t) { + return 0.5 + 0.5 * Math.cos(Math.PI * t); + } + + public final double support() { + return 1.0; + } + } + + static class HammingFilter implements InterpolationFilter { + public final double filter(final double t) { + return 0.54 + 0.46 * Math.cos(Math.PI * t); + } + + public final double support() { + return 1.0; + } + } + + static class BlackmanBesselFilter implements InterpolationFilter { + public final double filter(final double t) { + return blackman(t / support()) * bessel(t); + } + + public final double support() { + return 3.2383; + } + } + + static class BlackmanSincFilter implements InterpolationFilter { + public final double filter(final double t) { + return blackman(t / support()) * sinc(t); + } + + public final double support() { + return 4.0; + } + } + + /* + * image rescaling routine + */ + class Contributor { + int pixel; + double weight; + } + + class ContributorList { + int n;/* number of contributors (may be < p.length) */ + Contributor[] p;/* pointer to list of contributions */ + } + + /* + round() + + Round an FP value to its closest int representation. + General routine; ideally belongs in general math lib file. + */ + + static int round(double d) { + // NOTE: This code seems to be faster than Math.round(double)... + // Version that uses no function calls at all. + int n = (int) d; + double diff = d - (double) n; + if (diff < 0) { + diff = -diff; + } + if (diff >= 0.5) { + if (d < 0) { + n--; + } + else { + n++; + } + } + return n; + }/* round */ + + /* + calcXContrib() + + Calculates the filter weights for a single target column. + contribX->p must be freed afterwards. + + Returns -1 if error, 0 otherwise. + */ + private ContributorList calcXContrib(double xscale, double fwidth, int srcwidth, InterpolationFilter pFilter, int i) { + // TODO: What to do when fwidth > srcwidyj or dstwidth + + double width; + double fscale; + double center; + double weight; + + ContributorList contribX = new ContributorList(); + + if (xscale < 1.0) { + /* Shrinking image */ + width = fwidth / xscale; + fscale = 1.0 / xscale; + + if (width <= .5) { + // Reduce to point sampling. + width = .5 + 1.0e-6; + fscale = 1.0; + } + + //contribX.n = 0; + contribX.p = new Contributor[(int) (width * 2.0 + 1.0 + 0.5)]; + + center = (double) i / xscale; + int left = (int) Math.ceil(center - width);// Note: Assumes width <= .5 + int right = (int) Math.floor(center + width); + + double density = 0.0; + + for (int j = left; j <= right; j++) { + weight = center - (double) j; + weight = pFilter.filter(weight / fscale) / fscale; + int n; + if (j < 0) { + n = -j; + } + else if (j >= srcwidth) { + n = (srcwidth - j) + srcwidth - 1; + } + else { + n = j; + } + + /**/ + if (n >= srcwidth) { + n = n % srcwidth; + } + else if (n < 0) { + n = srcwidth - 1; + } + /**/ + + int k = contribX.n++; + contribX.p[k] = new Contributor(); + contribX.p[k].pixel = n; + contribX.p[k].weight = weight; + + density += weight; + + } + + if ((density != 0.0) && (density != 1.0)) { + //Normalize. + density = 1.0 / density; + for (int k = 0; k < contribX.n; k++) { + contribX.p[k].weight *= density; + } + } + } + else { + /* Expanding image */ + //contribX.n = 0; + contribX.p = new Contributor[(int) (fwidth * 2.0 + 1.0 + 0.5)]; + + center = (double) i / xscale; + int left = (int) Math.ceil(center - fwidth); + int right = (int) Math.floor(center + fwidth); + + for (int j = left; j <= right; j++) { + weight = center - (double) j; + weight = pFilter.filter(weight); + + int n; + if (j < 0) { + n = -j; + } + else if (j >= srcwidth) { + n = (srcwidth - j) + srcwidth - 1; + } + else { + n = j; + } + + /**/ + if (n >= srcwidth) { + n = n % srcwidth; + } + else if (n < 0) { + n = srcwidth - 1; + } + /**/ + + int k = contribX.n++; + contribX.p[k] = new Contributor(); + contribX.p[k].pixel = n; + contribX.p[k].weight = weight; + } + } + return contribX; + }/* calcXContrib */ + + /* + resample() + + Resizes bitmaps while resampling them. + */ + private BufferedImage resample(BufferedImage pSource, BufferedImage pDest, InterpolationFilter pFilter) { + final int dstWidth = pDest.getWidth(); + final int dstHeight = pDest.getHeight(); + + final int srcWidth = pSource.getWidth(); + final int srcHeight = pSource.getHeight(); + + /* create intermediate column to hold horizontal dst column zoom */ + final ColorModel cm = pSource.getColorModel(); +// 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; + + ContributorList[] contribY = new ContributorList[dstHeight]; + for (int i = 0; i < contribY.length; i++) { + contribY[i] = new ContributorList(); + } + + // TODO: What to do when fwidth > srcHeight or dstHeight + double fwidth = pFilter.support(); + if (yscale < 1.0) { + double width = fwidth / yscale; + double fscale = 1.0 / yscale; + + if (width <= .5) { + // Reduce to point sampling. + width = .5 + 1.0e-6; + fscale = 1.0; + } + + for (int i = 0; i < dstHeight; i++) { + //contribY[i].n = 0; + contribY[i].p = new Contributor[(int) (width * 2.0 + 1 + 0.5)]; + + double center = (double) i / yscale; + int left = (int) Math.ceil(center - width); + int right = (int) Math.floor(center + width); + + double density = 0.0; + + for (int j = left; j <= right; j++) { + double weight = center - (double) j; + weight = pFilter.filter(weight / fscale) / fscale; + int n; + if (j < 0) { + n = -j; + } + else if (j >= srcHeight) { + n = (srcHeight - j) + srcHeight - 1; + } + else { + n = j; + } + + /**/ + if (n >= srcHeight) { + n = n % srcHeight; + } + else if (n < 0) { + n = srcHeight - 1; + } + /**/ + + int k = contribY[i].n++; + contribY[i].p[k] = new Contributor(); + contribY[i].p[k].pixel = n; + contribY[i].p[k].weight = weight; + + density += weight; + } + + if ((density != 0.0) && (density != 1.0)) { + //Normalize. + density = 1.0 / density; + for (int k = 0; k < contribY[i].n; k++) { + contribY[i].p[k].weight *= density; + } + } + } + } + else { + for (int i = 0; i < dstHeight; ++i) { + //contribY[i].n = 0; + contribY[i].p = new Contributor[(int) (fwidth * 2 + 1 + 0.5)]; + + double center = (double) i / yscale; + double left = Math.ceil(center - fwidth); + double right = Math.floor(center + fwidth); + for (int j = (int) left; j <= right; ++j) { + double weight = center - (double) j; + weight = pFilter.filter(weight); + int n; + if (j < 0) { + n = -j; + } + else if (j >= srcHeight) { + n = (srcHeight - j) + srcHeight - 1; + } + else { + n = j; + } + + /**/ + if (n >= srcHeight) { + n = n % srcHeight; + } + else if (n < 0) { + n = srcHeight - 1; + } + /**/ + + int k = contribY[i].n++; + contribY[i].p[k] = new Contributor(); + contribY[i].p[k].pixel = n; + contribY[i].p[k].weight = weight; + } + } + } + + final Raster raster = pSource.getRaster(); + 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 TODO?) + final int numChannels = raster.getNumBands(); + final int[] channelMax = new int[numChannels]; + for (int k = 0; k < numChannels; k++) { + channelMax[k] = (1 << pSource.getColorModel().getComponentSize(k)) - 1; + } + + for (int xx = 0; xx < dstWidth; xx++) { + ContributorList contribX = calcXContrib(xscale, fwidth, srcWidth, pFilter, xx); + /* Apply horiz filter to make dst column in tmp. */ + for (int k = 0; k < srcHeight; k++) { + for (int channel = 0; channel < numChannels; channel++) { + + double weight = 0.0; + boolean bPelDelta = false; + // TODO: This line throws index out of bounds, if the image + // is smaller than filter.support() + double pel = raster.getSample(contribX.p[0].pixel, k, channel); + for (int j = 0; j < contribX.n; j++) { + double pel2 = j == 0 ? pel : raster.getSample(contribX.p[j].pixel, k, channel); + if (pel2 != pel) { + bPelDelta = true; + } + weight += pel2 * contribX.p[j].weight; + } + weight = bPelDelta ? round(weight) : pel; + + if (weight < 0) { + weight = 0; + } + else if (weight > channelMax[channel]) { + weight = channelMax[channel]; + } + + work.setSample(0, k, channel, weight); + + } + }/* next row in temp column */ + + /* The temp column has been built. Now stretch it vertically into dst column. */ + for (int i = 0; i < dstHeight; i++) { + for (int channel = 0; channel < numChannels; channel++) { + + double weight = 0.0; + boolean bPelDelta = false; + double pel = work.getSample(0, contribY[i].p[0].pixel, channel); + + for (int j = 0; j < contribY[i].n; j++) { + // TODO: This line throws index out of bounds, if the image + // is smaller than filter.support() + double pel2 = j == 0 ? pel : work.getSample(0, contribY[i].p[j].pixel, channel); + if (pel2 != pel) { + bPelDelta = true; + } + weight += pel2 * contribY[i].p[j].weight; + } + weight = bPelDelta ? round(weight) : pel; + if (weight < 0) { + weight = 0; + } + else if (weight > channelMax[channel]) { + weight = channelMax[channel]; + } + + out.setSample(xx, i, channel, weight); + } + }/* next dst row */ + }/* next dst column */ + return pDest; + }/* resample */ } \ No newline at end of file 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 c553013c..226f4594 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 @@ -1,79 +1,80 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.ReplicateScaleFilter; - -/** - * An {@code ImageFilter} class for subsampling images. - *

- * It is meant to be used in conjunction with a {@code FilteredImageSource} - * object to produce subsampled versions of existing images. - * - * @see java.awt.image.FilteredImageSource - * - * @author Harald Kuhr - * @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 xSub; - private int ySub; - - /** - * Creates a {@code SubsamplingFilter}. - * - * @param pXSub - * @param pYSub - * - * @throws IllegalArgumentException if {@code pXSub} or {@code pYSub} is - * less than 1. - */ - public SubsamplingFilter(int pXSub, int pYSub) { - super(1, 1); // These are NOT REAL values, but we have to defer setting - // until w/h is available, in setDimensions below - - if (pXSub < 1 || pYSub < 1) { - throw new IllegalArgumentException("Subsampling factors must be positive."); - } - - xSub = pXSub; - ySub = pYSub; - } - - /** {@code ImageFilter} implementation, do not invoke. */ - public void setDimensions(int pWidth, int pHeight) { - destWidth = (pWidth + xSub - 1) / xSub; - destHeight = (pHeight + ySub - 1) / ySub; - - //System.out.println("Subsampling: " + xSub + "," + ySub + "-> " + destWidth + ", " + destHeight); - super.setDimensions(pWidth, pHeight); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.ReplicateScaleFilter; + +/** + * An {@code ImageFilter} class for subsampling images. + *

+ * It is meant to be used in conjunction with a {@code FilteredImageSource} + * object to produce subsampled versions of existing images. + *

+ * + * @see java.awt.image.FilteredImageSource + * + * @author Harald Kuhr + * @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 xSub; + private int ySub; + + /** + * Creates a {@code SubsamplingFilter}. + * + * @param pXSub + * @param pYSub + * + * @throws IllegalArgumentException if {@code pXSub} or {@code pYSub} is + * less than 1. + */ + public SubsamplingFilter(int pXSub, int pYSub) { + super(1, 1); // These are NOT REAL values, but we have to defer setting + // until w/h is available, in setDimensions below + + if (pXSub < 1 || pYSub < 1) { + throw new IllegalArgumentException("Subsampling factors must be positive."); + } + + xSub = pXSub; + ySub = pYSub; + } + + /** {@code ImageFilter} implementation, do not invoke. */ + public void setDimensions(int pWidth, int pHeight) { + destWidth = (pWidth + xSub - 1) / xSub; + destHeight = (pHeight + ySub - 1) / ySub; + + //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 4d688a6b..fa01928e 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 @@ -30,8 +30,9 @@ /** * Classes for image manipulation. - *

+ *

* See the class {@link com.twelvemonkeys.image.ImageUtil}. + *

* * @version 1.0 * @author Harald Kuhr 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 418760c5..afc87f14 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 @@ -218,9 +218,10 @@ abstract class AbstractCachedSeekableStream extends SeekableInputStream { /** * Writes a series of bytes at the current read/write position. The read/write position will be increased by * {@code pLength}. - *

+ *

* This implementation invokes {@link #write(int)} {@code pLength} times. * Subclasses may override this method for performance. + *

* * @param pBuffer the bytes to write. * @param pOffset the starting offset into the buffer. @@ -246,9 +247,10 @@ abstract class AbstractCachedSeekableStream extends SeekableInputStream { /** * Writes a series of bytes at the current read/write position. The read/write position will be increased by * {@code pLength}. - *

+ *

* This implementation invokes {@link #read()} {@code pLength} times. * Subclasses may override this method for performance. + *

* * @param pBuffer the bytes to write * @param pOffset the starting offset into the buffer. @@ -283,12 +285,14 @@ abstract class AbstractCachedSeekableStream extends SeekableInputStream { /** * Optionally flushes any data prior to the given position. - *

+ *

* Attempting to perform a seek operation, and/or a read or write operation to a position equal to or before * the flushed position may result in exceptions or undefined behaviour. - *

+ *

+ *

* Subclasses should override this method for performance reasons, to avoid holding on to unnecessary resources. * This implementation does nothing. + *

* * @param pPosition the last position to flush. */ 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 77911567..47279114 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 @@ -1,222 +1,221 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.Validate; - -import java.io.IOException; -import java.io.Reader; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -/** - * A Reader implementation that can read from multiple sources. - *

- * - * @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/io/CompoundReader.java#2 $ - */ -public class CompoundReader extends Reader { - - private Reader current; - private List readers; - - protected final Object finalLock; - - protected final boolean markSupported; - - private int currentReader; - private int markedReader; - private int mark; - private int mNext; - - /** - * Create a new compound reader. - * - * @param pReaders {@code Iterator} containting {@code Reader}s, - * providing the character stream. - * - * @throws NullPointerException if {@code pReaders} is {@code null}, or - * any of the elements in the iterator is {@code null}. - * @throws ClassCastException if any element of the iterator is not a - * {@code java.io.Reader} - */ - public CompoundReader(final Iterator pReaders) { - super(Validate.notNull(pReaders, "readers")); - - finalLock = pReaders; // NOTE: It's ok to sync on pReaders, as the - // reference can't change, only it's elements - - readers = new ArrayList(); - - boolean markSupported = true; - while (pReaders.hasNext()) { - Reader reader = pReaders.next(); - if (reader == null) { - throw new NullPointerException("readers cannot contain null-elements"); - } - readers.add(reader); - markSupported = markSupported && reader.markSupported(); - } - this.markSupported = markSupported; - - current = nextReader(); - } - - protected final Reader nextReader() { - if (currentReader >= readers.size()) { - current = new EmptyReader(); - } - else { - current = readers.get(currentReader++); - } - - // NOTE: Reset mNext for every reader, and record marked reader in mark/reset methods! - mNext = 0; - return current; - } - - /** - * Check to make sure that the stream has not been closed - * - * @throws IOException if the stream is closed - */ - protected final void ensureOpen() throws IOException { - if (readers == null) { - throw new IOException("Stream closed"); - } - } - - public void close() throws IOException { - // Close all readers - for (Reader reader : readers) { - reader.close(); - } - - readers = null; - } - - @Override - public void mark(int pReadLimit) throws IOException { - if (pReadLimit < 0) { - throw new IllegalArgumentException("Read limit < 0"); - } - - // TODO: It would be nice if we could actually close some readers now - - synchronized (finalLock) { - ensureOpen(); - mark = mNext; - markedReader = currentReader; - - current.mark(pReadLimit); - } - } - - @Override - public void reset() throws IOException { - synchronized (finalLock) { - ensureOpen(); - - if (currentReader != markedReader) { - // Reset any reader before this - for (int i = currentReader; i >= markedReader; i--) { - readers.get(i).reset(); - } - - currentReader = markedReader - 1; - nextReader(); - } - current.reset(); - - mNext = mark; - } - } - - @Override - public boolean markSupported() { - return markSupported; - } - - @Override - public int read() throws IOException { - synchronized (finalLock) { - int read = current.read(); - - if (read < 0 && currentReader < readers.size()) { - nextReader(); - return read(); // In case of 0-length readers - } - - mNext++; - - return read; - } - } - - public int read(char pBuffer[], int pOffset, int pLength) throws IOException { - synchronized (finalLock) { - int read = current.read(pBuffer, pOffset, pLength); - - if (read < 0 && currentReader < readers.size()) { - nextReader(); - return read(pBuffer, pOffset, pLength); // In case of 0-length readers - } - - mNext += read; - - return read; - } - } - - @Override - public boolean ready() throws IOException { - return current.ready(); - } - - @Override - public long skip(long pChars) throws IOException { - synchronized (finalLock) { - long skipped = current.skip(pChars); - - if (skipped == 0 && currentReader < readers.size()) { - nextReader(); - return skip(pChars); // In case of 0-length readers - } - - mNext += skipped; - - return skipped; - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.Validate; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * A Reader implementation that can read from multiple sources. + * + * @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/io/CompoundReader.java#2 $ + */ +public class CompoundReader extends Reader { + + private Reader current; + private List readers; + + protected final Object finalLock; + + protected final boolean markSupported; + + private int currentReader; + private int markedReader; + private int mark; + private int mNext; + + /** + * Create a new compound reader. + * + * @param pReaders {@code Iterator} containting {@code Reader}s, + * providing the character stream. + * + * @throws NullPointerException if {@code pReaders} is {@code null}, or + * any of the elements in the iterator is {@code null}. + * @throws ClassCastException if any element of the iterator is not a + * {@code java.io.Reader} + */ + public CompoundReader(final Iterator pReaders) { + super(Validate.notNull(pReaders, "readers")); + + finalLock = pReaders; // NOTE: It's ok to sync on pReaders, as the + // reference can't change, only it's elements + + readers = new ArrayList(); + + boolean markSupported = true; + while (pReaders.hasNext()) { + Reader reader = pReaders.next(); + if (reader == null) { + throw new NullPointerException("readers cannot contain null-elements"); + } + readers.add(reader); + markSupported = markSupported && reader.markSupported(); + } + this.markSupported = markSupported; + + current = nextReader(); + } + + protected final Reader nextReader() { + if (currentReader >= readers.size()) { + current = new EmptyReader(); + } + else { + current = readers.get(currentReader++); + } + + // NOTE: Reset mNext for every reader, and record marked reader in mark/reset methods! + mNext = 0; + return current; + } + + /** + * Check to make sure that the stream has not been closed + * + * @throws IOException if the stream is closed + */ + protected final void ensureOpen() throws IOException { + if (readers == null) { + throw new IOException("Stream closed"); + } + } + + public void close() throws IOException { + // Close all readers + for (Reader reader : readers) { + reader.close(); + } + + readers = null; + } + + @Override + public void mark(int pReadLimit) throws IOException { + if (pReadLimit < 0) { + throw new IllegalArgumentException("Read limit < 0"); + } + + // TODO: It would be nice if we could actually close some readers now + + synchronized (finalLock) { + ensureOpen(); + mark = mNext; + markedReader = currentReader; + + current.mark(pReadLimit); + } + } + + @Override + public void reset() throws IOException { + synchronized (finalLock) { + ensureOpen(); + + if (currentReader != markedReader) { + // Reset any reader before this + for (int i = currentReader; i >= markedReader; i--) { + readers.get(i).reset(); + } + + currentReader = markedReader - 1; + nextReader(); + } + current.reset(); + + mNext = mark; + } + } + + @Override + public boolean markSupported() { + return markSupported; + } + + @Override + public int read() throws IOException { + synchronized (finalLock) { + int read = current.read(); + + if (read < 0 && currentReader < readers.size()) { + nextReader(); + return read(); // In case of 0-length readers + } + + mNext++; + + return read; + } + } + + public int read(char pBuffer[], int pOffset, int pLength) throws IOException { + synchronized (finalLock) { + int read = current.read(pBuffer, pOffset, pLength); + + if (read < 0 && currentReader < readers.size()) { + nextReader(); + return read(pBuffer, pOffset, pLength); // In case of 0-length readers + } + + mNext += read; + + return read; + } + } + + @Override + public boolean ready() throws IOException { + return current.ready(); + } + + @Override + public long skip(long pChars) throws IOException { + synchronized (finalLock) { + long skipped = current.skip(pChars); + + if (skipped == 0 && currentReader < readers.size()) { + nextReader(); + return skip(pChars); // In case of 0-length readers + } + + mNext += skipped; + + return skipped; + } + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/EmptyReader.java b/common/common-io/src/main/java/com/twelvemonkeys/io/EmptyReader.java index ea1a1abf..6ff1a7a4 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/EmptyReader.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/EmptyReader.java @@ -1,47 +1,46 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.StringReader; - -/** - * EmptyReader - *

- * - * @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/io/EmptyReader.java#1 $ - */ -final class EmptyReader extends StringReader { - public EmptyReader() { - super(""); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.StringReader; + +/** + * EmptyReader + * + * @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/io/EmptyReader.java#1 $ + */ +final class EmptyReader extends StringReader { + public EmptyReader() { + super(""); + } +} 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 4bac2b82..69018a23 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 @@ -1,136 +1,137 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * An unsynchronized {@code ByteArrayOutputStream} implementation. This version - * also has a constructor that lets you create a stream with initial content. - *

- * - * @author Harald Kuhr - * @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 amount of bytes) */ - protected int maxGrowSize = 1024 * 1024; // 1 MB - - /** - * Creates a {@code ByteArrayOutputStream} with the given initial buffer - * size. - * - * @param pSize initial buffer size - */ - public FastByteArrayOutputStream(int pSize) { - super(pSize); - } - - /** - * Creates a {@code ByteArrayOutputStream} with the given initial content. - *

- * Note that the buffer is not cloned, for maximum performance. - * - * @param pBuffer initial buffer - */ - public FastByteArrayOutputStream(byte[] pBuffer) { - super(0); // Don't allocate array - buf = pBuffer; - count = pBuffer.length; - } - - @Override - 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(); - } - else if (pLength == 0) { - return; - } - - int newCount = count + pLength; - growIfNeeded(newCount); - System.arraycopy(pBytes, pOffset, buf, count, pLength); - count = newCount; - } - - @Override - public void write(int pByte) { - int newCount = count + 1; - growIfNeeded(newCount); - buf[count] = (byte) pByte; - count = newCount; - } - - 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; - } - } - - // Non-synchronized version of writeTo - @Override - public void writeTo(OutputStream pOut) throws IOException { - pOut.write(buf, 0, count); - } - - // Non-synchronized version of toByteArray - @Override - public byte[] toByteArray() { - byte newBuf[] = new byte[count]; - System.arraycopy(buf, 0, newBuf, 0, count); - - return newBuf; - } - - /** - * Creates a {@code ByteArrayInputStream} that reads directly from this - * {@code FastByteArrayOutputStream}'s byte buffer. - * The buffer is not cloned, for maximum performance. - *

- * 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 behaviour. - * - * @return a new {@code ByteArrayInputStream}, reading from this stream's buffer. - */ - public ByteArrayInputStream createInputStream() { - return new ByteArrayInputStream(buf, 0, count); - } +/* + * 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 of the copyright holder 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 HOLDER 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.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * An unsynchronized {@code ByteArrayOutputStream} implementation. This version + * also has a constructor that lets you create a stream with initial content. + * + * @author Harald Kuhr + * @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 amount of bytes) */ + protected int maxGrowSize = 1024 * 1024; // 1 MB + + /** + * Creates a {@code ByteArrayOutputStream} with the given initial buffer + * size. + * + * @param pSize initial buffer size + */ + public FastByteArrayOutputStream(int pSize) { + super(pSize); + } + + /** + * Creates a {@code ByteArrayOutputStream} with the given initial content. + *

+ * Note that the buffer is not cloned, for maximum performance. + *

+ * + * @param pBuffer initial buffer + */ + public FastByteArrayOutputStream(byte[] pBuffer) { + super(0); // Don't allocate array + buf = pBuffer; + count = pBuffer.length; + } + + @Override + 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(); + } + else if (pLength == 0) { + return; + } + + int newCount = count + pLength; + growIfNeeded(newCount); + System.arraycopy(pBytes, pOffset, buf, count, pLength); + count = newCount; + } + + @Override + public void write(int pByte) { + int newCount = count + 1; + growIfNeeded(newCount); + buf[count] = (byte) pByte; + count = newCount; + } + + 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; + } + } + + // Non-synchronized version of writeTo + @Override + public void writeTo(OutputStream pOut) throws IOException { + pOut.write(buf, 0, count); + } + + // Non-synchronized version of toByteArray + @Override + public byte[] toByteArray() { + byte newBuf[] = new byte[count]; + System.arraycopy(buf, 0, newBuf, 0, count); + + return newBuf; + } + + /** + * Creates a {@code ByteArrayInputStream} that reads directly from this + * {@code FastByteArrayOutputStream}'s byte buffer. + * The buffer is not cloned, for maximum performance. + *

+ * 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 behaviour. + *

+ * + * @return a new {@code ByteArrayInputStream}, reading from this stream's buffer. + */ + public ByteArrayInputStream createInputStream() { + return new ByteArrayInputStream(buf, 0, count); + } } \ No newline at end of file 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 a857b817..d02225d9 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 @@ -1,240 +1,241 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.Validate; - -import java.io.*; - -/** - * A {@code SeekableInputStream} implementation that caches data in a temporary {@code File}. - *

- * Temporary files are created as specified in {@link File#createTempFile(String, String, java.io.File)}. - * - * @see MemoryCacheSeekableStream - * @see FileSeekableStream - * - * @see File#createTempFile(String, String) - * @see RandomAccessFile - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileCacheSeekableStream.java#5 $ - */ -public final class FileCacheSeekableStream extends AbstractCachedSeekableStream { - - private byte[] buffer; - - /** - * Creates a {@code FileCacheSeekableStream} reading from the given - * {@code InputStream}. Data will be cached in a temporary file. - * - * @param pStream the {@code InputStream} to read from - * - * @throws IOException if the temporary file cannot be created, - * or cannot be opened for random access. - */ - public FileCacheSeekableStream(final InputStream pStream) throws IOException { - this(pStream, "iocache", null); - } - - /** - * Creates a {@code FileCacheSeekableStream} reading from the given - * {@code InputStream}. Data will be cached in a temporary file, with - * the given base name. - * - * @param pStream the {@code InputStream} to read from - * @param pTempBaseName optional base name for the temporary file - * - * @throws IOException if the temporary file cannot be created, - * or cannot be opened for random access. - */ - public FileCacheSeekableStream(final InputStream pStream, final String pTempBaseName) throws IOException { - this(pStream, pTempBaseName, null); - } - - /** - * Creates a {@code FileCacheSeekableStream} reading from the given - * {@code InputStream}. Data will be cached in a temporary file, with - * the given base name, in the given directory - * - * @param pStream the {@code InputStream} to read from - * @param pTempBaseName optional base name for the temporary file - * @param pTempDir optional temp directory - * - * @throws IOException if the temporary file cannot be created, - * or cannot be opened for random access. - */ - public FileCacheSeekableStream(final InputStream pStream, final String pTempBaseName, final File pTempDir) throws IOException { - // NOTE: We do validation BEFORE we create temp file, to avoid orphan files - this(Validate.notNull(pStream, "stream"), createTempFile(pTempBaseName, pTempDir)); - } - - /*protected*/ static File createTempFile(String pTempBaseName, File pTempDir) throws IOException { - Validate.notNull(pTempBaseName, "tempBaseName"); - - File file = File.createTempFile(pTempBaseName, null, pTempDir); - file.deleteOnExit(); - - return file; - } - - // TODO: Consider exposing this for external use - /*protected*/ FileCacheSeekableStream(final InputStream pStream, final File pFile) throws FileNotFoundException { - super(pStream, new FileCache(pFile)); - - // TODO: Allow for custom buffer sizes? - buffer = new byte[1024]; - } - - public final boolean isCachedMemory() { - return false; - } - - public final boolean isCachedFile() { - return true; - } - - @Override - protected void closeImpl() throws IOException { - // TODO: Close cache file - super.closeImpl(); - - buffer = null; - } - - @Override - public int read() throws IOException { - checkOpen(); - - int read; - if (position == streamPosition) { - // Read ahead into buffer, for performance - read = readAhead(buffer, 0, buffer.length); - if (read >= 0) { - read = buffer[0] & 0xff; - } - - //System.out.println("Read 1 byte from stream: " + Integer.toHexString(read & 0xff)); - } - else { - // ..or read byte from the cache - syncPosition(); - read = getCache().read(); - - //System.out.println("Read 1 byte from cache: " + Integer.toHexString(read & 0xff)); - } - - // TODO: This field is not REALLY considered accessible.. :-P - if (read != -1) { - position++; - } - return read; - } - - @Override - public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { - checkOpen(); - - int length; - if (position == streamPosition) { - // Read bytes from the stream - length = readAhead(pBytes, pOffset, pLength); - - //System.out.println("Read " + length + " byte from stream"); - } - else { - // ...or read bytes from the cache - syncPosition(); - 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) { - position += length; - } - return length; - } - - private int readAhead(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - int length; - length = stream.read(pBytes, pOffset, pLength); - - if (length > 0) { - streamPosition += length; - getCache().write(pBytes, pOffset, length); - } - return length; - } - - // TODO: We need to close the cache file!!! Otherwise we are leaking file descriptors - - final static class FileCache extends StreamCache { - private RandomAccessFile cacheFile; - - public FileCache(final File pFile) throws FileNotFoundException { - Validate.notNull(pFile, "file"); - cacheFile = new RandomAccessFile(pFile, "rw"); - } - - public void write(final int pByte) throws IOException { - cacheFile.write(pByte); - } - - @Override - public void write(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { - cacheFile.write(pBuffer, pOffset, pLength); - } - - public int read() throws IOException { - return cacheFile.read(); - } - - @Override - public int read(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { - return cacheFile.read(pBuffer, pOffset, pLength); - } - - public void seek(final long pPosition) throws IOException { - cacheFile.seek(pPosition); - } - - public long getPosition() throws IOException { - return cacheFile.getFilePointer(); - } - - @Override - void close() throws IOException { - cacheFile.close(); - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.Validate; + +import java.io.*; + +/** + * A {@code SeekableInputStream} implementation that caches data in a temporary {@code File}. + *

+ * Temporary files are created as specified in {@link File#createTempFile(String, String, java.io.File)}. + *

+ * + * @see MemoryCacheSeekableStream + * @see FileSeekableStream + * + * @see File#createTempFile(String, String) + * @see RandomAccessFile + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileCacheSeekableStream.java#5 $ + */ +public final class FileCacheSeekableStream extends AbstractCachedSeekableStream { + + private byte[] buffer; + + /** + * Creates a {@code FileCacheSeekableStream} reading from the given + * {@code InputStream}. Data will be cached in a temporary file. + * + * @param pStream the {@code InputStream} to read from + * + * @throws IOException if the temporary file cannot be created, + * or cannot be opened for random access. + */ + public FileCacheSeekableStream(final InputStream pStream) throws IOException { + this(pStream, "iocache", null); + } + + /** + * Creates a {@code FileCacheSeekableStream} reading from the given + * {@code InputStream}. Data will be cached in a temporary file, with + * the given base name. + * + * @param pStream the {@code InputStream} to read from + * @param pTempBaseName optional base name for the temporary file + * + * @throws IOException if the temporary file cannot be created, + * or cannot be opened for random access. + */ + public FileCacheSeekableStream(final InputStream pStream, final String pTempBaseName) throws IOException { + this(pStream, pTempBaseName, null); + } + + /** + * Creates a {@code FileCacheSeekableStream} reading from the given + * {@code InputStream}. Data will be cached in a temporary file, with + * the given base name, in the given directory + * + * @param pStream the {@code InputStream} to read from + * @param pTempBaseName optional base name for the temporary file + * @param pTempDir optional temp directory + * + * @throws IOException if the temporary file cannot be created, + * or cannot be opened for random access. + */ + public FileCacheSeekableStream(final InputStream pStream, final String pTempBaseName, final File pTempDir) throws IOException { + // NOTE: We do validation BEFORE we create temp file, to avoid orphan files + this(Validate.notNull(pStream, "stream"), createTempFile(pTempBaseName, pTempDir)); + } + + /*protected*/ static File createTempFile(String pTempBaseName, File pTempDir) throws IOException { + Validate.notNull(pTempBaseName, "tempBaseName"); + + File file = File.createTempFile(pTempBaseName, null, pTempDir); + file.deleteOnExit(); + + return file; + } + + // TODO: Consider exposing this for external use + /*protected*/ FileCacheSeekableStream(final InputStream pStream, final File pFile) throws FileNotFoundException { + super(pStream, new FileCache(pFile)); + + // TODO: Allow for custom buffer sizes? + buffer = new byte[1024]; + } + + public final boolean isCachedMemory() { + return false; + } + + public final boolean isCachedFile() { + return true; + } + + @Override + protected void closeImpl() throws IOException { + // TODO: Close cache file + super.closeImpl(); + + buffer = null; + } + + @Override + public int read() throws IOException { + checkOpen(); + + int read; + if (position == streamPosition) { + // Read ahead into buffer, for performance + read = readAhead(buffer, 0, buffer.length); + if (read >= 0) { + read = buffer[0] & 0xff; + } + + //System.out.println("Read 1 byte from stream: " + Integer.toHexString(read & 0xff)); + } + else { + // ..or read byte from the cache + syncPosition(); + read = getCache().read(); + + //System.out.println("Read 1 byte from cache: " + Integer.toHexString(read & 0xff)); + } + + // TODO: This field is not REALLY considered accessible.. :-P + if (read != -1) { + position++; + } + return read; + } + + @Override + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + checkOpen(); + + int length; + if (position == streamPosition) { + // Read bytes from the stream + length = readAhead(pBytes, pOffset, pLength); + + //System.out.println("Read " + length + " byte from stream"); + } + else { + // ...or read bytes from the cache + syncPosition(); + 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) { + position += length; + } + return length; + } + + private int readAhead(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { + int length; + length = stream.read(pBytes, pOffset, pLength); + + if (length > 0) { + streamPosition += length; + getCache().write(pBytes, pOffset, length); + } + return length; + } + + // TODO: We need to close the cache file!!! Otherwise we are leaking file descriptors + + final static class FileCache extends StreamCache { + private RandomAccessFile cacheFile; + + public FileCache(final File pFile) throws FileNotFoundException { + Validate.notNull(pFile, "file"); + cacheFile = new RandomAccessFile(pFile, "rw"); + } + + public void write(final int pByte) throws IOException { + cacheFile.write(pByte); + } + + @Override + public void write(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { + cacheFile.write(pBuffer, pOffset, pLength); + } + + public int read() throws IOException { + return cacheFile.read(); + } + + @Override + public int read(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { + return cacheFile.read(pBuffer, pOffset, pLength); + } + + public void seek(final long pPosition) throws IOException { + cacheFile.seek(pPosition); + } + + public long getPosition() throws IOException { + return cacheFile.getFilePointer(); + } + + @Override + void close() throws IOException { + cacheFile.close(); + } + } +} 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 86c92e39..c4be9cfd 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 @@ -1,135 +1,135 @@ -/* - * 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 of the copyright holder 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 HOLDER 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; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; - -/** - * A {@code SeekableInputStream} implementation that uses random access directly to a {@code File}. - *

- * @see FileCacheSeekableStream - * @see MemoryCacheSeekableStream - * @see RandomAccessFile - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSeekableStream.java#4 $ - */ -public final class FileSeekableStream extends SeekableInputStream { - - // TODO: Figure out why this class is SLOWER than FileCacheSeekableStream in - // my tests..? - - final RandomAccessFile mRandomAccess; - - /** - * Creates a {@code FileSeekableStream} that reads from the given - * {@code File}. - * - * @param pInput file to read from - * @throws FileNotFoundException if {@code pInput} does not exist - */ - public FileSeekableStream(final File pInput) throws FileNotFoundException { - this(new RandomAccessFile(pInput, "r")); - } - - /** - * Creates a {@code FileSeekableStream} that reads from the given file. - * The {@code RandomAccessFile} needs only to be open in read - * ({@code "r"}) mode. - * - * @param pInput file to read from - */ - public FileSeekableStream(final RandomAccessFile pInput) { - mRandomAccess = pInput; - } - - /// Seekable - - public boolean isCached() { - return false; - } - - public boolean isCachedFile() { - return false; - } - - public boolean isCachedMemory() { - return false; - } - - /// InputStream - - @Override - public int available() throws IOException { - long length = mRandomAccess.length() - position; - return length > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) length; - } - - public void closeImpl() throws IOException { - mRandomAccess.close(); - } - - public int read() throws IOException { - checkOpen(); - - int read = mRandomAccess.read(); - if (read >= 0) { - position++; - } - return read; - } - - @Override - public int read(byte pBytes[], int pOffset, int pLength) throws IOException { - checkOpen(); - - int read = mRandomAccess.read(pBytes, pOffset, pLength); - if (read > 0) { - position += read; - } - return read; - } - - /** - * Does nothing, as we don't really do any caching here. - * - * @param pPosition the position to flush to - */ - protected void flushBeforeImpl(long pPosition) { - } - - protected void seekImpl(long pPosition) throws IOException { - mRandomAccess.seek(pPosition); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +/** + * A {@code SeekableInputStream} implementation that uses random access directly to a {@code File}. + + * @see FileCacheSeekableStream + * @see MemoryCacheSeekableStream + * @see RandomAccessFile + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSeekableStream.java#4 $ + */ +public final class FileSeekableStream extends SeekableInputStream { + + // TODO: Figure out why this class is SLOWER than FileCacheSeekableStream in + // my tests..? + + final RandomAccessFile mRandomAccess; + + /** + * Creates a {@code FileSeekableStream} that reads from the given + * {@code File}. + * + * @param pInput file to read from + * @throws FileNotFoundException if {@code pInput} does not exist + */ + public FileSeekableStream(final File pInput) throws FileNotFoundException { + this(new RandomAccessFile(pInput, "r")); + } + + /** + * Creates a {@code FileSeekableStream} that reads from the given file. + * The {@code RandomAccessFile} needs only to be open in read + * ({@code "r"}) mode. + * + * @param pInput file to read from + */ + public FileSeekableStream(final RandomAccessFile pInput) { + mRandomAccess = pInput; + } + + /// Seekable + + public boolean isCached() { + return false; + } + + public boolean isCachedFile() { + return false; + } + + public boolean isCachedMemory() { + return false; + } + + /// InputStream + + @Override + public int available() throws IOException { + long length = mRandomAccess.length() - position; + return length > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) length; + } + + public void closeImpl() throws IOException { + mRandomAccess.close(); + } + + public int read() throws IOException { + checkOpen(); + + int read = mRandomAccess.read(); + if (read >= 0) { + position++; + } + return read; + } + + @Override + public int read(byte pBytes[], int pOffset, int pLength) throws IOException { + checkOpen(); + + int read = mRandomAccess.read(pBytes, pOffset, pLength); + if (read > 0) { + position += read; + } + return read; + } + + /** + * Does nothing, as we don't really do any caching here. + * + * @param pPosition the position to flush to + */ + protected void flushBeforeImpl(long pPosition) { + } + + protected void seekImpl(long pPosition) throws IOException { + mRandomAccess.seek(pPosition); + } +} 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 b275e5bf..81668a88 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 @@ -1,103 +1,102 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.InputStreamReader; - -/** - * FileSystem - *

- * - * @author Harald Kuhr - * @version $Id: FileSystem.java#1 $ - */ -abstract class FileSystem { - abstract long getFreeSpace(File pPath); - - abstract long getTotalSpace(File pPath); - - abstract String getName(); - - static BufferedReader exec(String[] pArgs) throws IOException { - Process cmd = Runtime.getRuntime().exec(pArgs); - return new BufferedReader(new InputStreamReader(cmd.getInputStream())); - } - - static FileSystem get() { - String os = System.getProperty("os.name"); - //System.out.println("os = " + os); - - os = os.toLowerCase(); - if (os.contains("windows")) { - return new Win32FileSystem(); - } - 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 { - return new UnknownFileSystem(os); - } - } - - private static class UnknownFileSystem extends FileSystem { - private final String osName; - - UnknownFileSystem(String pOSName) { - osName = pOSName; - } - - long getFreeSpace(File pPath) { - return 0l; - } - - long getTotalSpace(File pPath) { - return 0l; - } - - String getName() { - return "Unknown (" + osName + ")"; - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * FileSystem + * + * @author Harald Kuhr + * @version $Id: FileSystem.java#1 $ + */ +abstract class FileSystem { + abstract long getFreeSpace(File pPath); + + abstract long getTotalSpace(File pPath); + + abstract String getName(); + + static BufferedReader exec(String[] pArgs) throws IOException { + Process cmd = Runtime.getRuntime().exec(pArgs); + return new BufferedReader(new InputStreamReader(cmd.getInputStream())); + } + + static FileSystem get() { + String os = System.getProperty("os.name"); + //System.out.println("os = " + os); + + os = os.toLowerCase(); + if (os.contains("windows")) { + return new Win32FileSystem(); + } + 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 { + return new UnknownFileSystem(os); + } + } + + private static class UnknownFileSystem extends FileSystem { + private final String osName; + + UnknownFileSystem(String pOSName) { + osName = pOSName; + } + + long getFreeSpace(File pPath) { + return 0l; + } + + long getTotalSpace(File pPath) { + return 0l; + } + + String getName() { + 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 58ee8f7f..91a6a7d5 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 @@ -1,1083 +1,1082 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.lang.Validate; -import com.twelvemonkeys.util.Visitor; - -import java.io.*; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.UndeclaredThrowableException; -import java.net.URL; -import java.text.NumberFormat; - -/** - * A utility class with some useful file and i/o related methods. - *

- * Versions exists take Input and OutputStreams as parameters, to - * allow for copying streams (URL's etc.). - * - * @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/io/FileUtil.java#3 $ - */ -public final class FileUtil { - // TODO: Be more cosequent using resolve() all places where File objects are involved - // TODO: Parameter handling (allow null vs IllegalArgument) - // TODO: Exception handling - - /** - * The size of the buffer used for copying - */ - public final static int BUF_SIZE = 1024; - private static String TEMP_DIR = null; - - private final static FileSystem FS = FileSystem.get(); - - public static void main(String[] pArgs) throws IOException { - File file; - if (pArgs[0].startsWith("file:")) { - file = toFile(new URL(pArgs[0])); - System.out.println(file); - } - else { - file = new File(pArgs[0]); - System.out.println(file.toURL()); - } - - System.out.println("Free space: " + getFreeSpace(file) + "/" + getTotalSpace(file) + " bytes"); - } - - /* - * Method main for test only. - * - public static void main0(String[] pArgs) { - if (pArgs.length != 2) { - System.out.println("usage: java Copy in out"); - return; - } - try { - if (!copy(pArgs[0], pArgs[1])) { - System.out.println("Error copying"); - } - } - catch (IOException e) { - System.out.println(e.getMessage()); - } - } - //*/ - - // Avoid instances/constructor showing up in API doc - private FileUtil() {} - - /** - * Copies the fromFile to the toFile location. If toFile is a directory, a - * new file is created in that directory, with the name of the fromFile. - * If the toFile exists, the file will not be copied, unless owerWrite is - * true. - * - * @param pFromFileName The name of the file to copy from - * @param pToFileName The name of the file to copy to - * @return true if the file was copied successfully, - * false if the output file exists. In all other cases, an - * IOException is thrown, and the method does not return a value. - * @throws IOException if an i/o error occurs during copy - */ - public static boolean copy(String pFromFileName, String pToFileName) throws IOException { - return copy(new File(pFromFileName), new File(pToFileName), false); - } - - /** - * Copies the fromFile to the toFile location. If toFile is a directory, a - * new file is created in that directory, with the name of the fromFile. - * If the toFile exists, the file will not be copied, unless owerWrite is - * true. - * - * @param pFromFileName The name of the file to copy from - * @param pToFileName The name of the file to copy to - * @param pOverWrite Specifies if the toFile should be overwritten, if it - * exists. - * @return true if the file was copied successfully, - * false if the output file exists, and the owerWrite parameter is - * false. In all other cases, an - * IOException is thrown, and the method does not return a value. - * @throws IOException if an i/o error occurs during copy - */ - public static boolean copy(String pFromFileName, String pToFileName, boolean pOverWrite) throws IOException { - return copy(new File(pFromFileName), new File(pToFileName), pOverWrite); - } - - /** - * Copies the fromFile to the toFile location. If toFile is a directory, a - * new file is created in that directory, with the name of the fromFile. - * If the toFile exists, the file will not be copied, unless owerWrite is - * true. - * - * @param pFromFile The file to copy from - * @param pToFile The file to copy to - * @return true if the file was copied successfully, - * false if the output file exists. In all other cases, an - * IOException is thrown, and the method does not return a value. - * @throws IOException if an i/o error occurs during copy - */ - public static boolean copy(File pFromFile, File pToFile) throws IOException { - return copy(pFromFile, pToFile, false); - } - - /** - * Copies the fromFile to the toFile location. If toFile is a directory, a - * new file is created in that directory, with the name of the fromFile. - * If the toFile exists, the file will not be copied, unless owerWrite is - * true. - * - * @param pFromFile The file to copy from - * @param pToFile The file to copy to - * @param pOverWrite Specifies if the toFile should be overwritten, if it - * exists. - * @return {@code true} if the file was copied successfully, - * {@code false} if the output file exists, and the - * {@code pOwerWrite} parameter is - * {@code false}. In all other cases, an - * {@code IOExceptio}n is thrown, and the method does not return. - * @throws IOException if an i/o error occurs during copy - * @todo Test copyDir functionality! - */ - public static boolean copy(File pFromFile, File pToFile, boolean pOverWrite) throws IOException { - // Copy all directory structure - if (pFromFile.isDirectory()) { - return copyDir(pFromFile, pToFile, pOverWrite); - } - - // Check if destination is a directory - if (pToFile.isDirectory()) { - // Create a new file with same name as from - pToFile = new File(pToFile, pFromFile.getName()); - } - - // Check if file exists, and return false if overWrite is false - if (!pOverWrite && pToFile.exists()) { - return false; - } - - InputStream in = null; - OutputStream out = null; - - try { - // Use buffer size two times byte array, to avoid i/o bottleneck - in = new FileInputStream(pFromFile); - out = new FileOutputStream(pToFile); - - // Copy from inputStream to outputStream - copy(in, out); - } - //Just pass any IOException on up the stack - finally { - close(in); - close(out); - } - - return true; // If we got here, everything is probably okay.. ;-) - } - - /** - * Tries to close the given stream. - * NOTE: If the stream cannot be closed, the IOException thrown is silently - * ignored. - * - * @param pInput the stream to close - */ - public static void close(InputStream pInput) { - try { - if (pInput != null) { - pInput.close(); - } - } - catch (IOException ignore) { - // Non critical error - } - } - - /** - * Tries to close the given stream. - * NOTE: If the stream cannot be closed, the IOException thrown is silently - * ignored. - * - * @param pOutput the stream to close - */ - public static void close(OutputStream pOutput) { - try { - if (pOutput != null) { - pOutput.close(); - } - } - catch (IOException ignore) { - // Non critical error - } - } - - static void close(Reader pReader) { - try { - if (pReader != null) { - pReader.close(); - } - } - catch (IOException ignore) { - // Non critical error - } - } - - static void close(Writer pWriter) { - try { - if (pWriter != null) { - pWriter.close(); - } - } - catch (IOException ignore) { - // Non critical error - } - } - - /** - * Copies a directory recursively. If the destination folder does not exist, - * it is created - * - * @param pFrom the source directory - * @param pTo the destination directory - * @param pOverWrite {@code true} if we should allow overwrting existing files - * @return {@code true} if all files were copied sucessfully - * @throws IOException if {@code pTo} exists, and it not a directory, - * or if copying of any of the files in the folder fails - */ - private static boolean copyDir(File pFrom, File pTo, boolean pOverWrite) throws IOException { - if (pTo.exists() && !pTo.isDirectory()) { - throw new IOException("A directory may only be copied to another directory, not to a file"); - } - pTo.mkdirs(); // mkdir? - boolean allOkay = true; - File[] files = pFrom.listFiles(); - - for (File file : files) { - if (!copy(file, new File(pTo, file.getName()), pOverWrite)) { - allOkay = false; - } - } - return allOkay; - } - - /** - * Copies all data from one stream to another. - * The data is copied from the fromStream to the toStream using buffered - * streams for efficiency. - * - * @param pFrom The input srteam to copy from - * @param pTo The output stream to copy to - * @return true. Otherwise, an - * IOException is thrown, and the method does not return a value. - * @throws IOException if an i/o error occurs during copy - * @throws IllegalArgumentException if either {@code pFrom} or {@code pTo} is - * {@code null} - */ - public static boolean copy(InputStream pFrom, OutputStream pTo) throws IOException { - 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); - OutputStream out = new BufferedOutputStream(pTo, BUF_SIZE * 2); - - byte[] buffer = new byte[BUF_SIZE]; - int count; - - while ((count = in.read(buffer)) != -1) { - out.write(buffer, 0, count); - } - - // Flush out stream, to write any remaining buffered data - out.flush(); - - return true; // If we got here, everything is probably okay.. ;-) - } - - /** - * Gets the file (type) extension of the given file. - * A file extension is the part of the filename, after the last occurence - * of a period {@code '.'}. - * If the filename contains no period, {@code null} is returned. - * - * @param pFileName the full filename with extension - * @return the extension (type) of the file, or {@code null} - */ - public static String getExtension(final String pFileName) { - return getExtension0(getFilename(pFileName)); - } - - /** - * Gets the file (type) extension of the given file. - * A file extension is the part of the filename, after the last occurence - * of a period {@code '.'}. - * If the filename contains no period, {@code null} is returned. - * - * @param pFile the file - * @return the extension (type) of the file, or {@code null} - */ - public static String getExtension(final File pFile) { - return getExtension0(pFile.getName()); - } - - // NOTE: Assumes filename and no path - private static String getExtension0(final String pFileName) { - int index = pFileName.lastIndexOf('.'); - - if (index >= 0) { - return pFileName.substring(index + 1); - } - - // No period found - return null; - } - - - /** - * Gets the file name of the given file, without the extension (type). - * A file extension is the part of the filename, after the last occurence - * of a period {@code '.'}. - * If the filename contains no period, the complete file name is returned - * (same as {@code pFileName}, if the string contains no path elements). - * - * @param pFileName the full filename with extension - * @return the base name of the file - */ - public static String getBasename(final String pFileName) { - return getBasename0(getFilename(pFileName)); - } - - /** - * Gets the file name of the given file, without the extension (type). - * A file extension is the part of the filename, after the last occurence - * of a period {@code '.'}. - * If the filename contains no period, {@code pFile.getName()} is returned. - * - * @param pFile the file - * @return the base name of the file - */ - public static String getBasename(final File pFile) { - return getBasename0(pFile.getName()); - } - - // NOTE: Assumes filename and no path - public static String getBasename0(final String pFileName) { - int index = pFileName.lastIndexOf('.'); - - if (index >= 0) { - return pFileName.substring(0, index); - } - - // No period found - return pFileName; - } - - /** - * Extracts the directory path without the filename, from a complete - * filename path. - * - * @param pPath The full filename path. - * @return the path without the filename. - * @see File#getParent - * @see #getFilename - */ - public static String getDirectoryname(final String pPath) { - return getDirectoryname(pPath, File.separatorChar); - } - - /** - * Extracts the directory path without the filename, from a complete - * filename path. - * - * @param pPath The full filename path. - * @param pSeparator the separator char used in {@code pPath} - * @return the path without the filename. - * @see File#getParent - * @see #getFilename - */ - public static String getDirectoryname(final String pPath, final char pSeparator) { - int index = pPath.lastIndexOf(pSeparator); - - if (index < 0) { - return ""; // Assume only filename - } - return pPath.substring(0, index); - } - - /** - * Extracts the filename of a complete filename path. - * - * @param pPath The full filename path. - * @return the extracted filename. - * @see File#getName - * @see #getDirectoryname - */ - public static String getFilename(final String pPath) { - return getFilename(pPath, File.separatorChar); - } - - /** - * Extracts the filename of a complete filename path. - * - * @param pPath The full filename path. - * @param pSeparator The file separator. - * @return the extracted filename. - * @see File#getName - * @see #getDirectoryname - */ - public static String getFilename(final String pPath, final char pSeparator) { - int index = pPath.lastIndexOf(pSeparator); - - if (index < 0) { - return pPath; // Assume only filename - } - - return pPath.substring(index + 1); - } - - - /** - * Tests if a file or directory has no content. - * A file is empty if it has a length of 0L. A non-existing file is also - * considered empty. - * A directory is considered empty if it contains no files. - * - * @param pFile The file to test - * @return {@code true} if the file is empty, otherwise - * {@code false}. - */ - public static boolean isEmpty(File pFile) { - if (pFile.isDirectory()) { - return (pFile.list().length == 0); - } - return (pFile.length() == 0); - } - - /** - * Gets the default temp directory for the system as a File. - * - * @return a {@code File}, representing the default temp directory. - * @see File#createTempFile - */ - public static File getTempDirFile() { - return new File(getTempDir()); - } - - /** - * Gets the default temp directory for the system. - * - * @return a {@code String}, representing the path to the default temp - * directory. - * @see File#createTempFile - */ - public static String getTempDir() { - synchronized (FileUtil.class) { - if (TEMP_DIR == null) { - // Get the 'java.io.tmpdir' property - String tmpDir = System.getProperty("java.io.tmpdir"); - - if (StringUtil.isEmpty(tmpDir)) { - // Stupid fallback... - // TODO: Delegate to FileSystem? - if (new File("/temp").exists()) { - tmpDir = "/temp"; // Windows - } - else { - tmpDir = "/tmp"; // Unix - } - } - TEMP_DIR = tmpDir; - } - } - return TEMP_DIR; - } - - /** - * Gets the contents of the given file, as a byte array. - * - * @param pFilename the name of the file to get content from - * @return the content of the file as a byte array. - * @throws IOException if the read operation fails - */ - public static byte[] read(String pFilename) throws IOException { - return read(new File(pFilename)); - } - - /** - * Gets the contents of the given file, as a byte array. - * - * @param pFile the file to get content from - * @return the content of the file as a byte array. - * @throws IOException if the read operation fails - */ - public static byte[] read(File pFile) throws IOException { - // Custom implementation, as we know the size of a file - if (!pFile.exists()) { - throw new FileNotFoundException(pFile.toString()); - } - - byte[] bytes = new byte[(int) pFile.length()]; - InputStream in = null; - - try { - // Use buffer size two times byte array, to avoid i/o bottleneck - in = new BufferedInputStream(new FileInputStream(pFile), BUF_SIZE * 2); - - int off = 0; - int len; - while ((len = in.read(bytes, off, in.available())) != -1 && (off < bytes.length)) { - off += len; - // System.out.println("read:" + len); - } - } - // Just pass any IOException on up the stack - finally { - close(in); - } - - return bytes; - } - - /** - * Reads all data from the input stream to a byte array. - * - * @param pInput The input stream to read from - * @return The content of the stream as a byte array. - * @throws IOException if an i/o error occurs during read. - */ - public static byte[] read(InputStream pInput) throws IOException { - // Create byte array - ByteArrayOutputStream bytes = new FastByteArrayOutputStream(BUF_SIZE); - - // Copy from stream to byte array - copy(pInput, bytes); - - return bytes.toByteArray(); - } - - /** - * Writes the contents from a byte array to an output stream. - * - * @param pOutput The output stream to write to - * @param pData The byte array to write - * @return {@code true}, otherwise an IOException is thrown. - * @throws IOException if an i/o error occurs during write. - */ - public static boolean write(OutputStream pOutput, byte[] pData) throws IOException { - // Write data - pOutput.write(pData); - - // If we got here, all is okay - return true; - } - - /** - * Writes the contents from a byte array to a file. - * - * @param pFile The file to write to - * @param pData The byte array to write - * @return {@code true}, otherwise an IOException is thrown. - * @throws IOException if an i/o error occurs during write. - */ - public static boolean write(File pFile, byte[] pData) throws IOException { - boolean success = false; - OutputStream out = null; - - try { - out = new BufferedOutputStream(new FileOutputStream(pFile)); - success = write(out, pData); - } - finally { - close(out); - } - return success; - } - - /** - * Writes the contents from a byte array to a file. - * - * @param pFilename The name of the file to write to - * @param pData The byte array to write - * @return {@code true}, otherwise an IOException is thrown. - * @throws IOException if an i/o error occurs during write. - */ - public static boolean write(String pFilename, byte[] pData) throws IOException { - return write(new File(pFilename), pData); - } - - /** - * Deletes the specified file. - * - * @param pFile The file to delete - * @param pForce Forces delete, even if the parameter is a directory, and - * is not empty. Be careful! - * @return {@code true}, if the file existed and was deleted. - * @throws IOException if an i/o error occurs during delete. - */ - public static boolean delete(final File pFile, final boolean pForce) throws IOException { - if (pForce && pFile.isDirectory()) { - return deleteDir(pFile); - } - return pFile.exists() && pFile.delete(); - } - - /** - * Deletes a directory recursively. - * - * @param pFile the file to delete - * @return {@code true} if the file was deleted sucessfully - * @throws IOException if an i/o error occurs during delete. - */ - private static boolean deleteDir(final File pFile) throws IOException { - // Recusively delete all files/subfolders - // Deletes the files using visitor pattern, to avoid allocating - // a file array, which may throw OutOfMemoryExceptions for - // large directories/in low memory situations - class DeleteFilesVisitor implements Visitor { - private int failedCount = 0; - private IOException exception = null; - - public void visit(final File pFile) { - try { - if (!delete(pFile, true)) { - failedCount++; - } - } - catch (IOException e) { - failedCount++; - if (exception == null) { - exception = e; - } - } - } - - boolean succeeded() throws IOException { - if (exception != null) { - throw exception; - } - return failedCount == 0; - } - } - DeleteFilesVisitor fileDeleter = new DeleteFilesVisitor(); - visitFiles(pFile, null, fileDeleter); - - // If any of the deletes above failed, this will fail (or return false) - return fileDeleter.succeeded() && pFile.delete(); - } - - /** - * Deletes the specified file. - * - * @param pFilename The name of file to delete - * @param pForce Forces delete, even if the parameter is a directory, and - * is not empty. Careful! - * @return {@code true}, if the file existed and was deleted. - * @throws java.io.IOException if deletion fails - */ - public static boolean delete(String pFilename, boolean pForce) throws IOException { - return delete(new File(pFilename), pForce); - } - - /** - * Deletes the specified file. - * - * @param pFile The file to delete - * @return {@code true}, if the file existed and was deleted. - * @throws java.io.IOException if deletion fails - */ - public static boolean delete(File pFile) throws IOException { - return delete(pFile, false); - } - - /** - * Deletes the specified file. - * - * @param pFilename The name of file to delete - * @return {@code true}, if the file existed and was deleted. - * @throws java.io.IOException if deletion fails - */ - public static boolean delete(String pFilename) throws IOException { - return delete(new File(pFilename), false); - } - - /** - * Renames the specified file. - * If the destination is a directory (and the source is not), the source - * file is simply moved to the destination directory. - * - * @param pFrom The file to rename - * @param pTo The new file - * @param pOverWrite Specifies if the tofile should be overwritten, if it - * exists - * @return {@code true}, if the file was renamed. - * - * @throws FileNotFoundException if {@code pFrom} does not exist. - */ - public static boolean rename(File pFrom, File pTo, boolean pOverWrite) throws IOException { - if (!pFrom.exists()) { - throw new FileNotFoundException(pFrom.getAbsolutePath()); - } - - if (pFrom.isFile() && pTo.isDirectory()) { - pTo = new File(pTo, pFrom.getName()); - } - return (pOverWrite || !pTo.exists()) && pFrom.renameTo(pTo); - - } - - /** - * Renames the specified file, if the destination does not exist. - * If the destination is a directory (and the source is not), the source - * file is simply moved to the destination directory. - * - * @param pFrom The file to rename - * @param pTo The new file - * @return {@code true}, if the file was renamed. - * @throws java.io.IOException if rename fails - */ - public static boolean rename(File pFrom, File pTo) throws IOException { - return rename(pFrom, pTo, false); - } - - /** - * Renames the specified file. - * If the destination is a directory (and the source is not), the source - * file is simply moved to the destination directory. - * - * @param pFrom The file to rename - * @param pTo The new name of the file - * @param pOverWrite Specifies if the tofile should be overwritten, if it - * exists - * @return {@code true}, if the file was renamed. - * @throws java.io.IOException if rename fails - */ - public static boolean rename(File pFrom, String pTo, boolean pOverWrite) throws IOException { - return rename(pFrom, new File(pTo), pOverWrite); - } - - /** - * Renames the specified file, if the destination does not exist. - * If the destination is a directory (and the source is not), the source - * file is simply moved to the destination directory. - * - * @param pFrom The file to rename - * @param pTo The new name of the file - * @return {@code true}, if the file was renamed. - * @throws java.io.IOException if rename fails - */ - public static boolean rename(File pFrom, String pTo) throws IOException { - return rename(pFrom, new File(pTo), false); - } - - /** - * Renames the specified file. - * If the destination is a directory (and the source is not), the source - * file is simply moved to the destination directory. - * - * @param pFrom The name of the file to rename - * @param pTo The new name of the file - * @param pOverWrite Specifies if the tofile should be overwritten, if it - * exists - * @return {@code true}, if the file was renamed. - * @throws java.io.IOException if rename fails - */ - public static boolean rename(String pFrom, String pTo, boolean pOverWrite) throws IOException { - return rename(new File(pFrom), new File(pTo), pOverWrite); - } - - /** - * Renames the specified file, if the destination does not exist. - * If the destination is a directory (and the source is not), the source - * file is simply moved to the destination directory. - * - * @param pFrom The name of the file to rename - * @param pTo The new name of the file - * @return {@code true}, if the file was renamed. - * @throws java.io.IOException if rename fails - */ - public static boolean rename(String pFrom, String pTo) throws IOException { - return rename(new File(pFrom), new File(pTo), false); - } - - /** - * Lists all files (and directories) in a specific folder. - * - * @param pFolder The folder to list - * @return a list of {@code java.io.File} objects. - * @throws FileNotFoundException if {@code pFolder} is not a readable file - */ - public static File[] list(final String pFolder) throws FileNotFoundException { - return list(pFolder, null); - } - - /** - * Lists all files (and directories) in a specific folder which are - * embraced by the wildcard filename mask provided. - * - * @param pFolder The folder to list - * @param pFilenameMask The wildcard filename mask - * @return a list of {@code java.io.File} objects. - * @see File#listFiles(FilenameFilter) - * @throws FileNotFoundException if {@code pFolder} is not a readable file - */ - public static File[] list(final String pFolder, final String pFilenameMask) throws FileNotFoundException { - if (StringUtil.isEmpty(pFolder)) { - return null; - } - - File folder = resolve(pFolder); - if (!(/*folder.exists() &&*/folder.isDirectory() && folder.canRead())) { - // NOTE: exists is implicitly called by isDirectory - throw new FileNotFoundException("\"" + pFolder + "\" is not a directory or is not readable."); - } - - if (StringUtil.isEmpty(pFilenameMask)) { - return folder.listFiles(); - } - - // TODO: Rewrite to use regexp - - FilenameFilter filter = new FilenameMaskFilter(pFilenameMask); - return folder.listFiles(filter); - } - - /** - * Creates a {@code File} based on the path part of the URL, for - * file-protocol ({@code file:}) based URLs. - * - * @param pURL the {@code file:} URL - * @return a new {@code File} object representing the URL - * - * @throws NullPointerException if {@code pURL} is {@code null} - * @throws IllegalArgumentException if {@code pURL} is - * not a file-protocol URL. - * - * @see java.io.File#toURI() - * @see java.io.File#File(java.net.URI) - */ - public static File toFile(URL pURL) { - if (pURL == null) { - throw new NullPointerException("URL == null"); - } - - // NOTE: Precondition tests below is based on the File(URI) constructor, - // and is most likely overkill... - // NOTE: A URI is absolute iff it has a scheme component - // As the scheme has to be "file", this is implicitly tested below - // NOTE: A URI is opaque iff it is absolute and it's shceme-specific - // part does not begin with a '/', see below - if (!"file".equals(pURL.getProtocol())) { - // URL protocol => URI scheme - throw new IllegalArgumentException("URL scheme is not \"file\""); - } - if (pURL.getAuthority() != null) { - throw new IllegalArgumentException("URL has an authority component"); - } - if (pURL.getRef() != null) { - // URL ref (anchor) => URI fragment - throw new IllegalArgumentException("URI has a fragment component"); - } - if (pURL.getQuery() != null) { - throw new IllegalArgumentException("URL has a query component"); - } - String path = pURL.getPath(); - if (!path.startsWith("/")) { - // A URL should never be able to represent an opaque URI, test anyway - throw new IllegalArgumentException("URI is not hierarchical"); - } - if (path.equals("")) { - throw new IllegalArgumentException("URI path component is empty"); - } - - // Convert separator, doesn't seem to be neccessary on Windows/Unix, - // but do it anyway to be compatible... - if (File.separatorChar != '/') { - path = path.replace('/', File.separatorChar); - } - - return resolve(path); - } - - public static File resolve(String pPath) { - return Win32File.wrap(new File(pPath)); - } - - public static File resolve(File pPath) { - return Win32File.wrap(pPath); - } - - public static File resolve(File pParent, String pChild) { - return Win32File.wrap(new File(pParent, pChild)); - } - - public static File[] resolve(File[] pPaths) { - return Win32File.wrap(pPaths); - } - - // TODO: Handle SecurityManagers in a deterministic way - // TODO: Exception handling - // TODO: What happens if the file does not exist? - public static long getFreeSpace(final File pPath) { - // NOTE: Allow null, to get space in current/system volume - File path = pPath != null ? pPath : new File("."); - - Long space = getSpace16("getFreeSpace", path); - if (space != null) { - return space; - } - - return FS.getFreeSpace(path); - } - - public static long getUsableSpace(final File pPath) { - // NOTE: Allow null, to get space in current/system volume - File path = pPath != null ? pPath : new File("."); - - Long space = getSpace16("getUsableSpace", path); - if (space != null) { - return space; - } - - return getTotalSpace(path); - } - - // TODO: FixMe for Windows, before making it public... - public static long getTotalSpace(final File pPath) { - // NOTE: Allow null, to get space in current/system volume - File path = pPath != null ? pPath : new File("."); - - Long space = getSpace16("getTotalSpace", path); - if (space != null) { - return space; - } - - return FS.getTotalSpace(path); - } - - private static Long getSpace16(final String pMethodName, final File pPath) { - try { - Method freeSpace = File.class.getMethod(pMethodName); - return (Long) freeSpace.invoke(pPath); - } - catch (NoSuchMethodException ignore) {} - catch (IllegalAccessException ignore) {} - catch (InvocationTargetException e) { - Throwable throwable = e.getTargetException(); - if (throwable instanceof SecurityException) { - throw (SecurityException) throwable; - } - throw new UndeclaredThrowableException(throwable); - } - - return null; - } - - /** - * Formats the given number to a human readable format. - * Kind of like {@code df -h}. - * - * @param pSizeInBytes the size in byte - * @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"; - } - else if (pSizeInBytes < (1024L << 10)) { - return getSizeFormat().format(pSizeInBytes / (double) (1024L)) + " KB"; - } - else if (pSizeInBytes < (1024L << 20)) { - return getSizeFormat().format(pSizeInBytes / (double) (1024L << 10)) + " MB"; - } - else if (pSizeInBytes < (1024L << 30)) { - return getSizeFormat().format(pSizeInBytes / (double) (1024L << 20)) + " GB"; - } - else if (pSizeInBytes < (1024L << 40)) { - return getSizeFormat().format(pSizeInBytes / (double) (1024L << 30)) + " TB"; - } - else { - return getSizeFormat().format(pSizeInBytes / (double) (1024L << 40)) + " PB"; - } - } - - // NumberFormat is not thread-safe, so we stick to thread-confined instances - private static ThreadLocal sNumberFormat = new ThreadLocal() { - protected NumberFormat initialValue() { - NumberFormat format = NumberFormat.getNumberInstance(); - // TODO: Consider making this locale/platform specific, OR a method parameter... -// format.setMaximumFractionDigits(2); - format.setMaximumFractionDigits(0); - return format; - } - }; - - private static NumberFormat getSizeFormat() { - return sNumberFormat.get(); - } - - /** - * Visits all files in {@code pDirectory}. Optionally filtered through a {@link FileFilter}. - * - * @param pDirectory the directory to visit files in - * @param pFilter the filter, may be {@code null}, meaning all files will be visited - * @param pVisitor the visitor - * - * @throws IllegalArgumentException if either {@code pDirectory} or {@code pVisitor} are {@code null} - * - * @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"); - - pDirectory.listFiles(new FileFilter() { - public boolean accept(final File pFile) { - if (pFilter == null || pFilter.accept(pFile)) { - pVisitor.visit(pFile); - } - - return false; - } - }); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.lang.Validate; +import com.twelvemonkeys.util.Visitor; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.net.URL; +import java.text.NumberFormat; + +/** + * A utility class with some useful file and i/o related methods. + *

+ * Versions exists take Input and OutputStreams as parameters, to + * allow for copying streams (URL's etc.). + * + * @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/io/FileUtil.java#3 $ + */ +public final class FileUtil { + // TODO: Be more cosequent using resolve() all places where File objects are involved + // TODO: Parameter handling (allow null vs IllegalArgument) + // TODO: Exception handling + + /** + * The size of the buffer used for copying + */ + public final static int BUF_SIZE = 1024; + private static String TEMP_DIR = null; + + private final static FileSystem FS = FileSystem.get(); + + public static void main(String[] pArgs) throws IOException { + File file; + if (pArgs[0].startsWith("file:")) { + file = toFile(new URL(pArgs[0])); + System.out.println(file); + } + else { + file = new File(pArgs[0]); + System.out.println(file.toURL()); + } + + System.out.println("Free space: " + getFreeSpace(file) + "/" + getTotalSpace(file) + " bytes"); + } + + /* + * Method main for test only. + * + public static void main0(String[] pArgs) { + if (pArgs.length != 2) { + System.out.println("usage: java Copy in out"); + return; + } + try { + if (!copy(pArgs[0], pArgs[1])) { + System.out.println("Error copying"); + } + } + catch (IOException e) { + System.out.println(e.getMessage()); + } + } + //*/ + + // Avoid instances/constructor showing up in API doc + private FileUtil() {} + + /** + * Copies the fromFile to the toFile location. If toFile is a directory, a + * new file is created in that directory, with the name of the fromFile. + * If the toFile exists, the file will not be copied, unless owerWrite is + * true. + * + * @param pFromFileName The name of the file to copy from + * @param pToFileName The name of the file to copy to + * @return true if the file was copied successfully, + * false if the output file exists. In all other cases, an + * IOException is thrown, and the method does not return a value. + * @throws IOException if an i/o error occurs during copy + */ + public static boolean copy(String pFromFileName, String pToFileName) throws IOException { + return copy(new File(pFromFileName), new File(pToFileName), false); + } + + /** + * Copies the fromFile to the toFile location. If toFile is a directory, a + * new file is created in that directory, with the name of the fromFile. + * If the toFile exists, the file will not be copied, unless owerWrite is + * true. + * + * @param pFromFileName The name of the file to copy from + * @param pToFileName The name of the file to copy to + * @param pOverWrite Specifies if the toFile should be overwritten, if it + * exists. + * @return true if the file was copied successfully, + * false if the output file exists, and the owerWrite parameter is + * false. In all other cases, an + * IOException is thrown, and the method does not return a value. + * @throws IOException if an i/o error occurs during copy + */ + public static boolean copy(String pFromFileName, String pToFileName, boolean pOverWrite) throws IOException { + return copy(new File(pFromFileName), new File(pToFileName), pOverWrite); + } + + /** + * Copies the fromFile to the toFile location. If toFile is a directory, a + * new file is created in that directory, with the name of the fromFile. + * If the toFile exists, the file will not be copied, unless owerWrite is + * true. + * + * @param pFromFile The file to copy from + * @param pToFile The file to copy to + * @return true if the file was copied successfully, + * false if the output file exists. In all other cases, an + * IOException is thrown, and the method does not return a value. + * @throws IOException if an i/o error occurs during copy + */ + public static boolean copy(File pFromFile, File pToFile) throws IOException { + return copy(pFromFile, pToFile, false); + } + + /** + * Copies the fromFile to the toFile location. If toFile is a directory, a + * new file is created in that directory, with the name of the fromFile. + * If the toFile exists, the file will not be copied, unless owerWrite is + * true. + * + * @param pFromFile The file to copy from + * @param pToFile The file to copy to + * @param pOverWrite Specifies if the toFile should be overwritten, if it + * exists. + * @return {@code true} if the file was copied successfully, + * {@code false} if the output file exists, and the + * {@code pOwerWrite} parameter is + * {@code false}. In all other cases, an + * {@code IOExceptio}n is thrown, and the method does not return. + * @throws IOException if an i/o error occurs during copy + */ + public static boolean copy(File pFromFile, File pToFile, boolean pOverWrite) throws IOException { + // Copy all directory structure + if (pFromFile.isDirectory()) { + return copyDir(pFromFile, pToFile, pOverWrite); + } + + // Check if destination is a directory + if (pToFile.isDirectory()) { + // Create a new file with same name as from + pToFile = new File(pToFile, pFromFile.getName()); + } + + // Check if file exists, and return false if overWrite is false + if (!pOverWrite && pToFile.exists()) { + return false; + } + + InputStream in = null; + OutputStream out = null; + + try { + // Use buffer size two times byte array, to avoid i/o bottleneck + in = new FileInputStream(pFromFile); + out = new FileOutputStream(pToFile); + + // Copy from inputStream to outputStream + copy(in, out); + } + //Just pass any IOException on up the stack + finally { + close(in); + close(out); + } + + return true; // If we got here, everything is probably okay.. ;-) + } + + /** + * Tries to close the given stream. + * NOTE: If the stream cannot be closed, the IOException thrown is silently + * ignored. + * + * @param pInput the stream to close + */ + public static void close(InputStream pInput) { + try { + if (pInput != null) { + pInput.close(); + } + } + catch (IOException ignore) { + // Non critical error + } + } + + /** + * Tries to close the given stream. + * NOTE: If the stream cannot be closed, the IOException thrown is silently + * ignored. + * + * @param pOutput the stream to close + */ + public static void close(OutputStream pOutput) { + try { + if (pOutput != null) { + pOutput.close(); + } + } + catch (IOException ignore) { + // Non critical error + } + } + + static void close(Reader pReader) { + try { + if (pReader != null) { + pReader.close(); + } + } + catch (IOException ignore) { + // Non critical error + } + } + + static void close(Writer pWriter) { + try { + if (pWriter != null) { + pWriter.close(); + } + } + catch (IOException ignore) { + // Non critical error + } + } + + /** + * Copies a directory recursively. If the destination folder does not exist, + * it is created + * + * @param pFrom the source directory + * @param pTo the destination directory + * @param pOverWrite {@code true} if we should allow overwrting existing files + * @return {@code true} if all files were copied sucessfully + * @throws IOException if {@code pTo} exists, and it not a directory, + * or if copying of any of the files in the folder fails + */ + private static boolean copyDir(File pFrom, File pTo, boolean pOverWrite) throws IOException { + if (pTo.exists() && !pTo.isDirectory()) { + throw new IOException("A directory may only be copied to another directory, not to a file"); + } + pTo.mkdirs(); // mkdir? + boolean allOkay = true; + File[] files = pFrom.listFiles(); + + for (File file : files) { + if (!copy(file, new File(pTo, file.getName()), pOverWrite)) { + allOkay = false; + } + } + return allOkay; + } + + /** + * Copies all data from one stream to another. + * The data is copied from the fromStream to the toStream using buffered + * streams for efficiency. + * + * @param pFrom The input srteam to copy from + * @param pTo The output stream to copy to + * @return true. Otherwise, an + * IOException is thrown, and the method does not return a value. + * @throws IOException if an i/o error occurs during copy + * @throws IllegalArgumentException if either {@code pFrom} or {@code pTo} is + * {@code null} + */ + public static boolean copy(InputStream pFrom, OutputStream pTo) throws IOException { + 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); + OutputStream out = new BufferedOutputStream(pTo, BUF_SIZE * 2); + + byte[] buffer = new byte[BUF_SIZE]; + int count; + + while ((count = in.read(buffer)) != -1) { + out.write(buffer, 0, count); + } + + // Flush out stream, to write any remaining buffered data + out.flush(); + + return true; // If we got here, everything is probably okay.. ;-) + } + + /** + * Gets the file (type) extension of the given file. + * A file extension is the part of the filename, after the last occurence + * of a period {@code '.'}. + * If the filename contains no period, {@code null} is returned. + * + * @param pFileName the full filename with extension + * @return the extension (type) of the file, or {@code null} + */ + public static String getExtension(final String pFileName) { + return getExtension0(getFilename(pFileName)); + } + + /** + * Gets the file (type) extension of the given file. + * A file extension is the part of the filename, after the last occurence + * of a period {@code '.'}. + * If the filename contains no period, {@code null} is returned. + * + * @param pFile the file + * @return the extension (type) of the file, or {@code null} + */ + public static String getExtension(final File pFile) { + return getExtension0(pFile.getName()); + } + + // NOTE: Assumes filename and no path + private static String getExtension0(final String pFileName) { + int index = pFileName.lastIndexOf('.'); + + if (index >= 0) { + return pFileName.substring(index + 1); + } + + // No period found + return null; + } + + + /** + * Gets the file name of the given file, without the extension (type). + * A file extension is the part of the filename, after the last occurence + * of a period {@code '.'}. + * If the filename contains no period, the complete file name is returned + * (same as {@code pFileName}, if the string contains no path elements). + * + * @param pFileName the full filename with extension + * @return the base name of the file + */ + public static String getBasename(final String pFileName) { + return getBasename0(getFilename(pFileName)); + } + + /** + * Gets the file name of the given file, without the extension (type). + * A file extension is the part of the filename, after the last occurence + * of a period {@code '.'}. + * If the filename contains no period, {@code pFile.getName()} is returned. + * + * @param pFile the file + * @return the base name of the file + */ + public static String getBasename(final File pFile) { + return getBasename0(pFile.getName()); + } + + // NOTE: Assumes filename and no path + public static String getBasename0(final String pFileName) { + int index = pFileName.lastIndexOf('.'); + + if (index >= 0) { + return pFileName.substring(0, index); + } + + // No period found + return pFileName; + } + + /** + * Extracts the directory path without the filename, from a complete + * filename path. + * + * @param pPath The full filename path. + * @return the path without the filename. + * @see File#getParent + * @see #getFilename + */ + public static String getDirectoryname(final String pPath) { + return getDirectoryname(pPath, File.separatorChar); + } + + /** + * Extracts the directory path without the filename, from a complete + * filename path. + * + * @param pPath The full filename path. + * @param pSeparator the separator char used in {@code pPath} + * @return the path without the filename. + * @see File#getParent + * @see #getFilename + */ + public static String getDirectoryname(final String pPath, final char pSeparator) { + int index = pPath.lastIndexOf(pSeparator); + + if (index < 0) { + return ""; // Assume only filename + } + return pPath.substring(0, index); + } + + /** + * Extracts the filename of a complete filename path. + * + * @param pPath The full filename path. + * @return the extracted filename. + * @see File#getName + * @see #getDirectoryname + */ + public static String getFilename(final String pPath) { + return getFilename(pPath, File.separatorChar); + } + + /** + * Extracts the filename of a complete filename path. + * + * @param pPath The full filename path. + * @param pSeparator The file separator. + * @return the extracted filename. + * @see File#getName + * @see #getDirectoryname + */ + public static String getFilename(final String pPath, final char pSeparator) { + int index = pPath.lastIndexOf(pSeparator); + + if (index < 0) { + return pPath; // Assume only filename + } + + return pPath.substring(index + 1); + } + + + /** + * Tests if a file or directory has no content. + * A file is empty if it has a length of 0L. A non-existing file is also + * considered empty. + * A directory is considered empty if it contains no files. + * + * @param pFile The file to test + * @return {@code true} if the file is empty, otherwise + * {@code false}. + */ + public static boolean isEmpty(File pFile) { + if (pFile.isDirectory()) { + return (pFile.list().length == 0); + } + return (pFile.length() == 0); + } + + /** + * Gets the default temp directory for the system as a File. + * + * @return a {@code File}, representing the default temp directory. + * @see File#createTempFile + */ + public static File getTempDirFile() { + return new File(getTempDir()); + } + + /** + * Gets the default temp directory for the system. + * + * @return a {@code String}, representing the path to the default temp + * directory. + * @see File#createTempFile + */ + public static String getTempDir() { + synchronized (FileUtil.class) { + if (TEMP_DIR == null) { + // Get the 'java.io.tmpdir' property + String tmpDir = System.getProperty("java.io.tmpdir"); + + if (StringUtil.isEmpty(tmpDir)) { + // Stupid fallback... + // TODO: Delegate to FileSystem? + if (new File("/temp").exists()) { + tmpDir = "/temp"; // Windows + } + else { + tmpDir = "/tmp"; // Unix + } + } + TEMP_DIR = tmpDir; + } + } + return TEMP_DIR; + } + + /** + * Gets the contents of the given file, as a byte array. + * + * @param pFilename the name of the file to get content from + * @return the content of the file as a byte array. + * @throws IOException if the read operation fails + */ + public static byte[] read(String pFilename) throws IOException { + return read(new File(pFilename)); + } + + /** + * Gets the contents of the given file, as a byte array. + * + * @param pFile the file to get content from + * @return the content of the file as a byte array. + * @throws IOException if the read operation fails + */ + public static byte[] read(File pFile) throws IOException { + // Custom implementation, as we know the size of a file + if (!pFile.exists()) { + throw new FileNotFoundException(pFile.toString()); + } + + byte[] bytes = new byte[(int) pFile.length()]; + InputStream in = null; + + try { + // Use buffer size two times byte array, to avoid i/o bottleneck + in = new BufferedInputStream(new FileInputStream(pFile), BUF_SIZE * 2); + + int off = 0; + int len; + while ((len = in.read(bytes, off, in.available())) != -1 && (off < bytes.length)) { + off += len; + // System.out.println("read:" + len); + } + } + // Just pass any IOException on up the stack + finally { + close(in); + } + + return bytes; + } + + /** + * Reads all data from the input stream to a byte array. + * + * @param pInput The input stream to read from + * @return The content of the stream as a byte array. + * @throws IOException if an i/o error occurs during read. + */ + public static byte[] read(InputStream pInput) throws IOException { + // Create byte array + ByteArrayOutputStream bytes = new FastByteArrayOutputStream(BUF_SIZE); + + // Copy from stream to byte array + copy(pInput, bytes); + + return bytes.toByteArray(); + } + + /** + * Writes the contents from a byte array to an output stream. + * + * @param pOutput The output stream to write to + * @param pData The byte array to write + * @return {@code true}, otherwise an IOException is thrown. + * @throws IOException if an i/o error occurs during write. + */ + public static boolean write(OutputStream pOutput, byte[] pData) throws IOException { + // Write data + pOutput.write(pData); + + // If we got here, all is okay + return true; + } + + /** + * Writes the contents from a byte array to a file. + * + * @param pFile The file to write to + * @param pData The byte array to write + * @return {@code true}, otherwise an IOException is thrown. + * @throws IOException if an i/o error occurs during write. + */ + public static boolean write(File pFile, byte[] pData) throws IOException { + boolean success = false; + OutputStream out = null; + + try { + out = new BufferedOutputStream(new FileOutputStream(pFile)); + success = write(out, pData); + } + finally { + close(out); + } + return success; + } + + /** + * Writes the contents from a byte array to a file. + * + * @param pFilename The name of the file to write to + * @param pData The byte array to write + * @return {@code true}, otherwise an IOException is thrown. + * @throws IOException if an i/o error occurs during write. + */ + public static boolean write(String pFilename, byte[] pData) throws IOException { + return write(new File(pFilename), pData); + } + + /** + * Deletes the specified file. + * + * @param pFile The file to delete + * @param pForce Forces delete, even if the parameter is a directory, and + * is not empty. Be careful! + * @return {@code true}, if the file existed and was deleted. + * @throws IOException if an i/o error occurs during delete. + */ + public static boolean delete(final File pFile, final boolean pForce) throws IOException { + if (pForce && pFile.isDirectory()) { + return deleteDir(pFile); + } + return pFile.exists() && pFile.delete(); + } + + /** + * Deletes a directory recursively. + * + * @param pFile the file to delete + * @return {@code true} if the file was deleted sucessfully + * @throws IOException if an i/o error occurs during delete. + */ + private static boolean deleteDir(final File pFile) throws IOException { + // Recusively delete all files/subfolders + // Deletes the files using visitor pattern, to avoid allocating + // a file array, which may throw OutOfMemoryExceptions for + // large directories/in low memory situations + class DeleteFilesVisitor implements Visitor { + private int failedCount = 0; + private IOException exception = null; + + public void visit(final File pFile) { + try { + if (!delete(pFile, true)) { + failedCount++; + } + } + catch (IOException e) { + failedCount++; + if (exception == null) { + exception = e; + } + } + } + + boolean succeeded() throws IOException { + if (exception != null) { + throw exception; + } + return failedCount == 0; + } + } + DeleteFilesVisitor fileDeleter = new DeleteFilesVisitor(); + visitFiles(pFile, null, fileDeleter); + + // If any of the deletes above failed, this will fail (or return false) + return fileDeleter.succeeded() && pFile.delete(); + } + + /** + * Deletes the specified file. + * + * @param pFilename The name of file to delete + * @param pForce Forces delete, even if the parameter is a directory, and + * is not empty. Careful! + * @return {@code true}, if the file existed and was deleted. + * @throws java.io.IOException if deletion fails + */ + public static boolean delete(String pFilename, boolean pForce) throws IOException { + return delete(new File(pFilename), pForce); + } + + /** + * Deletes the specified file. + * + * @param pFile The file to delete + * @return {@code true}, if the file existed and was deleted. + * @throws java.io.IOException if deletion fails + */ + public static boolean delete(File pFile) throws IOException { + return delete(pFile, false); + } + + /** + * Deletes the specified file. + * + * @param pFilename The name of file to delete + * @return {@code true}, if the file existed and was deleted. + * @throws java.io.IOException if deletion fails + */ + public static boolean delete(String pFilename) throws IOException { + return delete(new File(pFilename), false); + } + + /** + * Renames the specified file. + * If the destination is a directory (and the source is not), the source + * file is simply moved to the destination directory. + * + * @param pFrom The file to rename + * @param pTo The new file + * @param pOverWrite Specifies if the tofile should be overwritten, if it + * exists + * @return {@code true}, if the file was renamed. + * + * @throws FileNotFoundException if {@code pFrom} does not exist. + */ + public static boolean rename(File pFrom, File pTo, boolean pOverWrite) throws IOException { + if (!pFrom.exists()) { + throw new FileNotFoundException(pFrom.getAbsolutePath()); + } + + if (pFrom.isFile() && pTo.isDirectory()) { + pTo = new File(pTo, pFrom.getName()); + } + return (pOverWrite || !pTo.exists()) && pFrom.renameTo(pTo); + + } + + /** + * Renames the specified file, if the destination does not exist. + * If the destination is a directory (and the source is not), the source + * file is simply moved to the destination directory. + * + * @param pFrom The file to rename + * @param pTo The new file + * @return {@code true}, if the file was renamed. + * @throws java.io.IOException if rename fails + */ + public static boolean rename(File pFrom, File pTo) throws IOException { + return rename(pFrom, pTo, false); + } + + /** + * Renames the specified file. + * If the destination is a directory (and the source is not), the source + * file is simply moved to the destination directory. + * + * @param pFrom The file to rename + * @param pTo The new name of the file + * @param pOverWrite Specifies if the tofile should be overwritten, if it + * exists + * @return {@code true}, if the file was renamed. + * @throws java.io.IOException if rename fails + */ + public static boolean rename(File pFrom, String pTo, boolean pOverWrite) throws IOException { + return rename(pFrom, new File(pTo), pOverWrite); + } + + /** + * Renames the specified file, if the destination does not exist. + * If the destination is a directory (and the source is not), the source + * file is simply moved to the destination directory. + * + * @param pFrom The file to rename + * @param pTo The new name of the file + * @return {@code true}, if the file was renamed. + * @throws java.io.IOException if rename fails + */ + public static boolean rename(File pFrom, String pTo) throws IOException { + return rename(pFrom, new File(pTo), false); + } + + /** + * Renames the specified file. + * If the destination is a directory (and the source is not), the source + * file is simply moved to the destination directory. + * + * @param pFrom The name of the file to rename + * @param pTo The new name of the file + * @param pOverWrite Specifies if the tofile should be overwritten, if it + * exists + * @return {@code true}, if the file was renamed. + * @throws java.io.IOException if rename fails + */ + public static boolean rename(String pFrom, String pTo, boolean pOverWrite) throws IOException { + return rename(new File(pFrom), new File(pTo), pOverWrite); + } + + /** + * Renames the specified file, if the destination does not exist. + * If the destination is a directory (and the source is not), the source + * file is simply moved to the destination directory. + * + * @param pFrom The name of the file to rename + * @param pTo The new name of the file + * @return {@code true}, if the file was renamed. + * @throws java.io.IOException if rename fails + */ + public static boolean rename(String pFrom, String pTo) throws IOException { + return rename(new File(pFrom), new File(pTo), false); + } + + /** + * Lists all files (and directories) in a specific folder. + * + * @param pFolder The folder to list + * @return a list of {@code java.io.File} objects. + * @throws FileNotFoundException if {@code pFolder} is not a readable file + */ + public static File[] list(final String pFolder) throws FileNotFoundException { + return list(pFolder, null); + } + + /** + * Lists all files (and directories) in a specific folder which are + * embraced by the wildcard filename mask provided. + * + * @param pFolder The folder to list + * @param pFilenameMask The wildcard filename mask + * @return a list of {@code java.io.File} objects. + * @see File#listFiles(FilenameFilter) + * @throws FileNotFoundException if {@code pFolder} is not a readable file + */ + public static File[] list(final String pFolder, final String pFilenameMask) throws FileNotFoundException { + if (StringUtil.isEmpty(pFolder)) { + return null; + } + + File folder = resolve(pFolder); + if (!(/*folder.exists() &&*/folder.isDirectory() && folder.canRead())) { + // NOTE: exists is implicitly called by isDirectory + throw new FileNotFoundException("\"" + pFolder + "\" is not a directory or is not readable."); + } + + if (StringUtil.isEmpty(pFilenameMask)) { + return folder.listFiles(); + } + + // TODO: Rewrite to use regexp + + FilenameFilter filter = new FilenameMaskFilter(pFilenameMask); + return folder.listFiles(filter); + } + + /** + * Creates a {@code File} based on the path part of the URL, for + * file-protocol ({@code file:}) based URLs. + * + * @param pURL the {@code file:} URL + * @return a new {@code File} object representing the URL + * + * @throws NullPointerException if {@code pURL} is {@code null} + * @throws IllegalArgumentException if {@code pURL} is + * not a file-protocol URL. + * + * @see java.io.File#toURI() + * @see java.io.File#File(java.net.URI) + */ + public static File toFile(URL pURL) { + if (pURL == null) { + throw new NullPointerException("URL == null"); + } + + // NOTE: Precondition tests below is based on the File(URI) constructor, + // and is most likely overkill... + // NOTE: A URI is absolute iff it has a scheme component + // As the scheme has to be "file", this is implicitly tested below + // NOTE: A URI is opaque iff it is absolute and it's shceme-specific + // part does not begin with a '/', see below + if (!"file".equals(pURL.getProtocol())) { + // URL protocol => URI scheme + throw new IllegalArgumentException("URL scheme is not \"file\""); + } + if (pURL.getAuthority() != null) { + throw new IllegalArgumentException("URL has an authority component"); + } + if (pURL.getRef() != null) { + // URL ref (anchor) => URI fragment + throw new IllegalArgumentException("URI has a fragment component"); + } + if (pURL.getQuery() != null) { + throw new IllegalArgumentException("URL has a query component"); + } + String path = pURL.getPath(); + if (!path.startsWith("/")) { + // A URL should never be able to represent an opaque URI, test anyway + throw new IllegalArgumentException("URI is not hierarchical"); + } + if (path.equals("")) { + throw new IllegalArgumentException("URI path component is empty"); + } + + // Convert separator, doesn't seem to be neccessary on Windows/Unix, + // but do it anyway to be compatible... + if (File.separatorChar != '/') { + path = path.replace('/', File.separatorChar); + } + + return resolve(path); + } + + public static File resolve(String pPath) { + return Win32File.wrap(new File(pPath)); + } + + public static File resolve(File pPath) { + return Win32File.wrap(pPath); + } + + public static File resolve(File pParent, String pChild) { + return Win32File.wrap(new File(pParent, pChild)); + } + + public static File[] resolve(File[] pPaths) { + return Win32File.wrap(pPaths); + } + + // TODO: Handle SecurityManagers in a deterministic way + // TODO: Exception handling + // TODO: What happens if the file does not exist? + public static long getFreeSpace(final File pPath) { + // NOTE: Allow null, to get space in current/system volume + File path = pPath != null ? pPath : new File("."); + + Long space = getSpace16("getFreeSpace", path); + if (space != null) { + return space; + } + + return FS.getFreeSpace(path); + } + + public static long getUsableSpace(final File pPath) { + // NOTE: Allow null, to get space in current/system volume + File path = pPath != null ? pPath : new File("."); + + Long space = getSpace16("getUsableSpace", path); + if (space != null) { + return space; + } + + return getTotalSpace(path); + } + + // TODO: FixMe for Windows, before making it public... + public static long getTotalSpace(final File pPath) { + // NOTE: Allow null, to get space in current/system volume + File path = pPath != null ? pPath : new File("."); + + Long space = getSpace16("getTotalSpace", path); + if (space != null) { + return space; + } + + return FS.getTotalSpace(path); + } + + private static Long getSpace16(final String pMethodName, final File pPath) { + try { + Method freeSpace = File.class.getMethod(pMethodName); + return (Long) freeSpace.invoke(pPath); + } + catch (NoSuchMethodException ignore) {} + catch (IllegalAccessException ignore) {} + catch (InvocationTargetException e) { + Throwable throwable = e.getTargetException(); + if (throwable instanceof SecurityException) { + throw (SecurityException) throwable; + } + throw new UndeclaredThrowableException(throwable); + } + + return null; + } + + /** + * Formats the given number to a human readable format. + * Kind of like {@code df -h}. + * + * @param pSizeInBytes the size in byte + * @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"; + } + else if (pSizeInBytes < (1024L << 10)) { + return getSizeFormat().format(pSizeInBytes / (double) (1024L)) + " KB"; + } + else if (pSizeInBytes < (1024L << 20)) { + return getSizeFormat().format(pSizeInBytes / (double) (1024L << 10)) + " MB"; + } + else if (pSizeInBytes < (1024L << 30)) { + return getSizeFormat().format(pSizeInBytes / (double) (1024L << 20)) + " GB"; + } + else if (pSizeInBytes < (1024L << 40)) { + return getSizeFormat().format(pSizeInBytes / (double) (1024L << 30)) + " TB"; + } + else { + return getSizeFormat().format(pSizeInBytes / (double) (1024L << 40)) + " PB"; + } + } + + // NumberFormat is not thread-safe, so we stick to thread-confined instances + private static ThreadLocal sNumberFormat = new ThreadLocal() { + protected NumberFormat initialValue() { + NumberFormat format = NumberFormat.getNumberInstance(); + // TODO: Consider making this locale/platform specific, OR a method parameter... +// format.setMaximumFractionDigits(2); + format.setMaximumFractionDigits(0); + return format; + } + }; + + private static NumberFormat getSizeFormat() { + return sNumberFormat.get(); + } + + /** + * Visits all files in {@code pDirectory}. Optionally filtered through a {@link FileFilter}. + * + * @param pDirectory the directory to visit files in + * @param pFilter the filter, may be {@code null}, meaning all files will be visited + * @param pVisitor the visitor + * + * @throws IllegalArgumentException if either {@code pDirectory} or {@code pVisitor} are {@code null} + * + * @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"); + + pDirectory.listFiles(new FileFilter() { + public boolean accept(final File pFile) { + if (pFilter == null || pFilter.accept(pFile)) { + pVisitor.visit(pFile); + } + + return false; + } + }); + } +} 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 26f5e477..65e9e2de 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 @@ -1,244 +1,249 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.util.regex.WildcardStringParser; - -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 mask is given as a well-known DOS filename format, with '*' and '?' as - * wildcards. - * All other characters counts as ordinary characters. - *

- * The file name masks are used as a filter input and is given to the class via - * the string array property:
- *

{@code filenameMasksForInclusion} - Filename mask for exclusion of - * files (default if both properties are defined) - *
{@code filenameMasksForExclusion} - Filename mask for exclusion of - * files. - *

- * 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 - * @see WildcardStringParser - * @deprecated - */ -public class FilenameMaskFilter implements FilenameFilter { - - // TODO: Rewrite to use regexp, or create new class - - // Members - private String[] filenameMasksForInclusion; - private String[] filenameMasksForExclusion; - private boolean inclusion = true; - - - /** - * Creates a {@code FilenameMaskFilter} - */ - public FilenameMaskFilter() { - } - - /** - * Creates a {@code FilenameMaskFilter} - * - * @param pFilenameMask the filename mask - */ - public FilenameMaskFilter(final String pFilenameMask) { - String[] filenameMask = {pFilenameMask}; - setFilenameMasksForInclusion(filenameMask); - } - - /** - * Creates a {@code FilenameMaskFilter} - * - * @param pFilenameMasks the filename masks - */ - public FilenameMaskFilter(final String[] pFilenameMasks) { - this(pFilenameMasks, false); - } - - /** - * Creates a {@code FilenameMaskFilter} - * - * @param pFilenameMask the filename masks - * @param pExclusion if {@code true}, the masks will be excluded - */ - public FilenameMaskFilter(final String pFilenameMask, final boolean pExclusion) { - String[] filenameMask = {pFilenameMask}; - - if (pExclusion) { - setFilenameMasksForExclusion(filenameMask); - } - else { - setFilenameMasksForInclusion(filenameMask); - } - } - - /** - * Creates a {@code FilenameMaskFilter} - * - * @param pFilenameMasks the filename masks - * @param pExclusion if {@code true}, the masks will be excluded - */ - public FilenameMaskFilter(final String[] pFilenameMasks, final boolean pExclusion) { - if (pExclusion) { - setFilenameMasksForExclusion(pFilenameMasks); - } - else { - setFilenameMasksForInclusion(pFilenameMasks); - } - } - - /** - * - * @param pFilenameMasksForInclusion the filename masks to include - */ - public void setFilenameMasksForInclusion(String[] pFilenameMasksForInclusion) { - filenameMasksForInclusion = pFilenameMasksForInclusion; - } - - /** - * @return the current inclusion masks - */ - public String[] getFilenameMasksForInclusion() { - return filenameMasksForInclusion.clone(); - } - - /** - * @param pFilenameMasksForExclusion the filename masks to exclude - */ - public void setFilenameMasksForExclusion(String[] pFilenameMasksForExclusion) { - filenameMasksForExclusion = pFilenameMasksForExclusion; - inclusion = false; - } - - /** - * @return the current exclusion masks - */ - public String[] getFilenameMasksForExclusion() { - return filenameMasksForExclusion.clone(); - } - - /** - * This method implements the {@code java.io.FilenameFilter} interface. - * - * @param pDir the directory in which the file was found. - * @param pName the name of the file. - * @return {@code true} if the file {@code pName} should be included in the file - * list; {@code false} otherwise. - */ - public boolean accept(File pDir, String pName) { - WildcardStringParser parser; - - // Check each filename string mask whether the file is to be accepted - if (inclusion) { // Inclusion - for (String mask : filenameMasksForInclusion) { - parser = new WildcardStringParser(mask); - if (parser.parseString(pName)) { - - // The filename was accepted by the filename masks provided - // - include it in filename list - return true; - } - } - - // The filename not was accepted by any of the filename masks - // provided - NOT to be included in the filename list - return false; - } - else { - // Exclusion - for (String mask : filenameMasksForExclusion) { - parser = new WildcardStringParser(mask); - if (parser.parseString(pName)) { - - // The filename was accepted by the filename masks provided - // - NOT to be included in the filename list - return false; - } - } - - // The filename was not accepted by any of the filename masks - // provided - include it in filename list - return true; - } - } - - /** - * @return a string representation for debug purposes - */ - public String toString() { - StringBuilder retVal = new StringBuilder(); - int i; - - if (inclusion) { - // Inclusion - if (filenameMasksForInclusion == null) { - retVal.append("No filename masks set - property filenameMasksForInclusion is null!"); - } - else { - retVal.append(filenameMasksForInclusion.length); - retVal.append(" filename mask(s) - "); - for (i = 0; i < filenameMasksForInclusion.length; i++) { - retVal.append("\""); - retVal.append(filenameMasksForInclusion[i]); - retVal.append("\", \""); - } - } - } - else { - // Exclusion - if (filenameMasksForExclusion == null) { - retVal.append("No filename masks set - property filenameMasksForExclusion is null!"); - } - else { - retVal.append(filenameMasksForExclusion.length); - retVal.append(" exclusion filename mask(s) - "); - for (i = 0; i < filenameMasksForExclusion.length; i++) { - retVal.append("\""); - retVal.append(filenameMasksForExclusion[i]); - retVal.append("\", \""); - } - } - } - return retVal.toString(); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.util.regex.WildcardStringParser; + +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 mask is given as a well-known DOS filename format, with '*' and '?' as + * wildcards. + * All other characters counts as ordinary characters. + *

+ * The file name masks are used as a filter input and is given to the class via + * the string array property: + *

+ *
+ *
{@code filenameMasksForInclusion}
+ *
Filename mask for exclusion of + * files (default if both properties are defined).
+ *
{@code filenameMasksForExclusion}
+ *
Filename mask for exclusion of files.
+ *
+ *

+ * 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 + * @see WildcardStringParser + * @deprecated + */ +public class FilenameMaskFilter implements FilenameFilter { + + // TODO: Rewrite to use regexp, or create new class + + // Members + private String[] filenameMasksForInclusion; + private String[] filenameMasksForExclusion; + private boolean inclusion = true; + + + /** + * Creates a {@code FilenameMaskFilter} + */ + public FilenameMaskFilter() { + } + + /** + * Creates a {@code FilenameMaskFilter} + * + * @param pFilenameMask the filename mask + */ + public FilenameMaskFilter(final String pFilenameMask) { + String[] filenameMask = {pFilenameMask}; + setFilenameMasksForInclusion(filenameMask); + } + + /** + * Creates a {@code FilenameMaskFilter} + * + * @param pFilenameMasks the filename masks + */ + public FilenameMaskFilter(final String[] pFilenameMasks) { + this(pFilenameMasks, false); + } + + /** + * Creates a {@code FilenameMaskFilter} + * + * @param pFilenameMask the filename masks + * @param pExclusion if {@code true}, the masks will be excluded + */ + public FilenameMaskFilter(final String pFilenameMask, final boolean pExclusion) { + String[] filenameMask = {pFilenameMask}; + + if (pExclusion) { + setFilenameMasksForExclusion(filenameMask); + } + else { + setFilenameMasksForInclusion(filenameMask); + } + } + + /** + * Creates a {@code FilenameMaskFilter} + * + * @param pFilenameMasks the filename masks + * @param pExclusion if {@code true}, the masks will be excluded + */ + public FilenameMaskFilter(final String[] pFilenameMasks, final boolean pExclusion) { + if (pExclusion) { + setFilenameMasksForExclusion(pFilenameMasks); + } + else { + setFilenameMasksForInclusion(pFilenameMasks); + } + } + + /** + * + * @param pFilenameMasksForInclusion the filename masks to include + */ + public void setFilenameMasksForInclusion(String[] pFilenameMasksForInclusion) { + filenameMasksForInclusion = pFilenameMasksForInclusion; + } + + /** + * @return the current inclusion masks + */ + public String[] getFilenameMasksForInclusion() { + return filenameMasksForInclusion.clone(); + } + + /** + * @param pFilenameMasksForExclusion the filename masks to exclude + */ + public void setFilenameMasksForExclusion(String[] pFilenameMasksForExclusion) { + filenameMasksForExclusion = pFilenameMasksForExclusion; + inclusion = false; + } + + /** + * @return the current exclusion masks + */ + public String[] getFilenameMasksForExclusion() { + return filenameMasksForExclusion.clone(); + } + + /** + * This method implements the {@code java.io.FilenameFilter} interface. + * + * @param pDir the directory in which the file was found. + * @param pName the name of the file. + * @return {@code true} if the file {@code pName} should be included in the file + * list; {@code false} otherwise. + */ + public boolean accept(File pDir, String pName) { + WildcardStringParser parser; + + // Check each filename string mask whether the file is to be accepted + if (inclusion) { // Inclusion + for (String mask : filenameMasksForInclusion) { + parser = new WildcardStringParser(mask); + if (parser.parseString(pName)) { + + // The filename was accepted by the filename masks provided + // - include it in filename list + return true; + } + } + + // The filename not was accepted by any of the filename masks + // provided - NOT to be included in the filename list + return false; + } + else { + // Exclusion + for (String mask : filenameMasksForExclusion) { + parser = new WildcardStringParser(mask); + if (parser.parseString(pName)) { + + // The filename was accepted by the filename masks provided + // - NOT to be included in the filename list + return false; + } + } + + // The filename was not accepted by any of the filename masks + // provided - include it in filename list + return true; + } + } + + /** + * @return a string representation for debug purposes + */ + public String toString() { + StringBuilder retVal = new StringBuilder(); + int i; + + if (inclusion) { + // Inclusion + if (filenameMasksForInclusion == null) { + retVal.append("No filename masks set - property filenameMasksForInclusion is null!"); + } + else { + retVal.append(filenameMasksForInclusion.length); + retVal.append(" filename mask(s) - "); + for (i = 0; i < filenameMasksForInclusion.length; i++) { + retVal.append("\""); + retVal.append(filenameMasksForInclusion[i]); + retVal.append("\", \""); + } + } + } + else { + // Exclusion + if (filenameMasksForExclusion == null) { + retVal.append("No filename masks set - property filenameMasksForExclusion is null!"); + } + else { + retVal.append(filenameMasksForExclusion.length); + retVal.append(" exclusion filename mask(s) - "); + for (i = 0; i < filenameMasksForExclusion.length; i++) { + retVal.append("\""); + retVal.append(filenameMasksForExclusion[i]); + retVal.append("\", \""); + } + } + } + return retVal.toString(); + } +} 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 5f9276f9..2cb94a03 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 @@ -1,452 +1,449 @@ -/* - * 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 of the copyright holder 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 HOLDER 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. - */ -/* - * From http://www.cafeaulait.org/books/javaio/ioexamples/index.html: - * - * Please feel free to use any fragment of this code you need in your own work. - * As far as I am concerned, it's in the public domain. No permission is necessary - * or required. Credit is always appreciated if you use a large chunk or base a - * significant product on one of my examples, but that's not required either. - * - * Elliotte Rusty Harold - */ - -package com.twelvemonkeys.io; - -import com.twelvemonkeys.lang.Validate; - -import java.io.*; - -/** - * A little endian input stream reads two's complement, - * little endian integers, floating point numbers, and characters - * and returns them as Java primitive types. - *

- * The standard {@code java.io.DataInputStream} class - * which this class imitates reads big endian quantities. - *

- * Warning: - * The {@code DataInput} and {@code DataOutput} interfaces - * specifies big endian byte order in their documentation. - * This means that this class is, strictly speaking, not a proper - * implementation. However, I don't see a reason for the these interfaces to - * specify the byte order of their underlying representations. - * - * - * @see com.twelvemonkeys.io.LittleEndianRandomAccessFile - * @see java.io.DataInputStream - * @see java.io.DataInput - * @see java.io.DataOutput - * - * @author Elliotte Rusty Harold - * @author Harald Kuhr - * @version 2 - */ -public class LittleEndianDataInputStream extends FilterInputStream implements DataInput { - // TODO: Optimize by reading into a fixed size (8 bytes) buffer instead of individual read operations? - /** - * Creates a new little endian input stream and chains it to the - * input stream specified by the {@code pStream} argument. - * - * @param pStream the underlying input stream. - * @see java.io.FilterInputStream#in - */ - public LittleEndianDataInputStream(final InputStream pStream) { - super(Validate.notNull(pStream, "stream")); - } - - /** - * Reads a {@code boolean} from the underlying input stream by - * reading a single byte. If the byte is zero, false is returned. - * If the byte is positive, true is returned. - * - * @return the {@code boolean} value read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public boolean readBoolean() throws IOException { - int b = in.read(); - - if (b < 0) { - throw new EOFException(); - } - - return b != 0; - } - - /** - * Reads a signed {@code byte} from the underlying input stream - * with value between -128 and 127 - * - * @return the {@code byte} value read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public byte readByte() throws IOException { - int b = in.read(); - - if (b < 0) { - throw new EOFException(); - } - - return (byte) b; - - } - - /** - * Reads an unsigned {@code byte} from the underlying - * input stream with value between 0 and 255 - * - * @return the {@code byte} value read. - * @throws EOFException if the end of the underlying input - * stream has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public int readUnsignedByte() throws IOException { - int b = in.read(); - - if (b < 0) { - throw new EOFException(); - } - - return b; - } - - /** - * Reads a two byte signed {@code short} from the underlying - * input stream in little endian order, low byte first. - * - * @return the {@code short} read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - 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); - } - - /** - * Reads a two byte unsigned {@code short} from the underlying - * input stream in little endian order, low byte first. - * - * @return the int value of the unsigned short read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public int readUnsignedShort() throws IOException { - int byte1 = in.read(); - int byte2 = in.read(); - - if (byte2 < 0) { - throw new EOFException(); - } - - return (byte2 << 8) + byte1; - } - - /** - * Reads a two byte Unicode {@code char} from the underlying - * input stream in little endian order, low byte first. - * - * @return the int value of the unsigned short read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - 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)); - } - - - /** - * Reads a four byte signed {@code int} from the underlying - * input stream in little endian order, low byte first. - * - * @return the {@code int} read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public int readInt() throws IOException { - int byte1 = in.read(); - int byte2 = in.read(); - int byte3 = in.read(); - int byte4 = in.read(); - - if (byte4 < 0) { - throw new EOFException(); - } - - return (byte4 << 24) | ((byte3 << 24) >>> 8) - | ((byte2 << 24) >>> 16) | ((byte1 << 24) >>> 24); - } - - /** - * Reads an eight byte signed {@code int} from the underlying - * input stream in little endian order, low byte first. - * - * @return the {@code int} read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public long readLong() throws IOException { - long byte1 = in.read(); - long byte2 = in.read(); - long byte3 = in.read(); - long byte4 = in.read(); - long byte5 = in.read(); - long byte6 = in.read(); - long byte7 = in.read(); - long byte8 = in.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) - | ((byte2 << 56) >>> 48) | ((byte1 << 56) >>> 56); - } - - /** - * Reads a string of no more than 65,535 characters - * from the underlying input stream using UTF-8 - * encoding. This method first reads a two byte short - * in big endian order as required by the - * UTF-8 specification. This gives the number of bytes in - * the UTF-8 encoded version of the string. - * Next this many bytes are read and decoded as UTF-8 - * encoded characters. - * - * @return the decoded string - * @throws UTFDataFormatException if the string cannot be decoded - * @throws IOException if the underlying stream throws an IOException. - */ - 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; - - // The first four bits of c1 determine how many bytes are in this char - int test = c1 >> 4; - if (test < 8) { // one byte - numread++; - result[numchars++] = (char) c1; - } - 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)); - } - else { // malformed - throw new UTFDataFormatException(); - } - - } // end while - - return new String(result, 0, numchars); - - } - - /** - * @return the next eight bytes of this input stream, interpreted as a - * little endian {@code double}. - * @throws EOFException if end of stream occurs before eight bytes - * have been read. - * @throws IOException if an I/O error occurs. - */ - public final double readDouble() throws IOException { - return Double.longBitsToDouble(readLong()); - } - - /** - * @return the next four bytes of this input stream, interpreted as a - * little endian {@code int}. - * @throws EOFException if end of stream occurs before four bytes - * have been read. - * @throws IOException if an I/O error occurs. - */ - public final float readFloat() throws IOException { - return Float.intBitsToFloat(readInt()); - } - - /** - * See the general contract of the {@code skipBytes} - * method of {@code DataInput}. - *

- * Bytes for this operation are read from the contained input stream. - * - * @param pLength the number of bytes to be skipped. - * @return the actual number of bytes skipped. - * @exception IOException if an I/O error occurs. - */ - public final int skipBytes(int pLength) throws IOException { - // NOTE: There was probably a bug in ERH's original code here, as skip - // never returns -1, but returns 0 if no more bytes can be skipped... - int total = 0; - int skipped; - - while ((total < pLength) && ((skipped = (int) in.skip(pLength - total)) > 0)) { - total += skipped; - } - - return total; - } - - /** - * See the general contract of the {@code readFully} - * method of {@code DataInput}. - *

- * Bytes - * for this operation are read from the contained - * input stream. - * - * @param pBytes the buffer into which the data is read. - * @throws EOFException if this input stream reaches the end before - * reading all the bytes. - * @throws IOException if an I/O error occurs. - * @see java.io.FilterInputStream#in - */ - public final void readFully(byte pBytes[]) throws IOException { - readFully(pBytes, 0, pBytes.length); - } - - /** - * See the general contract of the {@code readFully} - * method of {@code DataInput}. - *

- * Bytes - * for this operation are read from the contained - * input stream. - * - * @param pBytes the buffer into which the data is read. - * @param pOffset the start offset of the data. - * @param pLength the number of bytes to read. - * @throws EOFException if this input stream reaches the end before - * reading all the bytes. - * @throws IOException if an I/O error occurs. - * @see java.io.FilterInputStream#in - */ - public final void readFully(byte pBytes[], int pOffset, int pLength) throws IOException { - 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; - } - } - - /** - * See the general contract of the {@code readLine} - * method of {@code DataInput}. - *

- * Bytes for this operation are read from the contained input stream. - * - * @deprecated This method does not properly convert bytes to characters. - * - * @return the next line of text from this input stream. - * @exception IOException if an I/O error occurs. - * @see java.io.BufferedReader#readLine() - * @see java.io.DataInputStream#readLine() - * @noinspection deprecation - */ - public String readLine() throws IOException { - DataInputStream ds = new DataInputStream(in); - return ds.readLine(); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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. + */ +/* + * From http://www.cafeaulait.org/books/javaio/ioexamples/index.html: + * + * Please feel free to use any fragment of this code you need in your own work. + * As far as I am concerned, it's in the public domain. No permission is necessary + * or required. Credit is always appreciated if you use a large chunk or base a + * significant product on one of my examples, but that's not required either. + * + * Elliotte Rusty Harold + */ + +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.Validate; + +import java.io.*; + +/** + * A little endian input stream reads two's complement, + * little endian integers, floating point numbers, and characters + * and returns them as Java primitive types. + *

+ * The standard {@code java.io.DataInputStream} class + * which this class imitates reads big endian quantities. + *

+ *

+ * Warning: + * The {@code DataInput} and {@code DataOutput} interfaces + * specifies big endian byte order in their documentation. + * This means that this class is, strictly speaking, not a proper + * implementation. However, I don't see a reason for the these interfaces to + * specify the byte order of their underlying representations. + * + *

+ * + * @see com.twelvemonkeys.io.LittleEndianRandomAccessFile + * @see java.io.DataInputStream + * @see java.io.DataInput + * @see java.io.DataOutput + * + * @author Elliotte Rusty Harold + * @author Harald Kuhr + * @version 2 + */ +public class LittleEndianDataInputStream extends FilterInputStream implements DataInput { + // TODO: Optimize by reading into a fixed size (8 bytes) buffer instead of individual read operations? + /** + * Creates a new little endian input stream and chains it to the + * input stream specified by the {@code pStream} argument. + * + * @param pStream the underlying input stream. + * @see java.io.FilterInputStream#in + */ + public LittleEndianDataInputStream(final InputStream pStream) { + super(Validate.notNull(pStream, "stream")); + } + + /** + * Reads a {@code boolean} from the underlying input stream by + * reading a single byte. If the byte is zero, false is returned. + * If the byte is positive, true is returned. + * + * @return the {@code boolean} value read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public boolean readBoolean() throws IOException { + int b = in.read(); + + if (b < 0) { + throw new EOFException(); + } + + return b != 0; + } + + /** + * Reads a signed {@code byte} from the underlying input stream + * with value between -128 and 127 + * + * @return the {@code byte} value read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public byte readByte() throws IOException { + int b = in.read(); + + if (b < 0) { + throw new EOFException(); + } + + return (byte) b; + + } + + /** + * Reads an unsigned {@code byte} from the underlying + * input stream with value between 0 and 255 + * + * @return the {@code byte} value read. + * @throws EOFException if the end of the underlying input + * stream has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public int readUnsignedByte() throws IOException { + int b = in.read(); + + if (b < 0) { + throw new EOFException(); + } + + return b; + } + + /** + * Reads a two byte signed {@code short} from the underlying + * input stream in little endian order, low byte first. + * + * @return the {@code short} read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + 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); + } + + /** + * Reads a two byte unsigned {@code short} from the underlying + * input stream in little endian order, low byte first. + * + * @return the int value of the unsigned short read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public int readUnsignedShort() throws IOException { + int byte1 = in.read(); + int byte2 = in.read(); + + if (byte2 < 0) { + throw new EOFException(); + } + + return (byte2 << 8) + byte1; + } + + /** + * Reads a two byte Unicode {@code char} from the underlying + * input stream in little endian order, low byte first. + * + * @return the int value of the unsigned short read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + 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)); + } + + + /** + * Reads a four byte signed {@code int} from the underlying + * input stream in little endian order, low byte first. + * + * @return the {@code int} read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public int readInt() throws IOException { + int byte1 = in.read(); + int byte2 = in.read(); + int byte3 = in.read(); + int byte4 = in.read(); + + if (byte4 < 0) { + throw new EOFException(); + } + + return (byte4 << 24) | ((byte3 << 24) >>> 8) + | ((byte2 << 24) >>> 16) | ((byte1 << 24) >>> 24); + } + + /** + * Reads an eight byte signed {@code int} from the underlying + * input stream in little endian order, low byte first. + * + * @return the {@code int} read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public long readLong() throws IOException { + long byte1 = in.read(); + long byte2 = in.read(); + long byte3 = in.read(); + long byte4 = in.read(); + long byte5 = in.read(); + long byte6 = in.read(); + long byte7 = in.read(); + long byte8 = in.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) + | ((byte2 << 56) >>> 48) | ((byte1 << 56) >>> 56); + } + + /** + * Reads a string of no more than 65,535 characters + * from the underlying input stream using UTF-8 + * encoding. This method first reads a two byte short + * in big endian order as required by the + * UTF-8 specification. This gives the number of bytes in + * the UTF-8 encoded version of the string. + * Next this many bytes are read and decoded as UTF-8 + * encoded characters. + * + * @return the decoded string + * @throws UTFDataFormatException if the string cannot be decoded + * @throws IOException if the underlying stream throws an IOException. + */ + 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; + + // The first four bits of c1 determine how many bytes are in this char + int test = c1 >> 4; + if (test < 8) { // one byte + numread++; + result[numchars++] = (char) c1; + } + 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)); + } + else { // malformed + throw new UTFDataFormatException(); + } + + } // end while + + return new String(result, 0, numchars); + + } + + /** + * @return the next eight bytes of this input stream, interpreted as a + * little endian {@code double}. + * @throws EOFException if end of stream occurs before eight bytes + * have been read. + * @throws IOException if an I/O error occurs. + */ + public final double readDouble() throws IOException { + return Double.longBitsToDouble(readLong()); + } + + /** + * @return the next four bytes of this input stream, interpreted as a + * little endian {@code int}. + * @throws EOFException if end of stream occurs before four bytes + * have been read. + * @throws IOException if an I/O error occurs. + */ + public final float readFloat() throws IOException { + return Float.intBitsToFloat(readInt()); + } + + /** + * See the general contract of the {@code skipBytes} + * method of {@code DataInput}. + *

+ * Bytes for this operation are read from the contained input stream. + * + * @param pLength the number of bytes to be skipped. + * @return the actual number of bytes skipped. + * @exception IOException if an I/O error occurs. + */ + public final int skipBytes(int pLength) throws IOException { + // NOTE: There was probably a bug in ERH's original code here, as skip + // never returns -1, but returns 0 if no more bytes can be skipped... + int total = 0; + int skipped; + + while ((total < pLength) && ((skipped = (int) in.skip(pLength - total)) > 0)) { + total += skipped; + } + + return total; + } + + /** + * See the general contract of the {@code readFully} method of {@code DataInput}. + *

+ * Bytes for this operation are read from the contained input stream. + *

+ * + * @param pBytes the buffer into which the data is read. + * @throws EOFException if this input stream reaches the end before + * reading all the bytes. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + public final void readFully(byte pBytes[]) throws IOException { + readFully(pBytes, 0, pBytes.length); + } + + /** + * See the general contract of the {@code readFully} method of {@code DataInput}. + *

+ * Bytes for this operation are read from the contained input stream. + *

+ * + * @param pBytes the buffer into which the data is read. + * @param pOffset the start offset of the data. + * @param pLength the number of bytes to read. + * @throws EOFException if this input stream reaches the end before + * reading all the bytes. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + public final void readFully(byte pBytes[], int pOffset, int pLength) throws IOException { + 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; + } + } + + /** + * See the general contract of the {@code readLine} + * method of {@code DataInput}. + *

+ * Bytes for this operation are read from the contained input stream. + * + * @deprecated This method does not properly convert bytes to characters. + * + * @return the next line of text from this input stream. + * @exception IOException if an I/O error occurs. + * @see java.io.BufferedReader#readLine() + * @see java.io.DataInputStream#readLine() + */ + public String readLine() throws IOException { + DataInputStream ds = new DataInputStream(in); + return ds.readLine(); + } +} 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 23c112f7..68905fa7 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 @@ -1,340 +1,342 @@ -/* - * 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 of the copyright holder 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 HOLDER 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. - */ -/* - * From http://www.cafeaulait.org/books/javaio/ioexamples/index.html: - * - * Please feel free to use any fragment of this code you need in your own work. - * As far as I am concerned, it's in the public domain. No permission is necessary - * or required. Credit is always appreciated if you use a large chunk or base a - * significant product on one of my examples, but that's not required either. - * - * Elliotte Rusty Harold - */ - -package com.twelvemonkeys.io; - -import com.twelvemonkeys.lang.Validate; - -import java.io.*; - -/** - * A little endian output stream writes primitive Java numbers - * and characters to an output stream in a little endian format. - *

- * The standard {@code java.io.DataOutputStream} class which this class - * imitates uses big endian integers. - *

- * Warning: - * The {@code DataInput} and {@code DataOutput} interfaces - * specifies big endian byte order in their documentation. - * This means that this class is, strictly speaking, not a proper - * implementation. However, I don't see a reason for the these interfaces to - * specify the byte order of their underlying representations. - * - * - * @see com.twelvemonkeys.io.LittleEndianRandomAccessFile - * @see java.io.DataOutputStream - * @see java.io.DataInput - * @see java.io.DataOutput - * - * @author Elliotte Rusty Harold - * @version 1.0.1, 19 May 1999 - */ -public class LittleEndianDataOutputStream extends FilterOutputStream implements DataOutput { - - /** - * The number of bytes written so far to the little endian output stream. - */ - protected int bytesWritten; - - /** - * Creates a new little endian output stream and chains it to the - * output stream specified by the {@code pStream} argument. - * - * @param pStream the underlying output stream. - * @see java.io.FilterOutputStream#out - */ - public LittleEndianDataOutputStream(OutputStream pStream) { - super(Validate.notNull(pStream, "stream")); - } - - /** - * Writes the specified byte value to the underlying output stream. - * - * @param pByte the {@code byte} value to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public synchronized void write(int pByte) throws IOException { - out.write(pByte); - bytesWritten++; - } - - /** - * Writes {@code pLength} bytes from the specified byte array - * starting at {@code pOffset} to the underlying output stream. - * - * @param pBytes the data. - * @param pOffset the start offset in the data. - * @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 { - out.write(pBytes, pOffset, pLength); - bytesWritten += pLength; - } - - - /** - * Writes a {@code boolean} to the underlying output stream as - * a single byte. If the argument is true, the byte value 1 is written. - * If the argument is false, the byte value {@code 0} in written. - * - * @param pBoolean the {@code boolean} value to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeBoolean(boolean pBoolean) throws IOException { - if (pBoolean) { - write(1); - } - else { - write(0); - } - } - - /** - * Writes out a {@code byte} to the underlying output stream - * - * @param pByte the {@code byte} value to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeByte(int pByte) throws IOException { - out.write(pByte); - bytesWritten++; - } - - /** - * Writes a two byte {@code short} to the underlying output stream in - * little endian order, low byte first. - * - * @param pShort the {@code short} to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeShort(int pShort) throws IOException { - out.write(pShort & 0xFF); - out.write((pShort >>> 8) & 0xFF); - bytesWritten += 2; - } - - /** - * Writes a two byte {@code char} to the underlying output stream - * in little endian order, low byte first. - * - * @param pChar the {@code char} value to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeChar(int pChar) throws IOException { - out.write(pChar & 0xFF); - out.write((pChar >>> 8) & 0xFF); - bytesWritten += 2; - } - - /** - * Writes a four-byte {@code int} to the underlying output stream - * in little endian order, low byte first, high byte last - * - * @param pInt the {@code int} to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeInt(int pInt) throws IOException { - out.write(pInt & 0xFF); - out.write((pInt >>> 8) & 0xFF); - out.write((pInt >>> 16) & 0xFF); - out.write((pInt >>> 24) & 0xFF); - bytesWritten += 4; - - } - - /** - * Writes an eight-byte {@code long} to the underlying output stream - * in little endian order, low byte first, high byte last - * - * @param pLong the {@code long} to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeLong(long pLong) throws IOException { - out.write((int) pLong & 0xFF); - out.write((int) (pLong >>> 8) & 0xFF); - out.write((int) (pLong >>> 16) & 0xFF); - out.write((int) (pLong >>> 24) & 0xFF); - out.write((int) (pLong >>> 32) & 0xFF); - out.write((int) (pLong >>> 40) & 0xFF); - out.write((int) (pLong >>> 48) & 0xFF); - out.write((int) (pLong >>> 56) & 0xFF); - bytesWritten += 8; - } - - /** - * Writes a 4 byte Java float to the underlying output stream in - * little endian order. - * - * @param f the {@code float} value to be written. - * @throws IOException if an I/O error occurs. - */ - public final void writeFloat(float f) throws IOException { - writeInt(Float.floatToIntBits(f)); - } - - /** - * Writes an 8 byte Java double to the underlying output stream in - * little endian order. - * - * @param d the {@code double} value to be written. - * @throws IOException if an I/O error occurs. - */ - public final void writeDouble(double d) throws IOException { - writeLong(Double.doubleToLongBits(d)); - } - - /** - * Writes a string to the underlying output stream as a sequence of - * bytes. Each character is written to the data output stream as - * if by the {@link #writeByte(int)} method. - * - * @param pString the {@code String} value to be written. - * @throws IOException if the underlying stream throws an IOException. - * @see #writeByte(int) - * @see #out - */ - public void writeBytes(String pString) throws IOException { - int length = pString.length(); - - for (int i = 0; i < length; i++) { - out.write((byte) pString.charAt(i)); - } - - bytesWritten += length; - } - - /** - * Writes a string to the underlying output stream as a sequence of - * characters. Each character is written to the data output stream as - * if by the {@code writeChar} method. - * - * @param pString a {@code String} value to be written. - * @throws IOException if the underlying stream throws an IOException. - * @see #writeChar(int) - * @see #out - */ - 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); - } - - bytesWritten += length * 2; - } - - /** - * Writes a string of no more than 65,535 characters - * to the underlying output stream using UTF-8 - * encoding. This method first writes a two byte short - * in big endian order as required by the - * UTF-8 specification. This gives the number of bytes in the - * UTF-8 encoded version of the string, not the number of characters - * in the string. Next each character of the string is written - * using the UTF-8 encoding for the character. - * - * @param pString the string to be written. - * @throws UTFDataFormatException if the string is longer than - * 65,535 characters. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeUTF(String pString) throws IOException { - int numchars = pString.length(); - int numbytes = 0; - - for (int i = 0; i < numchars; i++) { - int c = pString.charAt(i); - - if ((c >= 0x0001) && (c <= 0x007F)) { - numbytes++; - } - else if (c > 0x07FF) { - numbytes += 3; - } - else { - numbytes += 2; - } - } - - if (numbytes > 65535) { - throw new UTFDataFormatException(); - } - - 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); - } - else if (c > 0x07FF) { - out.write(0xE0 | ((c >> 12) & 0x0F)); - out.write(0x80 | ((c >> 6) & 0x3F)); - out.write(0x80 | (c & 0x3F)); - bytesWritten += 2; - } - else { - out.write(0xC0 | ((c >> 6) & 0x1F)); - out.write(0x80 | (c & 0x3F)); - bytesWritten += 1; - } - } - - bytesWritten += numchars + 2; - } - - /** - * Returns the number of bytes written to this little endian output stream. - * (This class is not thread-safe with respect to this method. It is - * possible that this number is temporarily less than the actual - * number of bytes written.) - * @return the value of the {@code written} field. - * @see #bytesWritten - */ - public int size() { - return bytesWritten; - } +/* + * 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 of the copyright holder 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 HOLDER 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. + */ +/* + * From http://www.cafeaulait.org/books/javaio/ioexamples/index.html: + * + * Please feel free to use any fragment of this code you need in your own work. + * As far as I am concerned, it's in the public domain. No permission is necessary + * or required. Credit is always appreciated if you use a large chunk or base a + * significant product on one of my examples, but that's not required either. + * + * Elliotte Rusty Harold + */ + +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.Validate; + +import java.io.*; + +/** + * A little endian output stream writes primitive Java numbers + * and characters to an output stream in a little endian format. + *

+ * The standard {@code java.io.DataOutputStream} class which this class + * imitates uses big endian integers. + *

+ *

+ * Warning: + * The {@code DataInput} and {@code DataOutput} interfaces + * specifies big endian byte order in their documentation. + * This means that this class is, strictly speaking, not a proper + * implementation. However, I don't see a reason for the these interfaces to + * specify the byte order of their underlying representations. + * + *

+ * + * @see com.twelvemonkeys.io.LittleEndianRandomAccessFile + * @see java.io.DataOutputStream + * @see java.io.DataInput + * @see java.io.DataOutput + * + * @author Elliotte Rusty Harold + * @version 1.0.1, 19 May 1999 + */ +public class LittleEndianDataOutputStream extends FilterOutputStream implements DataOutput { + + /** + * The number of bytes written so far to the little endian output stream. + */ + protected int bytesWritten; + + /** + * Creates a new little endian output stream and chains it to the + * output stream specified by the {@code pStream} argument. + * + * @param pStream the underlying output stream. + * @see java.io.FilterOutputStream#out + */ + public LittleEndianDataOutputStream(OutputStream pStream) { + super(Validate.notNull(pStream, "stream")); + } + + /** + * Writes the specified byte value to the underlying output stream. + * + * @param pByte the {@code byte} value to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public synchronized void write(int pByte) throws IOException { + out.write(pByte); + bytesWritten++; + } + + /** + * Writes {@code pLength} bytes from the specified byte array + * starting at {@code pOffset} to the underlying output stream. + * + * @param pBytes the data. + * @param pOffset the start offset in the data. + * @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 { + out.write(pBytes, pOffset, pLength); + bytesWritten += pLength; + } + + + /** + * Writes a {@code boolean} to the underlying output stream as + * a single byte. If the argument is true, the byte value 1 is written. + * If the argument is false, the byte value {@code 0} in written. + * + * @param pBoolean the {@code boolean} value to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeBoolean(boolean pBoolean) throws IOException { + if (pBoolean) { + write(1); + } + else { + write(0); + } + } + + /** + * Writes out a {@code byte} to the underlying output stream + * + * @param pByte the {@code byte} value to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeByte(int pByte) throws IOException { + out.write(pByte); + bytesWritten++; + } + + /** + * Writes a two byte {@code short} to the underlying output stream in + * little endian order, low byte first. + * + * @param pShort the {@code short} to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeShort(int pShort) throws IOException { + out.write(pShort & 0xFF); + out.write((pShort >>> 8) & 0xFF); + bytesWritten += 2; + } + + /** + * Writes a two byte {@code char} to the underlying output stream + * in little endian order, low byte first. + * + * @param pChar the {@code char} value to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeChar(int pChar) throws IOException { + out.write(pChar & 0xFF); + out.write((pChar >>> 8) & 0xFF); + bytesWritten += 2; + } + + /** + * Writes a four-byte {@code int} to the underlying output stream + * in little endian order, low byte first, high byte last + * + * @param pInt the {@code int} to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeInt(int pInt) throws IOException { + out.write(pInt & 0xFF); + out.write((pInt >>> 8) & 0xFF); + out.write((pInt >>> 16) & 0xFF); + out.write((pInt >>> 24) & 0xFF); + bytesWritten += 4; + + } + + /** + * Writes an eight-byte {@code long} to the underlying output stream + * in little endian order, low byte first, high byte last + * + * @param pLong the {@code long} to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeLong(long pLong) throws IOException { + out.write((int) pLong & 0xFF); + out.write((int) (pLong >>> 8) & 0xFF); + out.write((int) (pLong >>> 16) & 0xFF); + out.write((int) (pLong >>> 24) & 0xFF); + out.write((int) (pLong >>> 32) & 0xFF); + out.write((int) (pLong >>> 40) & 0xFF); + out.write((int) (pLong >>> 48) & 0xFF); + out.write((int) (pLong >>> 56) & 0xFF); + bytesWritten += 8; + } + + /** + * Writes a 4 byte Java float to the underlying output stream in + * little endian order. + * + * @param f the {@code float} value to be written. + * @throws IOException if an I/O error occurs. + */ + public final void writeFloat(float f) throws IOException { + writeInt(Float.floatToIntBits(f)); + } + + /** + * Writes an 8 byte Java double to the underlying output stream in + * little endian order. + * + * @param d the {@code double} value to be written. + * @throws IOException if an I/O error occurs. + */ + public final void writeDouble(double d) throws IOException { + writeLong(Double.doubleToLongBits(d)); + } + + /** + * Writes a string to the underlying output stream as a sequence of + * bytes. Each character is written to the data output stream as + * if by the {@link #writeByte(int)} method. + * + * @param pString the {@code String} value to be written. + * @throws IOException if the underlying stream throws an IOException. + * @see #writeByte(int) + * @see #out + */ + public void writeBytes(String pString) throws IOException { + int length = pString.length(); + + for (int i = 0; i < length; i++) { + out.write((byte) pString.charAt(i)); + } + + bytesWritten += length; + } + + /** + * Writes a string to the underlying output stream as a sequence of + * characters. Each character is written to the data output stream as + * if by the {@code writeChar} method. + * + * @param pString a {@code String} value to be written. + * @throws IOException if the underlying stream throws an IOException. + * @see #writeChar(int) + * @see #out + */ + 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); + } + + bytesWritten += length * 2; + } + + /** + * Writes a string of no more than 65,535 characters + * to the underlying output stream using UTF-8 + * encoding. This method first writes a two byte short + * in big endian order as required by the + * UTF-8 specification. This gives the number of bytes in the + * UTF-8 encoded version of the string, not the number of characters + * in the string. Next each character of the string is written + * using the UTF-8 encoding for the character. + * + * @param pString the string to be written. + * @throws UTFDataFormatException if the string is longer than + * 65,535 characters. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeUTF(String pString) throws IOException { + int numchars = pString.length(); + int numbytes = 0; + + for (int i = 0; i < numchars; i++) { + int c = pString.charAt(i); + + if ((c >= 0x0001) && (c <= 0x007F)) { + numbytes++; + } + else if (c > 0x07FF) { + numbytes += 3; + } + else { + numbytes += 2; + } + } + + if (numbytes > 65535) { + throw new UTFDataFormatException(); + } + + 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); + } + else if (c > 0x07FF) { + out.write(0xE0 | ((c >> 12) & 0x0F)); + out.write(0x80 | ((c >> 6) & 0x3F)); + out.write(0x80 | (c & 0x3F)); + bytesWritten += 2; + } + else { + out.write(0xC0 | ((c >> 6) & 0x1F)); + out.write(0x80 | (c & 0x3F)); + bytesWritten += 1; + } + } + + bytesWritten += numchars + 2; + } + + /** + * Returns the number of bytes written to this little endian output stream. + * (This class is not thread-safe with respect to this method. It is + * possible that this number is temporarily less than the actual + * number of bytes written.) + * @return the value of the {@code written} field. + * @see #bytesWritten + */ + public int size() { + 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 de2b32d0..98e54489 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 @@ -1,627 +1,628 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.FileChannel; - -/** - * A replacement for {@link java.io.RandomAccessFile} that is capable of reading - * and writing data in little endian byte order. - *

- * Warning: - * The {@code DataInput} and {@code DataOutput} interfaces - * specifies big endian byte order in their documentation. - * This means that this class is, strictly speaking, not a proper - * implementation. However, I don't see a reason for the these interfaces to - * specify the byte order of their underlying representations. - * - * - * @see com.twelvemonkeys.io.LittleEndianDataInputStream - * @see com.twelvemonkeys.io.LittleEndianDataOutputStream - * @see java.io.RandomAccessFile - * @see java.io.DataInput - * @see java.io.DataOutput - * - * @author Elliotte Rusty Harold - * @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/io/LittleEndianRandomAccessFile.java#1 $ - */ -public class LittleEndianRandomAccessFile implements DataInput, DataOutput { - 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 { - file = new RandomAccessFile(pFile, pMode); - } - - public void close() throws IOException { - file.close(); - } - - public FileChannel getChannel() { - return file.getChannel(); - } - - public FileDescriptor getFD() throws IOException { - return file.getFD(); - } - - public long getFilePointer() throws IOException { - return file.getFilePointer(); - } - - public long length() throws IOException { - return file.length(); - } - - public int read() throws IOException { - return file.read(); - } - - public int read(final byte[] b) throws IOException { - return file.read(b); - } - - public int read(final byte[] b, final int off, final int len) throws IOException { - return file.read(b, off, len); - } - - public void readFully(final byte[] b) throws IOException { - file.readFully(b); - } - - public void readFully(final byte[] b, final int off, final int len) throws IOException { - file.readFully(b, off, len); - } - - public String readLine() throws IOException { - return file.readLine(); - } - - /** - * Reads a {@code boolean} from the underlying input stream by - * reading a single byte. If the byte is zero, false is returned. - * If the byte is positive, true is returned. - * - * @return the {@code boolean} value read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public boolean readBoolean() throws IOException { - int b = file.read(); - - if (b < 0) { - throw new EOFException(); - } - - return b != 0; - } - - /** - * Reads a signed {@code byte} from the underlying input stream - * with value between -128 and 127 - * - * @return the {@code byte} value read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public byte readByte() throws IOException { - int b = file.read(); - - if (b < 0) { - throw new EOFException(); - } - - return (byte) b; - - } - - /** - * Reads an unsigned {@code byte} from the underlying - * input stream with value between 0 and 255 - * - * @return the {@code byte} value read. - * @throws EOFException if the end of the underlying input - * stream has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public int readUnsignedByte() throws IOException { - int b = file.read(); - - if (b < 0) { - throw new EOFException(); - } - - return b; - } - - /** - * Reads a two byte signed {@code short} from the underlying - * input stream in little endian order, low byte first. - * - * @return the {@code short} read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public short readShort() throws IOException { - 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); - } - - /** - * Reads a two byte unsigned {@code short} from the underlying - * input stream in little endian order, low byte first. - * - * @return the int value of the unsigned short read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public int readUnsignedShort() throws IOException { - 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; - } - - /** - * Reads a two byte Unicode {@code char} from the underlying - * input stream in little endian order, low byte first. - * - * @return the int value of the unsigned short read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public char readChar() throws IOException { - int byte1 = file.read(); - int byte2 = file.read(); - - if (byte2 < 0) { - throw new EOFException(); - } - - return (char) (((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24)); - } - - - /** - * Reads a four byte signed {@code int} from the underlying - * input stream in little endian order, low byte first. - * - * @return the {@code int} read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public int readInt() throws IOException { - 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); - } - - /** - * Reads an eight byte signed {@code int} from the underlying - * input stream in little endian order, low byte first. - * - * @return the {@code int} read. - * @throws EOFException if the end of the underlying input stream - * has been reached - * @throws IOException if the underlying stream throws an IOException. - */ - public long readLong() throws IOException { - 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) - + ((byte2 << 56) >>> 48) + ((byte1 << 56) >>> 56); - - } - - /** - * Reads a string of no more than 65,535 characters - * from the underlying input stream using UTF-8 - * encoding. This method first reads a two byte short - * in big endian order as required by the - * UTF-8 specification. This gives the number of bytes in - * the UTF-8 encoded version of the string. - * Next this many bytes are read and decoded as UTF-8 - * encoded characters. - * - * @return the decoded string - * @throws UTFDataFormatException if the string cannot be decoded - * @throws IOException if the underlying stream throws an IOException. - */ - public String readUTF() throws IOException { - 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; - int numchars = 0; - - while (numread < numbytes) { - - int c1 = readUnsignedByte(); - int c2, c3; - - // The first four bits of c1 determine how many bytes are in this char - int test = c1 >> 4; - if (test < 8) { // one byte - numread++; - result[numchars++] = (char) c1; - } - 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)); - } - else { // malformed - throw new UTFDataFormatException(); - } - - } // end while - - return new String(result, 0, numchars); - } - - /** - * @return the next eight bytes of this input stream, interpreted as a - * little endian {@code double}. - * @throws EOFException if end of stream occurs before eight bytes - * have been read. - * @throws IOException if an I/O error occurs. - */ - public final double readDouble() throws IOException { - return Double.longBitsToDouble(readLong()); - } - - /** - * @return the next four bytes of this input stream, interpreted as a - * little endian {@code int}. - * @throws EOFException if end of stream occurs before four bytes - * have been read. - * @throws IOException if an I/O error occurs. - */ - public final float readFloat() throws IOException { - return Float.intBitsToFloat(readInt()); - } - - /** - * Sets the file-pointer offset, measured from the beginning of this - * file, at which the next read or write occurs. The offset may be - * set beyond the end of the file. Setting the offset beyond the end - * of the file does not change the file length. The file length will - * change only by writing after the offset has been set beyond the end - * of the file. - * - * @param pos the offset position, measured in bytes from the - * beginning of the file, at which to set the file - * pointer. - * @exception IOException if {@code pos} is less than - * {@code 0} or if an I/O error occurs. - */ - public void seek(final long pos) throws IOException { - file.seek(pos); - } - - public void setLength(final long newLength) throws IOException { - file.setLength(newLength); - } - - public int skipBytes(final int n) throws IOException { - return file.skipBytes(n); - } - - public void write(final byte[] b) throws IOException { - file.write(b); - } - - public void write(final byte[] b, final int off, final int len) throws IOException { - file.write(b, off, len); - } - - public void write(final int b) throws IOException { - file.write(b); - } - - /** - * Writes a {@code boolean} to the underlying output stream as - * a single byte. If the argument is true, the byte value 1 is written. - * If the argument is false, the byte value {@code 0} in written. - * - * @param pBoolean the {@code boolean} value to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeBoolean(boolean pBoolean) throws IOException { - if (pBoolean) { - write(1); - } - else { - write(0); - } - } - - /** - * Writes out a {@code byte} to the underlying output stream - * - * @param pByte the {@code byte} value to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeByte(int pByte) throws IOException { - file.write(pByte); - } - - /** - * Writes a two byte {@code short} to the underlying output stream in - * little endian order, low byte first. - * - * @param pShort the {@code short} to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeShort(int pShort) throws IOException { - file.write(pShort & 0xFF); - file.write((pShort >>> 8) & 0xFF); - } - - /** - * Writes a two byte {@code char} to the underlying output stream - * in little endian order, low byte first. - * - * @param pChar the {@code char} value to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeChar(int pChar) throws IOException { - file.write(pChar & 0xFF); - file.write((pChar >>> 8) & 0xFF); - } - - /** - * Writes a four-byte {@code int} to the underlying output stream - * in little endian order, low byte first, high byte last - * - * @param pInt the {@code int} to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeInt(int pInt) throws IOException { - file.write(pInt & 0xFF); - file.write((pInt >>> 8) & 0xFF); - file.write((pInt >>> 16) & 0xFF); - file.write((pInt >>> 24) & 0xFF); - } - - /** - * Writes an eight-byte {@code long} to the underlying output stream - * in little endian order, low byte first, high byte last - * - * @param pLong the {@code long} to be written. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeLong(long pLong) throws IOException { - 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); - } - - /** - * Writes a 4 byte Java float to the underlying output stream in - * little endian order. - * - * @param f the {@code float} value to be written. - * @throws IOException if an I/O error occurs. - */ - public final void writeFloat(float f) throws IOException { - writeInt(Float.floatToIntBits(f)); - } - - /** - * Writes an 8 byte Java double to the underlying output stream in - * little endian order. - * - * @param d the {@code double} value to be written. - * @throws IOException if an I/O error occurs. - */ - public final void writeDouble(double d) throws IOException { - writeLong(Double.doubleToLongBits(d)); - } - - /** - * Writes a string to the underlying output stream as a sequence of - * bytes. Each character is written to the data output stream as - * if by the {@code writeByte()} method. - * - * @param pString the {@code String} value to be written. - * @throws IOException if the underlying stream throws an IOException. - * @see #writeByte(int) - * @see #file - */ - public void writeBytes(String pString) throws IOException { - int length = pString.length(); - - for (int i = 0; i < length; i++) { - file.write((byte) pString.charAt(i)); - } - } - - /** - * Writes a string to the underlying output stream as a sequence of - * characters. Each character is written to the data output stream as - * if by the {@code writeChar} method. - * - * @param pString a {@code String} value to be written. - * @throws IOException if the underlying stream throws an IOException. - * @see #writeChar(int) - * @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); - file.write(c & 0xFF); - file.write((c >>> 8) & 0xFF); - } - } - - /** - * Writes a string of no more than 65,535 characters - * to the underlying output stream using UTF-8 - * encoding. This method first writes a two byte short - * in big endian order as required by the - * UTF-8 specification. This gives the number of bytes in the - * UTF-8 encoded version of the string, not the number of characters - * in the string. Next each character of the string is written - * using the UTF-8 encoding for the character. - * - * @param pString the string to be written. - * @throws UTFDataFormatException if the string is longer than - * 65,535 characters. - * @throws IOException if the underlying stream throws an IOException. - */ - public void writeUTF(String pString) throws IOException { - int numchars = pString.length(); - int numbytes = 0; - - for (int i = 0; i < numchars; i++) { - int c = pString.charAt(i); - - if ((c >= 0x0001) && (c <= 0x007F)) { - numbytes++; - } - else if (c > 0x07FF) { - numbytes += 3; - } - else { - numbytes += 2; - } - } - - if (numbytes > 65535) { - throw new UTFDataFormatException(); - } - - 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)) { - file.write(c); - } - else if (c > 0x07FF) { - file.write(0xE0 | ((c >> 12) & 0x0F)); - file.write(0x80 | ((c >> 6) & 0x3F)); - file.write(0x80 | (c & 0x3F)); - } - else { - file.write(0xC0 | ((c >> 6) & 0x1F)); - file.write(0x80 | (c & 0x3F)); - } - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.FileChannel; + +/** + * A replacement for {@link java.io.RandomAccessFile} that is capable of reading + * and writing data in little endian byte order. + *

+ * Warning: + * The {@code DataInput} and {@code DataOutput} interfaces + * specifies big endian byte order in their documentation. + * This means that this class is, strictly speaking, not a proper + * implementation. However, I don't see a reason for the these interfaces to + * specify the byte order of their underlying representations. + * + *

+ * + * @see com.twelvemonkeys.io.LittleEndianDataInputStream + * @see com.twelvemonkeys.io.LittleEndianDataOutputStream + * @see java.io.RandomAccessFile + * @see java.io.DataInput + * @see java.io.DataOutput + * + * @author Elliotte Rusty Harold + * @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/io/LittleEndianRandomAccessFile.java#1 $ + */ +public class LittleEndianRandomAccessFile implements DataInput, DataOutput { + 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 { + file = new RandomAccessFile(pFile, pMode); + } + + public void close() throws IOException { + file.close(); + } + + public FileChannel getChannel() { + return file.getChannel(); + } + + public FileDescriptor getFD() throws IOException { + return file.getFD(); + } + + public long getFilePointer() throws IOException { + return file.getFilePointer(); + } + + public long length() throws IOException { + return file.length(); + } + + public int read() throws IOException { + return file.read(); + } + + public int read(final byte[] b) throws IOException { + return file.read(b); + } + + public int read(final byte[] b, final int off, final int len) throws IOException { + return file.read(b, off, len); + } + + public void readFully(final byte[] b) throws IOException { + file.readFully(b); + } + + public void readFully(final byte[] b, final int off, final int len) throws IOException { + file.readFully(b, off, len); + } + + public String readLine() throws IOException { + return file.readLine(); + } + + /** + * Reads a {@code boolean} from the underlying input stream by + * reading a single byte. If the byte is zero, false is returned. + * If the byte is positive, true is returned. + * + * @return the {@code boolean} value read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public boolean readBoolean() throws IOException { + int b = file.read(); + + if (b < 0) { + throw new EOFException(); + } + + return b != 0; + } + + /** + * Reads a signed {@code byte} from the underlying input stream + * with value between -128 and 127 + * + * @return the {@code byte} value read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public byte readByte() throws IOException { + int b = file.read(); + + if (b < 0) { + throw new EOFException(); + } + + return (byte) b; + + } + + /** + * Reads an unsigned {@code byte} from the underlying + * input stream with value between 0 and 255 + * + * @return the {@code byte} value read. + * @throws EOFException if the end of the underlying input + * stream has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public int readUnsignedByte() throws IOException { + int b = file.read(); + + if (b < 0) { + throw new EOFException(); + } + + return b; + } + + /** + * Reads a two byte signed {@code short} from the underlying + * input stream in little endian order, low byte first. + * + * @return the {@code short} read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public short readShort() throws IOException { + 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); + } + + /** + * Reads a two byte unsigned {@code short} from the underlying + * input stream in little endian order, low byte first. + * + * @return the int value of the unsigned short read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public int readUnsignedShort() throws IOException { + 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; + } + + /** + * Reads a two byte Unicode {@code char} from the underlying + * input stream in little endian order, low byte first. + * + * @return the int value of the unsigned short read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public char readChar() throws IOException { + int byte1 = file.read(); + int byte2 = file.read(); + + if (byte2 < 0) { + throw new EOFException(); + } + + return (char) (((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24)); + } + + + /** + * Reads a four byte signed {@code int} from the underlying + * input stream in little endian order, low byte first. + * + * @return the {@code int} read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public int readInt() throws IOException { + 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); + } + + /** + * Reads an eight byte signed {@code int} from the underlying + * input stream in little endian order, low byte first. + * + * @return the {@code int} read. + * @throws EOFException if the end of the underlying input stream + * has been reached + * @throws IOException if the underlying stream throws an IOException. + */ + public long readLong() throws IOException { + 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) + + ((byte2 << 56) >>> 48) + ((byte1 << 56) >>> 56); + + } + + /** + * Reads a string of no more than 65,535 characters + * from the underlying input stream using UTF-8 + * encoding. This method first reads a two byte short + * in big endian order as required by the + * UTF-8 specification. This gives the number of bytes in + * the UTF-8 encoded version of the string. + * Next this many bytes are read and decoded as UTF-8 + * encoded characters. + * + * @return the decoded string + * @throws UTFDataFormatException if the string cannot be decoded + * @throws IOException if the underlying stream throws an IOException. + */ + public String readUTF() throws IOException { + 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; + int numchars = 0; + + while (numread < numbytes) { + + int c1 = readUnsignedByte(); + int c2, c3; + + // The first four bits of c1 determine how many bytes are in this char + int test = c1 >> 4; + if (test < 8) { // one byte + numread++; + result[numchars++] = (char) c1; + } + 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)); + } + else { // malformed + throw new UTFDataFormatException(); + } + + } // end while + + return new String(result, 0, numchars); + } + + /** + * @return the next eight bytes of this input stream, interpreted as a + * little endian {@code double}. + * @throws EOFException if end of stream occurs before eight bytes + * have been read. + * @throws IOException if an I/O error occurs. + */ + public final double readDouble() throws IOException { + return Double.longBitsToDouble(readLong()); + } + + /** + * @return the next four bytes of this input stream, interpreted as a + * little endian {@code int}. + * @throws EOFException if end of stream occurs before four bytes + * have been read. + * @throws IOException if an I/O error occurs. + */ + public final float readFloat() throws IOException { + return Float.intBitsToFloat(readInt()); + } + + /** + * Sets the file-pointer offset, measured from the beginning of this + * file, at which the next read or write occurs. The offset may be + * set beyond the end of the file. Setting the offset beyond the end + * of the file does not change the file length. The file length will + * change only by writing after the offset has been set beyond the end + * of the file. + * + * @param pos the offset position, measured in bytes from the + * beginning of the file, at which to set the file + * pointer. + * @exception IOException if {@code pos} is less than + * {@code 0} or if an I/O error occurs. + */ + public void seek(final long pos) throws IOException { + file.seek(pos); + } + + public void setLength(final long newLength) throws IOException { + file.setLength(newLength); + } + + public int skipBytes(final int n) throws IOException { + return file.skipBytes(n); + } + + public void write(final byte[] b) throws IOException { + file.write(b); + } + + public void write(final byte[] b, final int off, final int len) throws IOException { + file.write(b, off, len); + } + + public void write(final int b) throws IOException { + file.write(b); + } + + /** + * Writes a {@code boolean} to the underlying output stream as + * a single byte. If the argument is true, the byte value 1 is written. + * If the argument is false, the byte value {@code 0} in written. + * + * @param pBoolean the {@code boolean} value to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeBoolean(boolean pBoolean) throws IOException { + if (pBoolean) { + write(1); + } + else { + write(0); + } + } + + /** + * Writes out a {@code byte} to the underlying output stream + * + * @param pByte the {@code byte} value to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeByte(int pByte) throws IOException { + file.write(pByte); + } + + /** + * Writes a two byte {@code short} to the underlying output stream in + * little endian order, low byte first. + * + * @param pShort the {@code short} to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeShort(int pShort) throws IOException { + file.write(pShort & 0xFF); + file.write((pShort >>> 8) & 0xFF); + } + + /** + * Writes a two byte {@code char} to the underlying output stream + * in little endian order, low byte first. + * + * @param pChar the {@code char} value to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeChar(int pChar) throws IOException { + file.write(pChar & 0xFF); + file.write((pChar >>> 8) & 0xFF); + } + + /** + * Writes a four-byte {@code int} to the underlying output stream + * in little endian order, low byte first, high byte last + * + * @param pInt the {@code int} to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeInt(int pInt) throws IOException { + file.write(pInt & 0xFF); + file.write((pInt >>> 8) & 0xFF); + file.write((pInt >>> 16) & 0xFF); + file.write((pInt >>> 24) & 0xFF); + } + + /** + * Writes an eight-byte {@code long} to the underlying output stream + * in little endian order, low byte first, high byte last + * + * @param pLong the {@code long} to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeLong(long pLong) throws IOException { + 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); + } + + /** + * Writes a 4 byte Java float to the underlying output stream in + * little endian order. + * + * @param f the {@code float} value to be written. + * @throws IOException if an I/O error occurs. + */ + public final void writeFloat(float f) throws IOException { + writeInt(Float.floatToIntBits(f)); + } + + /** + * Writes an 8 byte Java double to the underlying output stream in + * little endian order. + * + * @param d the {@code double} value to be written. + * @throws IOException if an I/O error occurs. + */ + public final void writeDouble(double d) throws IOException { + writeLong(Double.doubleToLongBits(d)); + } + + /** + * Writes a string to the underlying output stream as a sequence of + * bytes. Each character is written to the data output stream as + * if by the {@code writeByte()} method. + * + * @param pString the {@code String} value to be written. + * @throws IOException if the underlying stream throws an IOException. + * @see #writeByte(int) + * @see #file + */ + public void writeBytes(String pString) throws IOException { + int length = pString.length(); + + for (int i = 0; i < length; i++) { + file.write((byte) pString.charAt(i)); + } + } + + /** + * Writes a string to the underlying output stream as a sequence of + * characters. Each character is written to the data output stream as + * if by the {@code writeChar} method. + * + * @param pString a {@code String} value to be written. + * @throws IOException if the underlying stream throws an IOException. + * @see #writeChar(int) + * @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); + file.write(c & 0xFF); + file.write((c >>> 8) & 0xFF); + } + } + + /** + * Writes a string of no more than 65,535 characters + * to the underlying output stream using UTF-8 + * encoding. This method first writes a two byte short + * in big endian order as required by the + * UTF-8 specification. This gives the number of bytes in the + * UTF-8 encoded version of the string, not the number of characters + * in the string. Next each character of the string is written + * using the UTF-8 encoding for the character. + * + * @param pString the string to be written. + * @throws UTFDataFormatException if the string is longer than + * 65,535 characters. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeUTF(String pString) throws IOException { + int numchars = pString.length(); + int numbytes = 0; + + for (int i = 0; i < numchars; i++) { + int c = pString.charAt(i); + + if ((c >= 0x0001) && (c <= 0x007F)) { + numbytes++; + } + else if (c > 0x07FF) { + numbytes += 3; + } + else { + numbytes += 2; + } + } + + if (numbytes > 65535) { + throw new UTFDataFormatException(); + } + + 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)) { + file.write(c); + } + else if (c > 0x07FF) { + file.write(0xE0 | ((c >> 12) & 0x0F)); + file.write(0x80 | ((c >> 6) & 0x3F)); + file.write(0x80 | (c & 0x3F)); + } + else { + 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 d17546b6..3a8d89d3 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 @@ -1,198 +1,197 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -/** - * A {@code SeekableInputStream} implementation that caches data in memory. - *

- * - * @see FileCacheSeekableStream - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/MemoryCacheSeekableStream.java#3 $ - */ -public final class MemoryCacheSeekableStream extends AbstractCachedSeekableStream { - - /** - * Creates a {@code MemoryCacheSeekableStream}, reading from the given - * {@code InputStream}. Data will be cached in memory. - * - * @param pStream the {@code InputStream} to read from. - */ - public MemoryCacheSeekableStream(final InputStream pStream) { - super(pStream, new MemoryCache()); - } - - public final boolean isCachedMemory() { - return true; - } - - public final boolean isCachedFile() { - return false; - } - - final static class MemoryCache extends StreamCache { - final static int BLOCK_SIZE = 1 << 13; - - private final List cache = new ArrayList<>(); - private long length; - private long position; - private long start; - - private byte[] getBlock() throws IOException { - final long currPos = position - start; - if (currPos < 0) { - throw new IOException("StreamCache flushed before read position"); - } - - long index = currPos / BLOCK_SIZE; - - if (index >= Integer.MAX_VALUE) { - throw new IOException("Memory cache max size exceeded"); - } - - if (index >= cache.size()) { - try { - cache.add(new byte[BLOCK_SIZE]); -// System.out.println("Allocating new block, size: " + BLOCK_SIZE); -// System.out.println("New total size: " + cache.size() * BLOCK_SIZE + " (" + cache.size() + " blocks)"); - } - catch (OutOfMemoryError e) { - throw new IOException("No more memory for cache: " + cache.size() * BLOCK_SIZE); - } - } - - //System.out.println("index: " + index); - - return cache.get((int) index); - } - - public void write(final int pByte) throws IOException { - byte[] buffer = getBlock(); - - int idx = (int) (position % BLOCK_SIZE); - buffer[idx] = (byte) pByte; - position++; - - if (position > length) { - length = position; - } - } - - // TODO: OptimizeMe!!! - @Override - 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) position % BLOCK_SIZE; - if (index == 0) { - buffer = getBlock(); - } - buffer[index] = pBuffer[pOffset + i]; - - position++; - } - if (position > length) { - length = position; - } - } - - public int read() throws IOException { - if (position >= length) { - return -1; - } - - byte[] buffer = getBlock(); - - int idx = (int) (position % BLOCK_SIZE); - position++; - - return buffer[idx] & 0xff; - } - - // TODO: OptimizeMe!!! - @Override - public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - if (position >= length) { - return -1; - } - - byte[] buffer = getBlock(); - - 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), length - position); - - int 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]; - } - - position += i; - - return i; - } - - public void seek(final long pPosition) throws IOException { - if (pPosition < start) { - throw new IOException("Seek before flush position"); - } - position = pPosition; - } - - @Override - public void flush(final long pPosition) { - int firstPos = (int) (pPosition / BLOCK_SIZE) - 1; - - for (int i = 0; i < firstPos; i++) { - cache.remove(0); - } - - start = pPosition; - } - - @Override - void close() throws IOException { - cache.clear(); - } - - public long getPosition() { - return position; - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * A {@code SeekableInputStream} implementation that caches data in memory. + * + * @see FileCacheSeekableStream + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/MemoryCacheSeekableStream.java#3 $ + */ +public final class MemoryCacheSeekableStream extends AbstractCachedSeekableStream { + + /** + * Creates a {@code MemoryCacheSeekableStream}, reading from the given + * {@code InputStream}. Data will be cached in memory. + * + * @param pStream the {@code InputStream} to read from. + */ + public MemoryCacheSeekableStream(final InputStream pStream) { + super(pStream, new MemoryCache()); + } + + public final boolean isCachedMemory() { + return true; + } + + public final boolean isCachedFile() { + return false; + } + + final static class MemoryCache extends StreamCache { + final static int BLOCK_SIZE = 1 << 13; + + private final List cache = new ArrayList<>(); + private long length; + private long position; + private long start; + + private byte[] getBlock() throws IOException { + final long currPos = position - start; + if (currPos < 0) { + throw new IOException("StreamCache flushed before read position"); + } + + long index = currPos / BLOCK_SIZE; + + if (index >= Integer.MAX_VALUE) { + throw new IOException("Memory cache max size exceeded"); + } + + if (index >= cache.size()) { + try { + cache.add(new byte[BLOCK_SIZE]); +// System.out.println("Allocating new block, size: " + BLOCK_SIZE); +// System.out.println("New total size: " + cache.size() * BLOCK_SIZE + " (" + cache.size() + " blocks)"); + } + catch (OutOfMemoryError e) { + throw new IOException("No more memory for cache: " + cache.size() * BLOCK_SIZE); + } + } + + //System.out.println("index: " + index); + + return cache.get((int) index); + } + + public void write(final int pByte) throws IOException { + byte[] buffer = getBlock(); + + int idx = (int) (position % BLOCK_SIZE); + buffer[idx] = (byte) pByte; + position++; + + if (position > length) { + length = position; + } + } + + // TODO: OptimizeMe!!! + @Override + 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) position % BLOCK_SIZE; + if (index == 0) { + buffer = getBlock(); + } + buffer[index] = pBuffer[pOffset + i]; + + position++; + } + if (position > length) { + length = position; + } + } + + public int read() throws IOException { + if (position >= length) { + return -1; + } + + byte[] buffer = getBlock(); + + int idx = (int) (position % BLOCK_SIZE); + position++; + + return buffer[idx] & 0xff; + } + + // TODO: OptimizeMe!!! + @Override + public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { + if (position >= length) { + return -1; + } + + byte[] buffer = getBlock(); + + 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), length - position); + + int 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]; + } + + position += i; + + return i; + } + + public void seek(final long pPosition) throws IOException { + if (pPosition < start) { + throw new IOException("Seek before flush position"); + } + position = pPosition; + } + + @Override + public void flush(final long pPosition) { + int firstPos = (int) (pPosition / BLOCK_SIZE) - 1; + + for (int i = 0; i < firstPos; i++) { + cache.remove(0); + } + + start = pPosition; + } + + @Override + void close() throws IOException { + cache.clear(); + } + + public long getPosition() { + return position; + } + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/NullInputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/NullInputStream.java index 9e7af300..02e45387 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/NullInputStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/NullInputStream.java @@ -1,82 +1,81 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.IOException; -import java.io.InputStream; - -/** - * An {@code InputStream} that contains no bytes. - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullInputStream.java#2 $ - */ -public class NullInputStream extends InputStream { - - /** - * Creates a {@code NullInputStream}. - */ - public NullInputStream() { - } - - /** - * This implementation returns {@code -1} (EOF), always. - * - * @return {@code -1} - * @throws IOException - */ - public int read() throws IOException { - return -1; - } - - /** - * This implementation returns {@code 0}, always. - * - * @return {@code 0} - * @throws IOException - */ - @Override - public int available() throws IOException { - return 0; - } - - /** - * This implementation returns {@code 0}, always. - * - * @return {@code 0} - * @throws IOException - */ - @Override - public long skip(long pOffset) throws IOException { - return 0l; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.IOException; +import java.io.InputStream; + +/** + * An {@code InputStream} that contains no bytes. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullInputStream.java#2 $ + */ +public class NullInputStream extends InputStream { + + /** + * Creates a {@code NullInputStream}. + */ + public NullInputStream() { + } + + /** + * This implementation returns {@code -1} (EOF), always. + * + * @return {@code -1} + * @throws IOException + */ + public int read() throws IOException { + return -1; + } + + /** + * This implementation returns {@code 0}, always. + * + * @return {@code 0} + * @throws IOException + */ + @Override + public int available() throws IOException { + return 0; + } + + /** + * This implementation returns {@code 0}, always. + * + * @return {@code 0} + * @throws IOException + */ + @Override + public long skip(long pOffset) throws IOException { + return 0l; + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/NullOutputStream.java b/common/common-io/src/main/java/com/twelvemonkeys/io/NullOutputStream.java index 2b274a37..b584631a 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/NullOutputStream.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/NullOutputStream.java @@ -1,70 +1,69 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.IOException; -import java.io.OutputStream; - -/** - * An {@code OutputStream} implementation that works as a sink. - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullOutputStream.java#2 $ - */ -public class NullOutputStream extends OutputStream { - - /** - * Creates a {@code NullOutputStream}. - */ - public NullOutputStream() { - } - - /** - * This implementation does nothing. - */ - public void write(int pByte) throws IOException { - } - - /** - * This implementation does nothing. - */ - @Override - public void write(byte pBytes[]) throws IOException { - } - - /** - * This implementation does nothing. - */ - @Override - public void write(byte pBytes[], int pOffset, int pLength) throws IOException { - } +/* + * 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 of the copyright holder 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 HOLDER 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.IOException; +import java.io.OutputStream; + +/** + * An {@code OutputStream} implementation that works as a sink. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullOutputStream.java#2 $ + */ +public class NullOutputStream extends OutputStream { + + /** + * Creates a {@code NullOutputStream}. + */ + public NullOutputStream() { + } + + /** + * This implementation does nothing. + */ + public void write(int pByte) throws IOException { + } + + /** + * This implementation does nothing. + */ + @Override + public void write(byte pBytes[]) throws IOException { + } + + /** + * This implementation does nothing. + */ + @Override + public void write(byte pBytes[], int pOffset, int pLength) throws IOException { + } } \ No newline at end of file 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 79e09afe..e3e5051d 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 @@ -1,241 +1,242 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.DataInput; -import java.io.DataOutput; -import java.io.EOFException; -import java.io.IOException; - -/** - * A data stream that is both readable and writable, much like a - * {@code RandomAccessFile}, except it may be backed by something other than a file. - *

- * - * @see java.io.RandomAccessFile - * - * @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/io/RandomAccessStream.java#3 $ - */ -public abstract class RandomAccessStream implements Seekable, DataInput, DataOutput { - // TODO: Use a RandomAcceessFile as backing in impl, probably - // TODO: Create an in-memory implementation too? - // TODO: Package private SeekableDelegate? - - // TODO: Both read and write must update stream position - //private int position = -1; - - /** This random access stream, wrapped in an {@code InputStream} */ - SeekableInputStream inputView = null; - /** This random access stream, wrapped in an {@code OutputStream} */ - SeekableOutputStream outputView = null; - - // TODO: Create an Input and an Output interface matching InputStream and OutputStream? - public int read() throws IOException { - try { - return readByte() & 0xff; - } - catch (EOFException e) { - return -1; - } - } - - public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - if (pBytes == null) { - throw new NullPointerException("bytes == null"); - } - else if ((pOffset < 0) || (pOffset > pBytes.length) || (pLength < 0) || - ((pOffset + pLength) > pBytes.length) || ((pOffset + pLength) < 0)) { - throw new IndexOutOfBoundsException(); - } - else if (pLength == 0) { - return 0; - } - - // Special case, allready at EOF - int c = read(); - if (c == -1) { - return -1; - } - - // Otherwise, read as many as bytes as possible - pBytes[pOffset] = (byte) c; - - int i = 1; - try { - for (; i < pLength; i++) { - c = read(); - if (c == -1) { - break; - } - pBytes[pOffset + i] = (byte) c; - } - } - catch (IOException ignore) { - // Ignore exception, just return length - } - - return i; - } - - public final int read(byte[] pBytes) throws IOException { - return read(pBytes, 0, pBytes != null ? pBytes.length : 1); - } - - /** - * Returns an input view of this {@code RandomAccessStream}. - * Invoking this method several times, will return the same object. - *

- * Note that read access is NOT synchronized. - * - * @return a {@code SeekableInputStream} reading from this stream - */ - public final SeekableInputStream asInputStream() { - if (inputView == null) { - inputView = new InputStreamView(this); - } - return inputView; - } - - /** - * Returns an output view of this {@code RandomAccessStream}. - * Invoking this method several times, will return the same object. - *

- * Note that write access is NOT synchronized. - * - * @return a {@code SeekableOutputStream} writing to this stream - */ - public final SeekableOutputStream asOutputStream() { - if (outputView == null) { - outputView = new OutputStreamView(this); - } - return outputView; - } - - static final class InputStreamView extends SeekableInputStream { - // 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? - - final private RandomAccessStream mStream; - - public InputStreamView(RandomAccessStream pStream) { - if (pStream == null) { - throw new IllegalArgumentException("stream == null"); - } - mStream = pStream; - } - - public boolean isCached() { - return mStream.isCached(); - } - - public boolean isCachedFile() { - return mStream.isCachedFile(); - } - - public boolean isCachedMemory() { - return mStream.isCachedMemory(); - } - - protected void closeImpl() throws IOException { - mStream.close(); - } - - protected void flushBeforeImpl(long pPosition) throws IOException { - mStream.flushBefore(pPosition); - } - - protected void seekImpl(long pPosition) throws IOException { - mStream.seek(pPosition); - } - - public int read() throws IOException { - return mStream.read(); - } - - @Override - public int read(byte pBytes[], int pOffset, int pLength) throws IOException { - return mStream.read(pBytes, pOffset, pLength); - } - } - - static final class OutputStreamView extends SeekableOutputStream { - // 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? - - final private RandomAccessStream mStream; - - public OutputStreamView(RandomAccessStream pStream) { - if (pStream == null) { - throw new IllegalArgumentException("stream == null"); - } - mStream = pStream; - } - - public boolean isCached() { - return mStream.isCached(); - } - - public boolean isCachedFile() { - return mStream.isCachedFile(); - } - - public boolean isCachedMemory() { - return mStream.isCachedMemory(); - } - - protected void closeImpl() throws IOException { - mStream.close(); - } - - protected void flushBeforeImpl(long pPosition) throws IOException { - mStream.flushBefore(pPosition); - } - - protected void seekImpl(long pPosition) throws IOException { - mStream.seek(pPosition); - } - - public void write(int pByte) throws IOException { - mStream.write(pByte); - } - - @Override - public void write(byte pBytes[], int pOffset, int pLength) throws IOException { - mStream.write(pBytes, pOffset, pLength); - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.DataInput; +import java.io.DataOutput; +import java.io.EOFException; +import java.io.IOException; + +/** + * A data stream that is both readable and writable, much like a + * {@code RandomAccessFile}, except it may be backed by something other than a file. + * + * @see java.io.RandomAccessFile + * + * @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/io/RandomAccessStream.java#3 $ + */ +public abstract class RandomAccessStream implements Seekable, DataInput, DataOutput { + // TODO: Use a RandomAcceessFile as backing in impl, probably + // TODO: Create an in-memory implementation too? + // TODO: Package private SeekableDelegate? + + // TODO: Both read and write must update stream position + //private int position = -1; + + /** This random access stream, wrapped in an {@code InputStream} */ + SeekableInputStream inputView = null; + /** This random access stream, wrapped in an {@code OutputStream} */ + SeekableOutputStream outputView = null; + + // TODO: Create an Input and an Output interface matching InputStream and OutputStream? + public int read() throws IOException { + try { + return readByte() & 0xff; + } + catch (EOFException e) { + return -1; + } + } + + public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { + if (pBytes == null) { + throw new NullPointerException("bytes == null"); + } + else if ((pOffset < 0) || (pOffset > pBytes.length) || (pLength < 0) || + ((pOffset + pLength) > pBytes.length) || ((pOffset + pLength) < 0)) { + throw new IndexOutOfBoundsException(); + } + else if (pLength == 0) { + return 0; + } + + // Special case, allready at EOF + int c = read(); + if (c == -1) { + return -1; + } + + // Otherwise, read as many as bytes as possible + pBytes[pOffset] = (byte) c; + + int i = 1; + try { + for (; i < pLength; i++) { + c = read(); + if (c == -1) { + break; + } + pBytes[pOffset + i] = (byte) c; + } + } + catch (IOException ignore) { + // Ignore exception, just return length + } + + return i; + } + + public final int read(byte[] pBytes) throws IOException { + return read(pBytes, 0, pBytes != null ? pBytes.length : 1); + } + + /** + * Returns an input view of this {@code RandomAccessStream}. + * Invoking this method several times, will return the same object. + *

+ * Note that read access is NOT synchronized. + *

+ * + * @return a {@code SeekableInputStream} reading from this stream + */ + public final SeekableInputStream asInputStream() { + if (inputView == null) { + inputView = new InputStreamView(this); + } + return inputView; + } + + /** + * Returns an output view of this {@code RandomAccessStream}. + * Invoking this method several times, will return the same object. + *

+ * Note that write access is NOT synchronized. + *

+ * + * @return a {@code SeekableOutputStream} writing to this stream + */ + public final SeekableOutputStream asOutputStream() { + if (outputView == null) { + outputView = new OutputStreamView(this); + } + return outputView; + } + + static final class InputStreamView extends SeekableInputStream { + // 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? + + final private RandomAccessStream mStream; + + public InputStreamView(RandomAccessStream pStream) { + if (pStream == null) { + throw new IllegalArgumentException("stream == null"); + } + mStream = pStream; + } + + public boolean isCached() { + return mStream.isCached(); + } + + public boolean isCachedFile() { + return mStream.isCachedFile(); + } + + public boolean isCachedMemory() { + return mStream.isCachedMemory(); + } + + protected void closeImpl() throws IOException { + mStream.close(); + } + + protected void flushBeforeImpl(long pPosition) throws IOException { + mStream.flushBefore(pPosition); + } + + protected void seekImpl(long pPosition) throws IOException { + mStream.seek(pPosition); + } + + public int read() throws IOException { + return mStream.read(); + } + + @Override + public int read(byte pBytes[], int pOffset, int pLength) throws IOException { + return mStream.read(pBytes, pOffset, pLength); + } + } + + static final class OutputStreamView extends SeekableOutputStream { + // 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? + + final private RandomAccessStream mStream; + + public OutputStreamView(RandomAccessStream pStream) { + if (pStream == null) { + throw new IllegalArgumentException("stream == null"); + } + mStream = pStream; + } + + public boolean isCached() { + return mStream.isCached(); + } + + public boolean isCachedFile() { + return mStream.isCachedFile(); + } + + public boolean isCachedMemory() { + return mStream.isCachedMemory(); + } + + protected void closeImpl() throws IOException { + mStream.close(); + } + + protected void flushBeforeImpl(long pPosition) throws IOException { + mStream.flushBefore(pPosition); + } + + protected void seekImpl(long pPosition) throws IOException { + mStream.seek(pPosition); + } + + public void write(int pByte) throws IOException { + mStream.write(pByte); + } + + @Override + public void write(byte pBytes[], int pOffset, int pLength) throws IOException { + mStream.write(pBytes, pOffset, pLength); + } + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/Seekable.java b/common/common-io/src/main/java/com/twelvemonkeys/io/Seekable.java index 40b00ed9..204d1e10 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/Seekable.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/Seekable.java @@ -1,186 +1,193 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.IOException; - -/** - * Interface for seekable streams. - *

- * @see SeekableInputStream - * @see SeekableOutputStream - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Seekable.java#1 $ - */ -public interface Seekable { - - /** - * Returns the current byte position of the stream. The next read will take - * place starting at this offset. - * - * @return a {@code long} containing the position of the stream. - * @throws IOException if an I/O error occurs. - */ - long getStreamPosition() throws IOException; - - /** - * Sets the current stream position to the desired location. - * The next read will occur at this location. - *

- * An {@code IndexOutOfBoundsException} will be thrown if pPosition is smaller - * than the flushed position (as returned by {@link #getFlushedPosition()}). - *

- * It is legal to seek past the end of the file; an {@code EOFException} - * will be thrown only if a read is performed. - * - * @param pPosition a long containing the desired file pointer position. - * - * @throws IndexOutOfBoundsException if {@code pPosition} is smaller than - * the flushed position. - * @throws IOException if any other I/O error occurs. - */ - void seek(long pPosition) throws IOException; - - /** - * Marks a position in the stream to be returned to by a subsequent call to - * reset. - * Unlike a standard {@code InputStream}, all {@code Seekable} - * streams upport marking. Additionally, calls to {@code mark} and - * {@code reset} may be nested arbitrarily. - *

- * Unlike the {@code mark} methods declared by the {@code Reader} or - * {@code InputStream} - * interfaces, no {@code readLimit} parameter is used. An arbitrary amount - * of data may be read following the call to {@code mark}. - */ - void mark(); - - /** - * Returns the file pointer to its previous position, - * at the time of the most recent unmatched call to mark. - *

- * Calls to reset without a corresponding call to mark will either: - *

    - *
  • throw an {@code IOException}
  • - *
  • or, reset to the beginning of the stream.
  • - *
- * An {@code IOException} will be thrown if the previous marked position - * lies in the discarded portion of the stream. - * - * @throws IOException if an I/O error occurs. - * @see java.io.InputStream#reset() - */ - void reset() throws IOException; - - /** - * Discards the initial portion of the stream prior to the indicated - * postion. Attempting to seek to an offset within the flushed portion of - * the stream will result in an {@code IndexOutOfBoundsException}. - *

- * Calling {@code flushBefore} may allow classes implementing this - * interface to free up resources such as memory or disk space that are - * being used to store data from the stream. - * - * @param pPosition a long containing the length of the file prefix that - * may be flushed. - * - * @throws IndexOutOfBoundsException if {@code pPosition} lies in the - * flushed portion of the stream or past the current stream position. - * @throws IOException if an I/O error occurs. - */ - void flushBefore(long pPosition) throws IOException; - - /** - * Discards the initial position of the stream prior to the current stream - * position. Equivalent to {@code flushBefore(getStreamPosition())}. - * - * @throws IOException if an I/O error occurs. - */ - void flush() throws IOException; - - /** - * Returns the earliest position in the stream to which seeking may be - * performed. The returned value will be the maximum of all values passed - * into previous calls to {@code flushBefore}. - * - * @return the earliest legal position for seeking, as a {@code long}. - * - * @throws IOException if an I/O error occurs. - */ - long getFlushedPosition() throws IOException; - - /** - * Returns true if this {@code Seekable} stream caches data itself in order - * to allow seeking backwards. Applications may consult this in order to - * decide how frequently, or whether, to flush in order to conserve cache - * resources. - * - * @return {@code true} if this {@code Seekable} caches data. - * @see #isCachedMemory() - * @see #isCachedFile() - */ - boolean isCached(); - - /** - * Returns true if this {@code Seekable} stream caches data itself in order - * to allow seeking backwards, and the cache is kept in main memory. - * Applications may consult this in order to decide how frequently, or - * whether, to flush in order to conserve cache resources. - * - * @return {@code true} if this {@code Seekable} caches data in main - * memory. - * @see #isCached() - * @see #isCachedFile() - */ - boolean isCachedMemory(); - - /** - * Returns true if this {@code Seekable} stream caches data itself in - * order to allow seeking backwards, and the cache is kept in a - * temporary file. - * Applications may consult this in order to decide how frequently, - * or whether, to flush in order to conserve cache resources. - * - * @return {@code true} if this {@code Seekable} caches data in a - * temporary file. - * @see #isCached - * @see #isCachedMemory - */ - boolean isCachedFile(); - - /** - * Closes the stream. - * - * @throws java.io.IOException if the stream can't be closed. - */ - void close() throws IOException; -} +/* + * 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 of the copyright holder 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 HOLDER 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.IOException; + +/** + * Interface for seekable streams. + * + * @see SeekableInputStream + * @see SeekableOutputStream + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Seekable.java#1 $ + */ +public interface Seekable { + + /** + * Returns the current byte position of the stream. The next read will take + * place starting at this offset. + * + * @return a {@code long} containing the position of the stream. + * @throws IOException if an I/O error occurs. + */ + long getStreamPosition() throws IOException; + + /** + * Sets the current stream position to the desired location. + * The next read will occur at this location. + *

+ * An {@code IndexOutOfBoundsException} will be thrown if pPosition is smaller + * than the flushed position (as returned by {@link #getFlushedPosition()}). + *

+ *

+ * It is legal to seek past the end of the file; an {@code EOFException} + * will be thrown only if a read is performed. + *

+ * + * @param pPosition a long containing the desired file pointer position. + * + * @throws IndexOutOfBoundsException if {@code pPosition} is smaller than + * the flushed position. + * @throws IOException if any other I/O error occurs. + */ + void seek(long pPosition) throws IOException; + + /** + * Marks a position in the stream to be returned to by a subsequent call to + * reset. + * Unlike a standard {@code InputStream}, all {@code Seekable} + * streams upport marking. Additionally, calls to {@code mark} and + * {@code reset} may be nested arbitrarily. + *

+ * Unlike the {@code mark} methods declared by the {@code Reader} or + * {@code InputStream} + * interfaces, no {@code readLimit} parameter is used. An arbitrary amount + * of data may be read following the call to {@code mark}. + *

+ */ + void mark(); + + /** + * Returns the file pointer to its previous position, + * at the time of the most recent unmatched call to mark. + *

+ * Calls to reset without a corresponding call to mark will either: + *

+ *
    + *
  • throw an {@code IOException}
  • + *
  • or, reset to the beginning of the stream.
  • + *
+ *

+ * An {@code IOException} will be thrown if the previous marked position + * lies in the discarded portion of the stream. + *

+ * + * @throws IOException if an I/O error occurs. + * @see java.io.InputStream#reset() + */ + void reset() throws IOException; + + /** + * Discards the initial portion of the stream prior to the indicated + * postion. Attempting to seek to an offset within the flushed portion of + * the stream will result in an {@code IndexOutOfBoundsException}. + *

+ * Calling {@code flushBefore} may allow classes implementing this + * interface to free up resources such as memory or disk space that are + * being used to store data from the stream. + *

+ * + * @param pPosition a long containing the length of the file prefix that + * may be flushed. + * + * @throws IndexOutOfBoundsException if {@code pPosition} lies in the + * flushed portion of the stream or past the current stream position. + * @throws IOException if an I/O error occurs. + */ + void flushBefore(long pPosition) throws IOException; + + /** + * Discards the initial position of the stream prior to the current stream + * position. Equivalent to {@code flushBefore(getStreamPosition())}. + * + * @throws IOException if an I/O error occurs. + */ + void flush() throws IOException; + + /** + * Returns the earliest position in the stream to which seeking may be + * performed. The returned value will be the maximum of all values passed + * into previous calls to {@code flushBefore}. + * + * @return the earliest legal position for seeking, as a {@code long}. + * + * @throws IOException if an I/O error occurs. + */ + long getFlushedPosition() throws IOException; + + /** + * Returns true if this {@code Seekable} stream caches data itself in order + * to allow seeking backwards. Applications may consult this in order to + * decide how frequently, or whether, to flush in order to conserve cache + * resources. + * + * @return {@code true} if this {@code Seekable} caches data. + * @see #isCachedMemory() + * @see #isCachedFile() + */ + boolean isCached(); + + /** + * Returns true if this {@code Seekable} stream caches data itself in order + * to allow seeking backwards, and the cache is kept in main memory. + * Applications may consult this in order to decide how frequently, or + * whether, to flush in order to conserve cache resources. + * + * @return {@code true} if this {@code Seekable} caches data in main + * memory. + * @see #isCached() + * @see #isCachedFile() + */ + boolean isCachedMemory(); + + /** + * Returns true if this {@code Seekable} stream caches data itself in + * order to allow seeking backwards, and the cache is kept in a + * temporary file. + * Applications may consult this in order to decide how frequently, + * or whether, to flush in order to conserve cache resources. + * + * @return {@code true} if this {@code Seekable} caches data in a + * temporary file. + * @see #isCached + * @see #isCachedMemory + */ + boolean isCachedFile(); + + /** + * Closes the stream. + * + * @throws java.io.IOException if the stream can't be closed. + */ + void close() throws IOException; +} 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 6935bcc3..f585ebea 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 @@ -1,238 +1,238 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.IOException; -import java.io.InputStream; -import java.util.Stack; - -/** - * Abstract base class for {@code InputStream}s implementing the {@code Seekable} interface. - *

- * @see SeekableOutputStream - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableInputStream.java#4 $ - */ -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 position. position needs to be - // updated from the read/read/read methods... - - /** The stream position in this stream */ - long position; - long flushedPosition; - boolean closed; - - protected Stack markedPositions = new Stack(); - - /// InputStream overrides - @Override - public final int read(byte[] pBytes) throws IOException { - return read(pBytes, 0, pBytes != null ? pBytes.length : 1); - } - - /** - * Implemented using {@code seek(currentPos + pLength)}. - * - * @param pLength the number of bytes to skip - * @return the actual number of bytes skipped (may be equal to or less - * than {@code pLength}) - * - * @throws IOException if an I/O exception occurs during skip - */ - @Override - 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 might be allowed - // to seek past end of stream - 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 position - pos; - } - - @Override - public final void mark(int pLimit) { - mark(); - - // TODO: We don't really need to do this.. Is it a good idea? - try { - flushBefore(Math.max(position - pLimit, flushedPosition)); - } - catch (IOException ignore) { - // Ignore, as it's not really critical - } - } - - /** - * Returns {@code true}, as marking is always supported. - * - * @return {@code true}. - */ - @Override - public final boolean markSupported() { - return true; - } - - /// Seekable implementation - public final void seek(long pPosition) throws IOException { - checkOpen(); - - // NOTE: This is correct according to javax.imageio (IndexOutOfBoundsException), - // but it's kind of inconsistent with reset that throws IOException... - if (pPosition < flushedPosition) { - throw new IndexOutOfBoundsException("position < flushedPosition"); - } - - seekImpl(pPosition); - position = pPosition; - } - - protected abstract void seekImpl(long pPosition) throws IOException; - - public final void mark() { - markedPositions.push(position); - } - - @Override - public final void reset() throws IOException { - checkOpen(); - 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 < flushedPosition) { - throw new IOException("Previous marked position has been discarded"); - } - - seek(newPos); - } - else { - // TODO: To iron out some wrinkles due to conflicting contracts - // (InputStream and Seekable both declare reset), - // we might need to reset to the last marked position instead.. - // However, that becomes REALLY confusing if that position is after - // the current position... - seek(0); - } - } - - public final void flushBefore(long pPosition) throws IOException { - if (pPosition < flushedPosition) { - throw new IndexOutOfBoundsException("position < flushedPosition"); - } - if (pPosition > getStreamPosition()) { - throw new IndexOutOfBoundsException("position > stream position"); - } - checkOpen(); - flushBeforeImpl(pPosition); - flushedPosition = pPosition; - } - - /** - * Discards the initial portion of the stream prior to the indicated postion. - * - * @param pPosition the position to flush to - * @throws IOException if an I/O exception occurs during the flush operation - * - * @see #flushBefore(long) - */ - protected abstract void flushBeforeImpl(long pPosition) throws IOException; - - public final void flush() throws IOException { - flushBefore(flushedPosition); - } - - public final long getFlushedPosition() throws IOException { - checkOpen(); - return flushedPosition; - } - - public final long getStreamPosition() throws IOException { - checkOpen(); - return position; - } - - protected final void checkOpen() throws IOException { - if (closed) { - throw new IOException("closed"); - } - } - - @Override - public final void close() throws IOException { - checkOpen(); - closed = true; - closeImpl(); - } - - protected abstract void closeImpl() throws IOException; - - /** - * Finalizes this object prior to garbage collection. The - * {@code close} method is called to close any open input - * source. This method should not be called from application - * code. - * - * @exception Throwable if an error occurs during superclass - * finalization. - */ - @Override - protected void finalize() throws Throwable { - if (!closed) { - try { - close(); - } - catch (IOException ignore) { - // Ignroe - } - } - super.finalize(); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.IOException; +import java.io.InputStream; +import java.util.Stack; + +/** + * Abstract base class for {@code InputStream}s implementing the {@code Seekable} interface. + * + * @see SeekableOutputStream + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableInputStream.java#4 $ + */ +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 position. position needs to be + // updated from the read/read/read methods... + + /** The stream position in this stream */ + long position; + long flushedPosition; + boolean closed; + + protected Stack markedPositions = new Stack(); + + /// InputStream overrides + @Override + public final int read(byte[] pBytes) throws IOException { + return read(pBytes, 0, pBytes != null ? pBytes.length : 1); + } + + /** + * Implemented using {@code seek(currentPos + pLength)}. + * + * @param pLength the number of bytes to skip + * @return the actual number of bytes skipped (may be equal to or less + * than {@code pLength}) + * + * @throws IOException if an I/O exception occurs during skip + */ + @Override + 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 might be allowed + // to seek past end of stream + 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 position - pos; + } + + @Override + public final void mark(int pLimit) { + mark(); + + // TODO: We don't really need to do this.. Is it a good idea? + try { + flushBefore(Math.max(position - pLimit, flushedPosition)); + } + catch (IOException ignore) { + // Ignore, as it's not really critical + } + } + + /** + * Returns {@code true}, as marking is always supported. + * + * @return {@code true}. + */ + @Override + public final boolean markSupported() { + return true; + } + + /// Seekable implementation + public final void seek(long pPosition) throws IOException { + checkOpen(); + + // NOTE: This is correct according to javax.imageio (IndexOutOfBoundsException), + // but it's kind of inconsistent with reset that throws IOException... + if (pPosition < flushedPosition) { + throw new IndexOutOfBoundsException("position < flushedPosition"); + } + + seekImpl(pPosition); + position = pPosition; + } + + protected abstract void seekImpl(long pPosition) throws IOException; + + public final void mark() { + markedPositions.push(position); + } + + @Override + public final void reset() throws IOException { + checkOpen(); + 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 < flushedPosition) { + throw new IOException("Previous marked position has been discarded"); + } + + seek(newPos); + } + else { + // TODO: To iron out some wrinkles due to conflicting contracts + // (InputStream and Seekable both declare reset), + // we might need to reset to the last marked position instead.. + // However, that becomes REALLY confusing if that position is after + // the current position... + seek(0); + } + } + + public final void flushBefore(long pPosition) throws IOException { + if (pPosition < flushedPosition) { + throw new IndexOutOfBoundsException("position < flushedPosition"); + } + if (pPosition > getStreamPosition()) { + throw new IndexOutOfBoundsException("position > stream position"); + } + checkOpen(); + flushBeforeImpl(pPosition); + flushedPosition = pPosition; + } + + /** + * Discards the initial portion of the stream prior to the indicated postion. + * + * @param pPosition the position to flush to + * @throws IOException if an I/O exception occurs during the flush operation + * + * @see #flushBefore(long) + */ + protected abstract void flushBeforeImpl(long pPosition) throws IOException; + + public final void flush() throws IOException { + flushBefore(flushedPosition); + } + + public final long getFlushedPosition() throws IOException { + checkOpen(); + return flushedPosition; + } + + public final long getStreamPosition() throws IOException { + checkOpen(); + return position; + } + + protected final void checkOpen() throws IOException { + if (closed) { + throw new IOException("closed"); + } + } + + @Override + public final void close() throws IOException { + checkOpen(); + closed = true; + closeImpl(); + } + + protected abstract void closeImpl() throws IOException; + + /** + * Finalizes this object prior to garbage collection. The + * {@code close} method is called to close any open input + * source. This method should not be called from application + * code. + * + * @exception Throwable if an error occurs during superclass + * finalization. + */ + @Override + protected void finalize() throws Throwable { + if (!closed) { + try { + close(); + } + catch (IOException ignore) { + // Ignroe + } + } + super.finalize(); + } +} 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 cc99cf73..bdce032b 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 @@ -1,140 +1,140 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.IOException; -import java.io.OutputStream; -import java.util.Stack; - -/** - * Abstract base class for {@code OutputStream}s implementing the - * {@code Seekable} interface. - *

- * @see SeekableInputStream - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableOutputStream.java#2 $ - */ -public abstract class SeekableOutputStream extends OutputStream implements Seekable { - // TODO: Implement - long position; - long flushedPosition; - boolean closed; - - protected Stack markedPositions = new Stack(); - - /// Outputstream overrides - @Override - public final void write(byte pBytes[]) throws IOException { - write(pBytes, 0, pBytes != null ? pBytes.length : 1); - } - - /// Seekable implementation - // TODO: This is common behaviour/implementation with SeekableInputStream, - // probably a good idea to extract a delegate..? - public final void seek(long pPosition) throws IOException { - checkOpen(); - - // TODO: This is correct according to javax.imageio (IndexOutOfBoundsException), - // but it's inconsistent with reset that throws IOException... - if (pPosition < flushedPosition) { - throw new IndexOutOfBoundsException("position < flushedPosition!"); - } - - seekImpl(pPosition); - position = pPosition; - } - - protected abstract void seekImpl(long pPosition) throws IOException; - - public final void mark() { - markedPositions.push(position); - } - - public final void reset() throws IOException { - checkOpen(); - 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 < flushedPosition) { - throw new IOException("Previous marked position has been discarded!"); - } - - seek(newPos); - } - } - - public final void flushBefore(long pPosition) throws IOException { - if (pPosition < flushedPosition) { - throw new IndexOutOfBoundsException("position < flushedPosition!"); - } - if (pPosition > getStreamPosition()) { - throw new IndexOutOfBoundsException("position > getStreamPosition()!"); - } - checkOpen(); - flushBeforeImpl(pPosition); - flushedPosition = pPosition; - } - - protected abstract void flushBeforeImpl(long pPosition) throws IOException; - - @Override - public final void flush() throws IOException { - flushBefore(flushedPosition); - } - - public final long getFlushedPosition() throws IOException { - checkOpen(); - return flushedPosition; - } - - public final long getStreamPosition() throws IOException { - checkOpen(); - return position; - } - - protected final void checkOpen() throws IOException { - if (closed) { - throw new IOException("closed"); - } - } - - @Override - public final void close() throws IOException { - checkOpen(); - closed = true; - closeImpl(); - } - - protected abstract void closeImpl() throws IOException; -} +/* + * 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 of the copyright holder 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 HOLDER 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.IOException; +import java.io.OutputStream; +import java.util.Stack; + +/** + * Abstract base class for {@code OutputStream}s implementing the + * {@code Seekable} interface. + * + * @see SeekableInputStream + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableOutputStream.java#2 $ + */ +public abstract class SeekableOutputStream extends OutputStream implements Seekable { + // TODO: Implement + long position; + long flushedPosition; + boolean closed; + + protected Stack markedPositions = new Stack(); + + /// Outputstream overrides + @Override + public final void write(byte pBytes[]) throws IOException { + write(pBytes, 0, pBytes != null ? pBytes.length : 1); + } + + /// Seekable implementation + // TODO: This is common behaviour/implementation with SeekableInputStream, + // probably a good idea to extract a delegate..? + public final void seek(long pPosition) throws IOException { + checkOpen(); + + // TODO: This is correct according to javax.imageio (IndexOutOfBoundsException), + // but it's inconsistent with reset that throws IOException... + if (pPosition < flushedPosition) { + throw new IndexOutOfBoundsException("position < flushedPosition!"); + } + + seekImpl(pPosition); + position = pPosition; + } + + protected abstract void seekImpl(long pPosition) throws IOException; + + public final void mark() { + markedPositions.push(position); + } + + public final void reset() throws IOException { + checkOpen(); + 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 < flushedPosition) { + throw new IOException("Previous marked position has been discarded!"); + } + + seek(newPos); + } + } + + public final void flushBefore(long pPosition) throws IOException { + if (pPosition < flushedPosition) { + throw new IndexOutOfBoundsException("position < flushedPosition!"); + } + if (pPosition > getStreamPosition()) { + throw new IndexOutOfBoundsException("position > getStreamPosition()!"); + } + checkOpen(); + flushBeforeImpl(pPosition); + flushedPosition = pPosition; + } + + protected abstract void flushBeforeImpl(long pPosition) throws IOException; + + @Override + public final void flush() throws IOException { + flushBefore(flushedPosition); + } + + public final long getFlushedPosition() throws IOException { + checkOpen(); + return flushedPosition; + } + + public final long getStreamPosition() throws IOException { + checkOpen(); + return position; + } + + protected final void checkOpen() throws IOException { + if (closed) { + throw new IOException("closed"); + } + } + + @Override + public final void close() throws IOException { + checkOpen(); + closed = true; + closeImpl(); + } + + protected abstract void closeImpl() throws IOException; +} 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 0270ef0a..45ec3f9b 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 @@ -1,189 +1,188 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.Validate; - -import java.io.IOException; -import java.io.Reader; -import java.io.StringReader; - -/** - * StringArrayReader - *

- * - * @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/io/StringArrayReader.java#2 $ - */ -public class StringArrayReader extends StringReader { - - 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. - * - * @param pStrings {@code String}s providing the character stream. - */ - public StringArrayReader(final String[] pStrings) { - super(""); - - 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 - - strings = pStrings.clone(); // Defensive copy for content - nextReader(); - } - - protected final Reader nextReader() { - if (currentSting >= strings.length) { - current = new EmptyReader(); - } - else { - current = new StringReader(strings[currentSting++]); - } - - // NOTE: Reset next for every reader, and record marked reader in mark/reset methods! - next = 0; - - return current; - } - - /** - * Check to make sure that the stream has not been closed - * - * @throws IOException if the stream is closed - */ - protected final void ensureOpen() throws IOException { - if (strings == null) { - throw new IOException("Stream closed"); - } - } - - public void close() { - super.close(); - strings = null; - current.close(); - } - - public void mark(int pReadLimit) throws IOException { - if (pReadLimit < 0){ - throw new IllegalArgumentException("Read limit < 0"); - } - - synchronized (finalLock) { - ensureOpen(); - mark = next; - markedString = currentSting; - - current.mark(pReadLimit); - } - } - - public void reset() throws IOException { - synchronized (finalLock) { - ensureOpen(); - - if (currentSting != markedString) { - currentSting = markedString - 1; - nextReader(); - current.skip(mark); - } - else { - current.reset(); - } - - next = mark; - } - } - - public boolean markSupported() { - return true; - } - - public int read() throws IOException { - synchronized (finalLock) { - int read = current.read(); - - if (read < 0 && currentSting < strings.length) { - nextReader(); - return read(); // In case of empty strings - } - - next++; - - return read; - } - } - - public int read(char pBuffer[], int pOffset, int pLength) throws IOException { - synchronized (finalLock) { - int read = current.read(pBuffer, pOffset, pLength); - - if (read < 0 && currentSting < strings.length) { - nextReader(); - return read(pBuffer, pOffset, pLength); // In case of empty strings - } - - next += read; - - return read; - } - } - - public boolean ready() throws IOException { - return current.ready(); - } - - public long skip(long pChars) throws IOException { - synchronized (finalLock) { - long skipped = current.skip(pChars); - - if (skipped == 0 && currentSting < strings.length) { - nextReader(); - return skip(pChars); - } - - next += skipped; - - return skipped; - } - } - -} +/* + * 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 of the copyright holder 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 HOLDER 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.Validate; + +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; + +/** + * StringArrayReader + * + * @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/io/StringArrayReader.java#2 $ + */ +public class StringArrayReader extends StringReader { + + 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. + * + * @param pStrings {@code String}s providing the character stream. + */ + public StringArrayReader(final String[] pStrings) { + super(""); + + 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 + + strings = pStrings.clone(); // Defensive copy for content + nextReader(); + } + + protected final Reader nextReader() { + if (currentSting >= strings.length) { + current = new EmptyReader(); + } + else { + current = new StringReader(strings[currentSting++]); + } + + // NOTE: Reset next for every reader, and record marked reader in mark/reset methods! + next = 0; + + return current; + } + + /** + * Check to make sure that the stream has not been closed + * + * @throws IOException if the stream is closed + */ + protected final void ensureOpen() throws IOException { + if (strings == null) { + throw new IOException("Stream closed"); + } + } + + public void close() { + super.close(); + strings = null; + current.close(); + } + + public void mark(int pReadLimit) throws IOException { + if (pReadLimit < 0){ + throw new IllegalArgumentException("Read limit < 0"); + } + + synchronized (finalLock) { + ensureOpen(); + mark = next; + markedString = currentSting; + + current.mark(pReadLimit); + } + } + + public void reset() throws IOException { + synchronized (finalLock) { + ensureOpen(); + + if (currentSting != markedString) { + currentSting = markedString - 1; + nextReader(); + current.skip(mark); + } + else { + current.reset(); + } + + next = mark; + } + } + + public boolean markSupported() { + return true; + } + + public int read() throws IOException { + synchronized (finalLock) { + int read = current.read(); + + if (read < 0 && currentSting < strings.length) { + nextReader(); + return read(); // In case of empty strings + } + + next++; + + return read; + } + } + + public int read(char pBuffer[], int pOffset, int pLength) throws IOException { + synchronized (finalLock) { + int read = current.read(pBuffer, pOffset, pLength); + + if (read < 0 && currentSting < strings.length) { + nextReader(); + return read(pBuffer, pOffset, pLength); // In case of empty strings + } + + next += read; + + return read; + } + } + + public boolean ready() throws IOException { + return current.ready(); + } + + public long skip(long pChars) throws IOException { + synchronized (finalLock) { + long skipped = current.skip(pChars); + + if (skipped == 0 && currentSting < strings.length) { + nextReader(); + return skip(pChars); + } + + 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 023d0a1f..76a3c01f 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 @@ -1,137 +1,136 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.Validate; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * An {@code InputStream} reading up to a specified number of bytes from an - * underlying stream. - *

- * - * @author Harald Kuhr - * @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 bytesLeft; - private int markLimit; - - /** - * Creates a {@code SubStream} of the given {@code pStream}. - * - * @param pStream the underlying input stream - * @param pLength maximum number of bytes to read drom this stream - */ - public SubStream(final InputStream pStream, final long pLength) { - super(Validate.notNull(pStream, "stream")); - bytesLeft = pLength; - } - - /** - * Marks this stream as closed. - * This implementation does not close the underlying stream. - */ - @Override - public void close() throws IOException { - // NOTE: Do not close the underlying stream - while (bytesLeft > 0) { - //noinspection ResultOfMethodCallIgnored - skip(bytesLeft); - } - } - - @Override - public int available() throws IOException { - return (int) Math.min(super.available(), bytesLeft); - } - - @Override - public void mark(int pReadLimit) { - super.mark(pReadLimit);// This either succeeds or does nothing... - markLimit = pReadLimit; - } - - @Override - public void reset() throws IOException { - super.reset();// This either succeeds or throws IOException - bytesLeft += markLimit; - } - - @Override - public int read() throws IOException { - if (bytesLeft-- <= 0) { - return -1; - } - return super.read(); - } - - @Override - public final int read(byte[] pBytes) throws IOException { - return read(pBytes, 0, pBytes.length); - } - - @Override - public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { - if (bytesLeft <= 0) { - return -1; - } - - int read = super.read(pBytes, pOffset, (int) findMaxLen(pLength)); - bytesLeft = read < 0 ? 0 : bytesLeft - read; - return read; - } - - /** - * Finds the maximum number of bytes we can read or skip, from this stream. - * - * @param pLength the requested length - * @return the maximum number of bytes to read - */ - private long findMaxLen(long pLength) { - if (bytesLeft < pLength) { - return (int) Math.max(bytesLeft, 0); - } - else { - return pLength; - } - } - - @Override - public long skip(long pLength) throws IOException { - long skipped = super.skip(findMaxLen(pLength));// Skips 0 or more, never -1 - bytesLeft -= skipped; - return skipped; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.Validate; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * An {@code InputStream} reading up to a specified number of bytes from an + * underlying stream. + * + * @author Harald Kuhr + * @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 bytesLeft; + private int markLimit; + + /** + * Creates a {@code SubStream} of the given {@code pStream}. + * + * @param pStream the underlying input stream + * @param pLength maximum number of bytes to read drom this stream + */ + public SubStream(final InputStream pStream, final long pLength) { + super(Validate.notNull(pStream, "stream")); + bytesLeft = pLength; + } + + /** + * Marks this stream as closed. + * This implementation does not close the underlying stream. + */ + @Override + public void close() throws IOException { + // NOTE: Do not close the underlying stream + while (bytesLeft > 0) { + //noinspection ResultOfMethodCallIgnored + skip(bytesLeft); + } + } + + @Override + public int available() throws IOException { + return (int) Math.min(super.available(), bytesLeft); + } + + @Override + public void mark(int pReadLimit) { + super.mark(pReadLimit);// This either succeeds or does nothing... + markLimit = pReadLimit; + } + + @Override + public void reset() throws IOException { + super.reset();// This either succeeds or throws IOException + bytesLeft += markLimit; + } + + @Override + public int read() throws IOException { + if (bytesLeft-- <= 0) { + return -1; + } + return super.read(); + } + + @Override + public final int read(byte[] pBytes) throws IOException { + return read(pBytes, 0, pBytes.length); + } + + @Override + public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { + if (bytesLeft <= 0) { + return -1; + } + + int read = super.read(pBytes, pOffset, (int) findMaxLen(pLength)); + bytesLeft = read < 0 ? 0 : bytesLeft - read; + return read; + } + + /** + * Finds the maximum number of bytes we can read or skip, from this stream. + * + * @param pLength the requested length + * @return the maximum number of bytes to read + */ + private long findMaxLen(long pLength) { + if (bytesLeft < pLength) { + return (int) Math.max(bytesLeft, 0); + } + else { + return pLength; + } + } + + @Override + public long skip(long pLength) throws IOException { + long skipped = super.skip(findMaxLen(pLength));// Skips 0 or more, never -1 + bytesLeft -= skipped; + return skipped; + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/UnixFileSystem.java b/common/common-io/src/main/java/com/twelvemonkeys/io/UnixFileSystem.java index 761a2ab8..55caa5e9 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/UnixFileSystem.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/UnixFileSystem.java @@ -1,107 +1,106 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.util.StringTokenIterator; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; - -/** - * UnixFileSystem - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/UnixFileSystem.java#1 $ - */ -final class UnixFileSystem extends FileSystem { - long getFreeSpace(File pPath) { - try { - return getNumber(pPath, 3); - } - catch (IOException e) { - return 0l; - } - } - - long getTotalSpace(File pPath) { - try { - return getNumber(pPath, 5); - } - catch (IOException e) { - return 0l; - } - } - - private long getNumber(File pPath, int pIndex) throws IOException { - // TODO: Test on other platforms - // Tested on Mac OS X, CygWin - BufferedReader reader = exec(new String[] {"df", "-k", pPath.getAbsolutePath()}); - - String last = null; - String line; - try { - while ((line = reader.readLine()) != null) { - last = line; - } - } - finally { - FileUtil.close(reader); - } - - if (last != null) { - String blocks = null; - StringTokenIterator tokens = new StringTokenIterator(last, " ", StringTokenIterator.REVERSE); - int count = 0; - // We want the 3rd last token - while (count < pIndex && tokens.hasNext()) { - blocks = tokens.nextToken(); - count++; - } - - if (blocks != null) { - try { - return Long.parseLong(blocks) * 1024L; - } - catch (NumberFormatException ignore) { - // Ignore - } - } - } - - return 0l; - } - - String getName() { - return "Unix"; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.util.StringTokenIterator; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; + +/** + * UnixFileSystem + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/UnixFileSystem.java#1 $ + */ +final class UnixFileSystem extends FileSystem { + long getFreeSpace(File pPath) { + try { + return getNumber(pPath, 3); + } + catch (IOException e) { + return 0l; + } + } + + long getTotalSpace(File pPath) { + try { + return getNumber(pPath, 5); + } + catch (IOException e) { + return 0l; + } + } + + private long getNumber(File pPath, int pIndex) throws IOException { + // TODO: Test on other platforms + // Tested on Mac OS X, CygWin + BufferedReader reader = exec(new String[] {"df", "-k", pPath.getAbsolutePath()}); + + String last = null; + String line; + try { + while ((line = reader.readLine()) != null) { + last = line; + } + } + finally { + FileUtil.close(reader); + } + + if (last != null) { + String blocks = null; + StringTokenIterator tokens = new StringTokenIterator(last, " ", StringTokenIterator.REVERSE); + int count = 0; + // We want the 3rd last token + while (count < pIndex && tokens.hasNext()) { + blocks = tokens.nextToken(); + count++; + } + + if (blocks != null) { + try { + return Long.parseLong(blocks) * 1024L; + } + catch (NumberFormatException ignore) { + // Ignore + } + } + } + + return 0l; + } + + String getName() { + return "Unix"; + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/Win32File.java b/common/common-io/src/main/java/com/twelvemonkeys/io/Win32File.java index ee3e3e1b..fa6c82ef 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/Win32File.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/Win32File.java @@ -1,195 +1,194 @@ -/* - * 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 of the copyright holder 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 HOLDER 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; -import java.io.FileFilter; -import java.io.FilenameFilter; -import java.io.IOException; - -/** - * Win32File - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32File.java#2 $ - */ -final class Win32File extends File { - private final static boolean IS_WINDOWS = isWindows(); - - private static boolean isWindows() { - try { - String os = System.getProperty("os.name"); - return os.toLowerCase().indexOf("windows") >= 0; - } - catch (Throwable t) { - // Ignore - } - return false; - } - - private Win32File(File pPath) { - super(pPath.getPath()); - } - - public static void main(String[] pArgs) { - int argIdx = 0; - boolean recursive = false; - while (pArgs.length > argIdx + 1 && pArgs[argIdx].charAt(0) == '-' && pArgs[argIdx].length() > 1) { - if (pArgs[argIdx].charAt(1) == 'R' || pArgs[argIdx].equals("--recursive")) { - recursive = true; - } - else { - System.err.println("Unknown option: " + pArgs[argIdx]); - } - argIdx++; - } - - File file = wrap(new File(pArgs[argIdx])); - System.out.println("file: " + file); - System.out.println("file.getClass(): " + file.getClass()); - - listFiles(file, 0, recursive); - } - - private static void listFiles(File pFile, int pLevel, boolean pRecursive) { - if (pFile.isDirectory()) { - File[] files = pFile.listFiles(); - for (int l = 0; l < pLevel; l++) { - System.out.print(" "); - } - System.out.println("Contents of " + pFile + ": "); - for (File file : files) { - for (int l = 0; l < pLevel; l++) { - System.out.print(" "); - } - System.out.println(" " + file); - if (pRecursive) { - listFiles(file, pLevel + 1, pLevel < 4); - } - } - } - } - - /** - * Wraps a {@code File} object pointing to a Windows symbolic link - * ({@code .lnk} file) in a {@code Win32Lnk}. - * If the operating system is not Windows, the - * {@code pPath} parameter is returned unwrapped. - * - * @param pPath any path, possibly pointing to a Windows symbolic link file. - * May be {@code null}, in which case {@code null} is returned. - * - * @return a new {@code Win32Lnk} object if the current os is Windows, and - * the file is a Windows symbolic link ({@code .lnk} file), otherwise - * {@code pPath} - */ - public static File wrap(final File pPath) { - if (pPath == null) { - return null; - } - - if (IS_WINDOWS) { - // Don't wrap if allready wrapped - if (pPath instanceof Win32File || pPath instanceof Win32Lnk) { - return pPath; - } - - if (pPath.exists() && pPath.getName().endsWith(".lnk")) { - // If Win32 .lnk, let's wrap - try { - return new Win32Lnk(pPath); - } - catch (IOException e) { - // TODO: FixMe! - e.printStackTrace(); - } - } - - // Wwrap even if not a .lnk, as the listFiles() methods etc, - // could potentially return .lnk's, that we want to wrap later... - return new Win32File(pPath); - } - - return pPath; - } - - /** - * Wraps a {@code File} array, possibly pointing to Windows symbolic links - * ({@code .lnk} files) in {@code Win32Lnk}s. - * - * @param pPaths an array of {@code File}s, possibly pointing to Windows - * symbolic link files. - * May be {@code null}, in which case {@code null} is returned. - * - * @return {@code pPaths}, with any {@code File} representing a Windows - * symbolic link ({@code .lnk} file) wrapped in a {@code Win32Lnk}. - */ - public static File[] wrap(File[] pPaths) { - if (IS_WINDOWS) { - for (int i = 0; pPaths != null && i < pPaths.length; i++) { - pPaths[i] = wrap(pPaths[i]); - } - } - return pPaths; - } - - // File overrides - @Override - public File getAbsoluteFile() { - return wrap(super.getAbsoluteFile()); - } - - @Override - public File getCanonicalFile() throws IOException { - return wrap(super.getCanonicalFile()); - } - - @Override - public File getParentFile() { - return wrap(super.getParentFile()); - } - - @Override - public File[] listFiles() { - return wrap(super.listFiles()); - } - - @Override - public File[] listFiles(FileFilter filter) { - return wrap(super.listFiles(filter)); - } - - @Override - public File[] listFiles(FilenameFilter filter) { - return wrap(super.listFiles(filter)); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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; +import java.io.FileFilter; +import java.io.FilenameFilter; +import java.io.IOException; + +/** + * Win32File + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32File.java#2 $ + */ +final class Win32File extends File { + private final static boolean IS_WINDOWS = isWindows(); + + private static boolean isWindows() { + try { + String os = System.getProperty("os.name"); + return os.toLowerCase().indexOf("windows") >= 0; + } + catch (Throwable t) { + // Ignore + } + return false; + } + + private Win32File(File pPath) { + super(pPath.getPath()); + } + + public static void main(String[] pArgs) { + int argIdx = 0; + boolean recursive = false; + while (pArgs.length > argIdx + 1 && pArgs[argIdx].charAt(0) == '-' && pArgs[argIdx].length() > 1) { + if (pArgs[argIdx].charAt(1) == 'R' || pArgs[argIdx].equals("--recursive")) { + recursive = true; + } + else { + System.err.println("Unknown option: " + pArgs[argIdx]); + } + argIdx++; + } + + File file = wrap(new File(pArgs[argIdx])); + System.out.println("file: " + file); + System.out.println("file.getClass(): " + file.getClass()); + + listFiles(file, 0, recursive); + } + + private static void listFiles(File pFile, int pLevel, boolean pRecursive) { + if (pFile.isDirectory()) { + File[] files = pFile.listFiles(); + for (int l = 0; l < pLevel; l++) { + System.out.print(" "); + } + System.out.println("Contents of " + pFile + ": "); + for (File file : files) { + for (int l = 0; l < pLevel; l++) { + System.out.print(" "); + } + System.out.println(" " + file); + if (pRecursive) { + listFiles(file, pLevel + 1, pLevel < 4); + } + } + } + } + + /** + * Wraps a {@code File} object pointing to a Windows symbolic link + * ({@code .lnk} file) in a {@code Win32Lnk}. + * If the operating system is not Windows, the + * {@code pPath} parameter is returned unwrapped. + * + * @param pPath any path, possibly pointing to a Windows symbolic link file. + * May be {@code null}, in which case {@code null} is returned. + * + * @return a new {@code Win32Lnk} object if the current os is Windows, and + * the file is a Windows symbolic link ({@code .lnk} file), otherwise + * {@code pPath} + */ + public static File wrap(final File pPath) { + if (pPath == null) { + return null; + } + + if (IS_WINDOWS) { + // Don't wrap if allready wrapped + if (pPath instanceof Win32File || pPath instanceof Win32Lnk) { + return pPath; + } + + if (pPath.exists() && pPath.getName().endsWith(".lnk")) { + // If Win32 .lnk, let's wrap + try { + return new Win32Lnk(pPath); + } + catch (IOException e) { + // TODO: FixMe! + e.printStackTrace(); + } + } + + // Wwrap even if not a .lnk, as the listFiles() methods etc, + // could potentially return .lnk's, that we want to wrap later... + return new Win32File(pPath); + } + + return pPath; + } + + /** + * Wraps a {@code File} array, possibly pointing to Windows symbolic links + * ({@code .lnk} files) in {@code Win32Lnk}s. + * + * @param pPaths an array of {@code File}s, possibly pointing to Windows + * symbolic link files. + * May be {@code null}, in which case {@code null} is returned. + * + * @return {@code pPaths}, with any {@code File} representing a Windows + * symbolic link ({@code .lnk} file) wrapped in a {@code Win32Lnk}. + */ + public static File[] wrap(File[] pPaths) { + if (IS_WINDOWS) { + for (int i = 0; pPaths != null && i < pPaths.length; i++) { + pPaths[i] = wrap(pPaths[i]); + } + } + return pPaths; + } + + // File overrides + @Override + public File getAbsoluteFile() { + return wrap(super.getAbsoluteFile()); + } + + @Override + public File getCanonicalFile() throws IOException { + return wrap(super.getCanonicalFile()); + } + + @Override + public File getParentFile() { + return wrap(super.getParentFile()); + } + + @Override + public File[] listFiles() { + return wrap(super.listFiles()); + } + + @Override + public File[] listFiles(FileFilter filter) { + return wrap(super.listFiles(filter)); + } + + @Override + public File[] listFiles(FilenameFilter filter) { + return wrap(super.listFiles(filter)); + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/Win32FileSystem.java b/common/common-io/src/main/java/com/twelvemonkeys/io/Win32FileSystem.java index 400164d1..ae3d3608 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/Win32FileSystem.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/Win32FileSystem.java @@ -1,92 +1,91 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.BufferedReader; -import java.io.File; -import java.io.IOException; - -/** - * WindowsFileSystem - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32FileSystem.java#2 $ - */ -final class Win32FileSystem extends FileSystem { - public long getFreeSpace(File pPath) { - try { - // Windows version - // TODO: Test on W2K/95/98/etc... (tested on XP) - BufferedReader reader = exec(new String[] {"CMD.EXE", "/C", "DIR", "/-C", pPath.getAbsolutePath()}); - - String last = null; - String line; - try { - while ((line = reader.readLine()) != null) { - last = line; - } - } - finally { - FileUtil.close(reader); - } - - if (last != null) { - int end = last.lastIndexOf(" bytes free"); - int start = last.lastIndexOf(' ', end - 1); - - if (start >= 0 && end >= 0) { - try { - return Long.parseLong(last.substring(start + 1, end)); - } - catch (NumberFormatException ignore) { - // Ignore - } - } - } - } - catch (IOException ignore) { - // Ignore - } - - return 0l; - } - - long getTotalSpace(File pPath) { - // TODO: Implement, probably need some JNI stuff... - // Distribute df.exe and execute from temp!? ;-) - return getFreeSpace(pPath); - } - - String getName() { - return "Win32"; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.BufferedReader; +import java.io.File; +import java.io.IOException; + +/** + * WindowsFileSystem + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32FileSystem.java#2 $ + */ +final class Win32FileSystem extends FileSystem { + public long getFreeSpace(File pPath) { + try { + // Windows version + // TODO: Test on W2K/95/98/etc... (tested on XP) + BufferedReader reader = exec(new String[] {"CMD.EXE", "/C", "DIR", "/-C", pPath.getAbsolutePath()}); + + String last = null; + String line; + try { + while ((line = reader.readLine()) != null) { + last = line; + } + } + finally { + FileUtil.close(reader); + } + + if (last != null) { + int end = last.lastIndexOf(" bytes free"); + int start = last.lastIndexOf(' ', end - 1); + + if (start >= 0 && end >= 0) { + try { + return Long.parseLong(last.substring(start + 1, end)); + } + catch (NumberFormatException ignore) { + // Ignore + } + } + } + } + catch (IOException ignore) { + // Ignore + } + + return 0l; + } + + long getTotalSpace(File pPath) { + // TODO: Implement, probably need some JNI stuff... + // Distribute df.exe and execute from temp!? ;-) + return getFreeSpace(pPath); + } + + String getName() { + return "Win32"; + } +} 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 7833c0fa..34eb0be6 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 @@ -1,475 +1,477 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.util.Arrays; - -/** - * A {@code File} implementation that resolves the Windows {@code .lnk} files as symbolic links. - *

- * This class is based on example code from - * Swing Hacks, - * By Joshua Marinacci, Chris Adamson (O'Reilly, ISBN: 0-596-00907-0), Hack 30. - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32Lnk.java#2 $ - */ -final class Win32Lnk extends File { - private final static byte[] LNK_MAGIC = { - 'L', 0x00, 0x00, 0x00, // Magic - }; - private final static byte[] LNK_GUID = { - 0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // Shell Link GUID - (byte) 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'F' - }; - - 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; - private static final int FLAG_REL_PATH_STRING = 0x08; - private static final int FLAG_WORKING_DIRECTORY = 0x10; - private static final int FLAG_COMMAND_LINE_ARGS = 0x20; - private static final int FLAG_ICON_FILENAME = 0x40; - private static final int FLAG_ADDITIONAL_INFO = 0x80; - - private Win32Lnk(final String pPath) throws IOException { - super(pPath); - File target = parse(this); - if (target == this) { - // NOTE: This is a workaround - // target = this causes infinite loops in some methods - target = new File(pPath); - } - this.target = target; - } - - Win32Lnk(final File pPath) throws IOException { - this(pPath.getPath()); - } - - /** - * Parses a {@code .lnk} file to find the real file. - * - * @param pPath the path to the {@code .lnk} file - * @return a new file object that - * @throws java.io.IOException if the {@code .lnk} cannot be parsed - */ - static File parse(final File pPath) throws IOException { - if (!pPath.getName().endsWith(".lnk")) { - return pPath; - } - - File result = pPath; - - LittleEndianDataInputStream in = new LittleEndianDataInputStream(new BufferedInputStream(new FileInputStream(pPath))); - try { - byte[] magic = new byte[4]; - in.readFully(magic); - - byte[] guid = new byte[16]; - in.readFully(guid); - - if (!(Arrays.equals(LNK_MAGIC, magic) && Arrays.equals(LNK_GUID, guid))) { - //System.out.println("Not a symlink"); - // Not a symlink - return pPath; - } - - // Get the flags - int flags = in.readInt(); - //System.out.println("flags: " + Integer.toBinaryString(flags & 0xff)); - - // Get to the file settings - /*int attributes = */in.readInt(); - - // File attributes - // 0 Target is read only. - // 1 Target is hidden. - // 2 Target is a system file. - // 3 Target is a volume label. (Not possible) - // 4 Target is a directory. - // 5 Target has been modified since last backup. (archive) - // 6 Target is encrypted (NTFS EFS) - // 7 Target is Normal?? - // 8 Target is temporary. - // 9 Target is a sparse file. - // 10 Target has reparse point data. - // 11 Target is compressed. - // 12 Target is offline. - //System.out.println("attributes: " + Integer.toBinaryString(attributes)); - // NOTE: Cygwin .lnks are not directory links, can't rely on this.. :-/ - - in.skipBytes(48); // TODO: Make sense of this data... - - // Skipped data: - // long time 1 (creation) - // long time 2 (modification) - // long time 3 (last access) - // int file length - // int icon number - // int ShowVnd value - // int hotkey - // int, int - unknown: 0,0 - - // If the shell settings are present, skip them - if ((flags & FLAG_ITEM_ID_LIST) != 0) { - // Shell Item Id List present - //System.out.println("Shell Item Id List present"); - int shellLen = in.readShort(); // Short - //System.out.println("shellLen: " + shellLen); - - // TODO: Probably need to parse this data, to determine - // Cygwin folders... - - /* - int read = 2; - int itemLen = in.readShort(); - while (itemLen > 0) { - System.out.println("--> ITEM: " + itemLen); - - BufferedReader reader = new BufferedReader(new InputStreamReader(new SubStream(in, itemLen - 2))); - //byte[] itemBytes = new byte[itemLen - 2]; // NOTE: Lenght included - //in.readFully(itemBytes); - - String item = reader.readLine(); - System.out.println("item: \"" + item + "\""); - - itemLen = in.readShort(); - read += itemLen; - } - - System.out.println("read: " + read); - */ - - in.skipBytes(shellLen); - } - - if ((flags & FLAG_FILE_LOC_INFO) != 0) { - // File Location Info Table present - //System.out.println("File Location Info Table present"); - - // 0h 1 dword This is the total length of this structure and all following data - // 4h 1 dword This is a pointer to first offset after this structure. 1Ch - // 8h 1 dword Flags - // Ch 1 dword Offset of local volume info - // 10h 1 dword Offset of base pathname on local system - // 14h 1 dword Offset of network volume info - // 18h 1 dword Offset of remaining pathname - - // Flags: - // Bit Meaning - // 0 Available on a local volume - // 1 Available on a network share - // TODO: Make sure the path is on a local disk, etc.. - - int tableLen = in.readInt(); // Int - //System.out.println("tableLen: " + tableLen); - - in.readInt(); // Skip - - int locFlags = in.readInt(); - //System.out.println("locFlags: " + Integer.toBinaryString(locFlags)); - if ((locFlags & 0x01) != 0) { - //System.out.println("Available local"); - } - if ((locFlags & 0x02) != 0) { - //System.err.println("Available on network path"); - } - - // Get the local volume and local system values - in.skipBytes(4); // TODO: see above for structure - - int localSysOff = in.readInt(); - //System.out.println("localSysOff: " + localSysOff); - in.skipBytes(localSysOff - 20); // Relative to start of chunk - - byte[] pathBytes = new byte[tableLen - localSysOff - 1]; - in.readFully(pathBytes, 0, pathBytes.length); - String path = new String(pathBytes, 0, pathBytes.length - 1); - /* - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - byte read; - // Read bytes until the null (0) character - while (true) { - read = in.readByte(); - if (read == 0) { - break; - } - bytes.write(read & 0xff); - } - - String path = new String(bytes.toByteArray(), 0, bytes.size()); - //*/ - - // Recurse to end of link chain - // TODO: This may cause endless loop if cyclic chain... - //System.out.println("path: \"" + path + "\""); - try { - result = parse(new File(path)); - } - catch (StackOverflowError e) { - throw new IOException("Cannot resolve cyclic link: " + e.getMessage()); - } - } - - if ((flags & FLAG_DESC_STRING) != 0) { - // Description String present, skip it. - //System.out.println("Description String present"); - - // The string length is the first word which must also be skipped. - int descLen = in.readShort(); - //System.out.println("descLen: " + descLen); - - byte[] descBytes = new byte[descLen]; - in.readFully(descBytes, 0, descLen); - - //String desc = new String(descBytes, 0, descLen); - //System.out.println("desc: " + desc); - } - - if ((flags & FLAG_REL_PATH_STRING) != 0) { - // Relative Path String present - //System.out.println("Relative Path String present"); - - // The string length is the first word which must also be skipped. - int pathLen = in.readShort(); - //System.out.println("pathLen: " + pathLen); - - byte[] pathBytes = new byte[pathLen]; - in.readFully(pathBytes, 0, pathLen); - - String path = new String(pathBytes, 0, pathLen); - - // TODO: This may cause endless loop if cyclic chain... - //System.out.println("path: \"" + path + "\""); - if (result == pPath) { - try { - result = parse(new File(pPath.getParentFile(), path)); - } - catch (StackOverflowError e) { - throw new IOException("Cannot resolve cyclic link: " + e.getMessage()); - } - } - } - - if ((flags & FLAG_WORKING_DIRECTORY) != 0) { - //System.out.println("Working Directory present"); - } - if ((flags & FLAG_COMMAND_LINE_ARGS) != 0) { - //System.out.println("Command Line Arguments present"); - // NOTE: This means this .lnk is not a folder, don't follow - result = pPath; - } - if ((flags & FLAG_ICON_FILENAME) != 0) { - //System.out.println("Icon Filename present"); - } - if ((flags & FLAG_ADDITIONAL_INFO) != 0) { - //System.out.println("Additional Info present"); - } - } - finally { - in.close(); - } - - return result; - } - - /* - private static String getNullDelimitedString(byte[] bytes, int off) { - int len = 0; - // Count bytes until the null (0) character - while (true) { - if (bytes[off + len] == 0) { - break; - } - len++; - } - - System.err.println("--> " + len); - - return new String(bytes, off, len); - } - */ - - /** - * Converts two bytes into a short. - *

- * NOTE: this is little endian because it's for an - * Intel only OS - * - * @ param bytes - * @ param off - * @return the bytes as a short. - */ - /* - private static int bytes2short(byte[] bytes, int off) { - return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff); - } - */ - - public File getTarget() { - return target; - } - - // java.io.File overrides below - - @Override - public boolean isDirectory() { - return target.isDirectory(); - } - - @Override - public boolean canRead() { - return target.canRead(); - } - - @Override - public boolean canWrite() { - return target.canWrite(); - } - - // NOTE: equals is implemented using compareto == 0 - /* - public int compareTo(File pathname) { - // TODO: Verify this - // Probably not a good idea, as it IS NOT THE SAME file - // It's probably better to not override - return target.compareTo(pathname); - } - */ - - // Should probably never allow creating a new .lnk - // public boolean createNewFile() throws IOException - - // Deletes only the .lnk - // public boolean delete() { - //public void deleteOnExit() { - - @Override - public boolean exists() { - return target.exists(); - } - - // A .lnk may be absolute - //public File getAbsoluteFile() { - //public String getAbsolutePath() { - - // Theses should be resolved according to the API (for Unix). - @Override - public File getCanonicalFile() throws IOException { - return target.getCanonicalFile(); - } - - @Override - public String getCanonicalPath() throws IOException { - return target.getCanonicalPath(); - } - - //public String getName() { - - // I guess the parent should be the parent of the .lnk, not the target - //public String getParent() { - //public File getParentFile() { - - // public boolean isAbsolute() { - @Override - public boolean isFile() { - return target.isFile(); - } - - @Override - public boolean isHidden() { - return target.isHidden(); - } - - @Override - public long lastModified() { - return target.lastModified(); - } - - @Override - public long length() { - return target.length(); - } - - @Override - public String[] list() { - return target.list(); - } - - @Override - public String[] list(final FilenameFilter filter) { - return target.list(filter); - } - - @Override - public File[] listFiles() { - return Win32File.wrap(target.listFiles()); - } - - @Override - public File[] listFiles(final FileFilter filter) { - return Win32File.wrap(target.listFiles(filter)); - } - - @Override - public File[] listFiles(final FilenameFilter filter) { - return Win32File.wrap(target.listFiles(filter)); - } - - // Makes no sense, does it? - //public boolean mkdir() { - //public boolean mkdirs() { - - // Only rename the lnk - //public boolean renameTo(File dest) { - - @Override - public boolean setLastModified(long time) { - return target.setLastModified(time); - } - - @Override - public boolean setReadOnly() { - return target.setReadOnly(); - } - - @Override - public String toString() { - if (target.equals(this)) { - return super.toString(); - } - return super.toString() + " -> " + target.toString(); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.util.Arrays; + +/** + * A {@code File} implementation that resolves the Windows {@code .lnk} files as symbolic links. + *

+ * This class is based on example code from + * Swing Hacks, + * By Joshua Marinacci, Chris Adamson (O'Reilly, ISBN: 0-596-00907-0), Hack 30. + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32Lnk.java#2 $ + */ +final class Win32Lnk extends File { + private final static byte[] LNK_MAGIC = { + 'L', 0x00, 0x00, 0x00, // Magic + }; + private final static byte[] LNK_GUID = { + 0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // Shell Link GUID + (byte) 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 'F' + }; + + 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; + private static final int FLAG_REL_PATH_STRING = 0x08; + private static final int FLAG_WORKING_DIRECTORY = 0x10; + private static final int FLAG_COMMAND_LINE_ARGS = 0x20; + private static final int FLAG_ICON_FILENAME = 0x40; + private static final int FLAG_ADDITIONAL_INFO = 0x80; + + private Win32Lnk(final String pPath) throws IOException { + super(pPath); + File target = parse(this); + if (target == this) { + // NOTE: This is a workaround + // target = this causes infinite loops in some methods + target = new File(pPath); + } + this.target = target; + } + + Win32Lnk(final File pPath) throws IOException { + this(pPath.getPath()); + } + + /** + * Parses a {@code .lnk} file to find the real file. + * + * @param pPath the path to the {@code .lnk} file + * @return a new file object that + * @throws java.io.IOException if the {@code .lnk} cannot be parsed + */ + static File parse(final File pPath) throws IOException { + if (!pPath.getName().endsWith(".lnk")) { + return pPath; + } + + File result = pPath; + + LittleEndianDataInputStream in = new LittleEndianDataInputStream(new BufferedInputStream(new FileInputStream(pPath))); + try { + byte[] magic = new byte[4]; + in.readFully(magic); + + byte[] guid = new byte[16]; + in.readFully(guid); + + if (!(Arrays.equals(LNK_MAGIC, magic) && Arrays.equals(LNK_GUID, guid))) { + //System.out.println("Not a symlink"); + // Not a symlink + return pPath; + } + + // Get the flags + int flags = in.readInt(); + //System.out.println("flags: " + Integer.toBinaryString(flags & 0xff)); + + // Get to the file settings + /*int attributes = */in.readInt(); + + // File attributes + // 0 Target is read only. + // 1 Target is hidden. + // 2 Target is a system file. + // 3 Target is a volume label. (Not possible) + // 4 Target is a directory. + // 5 Target has been modified since last backup. (archive) + // 6 Target is encrypted (NTFS EFS) + // 7 Target is Normal?? + // 8 Target is temporary. + // 9 Target is a sparse file. + // 10 Target has reparse point data. + // 11 Target is compressed. + // 12 Target is offline. + //System.out.println("attributes: " + Integer.toBinaryString(attributes)); + // NOTE: Cygwin .lnks are not directory links, can't rely on this.. :-/ + + in.skipBytes(48); // TODO: Make sense of this data... + + // Skipped data: + // long time 1 (creation) + // long time 2 (modification) + // long time 3 (last access) + // int file length + // int icon number + // int ShowVnd value + // int hotkey + // int, int - unknown: 0,0 + + // If the shell settings are present, skip them + if ((flags & FLAG_ITEM_ID_LIST) != 0) { + // Shell Item Id List present + //System.out.println("Shell Item Id List present"); + int shellLen = in.readShort(); // Short + //System.out.println("shellLen: " + shellLen); + + // TODO: Probably need to parse this data, to determine + // Cygwin folders... + + /* + int read = 2; + int itemLen = in.readShort(); + while (itemLen > 0) { + System.out.println("--> ITEM: " + itemLen); + + BufferedReader reader = new BufferedReader(new InputStreamReader(new SubStream(in, itemLen - 2))); + //byte[] itemBytes = new byte[itemLen - 2]; // NOTE: Lenght included + //in.readFully(itemBytes); + + String item = reader.readLine(); + System.out.println("item: \"" + item + "\""); + + itemLen = in.readShort(); + read += itemLen; + } + + System.out.println("read: " + read); + */ + + in.skipBytes(shellLen); + } + + if ((flags & FLAG_FILE_LOC_INFO) != 0) { + // File Location Info Table present + //System.out.println("File Location Info Table present"); + + // 0h 1 dword This is the total length of this structure and all following data + // 4h 1 dword This is a pointer to first offset after this structure. 1Ch + // 8h 1 dword Flags + // Ch 1 dword Offset of local volume info + // 10h 1 dword Offset of base pathname on local system + // 14h 1 dword Offset of network volume info + // 18h 1 dword Offset of remaining pathname + + // Flags: + // Bit Meaning + // 0 Available on a local volume + // 1 Available on a network share + // TODO: Make sure the path is on a local disk, etc.. + + int tableLen = in.readInt(); // Int + //System.out.println("tableLen: " + tableLen); + + in.readInt(); // Skip + + int locFlags = in.readInt(); + //System.out.println("locFlags: " + Integer.toBinaryString(locFlags)); + if ((locFlags & 0x01) != 0) { + //System.out.println("Available local"); + } + if ((locFlags & 0x02) != 0) { + //System.err.println("Available on network path"); + } + + // Get the local volume and local system values + in.skipBytes(4); // TODO: see above for structure + + int localSysOff = in.readInt(); + //System.out.println("localSysOff: " + localSysOff); + in.skipBytes(localSysOff - 20); // Relative to start of chunk + + byte[] pathBytes = new byte[tableLen - localSysOff - 1]; + in.readFully(pathBytes, 0, pathBytes.length); + String path = new String(pathBytes, 0, pathBytes.length - 1); + /* + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + byte read; + // Read bytes until the null (0) character + while (true) { + read = in.readByte(); + if (read == 0) { + break; + } + bytes.write(read & 0xff); + } + + String path = new String(bytes.toByteArray(), 0, bytes.size()); + //*/ + + // Recurse to end of link chain + // TODO: This may cause endless loop if cyclic chain... + //System.out.println("path: \"" + path + "\""); + try { + result = parse(new File(path)); + } + catch (StackOverflowError e) { + throw new IOException("Cannot resolve cyclic link: " + e.getMessage()); + } + } + + if ((flags & FLAG_DESC_STRING) != 0) { + // Description String present, skip it. + //System.out.println("Description String present"); + + // The string length is the first word which must also be skipped. + int descLen = in.readShort(); + //System.out.println("descLen: " + descLen); + + byte[] descBytes = new byte[descLen]; + in.readFully(descBytes, 0, descLen); + + //String desc = new String(descBytes, 0, descLen); + //System.out.println("desc: " + desc); + } + + if ((flags & FLAG_REL_PATH_STRING) != 0) { + // Relative Path String present + //System.out.println("Relative Path String present"); + + // The string length is the first word which must also be skipped. + int pathLen = in.readShort(); + //System.out.println("pathLen: " + pathLen); + + byte[] pathBytes = new byte[pathLen]; + in.readFully(pathBytes, 0, pathLen); + + String path = new String(pathBytes, 0, pathLen); + + // TODO: This may cause endless loop if cyclic chain... + //System.out.println("path: \"" + path + "\""); + if (result == pPath) { + try { + result = parse(new File(pPath.getParentFile(), path)); + } + catch (StackOverflowError e) { + throw new IOException("Cannot resolve cyclic link: " + e.getMessage()); + } + } + } + + if ((flags & FLAG_WORKING_DIRECTORY) != 0) { + //System.out.println("Working Directory present"); + } + if ((flags & FLAG_COMMAND_LINE_ARGS) != 0) { + //System.out.println("Command Line Arguments present"); + // NOTE: This means this .lnk is not a folder, don't follow + result = pPath; + } + if ((flags & FLAG_ICON_FILENAME) != 0) { + //System.out.println("Icon Filename present"); + } + if ((flags & FLAG_ADDITIONAL_INFO) != 0) { + //System.out.println("Additional Info present"); + } + } + finally { + in.close(); + } + + return result; + } + + /* + private static String getNullDelimitedString(byte[] bytes, int off) { + int len = 0; + // Count bytes until the null (0) character + while (true) { + if (bytes[off + len] == 0) { + break; + } + len++; + } + + System.err.println("--> " + len); + + return new String(bytes, off, len); + } + */ + + /** + * Converts two bytes into a short. + *

+ * NOTE: this is little endian because it's for an + * Intel only OS + *

+ * + * @ param bytes + * @ param off + * @return the bytes as a short. + */ + /* + private static int bytes2short(byte[] bytes, int off) { + return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff); + } + */ + + public File getTarget() { + return target; + } + + // java.io.File overrides below + + @Override + public boolean isDirectory() { + return target.isDirectory(); + } + + @Override + public boolean canRead() { + return target.canRead(); + } + + @Override + public boolean canWrite() { + return target.canWrite(); + } + + // NOTE: equals is implemented using compareto == 0 + /* + public int compareTo(File pathname) { + // TODO: Verify this + // Probably not a good idea, as it IS NOT THE SAME file + // It's probably better to not override + return target.compareTo(pathname); + } + */ + + // Should probably never allow creating a new .lnk + // public boolean createNewFile() throws IOException + + // Deletes only the .lnk + // public boolean delete() { + //public void deleteOnExit() { + + @Override + public boolean exists() { + return target.exists(); + } + + // A .lnk may be absolute + //public File getAbsoluteFile() { + //public String getAbsolutePath() { + + // Theses should be resolved according to the API (for Unix). + @Override + public File getCanonicalFile() throws IOException { + return target.getCanonicalFile(); + } + + @Override + public String getCanonicalPath() throws IOException { + return target.getCanonicalPath(); + } + + //public String getName() { + + // I guess the parent should be the parent of the .lnk, not the target + //public String getParent() { + //public File getParentFile() { + + // public boolean isAbsolute() { + @Override + public boolean isFile() { + return target.isFile(); + } + + @Override + public boolean isHidden() { + return target.isHidden(); + } + + @Override + public long lastModified() { + return target.lastModified(); + } + + @Override + public long length() { + return target.length(); + } + + @Override + public String[] list() { + return target.list(); + } + + @Override + public String[] list(final FilenameFilter filter) { + return target.list(filter); + } + + @Override + public File[] listFiles() { + return Win32File.wrap(target.listFiles()); + } + + @Override + public File[] listFiles(final FileFilter filter) { + return Win32File.wrap(target.listFiles(filter)); + } + + @Override + public File[] listFiles(final FilenameFilter filter) { + return Win32File.wrap(target.listFiles(filter)); + } + + // Makes no sense, does it? + //public boolean mkdir() { + //public boolean mkdirs() { + + // Only rename the lnk + //public boolean renameTo(File dest) { + + @Override + public boolean setLastModified(long time) { + return target.setLastModified(time); + } + + @Override + public boolean setReadOnly() { + return target.setReadOnly(); + } + + @Override + public String toString() { + if (target.equals(this)) { + return super.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 b9f9f39e..ff0893ad 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 @@ -1,239 +1,240 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.DateUtil; - -import java.io.*; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.Charset; - -/** - * Wraps a {@code Writer} in an {@code OutputStream}. - *

- * Instances of this class are not thread-safe. - *

- * NOTE: This class is probably not the right way of solving your problem, - * however it might prove useful in JSPs etc. - * If possible, it's always better to use the {@code Writer}'s underlying - * {@code OutputStream}, or wrap it's native backing. - * - *

- * - * @author Harald Kuhr - * @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 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(); - - private static boolean isNIOAvailable() { - try { - Class.forName("java.nio.charset.Charset"); - return true; - } - catch (Throwable t) { - // Ignore - } - - return false; - } - - public WriterOutputStream(final Writer pWriter, final String pCharset) { - writer = pWriter; - decoder = getDecoder(pCharset); - } - - public WriterOutputStream(final Writer pWriter) { - this(pWriter, null); - } - - private static Decoder getDecoder(final String pCharset) { - // NOTE: The CharsetDecoder is typically 10-20% faster than - // StringDecoder according to my tests - // StringEncoder is horribly slow on 1.2 systems, but there's no - // alternative... - if (NIO_AVAILABLE) { - return new CharsetDecoder(pCharset); - } - - return new StringDecoder(pCharset); - } - - @Override - public void close() throws IOException { - flush(); - writer.close(); - writer = null; - } - - @Override - public void flush() throws IOException { - flushBuffer(); - writer.flush(); - } - - @Override - public final void write(byte[] pBytes) throws IOException { - if (pBytes == null) { - throw new NullPointerException("bytes == null"); - } - write(pBytes, 0, pBytes.length); - } - - @Override - public final void write(byte[] pBytes, int pOffset, int pLength) throws IOException { - flushBuffer(); - 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.. - bufferStream.write(pByte); - } - - private void flushBuffer() throws IOException { - if (!isFlushing && bufferStream.size() > 0) { - isFlushing = true; - bufferStream.writeTo(this); // NOTE: Avoids cloning buffer array - bufferStream.reset(); - isFlushing = false; - } - } - - /////////////////////////////////////////////////////////////////////////// - public static void main(String[] pArgs) throws IOException { - int iterations = 1000000; - - byte[] bytes = "������ klashf lkash ljah lhaaklhghdfgu ksd".getBytes("UTF-8"); - - Decoder d; - long start; - long time; - Writer sink = new PrintWriter(new NullOutputStream()); - StringWriter writer; - String str; - - d = new StringDecoder("UTF-8"); - for (int i = 0; i < 10000; i++) { - d.decodeTo(sink, bytes, 0, bytes.length); - } - start = System.currentTimeMillis(); - for (int i = 0; i < iterations; i++) { - d.decodeTo(sink, bytes, 0, bytes.length); - } - time = DateUtil.delta(start); - System.out.println("StringDecoder"); - System.out.println("time: " + time); - - writer = new StringWriter(); - d.decodeTo(writer, bytes, 0, bytes.length); - str = writer.toString(); - System.out.println("str: \"" + str + "\""); - System.out.println("chars.length: " + str.length()); - System.out.println(); - - if (NIO_AVAILABLE) { - d = new CharsetDecoder("UTF-8"); - for (int i = 0; i < 10000; i++) { - d.decodeTo(sink, bytes, 0, bytes.length); - } - start = System.currentTimeMillis(); - for (int i = 0; i < iterations; i++) { - d.decodeTo(sink, bytes, 0, bytes.length); - } - time = DateUtil.delta(start); - System.out.println("CharsetDecoder"); - System.out.println("time: " + time); - writer = new StringWriter(); - d.decodeTo(writer, bytes, 0, bytes.length); - str = writer.toString(); - System.out.println("str: \"" + str + "\""); - System.out.println("chars.length: " + str.length()); - System.out.println(); - } - - OutputStream os = new WriterOutputStream(new PrintWriter(System.out), "UTF-8"); - os.write(bytes); - os.flush(); - System.out.println(); - - for (byte b : bytes) { - os.write(b & 0xff); - } - os.flush(); - } - - /////////////////////////////////////////////////////////////////////////// - private static interface Decoder { - void decodeTo(Writer pWriter, byte[] pBytes, int pOffset, int pLength) throws IOException; - } - - private static final class CharsetDecoder implements Decoder { - final Charset mCharset; - - CharsetDecoder(String pCharset) { - // Handle null-case, to get default charset - String charset = pCharset != null ? pCharset : - System.getProperty("file.encoding", "ISO-8859-1"); - mCharset = Charset.forName(charset); - } - - public void decodeTo(Writer pWriter, byte[] pBytes, int pOffset, int pLength) throws IOException { - CharBuffer cb = mCharset.decode(ByteBuffer.wrap(pBytes, pOffset, pLength)); - pWriter.write(cb.array(), 0, cb.length()); - } - } - - private static final class StringDecoder implements Decoder { - final String mCharset; - - StringDecoder(String pCharset) { - mCharset = pCharset; - } - - public void decodeTo(Writer pWriter, byte[] pBytes, int pOffset, int pLength) throws IOException { - String str = mCharset == null ? - new String(pBytes, pOffset, pLength) : - new String(pBytes, pOffset, pLength, mCharset); - - pWriter.write(str); - } - } +/* + * 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 of the copyright holder 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 HOLDER 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.DateUtil; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +/** + * Wraps a {@code Writer} in an {@code OutputStream}. + *

+ * Instances of this class are not thread-safe. + *

+ *

+ * NOTE: This class is probably not the right way of solving your problem, + * however it might prove useful in JSPs etc. + * If possible, it's always better to use the {@code Writer}'s underlying + * {@code OutputStream}, or wrap it's native backing. + * + *

+ * + * @author Harald Kuhr + * @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 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(); + + private static boolean isNIOAvailable() { + try { + Class.forName("java.nio.charset.Charset"); + return true; + } + catch (Throwable t) { + // Ignore + } + + return false; + } + + public WriterOutputStream(final Writer pWriter, final String pCharset) { + writer = pWriter; + decoder = getDecoder(pCharset); + } + + public WriterOutputStream(final Writer pWriter) { + this(pWriter, null); + } + + private static Decoder getDecoder(final String pCharset) { + // NOTE: The CharsetDecoder is typically 10-20% faster than + // StringDecoder according to my tests + // StringEncoder is horribly slow on 1.2 systems, but there's no + // alternative... + if (NIO_AVAILABLE) { + return new CharsetDecoder(pCharset); + } + + return new StringDecoder(pCharset); + } + + @Override + public void close() throws IOException { + flush(); + writer.close(); + writer = null; + } + + @Override + public void flush() throws IOException { + flushBuffer(); + writer.flush(); + } + + @Override + public final void write(byte[] pBytes) throws IOException { + if (pBytes == null) { + throw new NullPointerException("bytes == null"); + } + write(pBytes, 0, pBytes.length); + } + + @Override + public final void write(byte[] pBytes, int pOffset, int pLength) throws IOException { + flushBuffer(); + 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.. + bufferStream.write(pByte); + } + + private void flushBuffer() throws IOException { + if (!isFlushing && bufferStream.size() > 0) { + isFlushing = true; + bufferStream.writeTo(this); // NOTE: Avoids cloning buffer array + bufferStream.reset(); + isFlushing = false; + } + } + + /////////////////////////////////////////////////////////////////////////// + public static void main(String[] pArgs) throws IOException { + int iterations = 1000000; + + byte[] bytes = "������ klashf lkash ljah lhaaklhghdfgu ksd".getBytes("UTF-8"); + + Decoder d; + long start; + long time; + Writer sink = new PrintWriter(new NullOutputStream()); + StringWriter writer; + String str; + + d = new StringDecoder("UTF-8"); + for (int i = 0; i < 10000; i++) { + d.decodeTo(sink, bytes, 0, bytes.length); + } + start = System.currentTimeMillis(); + for (int i = 0; i < iterations; i++) { + d.decodeTo(sink, bytes, 0, bytes.length); + } + time = DateUtil.delta(start); + System.out.println("StringDecoder"); + System.out.println("time: " + time); + + writer = new StringWriter(); + d.decodeTo(writer, bytes, 0, bytes.length); + str = writer.toString(); + System.out.println("str: \"" + str + "\""); + System.out.println("chars.length: " + str.length()); + System.out.println(); + + if (NIO_AVAILABLE) { + d = new CharsetDecoder("UTF-8"); + for (int i = 0; i < 10000; i++) { + d.decodeTo(sink, bytes, 0, bytes.length); + } + start = System.currentTimeMillis(); + for (int i = 0; i < iterations; i++) { + d.decodeTo(sink, bytes, 0, bytes.length); + } + time = DateUtil.delta(start); + System.out.println("CharsetDecoder"); + System.out.println("time: " + time); + writer = new StringWriter(); + d.decodeTo(writer, bytes, 0, bytes.length); + str = writer.toString(); + System.out.println("str: \"" + str + "\""); + System.out.println("chars.length: " + str.length()); + System.out.println(); + } + + OutputStream os = new WriterOutputStream(new PrintWriter(System.out), "UTF-8"); + os.write(bytes); + os.flush(); + System.out.println(); + + for (byte b : bytes) { + os.write(b & 0xff); + } + os.flush(); + } + + /////////////////////////////////////////////////////////////////////////// + private static interface Decoder { + void decodeTo(Writer pWriter, byte[] pBytes, int pOffset, int pLength) throws IOException; + } + + private static final class CharsetDecoder implements Decoder { + final Charset mCharset; + + CharsetDecoder(String pCharset) { + // Handle null-case, to get default charset + String charset = pCharset != null ? pCharset : + System.getProperty("file.encoding", "ISO-8859-1"); + mCharset = Charset.forName(charset); + } + + public void decodeTo(Writer pWriter, byte[] pBytes, int pOffset, int pLength) throws IOException { + CharBuffer cb = mCharset.decode(ByteBuffer.wrap(pBytes, pOffset, pLength)); + pWriter.write(cb.array(), 0, cb.length()); + } + } + + private static final class StringDecoder implements Decoder { + final String mCharset; + + StringDecoder(String pCharset) { + mCharset = pCharset; + } + + public void decodeTo(Writer pWriter, byte[] pBytes, int pOffset, int pLength) throws IOException { + String str = mCharset == null ? + new String(pBytes, pOffset, pLength) : + new String(pBytes, pOffset, pLength, mCharset); + + pWriter.write(str); + } + } } \ No newline at end of file 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 d433e387..2660b2f7 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 @@ -1,188 +1,188 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.enc; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * {@code Decoder} implementation for standard base64 encoding. - *

- * @see RFC 1421 - * @see - * - * @see Base64Encoder - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java#2 $ - */ -public final class Base64Decoder implements Decoder { - /** - * This array maps the characters to their 6 bit values - */ - final static byte[] PEM_ARRAY = { - //0 1 2 3 4 5 6 7 - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0 - 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1 - 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2 - 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3 - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4 - 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5 - 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6 - '4', '5', '6', '7', '8', '9', '+', '/' // 7 - }; - - final static byte[] PEM_CONVERT_ARRAY; - - private byte[] decodeBuffer = new byte[4]; - - static { - PEM_CONVERT_ARRAY = new byte[256]; - - for (int i = 0; i < 255; i++) { - PEM_CONVERT_ARRAY[i] = -1; - } - - for (int i = 0; i < PEM_ARRAY.length; i++) { - PEM_CONVERT_ARRAY[PEM_ARRAY[i]] = (byte) i; - } - } - - protected static int readFully(final InputStream pStream, final byte pBytes[], final int pOffset, final int pLength) - throws IOException - { - for (int i = 0; i < pLength; i++) { - int read = pStream.read(); - - if (read == -1) { - return i != 0 ? i : -1; - } - - pBytes[i + pOffset] = (byte) read; - } - - return pLength; - } - - protected boolean decodeAtom(final InputStream pInput, final ByteBuffer pOutput, final int pLength) - throws IOException { - - byte byte0 = -1; - byte byte1 = -1; - byte byte2 = -1; - byte byte3 = -1; - - if (pLength < 2) { - throw new IOException("BASE64Decoder: Not enough bytes for an atom."); - } - - int read; - - // Skip line feeds - do { - read = pInput.read(); - - if (read == -1) { - return false; - } - } while (read == 10 || read == 13); - - decodeBuffer[0] = (byte) read; - read = readFully(pInput, decodeBuffer, 1, pLength - 1); - - if (read == -1) { - return false; - } - - int length = pLength; - - if (length > 3 && decodeBuffer[3] == 61) { - length = 3; - } - - if (length > 2 && decodeBuffer[2] == 61) { - length = 2; - } - - switch (length) { - case 4: - byte3 = PEM_CONVERT_ARRAY[decodeBuffer[3] & 255]; - // fall through - case 3: - byte2 = PEM_CONVERT_ARRAY[decodeBuffer[2] & 255]; - // fall through - case 2: - byte1 = PEM_CONVERT_ARRAY[decodeBuffer[1] & 255]; - byte0 = PEM_CONVERT_ARRAY[decodeBuffer[0] & 255]; - // fall through - default: - switch (length) { - case 2: - pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); - break; - case 3: - pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); - pOutput.put((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15)); - break; - case 4: - pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); - pOutput.put((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15)); - pOutput.put((byte) (byte2 << 6 & 192 | byte3 & 63)); - break; - } - - break; - } - - return true; - } - - public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { - do { - int k = 72; - int i; - - for (i = 0; i + 4 < k; i += 4) { - if(!decodeAtom(stream, buffer, 4)) { - break; - } - } - - if (!decodeAtom(stream, buffer, k - i)) { - break; - } - } - while (buffer.remaining() > 54); // 72 char lines should produce no more than 54 bytes - - return buffer.position(); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.enc; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * {@code Decoder} implementation for standard base64 encoding. + * + * @see RFC 1421 + * @see RFC 2045 + * + * @see Base64Encoder + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java#2 $ + */ +public final class Base64Decoder implements Decoder { + /** + * This array maps the characters to their 6 bit values + */ + final static byte[] PEM_ARRAY = { + //0 1 2 3 4 5 6 7 + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', // 0 + 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', // 1 + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', // 2 + 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', // 3 + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', // 4 + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', // 5 + 'w', 'x', 'y', 'z', '0', '1', '2', '3', // 6 + '4', '5', '6', '7', '8', '9', '+', '/' // 7 + }; + + final static byte[] PEM_CONVERT_ARRAY; + + private byte[] decodeBuffer = new byte[4]; + + static { + PEM_CONVERT_ARRAY = new byte[256]; + + for (int i = 0; i < 255; i++) { + PEM_CONVERT_ARRAY[i] = -1; + } + + for (int i = 0; i < PEM_ARRAY.length; i++) { + PEM_CONVERT_ARRAY[PEM_ARRAY[i]] = (byte) i; + } + } + + protected static int readFully(final InputStream pStream, final byte pBytes[], final int pOffset, final int pLength) + throws IOException + { + for (int i = 0; i < pLength; i++) { + int read = pStream.read(); + + if (read == -1) { + return i != 0 ? i : -1; + } + + pBytes[i + pOffset] = (byte) read; + } + + return pLength; + } + + protected boolean decodeAtom(final InputStream pInput, final ByteBuffer pOutput, final int pLength) + throws IOException { + + byte byte0 = -1; + byte byte1 = -1; + byte byte2 = -1; + byte byte3 = -1; + + if (pLength < 2) { + throw new IOException("BASE64Decoder: Not enough bytes for an atom."); + } + + int read; + + // Skip line feeds + do { + read = pInput.read(); + + if (read == -1) { + return false; + } + } while (read == 10 || read == 13); + + decodeBuffer[0] = (byte) read; + read = readFully(pInput, decodeBuffer, 1, pLength - 1); + + if (read == -1) { + return false; + } + + int length = pLength; + + if (length > 3 && decodeBuffer[3] == 61) { + length = 3; + } + + if (length > 2 && decodeBuffer[2] == 61) { + length = 2; + } + + switch (length) { + case 4: + byte3 = PEM_CONVERT_ARRAY[decodeBuffer[3] & 255]; + // fall through + case 3: + byte2 = PEM_CONVERT_ARRAY[decodeBuffer[2] & 255]; + // fall through + case 2: + byte1 = PEM_CONVERT_ARRAY[decodeBuffer[1] & 255]; + byte0 = PEM_CONVERT_ARRAY[decodeBuffer[0] & 255]; + // fall through + default: + switch (length) { + case 2: + pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); + break; + case 3: + pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); + pOutput.put((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15)); + break; + case 4: + pOutput.put((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); + pOutput.put((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15)); + pOutput.put((byte) (byte2 << 6 & 192 | byte3 & 63)); + break; + } + + break; + } + + return true; + } + + public int decode(final InputStream stream, final ByteBuffer buffer) throws IOException { + do { + int k = 72; + int i; + + for (i = 0; i + 4 < k; i += 4) { + if(!decodeAtom(stream, buffer, 4)) { + break; + } + } + + if (!decodeAtom(stream, buffer, k - i)) { + break; + } + } + while (buffer.remaining() > 54); // 72 char lines should produce no more than 54 bytes + + return buffer.position(); + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java index a9317969..8a9eaa5f 100644 --- a/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java @@ -1,106 +1,106 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.enc; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -/** - * {@code Encoder} implementation for standard base64 encoding. - *

- * @see RFC 1421 - * @see - * - * @see Base64Decoder - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java#2 $ - */ -public class Base64Encoder implements Encoder { - - public void encode(final OutputStream stream, final ByteBuffer buffer) - throws IOException - { - - // TODO: Implement - // NOTE: This is impossible, given the current spec, as we need to either: - // - buffer all data in the EncoderStream - // - or have flush/end method(s) in the Encoder - // to ensure proper end of stream handling - - int length; - - // TODO: Temp impl, will only work for single writes - while (buffer.hasRemaining()) { - byte a, b, c; - -// if ((buffer.remaining()) > 2) { -// length = 3; -// } -// else { -// length = buffer.remaining(); -// } - length = Math.min(3, buffer.remaining()); - - switch (length) { - case 1: - a = buffer.get(); - b = 0; - stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); - stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - stream.write('='); - stream.write('='); - break; - - case 2: - a = buffer.get(); - b = buffer.get(); - c = 0; - stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); - stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - stream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); - stream.write('='); - break; - - default: - a = buffer.get(); - b = buffer.get(); - c = buffer.get(); - stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); - stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); - stream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); - stream.write(Base64Decoder.PEM_ARRAY[c & 0x3F]); - break; - } - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.enc; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * {@code Encoder} implementation for standard base64 encoding. + * + * @see RFC 1421 + * @see RFC 2045 + * + * @see Base64Decoder + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java#2 $ + */ +public class Base64Encoder implements Encoder { + + public void encode(final OutputStream stream, final ByteBuffer buffer) + throws IOException + { + + // TODO: Implement + // NOTE: This is impossible, given the current spec, as we need to either: + // - buffer all data in the EncoderStream + // - or have flush/end method(s) in the Encoder + // to ensure proper end of stream handling + + int length; + + // TODO: Temp impl, will only work for single writes + while (buffer.hasRemaining()) { + byte a, b, c; + +// if ((buffer.remaining()) > 2) { +// length = 3; +// } +// else { +// length = buffer.remaining(); +// } + length = Math.min(3, buffer.remaining()); + + switch (length) { + case 1: + a = buffer.get(); + b = 0; + stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); + stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + stream.write('='); + stream.write('='); + break; + + case 2: + a = buffer.get(); + b = buffer.get(); + c = 0; + stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); + stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + stream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + stream.write('='); + break; + + default: + a = buffer.get(); + b = buffer.get(); + c = buffer.get(); + stream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); + stream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + stream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + stream.write(Base64Decoder.PEM_ARRAY[c & 0x3F]); + break; + } + } + } +} 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 c54e8d09..c377d2d7 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 @@ -1,56 +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 of the copyright holder 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 HOLDER 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.enc; - -import java.io.IOException; - -/** - * Thrown by {@code Decoder}s when encoded data can not be decoded. - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecodeException.java#2 $ - */ -public class DecodeException extends IOException { - - public DecodeException(final String pMessage) { - super(pMessage); - } - - public DecodeException(final String pMessage, final Throwable pCause) { - super(pMessage); - initCause(pCause); - } - - public DecodeException(final Throwable pCause) { - this(pCause.getMessage(), pCause); - } +/* + * 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 of the copyright holder 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 HOLDER 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.enc; + +import java.io.IOException; + +/** + * Thrown by {@code Decoder}s when encoded data can not be decoded. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecodeException.java#2 $ + */ +public class DecodeException extends IOException { + + public DecodeException(final String pMessage) { + super(pMessage); + } + + public DecodeException(final String pMessage, final Throwable pCause) { + super(pMessage); + initCause(pCause); + } + + public DecodeException(final Throwable pCause) { + this(pCause.getMessage(), pCause); + } } \ No newline at end of file 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 75372bad..eae19df0 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 @@ -1,68 +1,69 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.enc; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * Interface for decoders. - * A {@code Decoder} may be used with a {@code DecoderStream}, to perform - * on-the-fly decoding from an {@code InputStream}. - *

- * Important note: Decoder implementations are typically not synchronized. - *

- * @see Encoder - * @see DecoderStream - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Decoder.java#2 $ - */ -public interface Decoder { - - /** - * Decodes up to {@code buffer.length} bytes from the given input stream, - * into the given buffer. - * - * @param stream the input stream to decode data from - * @param buffer buffer to store the read data - * - * @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. - * @throws IOException if an I/O error occurs. - * @throws java.io.EOFException if a premature end-of-file is encountered. - * @throws java.lang.NullPointerException if either argument is {@code null}. - */ - int decode(InputStream stream, ByteBuffer buffer) throws IOException; -} +/* + * 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 of the copyright holder 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 HOLDER 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.enc; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * Interface for decoders. + * A {@code Decoder} may be used with a {@code DecoderStream}, to perform + * on-the-fly decoding from an {@code InputStream}. + *

+ * Important note: Decoder implementations are typically not synchronized. + *

+ * + * @see Encoder + * @see DecoderStream + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Decoder.java#2 $ + */ +public interface Decoder { + + /** + * Decodes up to {@code buffer.length} bytes from the given input stream, + * into the given buffer. + * + * @param stream the input stream to decode data from + * @param buffer buffer to store the read data + * + * @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. + * @throws IOException if an I/O error occurs. + * @throws java.io.EOFException if a premature end-of-file is encountered. + * @throws java.lang.NullPointerException if either argument is {@code null}. + */ + int decode(InputStream stream, ByteBuffer buffer) throws IOException; +} 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 188f8f0f..ec542a46 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 @@ -1,200 +1,199 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.enc; - -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; - -/** - * An {@code InputStream} that provides on-the-fly decoding from an underlying - * stream. - *

- * @see EncoderStream - * @see Decoder - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java#2 $ - */ -public final class DecoderStream extends FilterInputStream { - protected final ByteBuffer buffer; - protected final Decoder decoder; - - /** - * Creates a new decoder stream and chains it to the - * input stream specified by the {@code pStream} argument. - * The stream will use a default decode buffer size. - * - * @param pStream the underlying input stream. - * @param pDecoder the decoder that will be used to decode the underlying stream - * - * @see java.io.FilterInputStream#in - */ - public DecoderStream(final InputStream pStream, final Decoder pDecoder) { - // TODO: Let the decoder decide preferred buffer size - this(pStream, pDecoder, 1024); - } - - /** - * Creates a new decoder stream and chains it to the - * input stream specified by the {@code pStream} argument. - * - * @param pStream the underlying input stream. - * @param pDecoder the decoder that will be used to decode the underlying stream - * @param pBufferSize the size of the decode buffer - * - * @see java.io.FilterInputStream#in - */ - public DecoderStream(final InputStream pStream, final Decoder pDecoder, final int pBufferSize) { - super(pStream); - - decoder = pDecoder; - buffer = ByteBuffer.allocate(pBufferSize); - buffer.flip(); - } - - public int available() throws IOException { - return buffer.remaining(); - } - - public int read() throws IOException { - if (!buffer.hasRemaining()) { - if (fill() < 0) { - return -1; - } - } - - return buffer.get() & 0xff; - } - - public int read(final byte pBytes[], final int pOffset, final int pLength) throws IOException { - if (pBytes == null) { - throw new NullPointerException(); - } - else if ((pOffset < 0) || (pOffset > pBytes.length) || (pLength < 0) || - ((pOffset + pLength) > pBytes.length) || ((pOffset + pLength) < 0)) { - throw new IndexOutOfBoundsException("bytes.length=" + pBytes.length + " offset=" + pOffset + " length=" + pLength); - } - else if (pLength == 0) { - return 0; - } - - // End of file? - if (!buffer.hasRemaining()) { - if (fill() < 0) { - return -1; - } - } - - // Read until we have read pLength bytes, or have reached EOF - int count = 0; - int off = pOffset; - - while (pLength > count) { - if (!buffer.hasRemaining()) { - if (fill() < 0) { - break; - } - } - - // Copy as many bytes as possible - int dstLen = Math.min(pLength - count, buffer.remaining()); - buffer.get(pBytes, off, dstLen); - - // Update offset (rest) - off += dstLen; - - // Increase count - count += dstLen; - } - - return count; - } - - public long skip(final long pLength) throws IOException { - // End of file? - if (!buffer.hasRemaining()) { - if (fill() < 0) { - return 0; // Yes, 0, not -1 - } - } - - // Skip until we have skipped pLength bytes, or have reached EOF - long total = 0; - - while (total < pLength) { - if (!buffer.hasRemaining()) { - if (fill() < 0) { - break; - } - } - - // NOTE: Skipped can never be more than avail, which is an int, so the cast is safe - int skipped = (int) Math.min(pLength - total, buffer.remaining()); - buffer.position(buffer.position() + skipped); - total += skipped; - } - - return total; - } - - /** - * Fills the buffer, by decoding data from the underlying input stream. - * - * @return the number of bytes decoded, or {@code -1} if the end of the - * file is reached - * - * @throws IOException if an I/O error occurs - */ - protected int fill() throws IOException { - buffer.clear(); - int read = decoder.decode(in, buffer); - - // TODO: Enforce this in test case, leave here to aid debugging - if (read > buffer.capacity()) { - throw new AssertionError( - String.format( - "Decode beyond buffer (%d): %d (using %s decoder)", - buffer.capacity(), read, decoder.getClass().getName() - ) - ); - } - - buffer.flip(); - - if (read == 0) { - return -1; - } - - return read; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.enc; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +/** + * An {@code InputStream} that provides on-the-fly decoding from an underlying stream. + * + * @see EncoderStream + * @see Decoder + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java#2 $ + */ +public final class DecoderStream extends FilterInputStream { + protected final ByteBuffer buffer; + protected final Decoder decoder; + + /** + * Creates a new decoder stream and chains it to the + * input stream specified by the {@code pStream} argument. + * The stream will use a default decode buffer size. + * + * @param pStream the underlying input stream. + * @param pDecoder the decoder that will be used to decode the underlying stream + * + * @see java.io.FilterInputStream#in + */ + public DecoderStream(final InputStream pStream, final Decoder pDecoder) { + // TODO: Let the decoder decide preferred buffer size + this(pStream, pDecoder, 1024); + } + + /** + * Creates a new decoder stream and chains it to the + * input stream specified by the {@code pStream} argument. + * + * @param pStream the underlying input stream. + * @param pDecoder the decoder that will be used to decode the underlying stream + * @param pBufferSize the size of the decode buffer + * + * @see java.io.FilterInputStream#in + */ + public DecoderStream(final InputStream pStream, final Decoder pDecoder, final int pBufferSize) { + super(pStream); + + decoder = pDecoder; + buffer = ByteBuffer.allocate(pBufferSize); + buffer.flip(); + } + + public int available() throws IOException { + return buffer.remaining(); + } + + public int read() throws IOException { + if (!buffer.hasRemaining()) { + if (fill() < 0) { + return -1; + } + } + + return buffer.get() & 0xff; + } + + public int read(final byte pBytes[], final int pOffset, final int pLength) throws IOException { + if (pBytes == null) { + throw new NullPointerException(); + } + else if ((pOffset < 0) || (pOffset > pBytes.length) || (pLength < 0) || + ((pOffset + pLength) > pBytes.length) || ((pOffset + pLength) < 0)) { + throw new IndexOutOfBoundsException("bytes.length=" + pBytes.length + " offset=" + pOffset + " length=" + pLength); + } + else if (pLength == 0) { + return 0; + } + + // End of file? + if (!buffer.hasRemaining()) { + if (fill() < 0) { + return -1; + } + } + + // Read until we have read pLength bytes, or have reached EOF + int count = 0; + int off = pOffset; + + while (pLength > count) { + if (!buffer.hasRemaining()) { + if (fill() < 0) { + break; + } + } + + // Copy as many bytes as possible + int dstLen = Math.min(pLength - count, buffer.remaining()); + buffer.get(pBytes, off, dstLen); + + // Update offset (rest) + off += dstLen; + + // Increase count + count += dstLen; + } + + return count; + } + + public long skip(final long pLength) throws IOException { + // End of file? + if (!buffer.hasRemaining()) { + if (fill() < 0) { + return 0; // Yes, 0, not -1 + } + } + + // Skip until we have skipped pLength bytes, or have reached EOF + long total = 0; + + while (total < pLength) { + if (!buffer.hasRemaining()) { + if (fill() < 0) { + break; + } + } + + // NOTE: Skipped can never be more than avail, which is an int, so the cast is safe + int skipped = (int) Math.min(pLength - total, buffer.remaining()); + buffer.position(buffer.position() + skipped); + total += skipped; + } + + return total; + } + + /** + * Fills the buffer, by decoding data from the underlying input stream. + * + * @return the number of bytes decoded, or {@code -1} if the end of the + * file is reached + * + * @throws IOException if an I/O error occurs + */ + protected int fill() throws IOException { + buffer.clear(); + int read = decoder.decode(in, buffer); + + // TODO: Enforce this in test case, leave here to aid debugging + if (read > buffer.capacity()) { + throw new AssertionError( + String.format( + "Decode beyond buffer (%d): %d (using %s decoder)", + buffer.capacity(), read, decoder.getClass().getName() + ) + ); + } + + buffer.flip(); + + if (read == 0) { + return -1; + } + + return read; + } +} 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 42e235e4..c5c8278c 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 @@ -1,65 +1,66 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.enc; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -/** - * Interface for encoders. - * An {@code Encoder} may be used with an {@code EncoderStream}, to perform - * on-the-fly encoding to an {@code OutputStream}. - *

- * Important note: Encoder implementations are typically not synchronized. - * - * @see Decoder - * @see EncoderStream - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Encoder.java#2 $ - */ -public interface Encoder { - - /** - * Encodes up to {@code buffer.remaining()} bytes into the given input stream, - * from the given buffer. - * - * @param stream the output stream to encode data to - * @param buffer buffer to read data from - * - * @throws java.io.IOException if an I/O error occurs - */ - void encode(OutputStream stream, ByteBuffer buffer) throws IOException; - - //TODO: int requiredBufferSize(): -1 == any, otherwise, use this buffer size - // void flush()? -} +/* + * 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 of the copyright holder 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 HOLDER 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.enc; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * Interface for encoders. + * An {@code Encoder} may be used with an {@code EncoderStream}, to perform + * on-the-fly encoding to an {@code OutputStream}. + *

+ * Important note: Encoder implementations are typically not synchronized. + *

+ * + * @see Decoder + * @see EncoderStream + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Encoder.java#2 $ + */ +public interface Encoder { + + /** + * Encodes up to {@code buffer.remaining()} bytes into the given input stream, + * from the given buffer. + * + * @param stream the output stream to encode data to + * @param buffer buffer to read data from + * + * @throws java.io.IOException if an I/O error occurs + */ + void encode(OutputStream stream, ByteBuffer buffer) 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 a22ad5f0..dafceb2e 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 @@ -1,137 +1,136 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.enc; - -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -/** - * An {@code OutputStream} that provides on-the-fly encoding to an underlying - * stream. - *

- * @see DecoderStream - * @see Encoder - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java#2 $ - */ -public final class EncoderStream extends FilterOutputStream { - // TODO: This class need a test case ASAP!!! - - protected final Encoder encoder; - private final boolean flushOnWrite; - - protected final ByteBuffer buffer; - - /** - * Creates an output stream filter built on top of the specified - * underlying output stream. - * - * @param pStream the underlying output stream - * @param pEncoder the encoder to use - */ - public EncoderStream(final OutputStream pStream, final Encoder pEncoder) { - this(pStream, pEncoder, false); - } - - /** - * Creates an output stream filter built on top of the specified - * underlying output stream. - * - * @param pStream the underlying output stream - * @param pEncoder the encoder to use - * @param pFlushOnWrite if {@code true}, calls to the byte-array - * {@code write} methods will automatically flush the buffer. - */ - public EncoderStream(final OutputStream pStream, final Encoder pEncoder, final boolean pFlushOnWrite) { - super(pStream); - - encoder = pEncoder; - flushOnWrite = pFlushOnWrite; - - buffer = ByteBuffer.allocate(1024); - buffer.flip(); - } - - public void close() throws IOException { - flush(); - super.close(); - } - - public void flush() throws IOException { - encodeBuffer(); - super.flush(); - } - - private void encodeBuffer() throws IOException { - if (buffer.position() != 0) { - buffer.flip(); - - // Make sure all remaining data in buffer is written to the stream - encoder.encode(out, buffer); - - // Reset buffer - buffer.clear(); - } - } - - public final void write(final byte[] pBytes) throws IOException { - write(pBytes, 0, pBytes.length); - } - - // TODO: Verify that this works for the general case (it probably won't)... - // TODO: We might need a way to explicitly flush the encoder, or specify - // 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 (!flushOnWrite && pLength < buffer.remaining()) { - // Buffer data - buffer.put(pBytes, pOffset, pLength); - } - else { - // Encode data already in the buffer - encodeBuffer(); - - // Encode rest without buffering - encoder.encode(out, ByteBuffer.wrap(pBytes, pOffset, pLength)); - } - } - - public void write(final int pByte) throws IOException { - if (!buffer.hasRemaining()) { - encodeBuffer(); // Resets bufferPos to 0 - } - - buffer.put((byte) pByte); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.enc; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * An {@code OutputStream} that provides on-the-fly encoding to an underlying stream. + * + * @see DecoderStream + * @see Encoder + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java#2 $ + */ +public final class EncoderStream extends FilterOutputStream { + // TODO: This class need a test case ASAP!!! + + protected final Encoder encoder; + private final boolean flushOnWrite; + + protected final ByteBuffer buffer; + + /** + * Creates an output stream filter built on top of the specified + * underlying output stream. + * + * @param pStream the underlying output stream + * @param pEncoder the encoder to use + */ + public EncoderStream(final OutputStream pStream, final Encoder pEncoder) { + this(pStream, pEncoder, false); + } + + /** + * Creates an output stream filter built on top of the specified + * underlying output stream. + * + * @param pStream the underlying output stream + * @param pEncoder the encoder to use + * @param pFlushOnWrite if {@code true}, calls to the byte-array + * {@code write} methods will automatically flush the buffer. + */ + public EncoderStream(final OutputStream pStream, final Encoder pEncoder, final boolean pFlushOnWrite) { + super(pStream); + + encoder = pEncoder; + flushOnWrite = pFlushOnWrite; + + buffer = ByteBuffer.allocate(1024); + buffer.flip(); + } + + public void close() throws IOException { + flush(); + super.close(); + } + + public void flush() throws IOException { + encodeBuffer(); + super.flush(); + } + + private void encodeBuffer() throws IOException { + if (buffer.position() != 0) { + buffer.flip(); + + // Make sure all remaining data in buffer is written to the stream + encoder.encode(out, buffer); + + // Reset buffer + buffer.clear(); + } + } + + public final void write(final byte[] pBytes) throws IOException { + write(pBytes, 0, pBytes.length); + } + + // TODO: Verify that this works for the general case (it probably won't)... + // TODO: We might need a way to explicitly flush the encoder, or specify + // 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 (!flushOnWrite && pLength < buffer.remaining()) { + // Buffer data + buffer.put(pBytes, pOffset, pLength); + } + else { + // Encode data already in the buffer + encodeBuffer(); + + // Encode rest without buffering + encoder.encode(out, ByteBuffer.wrap(pBytes, pOffset, pLength)); + } + } + + public void write(final int pByte) throws IOException { + if (!buffer.hasRemaining()) { + encodeBuffer(); // Resets bufferPos to 0 + } + + buffer.put((byte) pByte); + } +} 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 37d0bae2..69865e39 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 @@ -37,30 +37,36 @@ import java.nio.ByteBuffer; /** * Decoder implementation for Apple PackBits run-length encoding. - *

- * From Wikipedia, the free encyclopedia
+ *

+ * From Wikipedia, the free encyclopedia + *
* PackBits is a fast, simple compression scheme for run-length encoding of * data. - *

+ *

+ *

* Apple introduced the PackBits format with the release of MacPaint on the * Macintosh computer. This compression scheme is one of the types of * compression that can be used in TIFF-files. - *

+ *

+ *

* A PackBits data stream consists of packets of one byte of header followed by * data. The header is a signed byte; the data can be signed, unsigned, or * packed (such as MacPaint pixels). - *

- * - * - * - *
Header byteData
0 to 127 1 + n literal bytes of data
0 to -127 One byte of data, repeated 1 - n times in - * the decompressed output
-128 No operation
- *

+ *

+ * + * + * + * + * + * + *
PackBits
Header byteData
0 to 127 1 + n literal bytes of data
0 to -127 One byte of data, repeated 1 - n times in the decompressed output
-128 No operation
+ *

* Note that interpreting 0 as positive or negative makes no difference in the * output. Runs of two bytes adjacent to non-runs are typically written as * literal data. - *

- * See Understanding PackBits + *

+ * + * @see Understanding PackBits * * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java#1 $ @@ -80,10 +86,11 @@ 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. + *

* * @param disableNoOp {@code true} if {@code -128} should be treated as a compressed run, and not a no-op */ @@ -93,10 +100,11 @@ 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. + *

* * @param disableNoOp {@code true} if {@code -128} should be treated as a compressed run, and not a no-op */ 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 501e7c57..4bbfc15f 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 @@ -1,132 +1,138 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.enc; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -/** - * Encoder implementation for Apple PackBits run-length encoding. - *

- * From Wikipedia, the free encyclopedia
- * PackBits is a fast, simple compression scheme for run-length encoding of - * data. - *

- * Apple introduced the PackBits format with the release of MacPaint on the - * Macintosh computer. This compression scheme is one of the types of - * compression that can be used in TIFF-files. - *

- * A PackBits data stream consists of packets of one byte of header followed by - * data. The header is a signed byte; the data can be signed, unsigned, or - * packed (such as MacPaint pixels). - *

- * - * - * - *
Header byteData
0 to 127 1 + n literal bytes of data
0 to -127 One byte of data, repeated 1 - n times in - * the decompressed output
-128 No operation
- *

- * Note that interpreting 0 as positive or negative makes no difference in the - * output. Runs of two bytes adjacent to non-runs are typically written as - * literal data. - *

- * See Understanding PackBits - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java#1 $ - */ -public final class PackBitsEncoder implements Encoder { - - final private byte[] buffer = new byte[128]; - - /** - * Creates a {@code PackBitsEncoder}. - */ - public PackBitsEncoder() { - } - - public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException { - encode(stream, buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); - buffer.position(buffer.remaining()); - } - - private void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException { - // NOTE: It's best to encode a 2 byte repeat - // run as a replicate run except when preceded and followed by a - // literal run, in which case it's best to merge the three into one - // literal run. Always encode 3 byte repeats as replicate runs. - // NOTE: Worst case: output = input + (input + 127) / 128 - - int offset = pOffset; - final int max = pOffset + pLength - 1; - final int maxMinus1 = max - 1; - - while (offset <= max) { - // Compressed run - int run = 1; - byte replicate = pBuffer[offset]; - while (run < 127 && offset < max && pBuffer[offset] == pBuffer[offset + 1]) { - offset++; - run++; - } - - if (run > 1) { - offset++; - pStream.write(-(run - 1)); - pStream.write(replicate); - } - - // Literal run - run = 0; - while ((run < 128 && ((offset < max && pBuffer[offset] != pBuffer[offset + 1]) - || (offset < maxMinus1 && pBuffer[offset] != pBuffer[offset + 2])))) { - buffer[run++] = pBuffer[offset++]; - } - - // If last byte, include it in literal run, if space - if (offset == max && run > 0 && run < 128) { - buffer[run++] = pBuffer[offset++]; - } - - if (run > 0) { - pStream.write(run - 1); - pStream.write(buffer, 0, run); - } - - // If last byte, and not space, start new literal run - if (offset == max && (run <= 0 || run >= 128)) { - pStream.write(0); - pStream.write(pBuffer[offset++]); - } - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.enc; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * Encoder implementation for Apple PackBits run-length encoding. + *

+ * From Wikipedia, the free encyclopedia + *
+ * PackBits is a fast, simple compression scheme for run-length encoding of + * data. + *

+ *

+ * Apple introduced the PackBits format with the release of MacPaint on the + * Macintosh computer. This compression scheme is one of the types of + * compression that can be used in TIFF-files. + *

+ *

+ * A PackBits data stream consists of packets of one byte of header followed by + * data. The header is a signed byte; the data can be signed, unsigned, or + * packed (such as MacPaint pixels). + *

+ * + * + * + * + * + * + *
PackBits
Header byteData
0 to 127 1 + n literal bytes of data
0 to -127 One byte of data, repeated 1 - n times in the decompressed output
-128 No operation
+ *

+ * Note that interpreting 0 as positive or negative makes no difference in the + * output. Runs of two bytes adjacent to non-runs are typically written as + * literal data. + *

+ * + * @see Understanding PackBits + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java#1 $ + */ +public final class PackBitsEncoder implements Encoder { + + final private byte[] buffer = new byte[128]; + + /** + * Creates a {@code PackBitsEncoder}. + */ + public PackBitsEncoder() { + } + + public void encode(final OutputStream stream, final ByteBuffer buffer) throws IOException { + encode(stream, buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + buffer.position(buffer.remaining()); + } + + private void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException { + // NOTE: It's best to encode a 2 byte repeat + // run as a replicate run except when preceded and followed by a + // literal run, in which case it's best to merge the three into one + // literal run. Always encode 3 byte repeats as replicate runs. + // NOTE: Worst case: output = input + (input + 127) / 128 + + int offset = pOffset; + final int max = pOffset + pLength - 1; + final int maxMinus1 = max - 1; + + while (offset <= max) { + // Compressed run + int run = 1; + byte replicate = pBuffer[offset]; + while (run < 127 && offset < max && pBuffer[offset] == pBuffer[offset + 1]) { + offset++; + run++; + } + + if (run > 1) { + offset++; + pStream.write(-(run - 1)); + pStream.write(replicate); + } + + // Literal run + run = 0; + while ((run < 128 && ((offset < max && pBuffer[offset] != pBuffer[offset + 1]) + || (offset < maxMinus1 && pBuffer[offset] != pBuffer[offset + 2])))) { + buffer[run++] = pBuffer[offset++]; + } + + // If last byte, include it in literal run, if space + if (offset == max && run > 0 && run < 128) { + buffer[run++] = pBuffer[offset++]; + } + + if (run > 0) { + pStream.write(run - 1); + pStream.write(buffer, 0, run); + } + + // If last byte, and not space, start new literal run + if (offset == max && (run <= 0 || run >= 128)) { + pStream.write(0); + pStream.write(pBuffer[offset++]); + } + } + } +} 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 60cdf7ed..75aeb477 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 @@ -1,798 +1,801 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.lang.StringUtil; - -import javax.imageio.stream.ImageInputStream; -import java.io.*; -import java.nio.ByteOrder; -import java.util.Arrays; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.UUID; - -import static com.twelvemonkeys.lang.Validate.notNull; - -/** - * Represents a read-only OLE2 compound document. - *

- * - * NOTE: This class is not synchronized. Accessing the document or its - * entries from different threads, will need synchronization on the document - * instance. - * - * @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/io/ole2/CompoundDocument.java#4 $ - */ -public final class CompoundDocument implements AutoCloseable { - // TODO: Write support... - // TODO: Properties: http://support.microsoft.com/kb/186898 - - static final byte[] MAGIC = new byte[]{ - (byte) 0xD0, (byte) 0xCF, (byte) 0x11, (byte) 0xE0, - (byte) 0xA1, (byte) 0xB1, (byte) 0x1A, (byte) 0xE1, - }; - - 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}. - *

- * Warning! You must invoke {@link #close()} on the compound document - * created from this constructor when done, to avoid leaking file - * descriptors. - * - * @param file the file to read from - * - * @throws IOException if an I/O exception occurs while reading the header - */ - public CompoundDocument(final File file) throws IOException { - // TODO: We need to close this (or it's underlying RAF)! Otherwise we're leaking file descriptors! - input = new LittleEndianRandomAccessFile(FileUtil.resolve(file), "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 - // sure we're reading a valid document - readHeader(); - } - - /** - * Creates a read only {@code CompoundDocument}. - * - * @param pInput the input to read from. - * - * @throws IOException if an I/O exception occurs while reading the header - */ - public CompoundDocument(final InputStream pInput) throws IOException { - this(new MemoryCacheSeekableStream(pInput)); - } - - // For testing only, consider exposing later - CompoundDocument(final SeekableInputStream stream) throws IOException { - input = new SeekableLittleEndianDataInputStream(stream); - - // 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 - // sure we're reading a valid document - readHeader(); - } - - /** - * Creates a read only {@code CompoundDocument}. - * - * @param input the input to read from - * - * @throws IOException if an I/O exception occurs while reading the header - */ - public CompoundDocument(final ImageInputStream input) throws IOException { - this.input = notNull(input, "input"); - - // This implementation only supports little endian (Intel) CompoundDocuments - input.setByteOrder(ByteOrder.LITTLE_ENDIAN); - - // 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 - // sure we're reading a valid document - readHeader(); - } - - /** - * This method will close the underlying {@link RandomAccessFile} if any, - * but will leave any stream created outside the document open. - * - * @see #CompoundDocument(File) - * @see RandomAccessFile#close() - * - * @throws IOException if an I/O error occurs. - */ - @Override - public void close() throws IOException { - if (input instanceof RandomAccessFile) { - ((RandomAccessFile) input).close(); - } - else if (input instanceof LittleEndianRandomAccessFile) { - ((LittleEndianRandomAccessFile) input).close(); - } - - // Other streams are left open - } - - public static boolean canRead(final DataInput pInput) { - return canRead(pInput, true); - } - - // TODO: Refactor.. Figure out what we really need to expose to ImageIO for - // easy reading of the Thumbs.db file - // It's probably safer to create one version for InputStream and one for File - private static boolean canRead(final DataInput pInput, final boolean pReset) { - long pos = FREE_SID; - if (pReset) { - try { - if (pInput instanceof InputStream && ((InputStream) pInput).markSupported()) { - ((InputStream) pInput).mark(8); - } - else if (pInput instanceof ImageInputStream) { - ((ImageInputStream) pInput).mark(); - } - else if (pInput instanceof RandomAccessFile) { - pos = ((RandomAccessFile) pInput).getFilePointer(); - } - else if (pInput instanceof LittleEndianRandomAccessFile) { - pos = ((LittleEndianRandomAccessFile) pInput).getFilePointer(); - } - else { - return false; - } - } - catch (IOException ignore) { - return false; - } - } - - try { - byte[] magic = new byte[8]; - pInput.readFully(magic); - return Arrays.equals(magic, MAGIC); - } - catch (IOException ignore) { - // Ignore - } - finally { - if (pReset) { - try { - if (pInput instanceof InputStream && ((InputStream) pInput).markSupported()) { - ((InputStream) pInput).reset(); - } - else if (pInput instanceof ImageInputStream) { - ((ImageInputStream) pInput).reset(); - } - else if (pInput instanceof RandomAccessFile) { - ((RandomAccessFile) pInput).seek(pos); - } - else if (pInput instanceof LittleEndianRandomAccessFile) { - ((LittleEndianRandomAccessFile) pInput).seek(pos); - } - } - catch (IOException e) { - // TODO: This isn't actually good enough... - // Means something fucked up, and will fail... - e.printStackTrace(); - } - } - } - - return false; - } - - private void readHeader() throws IOException { - if (masterSAT != null) { - return; - } - - if (!canRead(input, false)) { - throw new CorruptDocumentException("Not an OLE 2 Compound Document"); - } - - // UID (seems to be all 0s) - uUID = new UUID(input.readLong(), input.readLong()); -// System.out.println("uUID: " + uUID); - - // int version = - input.readUnsignedShort(); -// System.out.println("version: " + version); - // int revision = - input.readUnsignedShort(); -// System.out.println("revision: " + revision); - - 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)); - } - - sectorSize = 1 << input.readUnsignedShort(); -// System.out.println("sectorSize: " + sectorSize + " bytes"); - shortSectorSize = 1 << input.readUnsignedShort(); -// System.out.println("shortSectorSize: " + shortSectorSize + " bytes"); - - // Reserved - if (skipBytesFully(10) != 10) { - throw new CorruptDocumentException(); - } - - int SATSize = input.readInt(); -// System.out.println("normalSATSize: " + SATSize); - - directorySId = input.readInt(); -// System.out.println("directorySId: " + directorySId); - - // Reserved - if (skipBytesFully(4) != 4) { - throw new CorruptDocumentException(); - } - - minStreamSize = input.readInt(); -// System.out.println("minStreamSize: " + minStreamSize + " bytes"); - - 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:"); - masterSAT = new int[SATSize]; - final int headerSIds = Math.min(SATSize, 109); - for (int i = 0; i < headerSIds; 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 (skipBytesFully(freeSIdLength) != freeSIdLength) { - throw new CorruptDocumentException(); - } - } - else { - // Parse the SIDs in the extended MasterSAT sectors... - seekToSId(masterSATSId, FREE_SID); - - int index = headerSIds; - for (int i = 0; i < masterSATSize; i++) { - for (int j = 0; j < 127; j++) { - int sid = input.readInt(); - switch (sid) { - case FREE_SID:// Free - break; - default: - masterSAT[index++] = sid; - break; - } - } - - int next = input.readInt(); - if (next == END_OF_CHAIN_SID) {// End of chain - break; - } - - seekToSId(next, FREE_SID); - } - } - } - - 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 (SAT != null) { - return; - } - - final int intsPerSector = sectorSize / 4; - - // Read the Sector Allocation Table - SAT = new int[masterSAT.length * intsPerSector]; - - for (int i = 0; i < masterSAT.length; i++) { - seekToSId(masterSAT[i], FREE_SID); - - for (int j = 0; j < intsPerSector; j++) { - int nextSID = input.readInt(); - int index = (j + (i * intsPerSector)); - - SAT[index] = nextSID; - } - } - - // Read the short-stream Sector Allocation Table - 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 = input.readInt(); - int index = (j + (i * intsPerSector)); - - shortSAT[index] = nextSID; - } - } - } - - /** - * Gets the SIdChain for the given stream Id - * - * @param pSId the stream Id - * @param pStreamSize the size of the stream, or -1 for system control streams - * @return the SIdChain for the given stream Id - * @throws IOException if an I/O exception occurs - */ - private SIdChain getSIdChain(final int pSId, final long pStreamSize) throws IOException { - SIdChain chain = new SIdChain(); - - int[] sat = isShortStream(pStreamSize) ? shortSAT : SAT; - - int sid = pSId; - while (sid != END_OF_CHAIN_SID && sid != FREE_SID) { - chain.addSID(sid); - sid = sat[sid]; - } - - return chain; - } - - private boolean isShortStream(final long pStreamSize) { - return pStreamSize != FREE_SID && pStreamSize < minStreamSize; - } - - /** - * Seeks to the start pos for the given stream Id - * - * @param pSId the stream Id - * @param pStreamSize the size of the stream, or -1 for system control streams - * @throws IOException if an I/O exception occurs - */ - private void seekToSId(final int pSId, final long pStreamSize) throws IOException { - long pos; - - if (isShortStream(pStreamSize)) { - // The short stream is not continuous... - Entry root = getRootEntry(); - if (shortStreamSIdChain == null) { - shortStreamSIdChain = getSIdChain(root.startSId, root.streamSize); - } - -// 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 - + (shortStreamSIdChain.get(offset) * (long) sectorSize) - + (shortOffset * (long) shortSectorSize); -// System.err.println("pos: " + pos); - } - else { - pos = HEADER_SIZE + pSId * (long) sectorSize; - } - - if (input instanceof LittleEndianRandomAccessFile) { - ((LittleEndianRandomAccessFile) input).seek(pos); - } - else if (input instanceof ImageInputStream) { - ((ImageInputStream) input).seek(pos); - } - else { - ((SeekableLittleEndianDataInputStream) input).seek(pos); - } - } - - private void seekToDId(final int pDId) throws IOException { - if (directorySIdChain == null) { - directorySIdChain = getSIdChain(directorySId, FREE_SID); - } - - int dIdsPerSId = sectorSize / Entry.LENGTH; - - int sIdOffset = pDId / dIdsPerSId; - int dIdOffset = pDId - (sIdOffset * dIdsPerSId); - - int sId = directorySIdChain.get(sIdOffset); - - seekToSId(sId, FREE_SID); - if (input instanceof LittleEndianRandomAccessFile) { - LittleEndianRandomAccessFile input = (LittleEndianRandomAccessFile) this.input; - input.seek(input.getFilePointer() + dIdOffset * Entry.LENGTH); - } - else if (input instanceof ImageInputStream) { - ImageInputStream input = (ImageInputStream) this.input; - input.seek(input.getStreamPosition() + dIdOffset * Entry.LENGTH); - } - else { - SeekableLittleEndianDataInputStream input = (SeekableLittleEndianDataInputStream) this.input; - input.seek(input.getStreamPosition() + dIdOffset * Entry.LENGTH); - } - } - - 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 < minStreamSize ? shortSectorSize : this.sectorSize; - - return new MemoryCacheSeekableStream(new Stream(chain, pStreamSize, sectorSize, this)); - } - - private InputStream getDirectoryStreamForDId(final int pDirectoryId) throws IOException { - // This is always exactly 128 bytes, so we'll just read it all, - // and buffer (we might want to optimize this later). - byte[] bytes = new byte[Entry.LENGTH]; - - seekToDId(pDirectoryId); - input.readFully(bytes); - - return new ByteArrayInputStream(bytes); - } - - Entry getEntry(final int pDirectoryId, Entry pParent) throws IOException { - Entry entry = Entry.readEntry(new LittleEndianDataInputStream( - getDirectoryStreamForDId(pDirectoryId) - )); - entry.parent = pParent; - entry.document = this; - return entry; - } - - SortedSet getEntries(final int pDirectoryId, final Entry pParent) - throws IOException { - return getEntriesRecursive(pDirectoryId, pParent, new TreeSet()); - } - - private SortedSet getEntriesRecursive(final int pDirectoryId, final Entry pParent, final SortedSet pEntries) - throws IOException { - - //System.out.println("pDirectoryId: " + pDirectoryId); - - Entry entry = getEntry(pDirectoryId, pParent); - - //System.out.println("entry: " + entry); - - if (!pEntries.add(entry)) { - // TODO: This occurs in some Thumbs.db files, and Windows will - // still parse the file gracefully somehow... - // Deleting and regenerating the file will remove the cyclic - // references, but... How can Windows parse this file? - throw new CorruptDocumentException("Cyclic chain reference for entry: " + pDirectoryId); - } - - if (entry.prevDId != FREE_SID) { - //System.out.println("prevDId: " + entry.prevDId); - getEntriesRecursive(entry.prevDId, pParent, pEntries); - } - if (entry.nextDId != FREE_SID) { - //System.out.println("nextDId: " + entry.nextDId); - getEntriesRecursive(entry.nextDId, pParent, pEntries); - } - - return pEntries; - } - - /*public*/ Entry getEntry(String pPath) throws IOException { - if (StringUtil.isEmpty(pPath) || !pPath.startsWith("/")) { - throw new IllegalArgumentException("Path must be absolute, and contain a valid path: " + pPath); - } - - Entry entry = getRootEntry(); - if (pPath.equals("/")) { - // '/' means root entry - return entry; - } - else { - // Otherwise get children recursively: - String[] pathElements = StringUtil.toStringArray(pPath, "/"); - for (String pathElement : pathElements) { - entry = entry.getChildEntry(pathElement); - - // No such child... - if (entry == null) { - break;// TODO: FileNotFoundException? Should behave like Entry.getChildEntry!! - } - } - return entry; - } - } - - public Entry getRootEntry() throws IOException { - if (rootEntry == null) { - readSAT(); - - rootEntry = getEntry(0, null); - - if (rootEntry.type != Entry.ROOT_STORAGE) { - throw new CorruptDocumentException("Invalid root storage type: " + rootEntry.type); - } - } - - return rootEntry; - } - - // This is useless, as most documents on file have all-zero UUIDs... -// @Override -// public int hashCode() { -// return uUID.hashCode(); -// } -// -// @Override -// public boolean equals(final Object pOther) { -// if (pOther == this) { -// return true; -// } -// -// if (pOther == null) { -// return true; -// } -// -// if (pOther.getClass() == getClass()) { -// return uUID.equals(((CompoundDocument) pOther).uUID); -// } -// -// return false; -// } - - @Override - public String toString() { - return String.format( - "%s[uuid: %s, sector size: %d/%d bytes, directory SID: %d, master SAT: %s entries]", - getClass().getSimpleName(), uUID, sectorSize, shortSectorSize, directorySId, masterSAT.length - ); - } - - /** - * Converts the given time stamp to standard Java time representation, - * milliseconds since January 1, 1970. - * The time stamp parameter is assumed to be in units of - * 100 nano seconds since January 1, 1601. - *

- * If the timestamp is {@code 0L} (meaning not specified), no conversion - * is done, to behave like {@code java.io.File}. - * - * @param pMSTime an unsigned long value representing the time stamp (in - * units of 100 nano seconds since January 1, 1601). - * - * @return the time stamp converted to Java time stamp in milliseconds, - * or {@code 0L} if {@code pMSTime == 0L} - */ - public static long toJavaTimeInMillis(final long pMSTime) { - // NOTE: The time stamp field is an unsigned 64-bit integer value that - // contains the time elapsed since 1601-Jan-01 00:00:00 (Gregorian - // calendar). - // One unit of this value is equal to 100 nanoseconds). - // That means, each second the time stamp value will be increased by - // 10 million units. - - if (pMSTime == 0L) { - return 0L; // This is just less confusing... - } - - // Convert to milliseconds (signed), - // then convert to Java std epoch (1970-Jan-01 00:00:00) - return ((pMSTime >> 1) / 5000) + EPOCH_OFFSET; - } - - static class Stream extends InputStream { - private final SIdChain chain; - private final CompoundDocument document; - private final long length; - - private long streamPos; - private int nextSectorPos; - private byte[] buffer; - private int bufferPos; - - public Stream(SIdChain chain, int streamSize, int sectorSize, CompoundDocument document) { - this.chain = chain; - this.length = streamSize; - - this.buffer = new byte[sectorSize]; - this.bufferPos = buffer.length; - - this.document = document; - } - - @Override - public int available() throws IOException { - return (int) Math.min(buffer.length - bufferPos, length - streamPos); - } - - public int read() throws IOException { - if (available() <= 0) { - if (!fillBuffer()) { - return -1; - } - } - - streamPos++; - - return buffer[bufferPos++] & 0xff; - } - - private boolean fillBuffer() throws IOException { - 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); - } - - nextSectorPos++; - bufferPos = 0; - - return true; - } - - return false; - } - - @Override - public int read(byte b[], int off, int len) throws IOException { - if (available() <= 0) { - if (!fillBuffer()) { - return -1; - } - } - - int toRead = Math.min(len, available()); - - System.arraycopy(buffer, bufferPos, b, off, toRead); - bufferPos += toRead; - streamPos += toRead; - - return toRead; - } - - @Override - public void close() throws IOException { - buffer = null; - } - } - - static class SeekableLittleEndianDataInputStream extends LittleEndianDataInputStream implements Seekable { - private final SeekableInputStream seekable; - - public SeekableLittleEndianDataInputStream(final SeekableInputStream pInput) { - super(pInput); - seekable = pInput; - } - - public void seek(final long pPosition) throws IOException { - seekable.seek(pPosition); - } - - public boolean isCachedFile() { - return seekable.isCachedFile(); - } - - public boolean isCachedMemory() { - return seekable.isCachedMemory(); - } - - public boolean isCached() { - return seekable.isCached(); - } - - public long getStreamPosition() throws IOException { - return seekable.getStreamPosition(); - } - - public long getFlushedPosition() throws IOException { - return seekable.getFlushedPosition(); - } - - public void flushBefore(final long pPosition) throws IOException { - seekable.flushBefore(pPosition); - } - - public void flush() throws IOException { - seekable.flush(); - } - - @Override - public void reset() throws IOException { - seekable.reset(); - } - - public void mark() { - seekable.mark(); - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.lang.StringUtil; + +import javax.imageio.stream.ImageInputStream; +import java.io.*; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.UUID; + +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * Represents a read-only OLE2 compound document. + *

+ * + * NOTE: This class is not synchronized. Accessing the document or its + * entries from different threads, will need synchronization on the document + * instance. + *

+ * + * @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/io/ole2/CompoundDocument.java#4 $ + */ +public final class CompoundDocument implements AutoCloseable { + // TODO: Write support... + // TODO: Properties: http://support.microsoft.com/kb/186898 + + static final byte[] MAGIC = new byte[]{ + (byte) 0xD0, (byte) 0xCF, (byte) 0x11, (byte) 0xE0, + (byte) 0xA1, (byte) 0xB1, (byte) 0x1A, (byte) 0xE1, + }; + + 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}. + *

+ * Warning! You must invoke {@link #close()} on the compound document + * created from this constructor when done, to avoid leaking file + * descriptors. + *

+ * + * @param file the file to read from + * + * @throws IOException if an I/O exception occurs while reading the header + */ + public CompoundDocument(final File file) throws IOException { + // TODO: We need to close this (or it's underlying RAF)! Otherwise we're leaking file descriptors! + input = new LittleEndianRandomAccessFile(FileUtil.resolve(file), "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 + // sure we're reading a valid document + readHeader(); + } + + /** + * Creates a read only {@code CompoundDocument}. + * + * @param pInput the input to read from. + * + * @throws IOException if an I/O exception occurs while reading the header + */ + public CompoundDocument(final InputStream pInput) throws IOException { + this(new MemoryCacheSeekableStream(pInput)); + } + + // For testing only, consider exposing later + CompoundDocument(final SeekableInputStream stream) throws IOException { + input = new SeekableLittleEndianDataInputStream(stream); + + // 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 + // sure we're reading a valid document + readHeader(); + } + + /** + * Creates a read only {@code CompoundDocument}. + * + * @param input the input to read from + * + * @throws IOException if an I/O exception occurs while reading the header + */ + public CompoundDocument(final ImageInputStream input) throws IOException { + this.input = notNull(input, "input"); + + // This implementation only supports little endian (Intel) CompoundDocuments + input.setByteOrder(ByteOrder.LITTLE_ENDIAN); + + // 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 + // sure we're reading a valid document + readHeader(); + } + + /** + * This method will close the underlying {@link RandomAccessFile} if any, + * but will leave any stream created outside the document open. + * + * @see #CompoundDocument(File) + * @see RandomAccessFile#close() + * + * @throws IOException if an I/O error occurs. + */ + @Override + public void close() throws IOException { + if (input instanceof RandomAccessFile) { + ((RandomAccessFile) input).close(); + } + else if (input instanceof LittleEndianRandomAccessFile) { + ((LittleEndianRandomAccessFile) input).close(); + } + + // Other streams are left open + } + + public static boolean canRead(final DataInput pInput) { + return canRead(pInput, true); + } + + // TODO: Refactor.. Figure out what we really need to expose to ImageIO for + // easy reading of the Thumbs.db file + // It's probably safer to create one version for InputStream and one for File + private static boolean canRead(final DataInput pInput, final boolean pReset) { + long pos = FREE_SID; + if (pReset) { + try { + if (pInput instanceof InputStream && ((InputStream) pInput).markSupported()) { + ((InputStream) pInput).mark(8); + } + else if (pInput instanceof ImageInputStream) { + ((ImageInputStream) pInput).mark(); + } + else if (pInput instanceof RandomAccessFile) { + pos = ((RandomAccessFile) pInput).getFilePointer(); + } + else if (pInput instanceof LittleEndianRandomAccessFile) { + pos = ((LittleEndianRandomAccessFile) pInput).getFilePointer(); + } + else { + return false; + } + } + catch (IOException ignore) { + return false; + } + } + + try { + byte[] magic = new byte[8]; + pInput.readFully(magic); + return Arrays.equals(magic, MAGIC); + } + catch (IOException ignore) { + // Ignore + } + finally { + if (pReset) { + try { + if (pInput instanceof InputStream && ((InputStream) pInput).markSupported()) { + ((InputStream) pInput).reset(); + } + else if (pInput instanceof ImageInputStream) { + ((ImageInputStream) pInput).reset(); + } + else if (pInput instanceof RandomAccessFile) { + ((RandomAccessFile) pInput).seek(pos); + } + else if (pInput instanceof LittleEndianRandomAccessFile) { + ((LittleEndianRandomAccessFile) pInput).seek(pos); + } + } + catch (IOException e) { + // TODO: This isn't actually good enough... + // Means something fucked up, and will fail... + e.printStackTrace(); + } + } + } + + return false; + } + + private void readHeader() throws IOException { + if (masterSAT != null) { + return; + } + + if (!canRead(input, false)) { + throw new CorruptDocumentException("Not an OLE 2 Compound Document"); + } + + // UID (seems to be all 0s) + uUID = new UUID(input.readLong(), input.readLong()); +// System.out.println("uUID: " + uUID); + + // int version = + input.readUnsignedShort(); +// System.out.println("version: " + version); + // int revision = + input.readUnsignedShort(); +// System.out.println("revision: " + revision); + + 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)); + } + + sectorSize = 1 << input.readUnsignedShort(); +// System.out.println("sectorSize: " + sectorSize + " bytes"); + shortSectorSize = 1 << input.readUnsignedShort(); +// System.out.println("shortSectorSize: " + shortSectorSize + " bytes"); + + // Reserved + if (skipBytesFully(10) != 10) { + throw new CorruptDocumentException(); + } + + int SATSize = input.readInt(); +// System.out.println("normalSATSize: " + SATSize); + + directorySId = input.readInt(); +// System.out.println("directorySId: " + directorySId); + + // Reserved + if (skipBytesFully(4) != 4) { + throw new CorruptDocumentException(); + } + + minStreamSize = input.readInt(); +// System.out.println("minStreamSize: " + minStreamSize + " bytes"); + + 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:"); + masterSAT = new int[SATSize]; + final int headerSIds = Math.min(SATSize, 109); + for (int i = 0; i < headerSIds; 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 (skipBytesFully(freeSIdLength) != freeSIdLength) { + throw new CorruptDocumentException(); + } + } + else { + // Parse the SIDs in the extended MasterSAT sectors... + seekToSId(masterSATSId, FREE_SID); + + int index = headerSIds; + for (int i = 0; i < masterSATSize; i++) { + for (int j = 0; j < 127; j++) { + int sid = input.readInt(); + switch (sid) { + case FREE_SID:// Free + break; + default: + masterSAT[index++] = sid; + break; + } + } + + int next = input.readInt(); + if (next == END_OF_CHAIN_SID) {// End of chain + break; + } + + seekToSId(next, FREE_SID); + } + } + } + + 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 (SAT != null) { + return; + } + + final int intsPerSector = sectorSize / 4; + + // Read the Sector Allocation Table + SAT = new int[masterSAT.length * intsPerSector]; + + for (int i = 0; i < masterSAT.length; i++) { + seekToSId(masterSAT[i], FREE_SID); + + for (int j = 0; j < intsPerSector; j++) { + int nextSID = input.readInt(); + int index = (j + (i * intsPerSector)); + + SAT[index] = nextSID; + } + } + + // Read the short-stream Sector Allocation Table + 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 = input.readInt(); + int index = (j + (i * intsPerSector)); + + shortSAT[index] = nextSID; + } + } + } + + /** + * Gets the SIdChain for the given stream Id + * + * @param pSId the stream Id + * @param pStreamSize the size of the stream, or -1 for system control streams + * @return the SIdChain for the given stream Id + * @throws IOException if an I/O exception occurs + */ + private SIdChain getSIdChain(final int pSId, final long pStreamSize) throws IOException { + SIdChain chain = new SIdChain(); + + int[] sat = isShortStream(pStreamSize) ? shortSAT : SAT; + + int sid = pSId; + while (sid != END_OF_CHAIN_SID && sid != FREE_SID) { + chain.addSID(sid); + sid = sat[sid]; + } + + return chain; + } + + private boolean isShortStream(final long pStreamSize) { + return pStreamSize != FREE_SID && pStreamSize < minStreamSize; + } + + /** + * Seeks to the start pos for the given stream Id + * + * @param pSId the stream Id + * @param pStreamSize the size of the stream, or -1 for system control streams + * @throws IOException if an I/O exception occurs + */ + private void seekToSId(final int pSId, final long pStreamSize) throws IOException { + long pos; + + if (isShortStream(pStreamSize)) { + // The short stream is not continuous... + Entry root = getRootEntry(); + if (shortStreamSIdChain == null) { + shortStreamSIdChain = getSIdChain(root.startSId, root.streamSize); + } + +// 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 + + (shortStreamSIdChain.get(offset) * (long) sectorSize) + + (shortOffset * (long) shortSectorSize); +// System.err.println("pos: " + pos); + } + else { + pos = HEADER_SIZE + pSId * (long) sectorSize; + } + + if (input instanceof LittleEndianRandomAccessFile) { + ((LittleEndianRandomAccessFile) input).seek(pos); + } + else if (input instanceof ImageInputStream) { + ((ImageInputStream) input).seek(pos); + } + else { + ((SeekableLittleEndianDataInputStream) input).seek(pos); + } + } + + private void seekToDId(final int pDId) throws IOException { + if (directorySIdChain == null) { + directorySIdChain = getSIdChain(directorySId, FREE_SID); + } + + int dIdsPerSId = sectorSize / Entry.LENGTH; + + int sIdOffset = pDId / dIdsPerSId; + int dIdOffset = pDId - (sIdOffset * dIdsPerSId); + + int sId = directorySIdChain.get(sIdOffset); + + seekToSId(sId, FREE_SID); + if (input instanceof LittleEndianRandomAccessFile) { + LittleEndianRandomAccessFile input = (LittleEndianRandomAccessFile) this.input; + input.seek(input.getFilePointer() + dIdOffset * Entry.LENGTH); + } + else if (input instanceof ImageInputStream) { + ImageInputStream input = (ImageInputStream) this.input; + input.seek(input.getStreamPosition() + dIdOffset * Entry.LENGTH); + } + else { + SeekableLittleEndianDataInputStream input = (SeekableLittleEndianDataInputStream) this.input; + input.seek(input.getStreamPosition() + dIdOffset * Entry.LENGTH); + } + } + + 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 < minStreamSize ? shortSectorSize : this.sectorSize; + + return new MemoryCacheSeekableStream(new Stream(chain, pStreamSize, sectorSize, this)); + } + + private InputStream getDirectoryStreamForDId(final int pDirectoryId) throws IOException { + // This is always exactly 128 bytes, so we'll just read it all, + // and buffer (we might want to optimize this later). + byte[] bytes = new byte[Entry.LENGTH]; + + seekToDId(pDirectoryId); + input.readFully(bytes); + + return new ByteArrayInputStream(bytes); + } + + Entry getEntry(final int pDirectoryId, Entry pParent) throws IOException { + Entry entry = Entry.readEntry(new LittleEndianDataInputStream( + getDirectoryStreamForDId(pDirectoryId) + )); + entry.parent = pParent; + entry.document = this; + return entry; + } + + SortedSet getEntries(final int pDirectoryId, final Entry pParent) + throws IOException { + return getEntriesRecursive(pDirectoryId, pParent, new TreeSet()); + } + + private SortedSet getEntriesRecursive(final int pDirectoryId, final Entry pParent, final SortedSet pEntries) + throws IOException { + + //System.out.println("pDirectoryId: " + pDirectoryId); + + Entry entry = getEntry(pDirectoryId, pParent); + + //System.out.println("entry: " + entry); + + if (!pEntries.add(entry)) { + // TODO: This occurs in some Thumbs.db files, and Windows will + // still parse the file gracefully somehow... + // Deleting and regenerating the file will remove the cyclic + // references, but... How can Windows parse this file? + throw new CorruptDocumentException("Cyclic chain reference for entry: " + pDirectoryId); + } + + if (entry.prevDId != FREE_SID) { + //System.out.println("prevDId: " + entry.prevDId); + getEntriesRecursive(entry.prevDId, pParent, pEntries); + } + if (entry.nextDId != FREE_SID) { + //System.out.println("nextDId: " + entry.nextDId); + getEntriesRecursive(entry.nextDId, pParent, pEntries); + } + + return pEntries; + } + + /*public*/ Entry getEntry(String pPath) throws IOException { + if (StringUtil.isEmpty(pPath) || !pPath.startsWith("/")) { + throw new IllegalArgumentException("Path must be absolute, and contain a valid path: " + pPath); + } + + Entry entry = getRootEntry(); + if (pPath.equals("/")) { + // '/' means root entry + return entry; + } + else { + // Otherwise get children recursively: + String[] pathElements = StringUtil.toStringArray(pPath, "/"); + for (String pathElement : pathElements) { + entry = entry.getChildEntry(pathElement); + + // No such child... + if (entry == null) { + break;// TODO: FileNotFoundException? Should behave like Entry.getChildEntry!! + } + } + return entry; + } + } + + public Entry getRootEntry() throws IOException { + if (rootEntry == null) { + readSAT(); + + rootEntry = getEntry(0, null); + + if (rootEntry.type != Entry.ROOT_STORAGE) { + throw new CorruptDocumentException("Invalid root storage type: " + rootEntry.type); + } + } + + return rootEntry; + } + + // This is useless, as most documents on file have all-zero UUIDs... +// @Override +// public int hashCode() { +// return uUID.hashCode(); +// } +// +// @Override +// public boolean equals(final Object pOther) { +// if (pOther == this) { +// return true; +// } +// +// if (pOther == null) { +// return true; +// } +// +// if (pOther.getClass() == getClass()) { +// return uUID.equals(((CompoundDocument) pOther).uUID); +// } +// +// return false; +// } + + @Override + public String toString() { + return String.format( + "%s[uuid: %s, sector size: %d/%d bytes, directory SID: %d, master SAT: %s entries]", + getClass().getSimpleName(), uUID, sectorSize, shortSectorSize, directorySId, masterSAT.length + ); + } + + /** + * Converts the given time stamp to standard Java time representation, + * milliseconds since January 1, 1970. + * The time stamp parameter is assumed to be in units of + * 100 nano seconds since January 1, 1601. + *

+ * If the timestamp is {@code 0L} (meaning not specified), no conversion + * is done, to behave like {@code java.io.File}. + *

+ * + * @param pMSTime an unsigned long value representing the time stamp (in + * units of 100 nano seconds since January 1, 1601). + * + * @return the time stamp converted to Java time stamp in milliseconds, + * or {@code 0L} if {@code pMSTime == 0L} + */ + public static long toJavaTimeInMillis(final long pMSTime) { + // NOTE: The time stamp field is an unsigned 64-bit integer value that + // contains the time elapsed since 1601-Jan-01 00:00:00 (Gregorian + // calendar). + // One unit of this value is equal to 100 nanoseconds). + // That means, each second the time stamp value will be increased by + // 10 million units. + + if (pMSTime == 0L) { + return 0L; // This is just less confusing... + } + + // Convert to milliseconds (signed), + // then convert to Java std epoch (1970-Jan-01 00:00:00) + return ((pMSTime >> 1) / 5000) + EPOCH_OFFSET; + } + + static class Stream extends InputStream { + private final SIdChain chain; + private final CompoundDocument document; + private final long length; + + private long streamPos; + private int nextSectorPos; + private byte[] buffer; + private int bufferPos; + + public Stream(SIdChain chain, int streamSize, int sectorSize, CompoundDocument document) { + this.chain = chain; + this.length = streamSize; + + this.buffer = new byte[sectorSize]; + this.bufferPos = buffer.length; + + this.document = document; + } + + @Override + public int available() throws IOException { + return (int) Math.min(buffer.length - bufferPos, length - streamPos); + } + + public int read() throws IOException { + if (available() <= 0) { + if (!fillBuffer()) { + return -1; + } + } + + streamPos++; + + return buffer[bufferPos++] & 0xff; + } + + private boolean fillBuffer() throws IOException { + 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); + } + + nextSectorPos++; + bufferPos = 0; + + return true; + } + + return false; + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + if (available() <= 0) { + if (!fillBuffer()) { + return -1; + } + } + + int toRead = Math.min(len, available()); + + System.arraycopy(buffer, bufferPos, b, off, toRead); + bufferPos += toRead; + streamPos += toRead; + + return toRead; + } + + @Override + public void close() throws IOException { + buffer = null; + } + } + + static class SeekableLittleEndianDataInputStream extends LittleEndianDataInputStream implements Seekable { + private final SeekableInputStream seekable; + + public SeekableLittleEndianDataInputStream(final SeekableInputStream pInput) { + super(pInput); + seekable = pInput; + } + + public void seek(final long pPosition) throws IOException { + seekable.seek(pPosition); + } + + public boolean isCachedFile() { + return seekable.isCachedFile(); + } + + public boolean isCachedMemory() { + return seekable.isCachedMemory(); + } + + public boolean isCached() { + return seekable.isCached(); + } + + public long getStreamPosition() throws IOException { + return seekable.getStreamPosition(); + } + + public long getFlushedPosition() throws IOException { + return seekable.getFlushedPosition(); + } + + public void flushBefore(final long pPosition) throws IOException { + seekable.flushBefore(pPosition); + } + + public void flush() throws IOException { + seekable.flush(); + } + + @Override + public void reset() throws IOException { + seekable.reset(); + } + + public void 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 c7951ff7..1364c7cc 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 @@ -1,342 +1,344 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.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; - -/** - * Represents an OLE 2 compound document entry. - * This is similar to a file in a file system, or an entry in a ZIP or JAR file. - * - * @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/io/ole2/Entry.java#4 $ - * @see com.twelvemonkeys.io.ole2.CompoundDocument - */ -// TODO: Consider extending java.io.File... -public final class Entry implements Comparable { - String name; - byte type; - byte nodeColor; - - int prevDId; - int nextDId; - int rootNodeDId; - - long createdTimestamp; - long modifiedTimestamp; - - int startSId; - int streamSize; - - CompoundDocument document; - Entry parent; - SortedSet children; - - public final static int LENGTH = 128; - - static final int EMPTY = 0; - static final int USER_STORAGE = 1; - static final int USER_STREAM = 2; - static final int LOCK_BYTES = 3; - static final int PROPERTY = 4; - static final int ROOT_STORAGE = 5; - - private static final SortedSet NO_CHILDREN = Collections.unmodifiableSortedSet(new TreeSet()); - - private Entry() { - } - - /** - * Reads an entry from the input. - * - * @param pInput the input data - * @return the {@code Entry} read from the input data - * @throws IOException if an i/o exception occurs during reading - */ - static Entry readEntry(final DataInput pInput) throws IOException { - Entry p = new Entry(); - p.read(pInput); - return p; - } - - /** - * Reads this entry - * - * @param pInput the input data - * @throws IOException if an i/o exception occurs during reading - */ - private void read(final DataInput pInput) throws IOException { - byte[] bytes = new byte[64]; - pInput.readFully(bytes); - - // NOTE: Length is in bytes, including the null-terminator... - int nameLength = pInput.readShort(); - name = new String(bytes, 0, nameLength - 2, Charset.forName("UTF-16LE")); -// System.out.println("name: " + name); - - type = pInput.readByte(); -// System.out.println("type: " + type); - - nodeColor = pInput.readByte(); -// System.out.println("nodeColor: " + nodeColor); - - prevDId = pInput.readInt(); -// System.out.println("prevDId: " + prevDId); - nextDId = pInput.readInt(); -// System.out.println("nextDId: " + nextDId); - rootNodeDId = pInput.readInt(); -// System.out.println("rootNodeDId: " + rootNodeDId); - - // UID (16) + user flags (4), ignored - if (pInput.skipBytes(20) != 20) { - throw new CorruptDocumentException(); - } - - createdTimestamp = CompoundDocument.toJavaTimeInMillis(pInput.readLong()); - modifiedTimestamp = CompoundDocument.toJavaTimeInMillis(pInput.readLong()); - - startSId = pInput.readInt(); -// System.out.println("startSId: " + startSId); - streamSize = pInput.readInt(); -// System.out.println("streamSize: " + streamSize); - - // Reserved - pInput.readInt(); - } - - /** - * If {@code true} this {@code Entry} is the root {@code Entry}. - * - * @return {@code true} if this is the root {@code Entry} - */ - public boolean isRoot() { - return type == ROOT_STORAGE; - } - - /** - * If {@code true} this {@code Entry} is a directory - * {@code Entry}. - * - * @return {@code true} if this is a directory {@code Entry} - */ - public boolean isDirectory() { - return type == USER_STORAGE; - } - - /** - * If {@code true} this {@code Entry} is a file (document) - * {@code Entry}. - * - * @return {@code true} if this is a document {@code Entry} - */ - public boolean isFile() { - return type == USER_STREAM; - } - - /** - * Returns the name of this {@code Entry} - * - * @return the name of this {@code Entry} - */ - public String getName() { - return name; - } - - /** - * Returns the {@code InputStream} for this {@code Entry} - * - * @return an {@code InputStream} containing the data for this - * {@code Entry} or {@code null} if this is a directory {@code Entry} - * @throws java.io.IOException if an I/O exception occurs - * @see #length() - */ - public SeekableInputStream getInputStream() throws IOException { - if (!isFile()) { - return null; - } - - return document.getInputStreamForSId(startSId, streamSize); - } - - /** - * Returns the length of this entry - * - * @return the length of the stream for this entry, or {@code 0} if this is - * a directory {@code Entry} - * @see #getInputStream() - */ - public long length() { - if (!isFile()) { - return 0L; - } - - return streamSize; - } - - /** - * Returns the time that this entry was created. - * The time is converted from its internal representation to standard Java - * representation, milliseconds since the epoch - * (00:00:00 GMT, January 1, 1970). - *

- * Note that most applications leaves this value empty ({@code 0L}). - * - * @return A {@code long} value representing the time this entry was - * created, measured in milliseconds since the epoch - * (00:00:00 GMT, January 1, 1970), or {@code 0L} if no - * creation time stamp exists for this entry. - */ - public long created() { - return createdTimestamp; - } - - /** - * Returns the time that this entry was last modified. - * The time is converted from its internal representation to standard Java - * representation, milliseconds since the epoch - * (00:00:00 GMT, January 1, 1970). - *

- * Note that many applications leaves this value empty ({@code 0L}). - * - * @return A {@code long} value representing the time this entry was - * last modified, measured in milliseconds since the epoch - * (00:00:00 GMT, January 1, 1970), or {@code 0L} if no - * modification time stamp exists for this entry. - */ - public long lastModified() { - return modifiedTimestamp; - } - - /** - * Return the parent of this {@code Entry} - * - * @return the parent of this {@code Entry}, or {@code null} if this is - * the root {@code Entry} - */ - public Entry getParentEntry() { - return parent; - } - - /** - * Returns the child of this {@code Entry} with the given name. - * - * @param pName the name of the child {@code Entry} - * @return the child {@code Entry} or {@code null} if thee is no such - * child - * @throws java.io.IOException if an I/O exception occurs - */ - public Entry getChildEntry(final String pName) throws IOException { - if (isFile() || rootNodeDId == -1) { - return null; - } - - Entry dummy = new Entry(); - dummy.name = pName; - dummy.parent = this; - - SortedSet child = getChildEntries().tailSet(dummy); - return (Entry) child.first(); - } - - /** - * Returns the children of this {@code Entry}. - * - * @return a {@code SortedSet} of {@code Entry} objects - * @throws java.io.IOException if an I/O exception occurs - */ - public SortedSet getChildEntries() throws IOException { - if (children == null) { - if (isFile() || rootNodeDId == -1) { - children = NO_CHILDREN; - } - else { - // Start at root node in R/B tree, and read to the left and right, - // re-build tree, according to the docs - children = Collections.unmodifiableSortedSet(document.getEntries(rootNodeDId, this)); - } - } - - return children; - } - - @Override - public String toString() { - return "\"" + name + "\"" - + " (" + (isFile() ? "Document" : (isDirectory() ? "Directory" : "Root")) - + (parent != null ? ", parent: \"" + parent.getName() + "\"" : "") - + (isFile() ? "" : ", children: " + (children != null ? String.valueOf(children.size()) : "(unknown)")) - + ", SId=" + startSId + ", length=" + streamSize + ")"; - } - - @Override - public boolean equals(final Object pOther) { - if (pOther == this) { - return true; - } - if (!(pOther instanceof Entry)) { - return false; - } - - Entry other = (Entry) pOther; - return name.equals(other.name) && (parent == other.parent - || (parent != null && parent.equals(other.parent))); - } - - @Override - public int hashCode() { - return name.hashCode() ^ startSId; - } - - public int compareTo(final Entry pOther) { - if (this == pOther) { - return 0; - } - - // NOTE: This is the sorting algorthm defined by the Compound Document: - // - first sort by name length - // - if lengths are equal, sort by comparing strings, case sensitive - - int diff = name.length() - pOther.name.length(); - if (diff != 0) { - return diff; - } - - return name.compareTo(pOther.name); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.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; + +/** + * Represents an OLE 2 compound document entry. + * This is similar to a file in a file system, or an entry in a ZIP or JAR file. + * + * @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/io/ole2/Entry.java#4 $ + * @see com.twelvemonkeys.io.ole2.CompoundDocument + */ +// TODO: Consider extending java.io.File... +public final class Entry implements Comparable { + String name; + byte type; + byte nodeColor; + + int prevDId; + int nextDId; + int rootNodeDId; + + long createdTimestamp; + long modifiedTimestamp; + + int startSId; + int streamSize; + + CompoundDocument document; + Entry parent; + SortedSet children; + + public final static int LENGTH = 128; + + static final int EMPTY = 0; + static final int USER_STORAGE = 1; + static final int USER_STREAM = 2; + static final int LOCK_BYTES = 3; + static final int PROPERTY = 4; + static final int ROOT_STORAGE = 5; + + private static final SortedSet NO_CHILDREN = Collections.unmodifiableSortedSet(new TreeSet()); + + private Entry() { + } + + /** + * Reads an entry from the input. + * + * @param pInput the input data + * @return the {@code Entry} read from the input data + * @throws IOException if an i/o exception occurs during reading + */ + static Entry readEntry(final DataInput pInput) throws IOException { + Entry p = new Entry(); + p.read(pInput); + return p; + } + + /** + * Reads this entry + * + * @param pInput the input data + * @throws IOException if an i/o exception occurs during reading + */ + private void read(final DataInput pInput) throws IOException { + byte[] bytes = new byte[64]; + pInput.readFully(bytes); + + // NOTE: Length is in bytes, including the null-terminator... + int nameLength = pInput.readShort(); + name = new String(bytes, 0, nameLength - 2, Charset.forName("UTF-16LE")); +// System.out.println("name: " + name); + + type = pInput.readByte(); +// System.out.println("type: " + type); + + nodeColor = pInput.readByte(); +// System.out.println("nodeColor: " + nodeColor); + + prevDId = pInput.readInt(); +// System.out.println("prevDId: " + prevDId); + nextDId = pInput.readInt(); +// System.out.println("nextDId: " + nextDId); + rootNodeDId = pInput.readInt(); +// System.out.println("rootNodeDId: " + rootNodeDId); + + // UID (16) + user flags (4), ignored + if (pInput.skipBytes(20) != 20) { + throw new CorruptDocumentException(); + } + + createdTimestamp = CompoundDocument.toJavaTimeInMillis(pInput.readLong()); + modifiedTimestamp = CompoundDocument.toJavaTimeInMillis(pInput.readLong()); + + startSId = pInput.readInt(); +// System.out.println("startSId: " + startSId); + streamSize = pInput.readInt(); +// System.out.println("streamSize: " + streamSize); + + // Reserved + pInput.readInt(); + } + + /** + * If {@code true} this {@code Entry} is the root {@code Entry}. + * + * @return {@code true} if this is the root {@code Entry} + */ + public boolean isRoot() { + return type == ROOT_STORAGE; + } + + /** + * If {@code true} this {@code Entry} is a directory + * {@code Entry}. + * + * @return {@code true} if this is a directory {@code Entry} + */ + public boolean isDirectory() { + return type == USER_STORAGE; + } + + /** + * If {@code true} this {@code Entry} is a file (document) + * {@code Entry}. + * + * @return {@code true} if this is a document {@code Entry} + */ + public boolean isFile() { + return type == USER_STREAM; + } + + /** + * Returns the name of this {@code Entry} + * + * @return the name of this {@code Entry} + */ + public String getName() { + return name; + } + + /** + * Returns the {@code InputStream} for this {@code Entry} + * + * @return an {@code InputStream} containing the data for this + * {@code Entry} or {@code null} if this is a directory {@code Entry} + * @throws java.io.IOException if an I/O exception occurs + * @see #length() + */ + public SeekableInputStream getInputStream() throws IOException { + if (!isFile()) { + return null; + } + + return document.getInputStreamForSId(startSId, streamSize); + } + + /** + * Returns the length of this entry + * + * @return the length of the stream for this entry, or {@code 0} if this is + * a directory {@code Entry} + * @see #getInputStream() + */ + public long length() { + if (!isFile()) { + return 0L; + } + + return streamSize; + } + + /** + * Returns the time that this entry was created. + * The time is converted from its internal representation to standard Java + * representation, milliseconds since the epoch + * (00:00:00 GMT, January 1, 1970). + *

+ * Note that most applications leaves this value empty ({@code 0L}). + *

+ * + * @return A {@code long} value representing the time this entry was + * created, measured in milliseconds since the epoch + * (00:00:00 GMT, January 1, 1970), or {@code 0L} if no + * creation time stamp exists for this entry. + */ + public long created() { + return createdTimestamp; + } + + /** + * Returns the time that this entry was last modified. + * The time is converted from its internal representation to standard Java + * representation, milliseconds since the epoch + * (00:00:00 GMT, January 1, 1970). + *

+ * Note that many applications leaves this value empty ({@code 0L}). + *

+ * + * @return A {@code long} value representing the time this entry was + * last modified, measured in milliseconds since the epoch + * (00:00:00 GMT, January 1, 1970), or {@code 0L} if no + * modification time stamp exists for this entry. + */ + public long lastModified() { + return modifiedTimestamp; + } + + /** + * Return the parent of this {@code Entry} + * + * @return the parent of this {@code Entry}, or {@code null} if this is + * the root {@code Entry} + */ + public Entry getParentEntry() { + return parent; + } + + /** + * Returns the child of this {@code Entry} with the given name. + * + * @param pName the name of the child {@code Entry} + * @return the child {@code Entry} or {@code null} if thee is no such + * child + * @throws java.io.IOException if an I/O exception occurs + */ + public Entry getChildEntry(final String pName) throws IOException { + if (isFile() || rootNodeDId == -1) { + return null; + } + + Entry dummy = new Entry(); + dummy.name = pName; + dummy.parent = this; + + SortedSet child = getChildEntries().tailSet(dummy); + return (Entry) child.first(); + } + + /** + * Returns the children of this {@code Entry}. + * + * @return a {@code SortedSet} of {@code Entry} objects + * @throws java.io.IOException if an I/O exception occurs + */ + public SortedSet getChildEntries() throws IOException { + if (children == null) { + if (isFile() || rootNodeDId == -1) { + children = NO_CHILDREN; + } + else { + // Start at root node in R/B tree, and read to the left and right, + // re-build tree, according to the docs + children = Collections.unmodifiableSortedSet(document.getEntries(rootNodeDId, this)); + } + } + + return children; + } + + @Override + public String toString() { + return "\"" + name + "\"" + + " (" + (isFile() ? "Document" : (isDirectory() ? "Directory" : "Root")) + + (parent != null ? ", parent: \"" + parent.getName() + "\"" : "") + + (isFile() ? "" : ", children: " + (children != null ? String.valueOf(children.size()) : "(unknown)")) + + ", SId=" + startSId + ", length=" + streamSize + ")"; + } + + @Override + public boolean equals(final Object pOther) { + if (pOther == this) { + return true; + } + if (!(pOther instanceof Entry)) { + return false; + } + + Entry other = (Entry) pOther; + return name.equals(other.name) && (parent == other.parent + || (parent != null && parent.equals(other.parent))); + } + + @Override + public int hashCode() { + return name.hashCode() ^ startSId; + } + + public int compareTo(final Entry pOther) { + if (this == pOther) { + return 0; + } + + // NOTE: This is the sorting algorthm defined by the Compound Document: + // - first sort by name length + // - if lengths are equal, sort by comparing strings, case sensitive + + int diff = name.length() - pOther.name.length(); + if (diff != 0) { + return diff; + } + + return name.compareTo(pOther.name); + } +} diff --git a/common/common-io/src/main/java/com/twelvemonkeys/net/MIMEUtil.java b/common/common-io/src/main/java/com/twelvemonkeys/net/MIMEUtil.java index a8196395..3b787b3b 100755 --- a/common/common-io/src/main/java/com/twelvemonkeys/net/MIMEUtil.java +++ b/common/common-io/src/main/java/com/twelvemonkeys/net/MIMEUtil.java @@ -1,314 +1,313 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.net; - -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.lang.SystemUtil; - -import java.io.IOException; -import java.util.*; - -/** - * Contains mappings from file extension to mime-types and from mime-type to file-types. - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/MIMEUtil.java#5 $ - * - * @see MIME Media Types - */ -public final class MIMEUtil { - // TODO: Piggy-back on the mappings form the JRE? (1.6 comes with javax.activation) - // TODO: Piggy-back on mappings from javax.activation? - // See: http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax/activation/MimetypesFileTypeMap.html - // See: http://java.sun.com/javase/6/docs/api/javax/activation/MimetypesFileTypeMap.html - // TODO: Use the format (and lookup) specified by the above URLs - // TODO: Allow 3rd party to add mappings? Will need application context support to do it safe.. :-P - - private static Map> sExtToMIME = new HashMap>(); - private static Map> sUnmodifiableExtToMIME = Collections.unmodifiableMap(sExtToMIME); - - private static Map> sMIMEToExt = new HashMap>(); - private static Map> sUnmodifiableMIMEToExt = Collections.unmodifiableMap(sMIMEToExt); - - static { - // Load mapping for MIMEUtil - try { - Properties mappings = SystemUtil.loadProperties(MIMEUtil.class); - - for (Map.Entry entry : mappings.entrySet()) { - // Convert and break up extensions and mimeTypes - String extStr = StringUtil.toLowerCase((String) entry.getKey()); - List extensions = - Collections.unmodifiableList(Arrays.asList(StringUtil.toStringArray(extStr, ";, "))); - - String typeStr = StringUtil.toLowerCase((String) entry.getValue()); - List mimeTypes = - Collections.unmodifiableList(Arrays.asList(StringUtil.toStringArray(typeStr, ";, "))); - - // TODO: Handle duplicates in MIME to extension mapping, like - // xhtml=application/xhtml+xml;application/xml - // xml=text/xml;application/xml - - // Populate normal and reverse MIME-mappings - for (String extension : extensions) { - sExtToMIME.put(extension, mimeTypes); - } - - for (String mimeType : mimeTypes) { - sMIMEToExt.put(mimeType, extensions); - } - } - } - catch (IOException e) { - System.err.println("Could not read properties for MIMEUtil: " + e.getMessage()); - e.printStackTrace(); - } - } - - // Disallow construction - private MIMEUtil() { - } - - /** - * Returns the default MIME type for the given file extension. - * - * @param pFileExt the file extension - * - * @return a {@code String} containing the MIME type, or {@code null} if - * there are no known MIME types for the given file extension. - */ - public static String getMIMEType(final String pFileExt) { - List types = sExtToMIME.get(StringUtil.toLowerCase(pFileExt)); - return (types == null || types.isEmpty()) ? null : types.get(0); - } - - /** - * Returns all MIME types for the given file extension. - * - * @param pFileExt the file extension - * - * @return a {@link List} of {@code String}s containing the MIME types, or an empty - * list, if there are no known MIME types for the given file extension. - */ - public static List getMIMETypes(final String pFileExt) { - List types = sExtToMIME.get(StringUtil.toLowerCase(pFileExt)); - return maskNull(types); - } - - /** - * Returns an unmodifiabale {@link Map} view of the extension to - * MIME mapping, to use as the default mapping in client applications. - * - * @return an unmodifiabale {@code Map} view of the extension to - * MIME mapping. - */ - public static Map> getMIMETypeMappings() { - return sUnmodifiableExtToMIME; - } - - /** - * Returns the default file extension for the given MIME type. - * Specifying a wildcard type will return {@code null}. - * - * @param pMIME the MIME type - * - * @return a {@code String} containing the file extension, or {@code null} - * if there are no known file extensions for the given MIME type. - */ - public static String getExtension(final String pMIME) { - String mime = bareMIME(StringUtil.toLowerCase(pMIME)); - List extensions = sMIMEToExt.get(mime); - return (extensions == null || extensions.isEmpty()) ? null : extensions.get(0); - } - - /** - * Returns all file extension for the given MIME type. - * The default extension will be the first in the list. - * Note that no specific order is given for wildcard types (image/*, */* etc). - * - * @param pMIME the MIME type - * - * @return a {@link List} of {@code String}s containing the MIME types, or an empty - * list, if there are no known file extensions for the given MIME type. - */ - public static List getExtensions(final String pMIME) { - String mime = bareMIME(StringUtil.toLowerCase(pMIME)); - if (mime.endsWith("/*")) { - return getExtensionForWildcard(mime); - } - List extensions = sMIMEToExt.get(mime); - return maskNull(extensions); - } - - // Gets all extensions for a wildcard MIME type - private static List getExtensionForWildcard(final String pMIME) { - final String family = pMIME.substring(0, pMIME.length() - 1); - Set extensions = new LinkedHashSet(); - for (Map.Entry> mimeToExt : sMIMEToExt.entrySet()) { - if ("*/".equals(family) || mimeToExt.getKey().startsWith(family)) { - extensions.addAll(mimeToExt.getValue()); - } - } - return Collections.unmodifiableList(new ArrayList(extensions)); - } - - /** - * Returns an unmodifiabale {@link Map} view of the MIME to - * extension mapping, to use as the default mapping in client applications. - * - * @return an unmodifiabale {@code Map} view of the MIME to - * extension mapping. - */ - public static Map> getExtensionMappings() { - return sUnmodifiableMIMEToExt; - } - - /** - * Tests wehter the type is a subtype of the type family. - * - * @param pTypeFamily the MIME type family ({@code image/*, */*}, etc) - * @param pType the MIME type - * @return {@code true} if {@code pType} is a subtype of {@code pTypeFamily}, otherwise {@code false} - */ - // TODO: Rename? isSubtype? - // TODO: Make public - static boolean includes(final String pTypeFamily, final String pType) { - // TODO: Handle null in a well-defined way - // - Is null family same as */*? - // - Is null subtype of any family? Subtype of no family? - - String type = bareMIME(pType); - return type.equals(pTypeFamily) - || "*/*".equals(pTypeFamily) - || pTypeFamily.endsWith("/*") && pTypeFamily.startsWith(type.substring(0, type.indexOf('/'))); - } - - /** - * Removes any charset or extra info from the mime-type string (anything after a semicolon, {@code ;}, inclusive). - * - * @param pMIME the mime-type string - * @return the bare mime-type - */ - public static String bareMIME(final String pMIME) { - int idx; - if (pMIME != null && (idx = pMIME.indexOf(';')) >= 0) { - return pMIME.substring(0, idx); - } - return pMIME; - } - - // Returns the list or empty list if list is null - private static List maskNull(List pTypes) { - return (pTypes == null) ? Collections.emptyList() : pTypes; - } - - /** - * For debugging. Prints all known MIME types and file extensions. - * - * @param pArgs command line arguments - */ - public static void main(String[] pArgs) { - if (pArgs.length > 1) { - String type = pArgs[0]; - String family = pArgs[1]; - boolean incuded = includes(family, type); - System.out.println( - "Mime type family " + family - + (incuded ? " includes " : " does not include ") - + "type " + type - ); - } - if (pArgs.length > 0) { - String str = pArgs[0]; - - if (str.indexOf('/') >= 0) { - // MIME - String extension = getExtension(str); - System.out.println("Default extension for MIME type '" + str + "' is " - + (extension != null ? ": '" + extension + "'" : "unknown") + "."); - System.out.println("All possible: " + getExtensions(str)); - } - else { - // EXT - String mimeType = getMIMEType(str); - System.out.println("Default MIME type for extension '" + str + "' is " - + (mimeType != null ? ": '" + mimeType + "'" : "unknown") + "."); - System.out.println("All possible: " + getMIMETypes(str)); - } - - return; - } - - Set set = sMIMEToExt.keySet(); - String[] mimeTypes = new String[set.size()]; - int i = 0; - for (Iterator iterator = set.iterator(); iterator.hasNext(); i++) { - String mime = (String) iterator.next(); - mimeTypes[i] = mime; - } - Arrays.sort(mimeTypes); - - System.out.println("Known MIME types (" + mimeTypes.length + "):"); - for (int j = 0; j < mimeTypes.length; j++) { - String mimeType = mimeTypes[j]; - - if (j != 0) { - System.out.print(", "); - } - - System.out.print(mimeType); - } - - System.out.println("\n"); - - set = sExtToMIME.keySet(); - String[] extensions = new String[set.size()]; - i = 0; - for (Iterator iterator = set.iterator(); iterator.hasNext(); i++) { - String ext = (String) iterator.next(); - extensions[i] = ext; - } - Arrays.sort(extensions); - - System.out.println("Known file types (" + extensions.length + "):"); - for (int j = 0; j < extensions.length; j++) { - String extension = extensions[j]; - - if (j != 0) { - System.out.print(", "); - } - - System.out.print(extension); - } - System.out.println(); - } +/* + * 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 of the copyright holder 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 HOLDER 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.net; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.SystemUtil; + +import java.io.IOException; +import java.util.*; + +/** + * Contains mappings from file extension to mime-types and from mime-type to file-types. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/MIMEUtil.java#5 $ + * + * @see MIME Media Types + */ +public final class MIMEUtil { + // TODO: Piggy-back on the mappings form the JRE? (1.6 comes with javax.activation) + // TODO: Piggy-back on mappings from javax.activation? + // See: http://java.sun.com/j2ee/sdk_1.3/techdocs/api/javax/activation/MimetypesFileTypeMap.html + // See: http://java.sun.com/javase/6/docs/api/javax/activation/MimetypesFileTypeMap.html + // TODO: Use the format (and lookup) specified by the above URLs + // TODO: Allow 3rd party to add mappings? Will need application context support to do it safe.. :-P + + private static Map> sExtToMIME = new HashMap>(); + private static Map> sUnmodifiableExtToMIME = Collections.unmodifiableMap(sExtToMIME); + + private static Map> sMIMEToExt = new HashMap>(); + private static Map> sUnmodifiableMIMEToExt = Collections.unmodifiableMap(sMIMEToExt); + + static { + // Load mapping for MIMEUtil + try { + Properties mappings = SystemUtil.loadProperties(MIMEUtil.class); + + for (Map.Entry entry : mappings.entrySet()) { + // Convert and break up extensions and mimeTypes + String extStr = StringUtil.toLowerCase((String) entry.getKey()); + List extensions = + Collections.unmodifiableList(Arrays.asList(StringUtil.toStringArray(extStr, ";, "))); + + String typeStr = StringUtil.toLowerCase((String) entry.getValue()); + List mimeTypes = + Collections.unmodifiableList(Arrays.asList(StringUtil.toStringArray(typeStr, ";, "))); + + // TODO: Handle duplicates in MIME to extension mapping, like + // xhtml=application/xhtml+xml;application/xml + // xml=text/xml;application/xml + + // Populate normal and reverse MIME-mappings + for (String extension : extensions) { + sExtToMIME.put(extension, mimeTypes); + } + + for (String mimeType : mimeTypes) { + sMIMEToExt.put(mimeType, extensions); + } + } + } + catch (IOException e) { + System.err.println("Could not read properties for MIMEUtil: " + e.getMessage()); + e.printStackTrace(); + } + } + + // Disallow construction + private MIMEUtil() { + } + + /** + * Returns the default MIME type for the given file extension. + * + * @param pFileExt the file extension + * + * @return a {@code String} containing the MIME type, or {@code null} if + * there are no known MIME types for the given file extension. + */ + public static String getMIMEType(final String pFileExt) { + List types = sExtToMIME.get(StringUtil.toLowerCase(pFileExt)); + return (types == null || types.isEmpty()) ? null : types.get(0); + } + + /** + * Returns all MIME types for the given file extension. + * + * @param pFileExt the file extension + * + * @return a {@link List} of {@code String}s containing the MIME types, or an empty + * list, if there are no known MIME types for the given file extension. + */ + public static List getMIMETypes(final String pFileExt) { + List types = sExtToMIME.get(StringUtil.toLowerCase(pFileExt)); + return maskNull(types); + } + + /** + * Returns an unmodifiabale {@link Map} view of the extension to + * MIME mapping, to use as the default mapping in client applications. + * + * @return an unmodifiabale {@code Map} view of the extension to + * MIME mapping. + */ + public static Map> getMIMETypeMappings() { + return sUnmodifiableExtToMIME; + } + + /** + * Returns the default file extension for the given MIME type. + * Specifying a wildcard type will return {@code null}. + * + * @param pMIME the MIME type + * + * @return a {@code String} containing the file extension, or {@code null} + * if there are no known file extensions for the given MIME type. + */ + public static String getExtension(final String pMIME) { + String mime = bareMIME(StringUtil.toLowerCase(pMIME)); + List extensions = sMIMEToExt.get(mime); + return (extensions == null || extensions.isEmpty()) ? null : extensions.get(0); + } + + /** + * Returns all file extension for the given MIME type. + * The default extension will be the first in the list. + * Note that no specific order is given for wildcard types (image/*, */* etc). + * + * @param pMIME the MIME type + * + * @return a {@link List} of {@code String}s containing the MIME types, or an empty + * list, if there are no known file extensions for the given MIME type. + */ + public static List getExtensions(final String pMIME) { + String mime = bareMIME(StringUtil.toLowerCase(pMIME)); + if (mime.endsWith("/*")) { + return getExtensionForWildcard(mime); + } + List extensions = sMIMEToExt.get(mime); + return maskNull(extensions); + } + + // Gets all extensions for a wildcard MIME type + private static List getExtensionForWildcard(final String pMIME) { + final String family = pMIME.substring(0, pMIME.length() - 1); + Set extensions = new LinkedHashSet(); + for (Map.Entry> mimeToExt : sMIMEToExt.entrySet()) { + if ("*/".equals(family) || mimeToExt.getKey().startsWith(family)) { + extensions.addAll(mimeToExt.getValue()); + } + } + return Collections.unmodifiableList(new ArrayList(extensions)); + } + + /** + * Returns an unmodifiabale {@link Map} view of the MIME to + * extension mapping, to use as the default mapping in client applications. + * + * @return an unmodifiabale {@code Map} view of the MIME to + * extension mapping. + */ + public static Map> getExtensionMappings() { + return sUnmodifiableMIMEToExt; + } + + /** + * Tests wehter the type is a subtype of the type family. + * + * @param pTypeFamily the MIME type family ({@code image/*, */*}, etc) + * @param pType the MIME type + * @return {@code true} if {@code pType} is a subtype of {@code pTypeFamily}, otherwise {@code false} + */ + // TODO: Rename? isSubtype? + // TODO: Make public + static boolean includes(final String pTypeFamily, final String pType) { + // TODO: Handle null in a well-defined way + // - Is null family same as */*? + // - Is null subtype of any family? Subtype of no family? + + String type = bareMIME(pType); + return type.equals(pTypeFamily) + || "*/*".equals(pTypeFamily) + || pTypeFamily.endsWith("/*") && pTypeFamily.startsWith(type.substring(0, type.indexOf('/'))); + } + + /** + * Removes any charset or extra info from the mime-type string (anything after a semicolon, {@code ;}, inclusive). + * + * @param pMIME the mime-type string + * @return the bare mime-type + */ + public static String bareMIME(final String pMIME) { + int idx; + if (pMIME != null && (idx = pMIME.indexOf(';')) >= 0) { + return pMIME.substring(0, idx); + } + return pMIME; + } + + // Returns the list or empty list if list is null + private static List maskNull(List pTypes) { + return (pTypes == null) ? Collections.emptyList() : pTypes; + } + + /** + * For debugging. Prints all known MIME types and file extensions. + * + * @param pArgs command line arguments + */ + public static void main(String[] pArgs) { + if (pArgs.length > 1) { + String type = pArgs[0]; + String family = pArgs[1]; + boolean incuded = includes(family, type); + System.out.println( + "Mime type family " + family + + (incuded ? " includes " : " does not include ") + + "type " + type + ); + } + if (pArgs.length > 0) { + String str = pArgs[0]; + + if (str.indexOf('/') >= 0) { + // MIME + String extension = getExtension(str); + System.out.println("Default extension for MIME type '" + str + "' is " + + (extension != null ? ": '" + extension + "'" : "unknown") + "."); + System.out.println("All possible: " + getExtensions(str)); + } + else { + // EXT + String mimeType = getMIMEType(str); + System.out.println("Default MIME type for extension '" + str + "' is " + + (mimeType != null ? ": '" + mimeType + "'" : "unknown") + "."); + System.out.println("All possible: " + getMIMETypes(str)); + } + + return; + } + + Set set = sMIMEToExt.keySet(); + String[] mimeTypes = new String[set.size()]; + int i = 0; + for (Iterator iterator = set.iterator(); iterator.hasNext(); i++) { + String mime = (String) iterator.next(); + mimeTypes[i] = mime; + } + Arrays.sort(mimeTypes); + + System.out.println("Known MIME types (" + mimeTypes.length + "):"); + for (int j = 0; j < mimeTypes.length; j++) { + String mimeType = mimeTypes[j]; + + if (j != 0) { + System.out.print(", "); + } + + System.out.print(mimeType); + } + + System.out.println("\n"); + + set = sExtToMIME.keySet(); + String[] extensions = new String[set.size()]; + i = 0; + for (Iterator iterator = set.iterator(); iterator.hasNext(); i++) { + String ext = (String) iterator.next(); + extensions[i] = ext; + } + Arrays.sort(extensions); + + System.out.println("Known file types (" + extensions.length + "):"); + for (int j = 0; j < extensions.length; j++) { + String extension = extensions[j]; + + if (j != 0) { + System.out.print(", "); + } + + System.out.print(extension); + } + System.out.println(); + } } \ No newline at end of file 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 30f716e9..e2975901 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 @@ -102,8 +102,9 @@ public final class DOMSerializer { /** * Specifies wether the serializer should use indentation and optimize for * readability. - *

- * Note: This is a hint, and may be ignored by DOM implemenations. + *

+ * Note: This is a hint, and may be ignored by DOM implementations. + *

* * @param pPrettyPrint {@code true} to enable pretty printing */ 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 dffd0bb3..45077253 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 @@ -1,605 +1,607 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.util.convert.ConversionException; -import com.twelvemonkeys.util.convert.Converter; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Arrays; -import java.util.Map; - -/** - * A utility class with some useful bean-related functions. - *

- * NOTE: This class is not considered part of the public API and may be changed without notice - * - * @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/BeanUtil.java#2 $ - */ -public final class BeanUtil { - - // Disallow creating objects of this type - private BeanUtil() { - } - - /** - * Gets a property value from the given object, using reflection. - * Now supports getting values from properties of properties - * (recursive). - * - * @param pObject The object to get the property from - * @param pProperty The name of the property - * - * @return A string containing the value of the given property, or {@code null} - * if it can not be found. - * @todo Remove System.err's... Create new Exception? Hmm.. - */ - public static Object getPropertyValue(Object pObject, String pProperty) { - // - // TODO: Support get(Object) method of Collections! - // Handle lists and arrays with [] (index) operator - // - - if (pObject == null || pProperty == null || pProperty.length() < 1) { - return null; - } - - Class objClass = pObject.getClass(); - - Object result = pObject; - - // Method for method... - String subProp; - int begIdx = 0; - int endIdx = begIdx; - - while (begIdx < pProperty.length() && begIdx >= 0) { - - endIdx = pProperty.indexOf(".", endIdx + 1); - if (endIdx > 0) { - subProp = pProperty.substring(begIdx, endIdx); - begIdx = endIdx + 1; - } - else { - // The final property! - // If there's just the first-level property, subProp will be - // equal to property - subProp = pProperty.substring(begIdx); - begIdx = -1; - } - - // Check for "[" and "]" - Object[] param = null; - Class[] paramClass = new Class[0]; - - int begBracket; - if ((begBracket = subProp.indexOf("[")) > 0) { - // An error if there is no matching bracket - if (!subProp.endsWith("]")) { - return null; - } - - String between = subProp.substring(begBracket + 1, - subProp.length() - 1); - subProp = subProp.substring(0, begBracket); - - // If brackets exist, check type of argument between brackets - param = new Object[1]; - paramClass = new Class[1]; - - //try { - // TODO: isNumber returns true, even if too big for integer... - if (StringUtil.isNumber(between)) { - // We have a number - // Integer -> array subscript -> getXXX(int i) - try { - // Insert param and it's Class - param[0] = Integer.valueOf(between); - paramClass[0] = Integer.TYPE; // int.class - } - catch (NumberFormatException e) { - // ?? - // Probably too small or too large value.. - } - } - else { - //catch (NumberFormatException e) { - // Not a number... Try String - // String -> Hashtable key -> getXXX(String str) - // Insert param and it's Class - param[0] = between.toLowerCase(); - paramClass[0] = String.class; - } - } - - Method method; - String methodName = "get" + StringUtil.capitalize(subProp); - try { - // Try to get the "get" method for the given property - method = objClass.getMethod(methodName, paramClass); - } - catch (NoSuchMethodException e) { - System.err.print("No method named \"" + methodName + "()\""); - // The array might be of size 0... - if (paramClass.length > 0 && paramClass[0] != null) { - System.err.print(" with the parameter " + paramClass[0].getName()); - } - - System.err.println(" in class " + objClass.getName() + "!"); - return null; - } - - // If method for some reason should be null, give up - if (method == null) { - return null; - } - - try { - // We have a method, try to invoke it - // The resutling object will be either the property we are - // Looking for, or the parent - - // System.err.println("Trying " + objClass.getName() + "." + method.getName() + "(" + ((param != null && param.length > 0) ? param[0] : "") + ")"); - result = method.invoke(result, param); - } - catch (InvocationTargetException e) { - System.err.println("property=" + pProperty + " & result=" + result + " & param=" + Arrays.toString(param)); - e.getTargetException().printStackTrace(); - e.printStackTrace(); - return null; - } - catch (IllegalAccessException e) { - e.printStackTrace(); - return null; - } - catch (NullPointerException e) { - System.err.println(objClass.getName() + "." + method.getName() + "(" + ((paramClass.length > 0 && paramClass[0] != null) ? paramClass[0].getName() : "") + ")"); - e.printStackTrace(); - return null; - } - - if (result != null) { - // Get the class of the reulting object - objClass = result.getClass(); - } - else { - return null; - } - } // while - - return result; - } - - /** - * Sets the property value to an object using reflection. - * Supports setting values of properties that are properties of - * properties (recursive). - * - * @param pObject The object to get a property from - * @param pProperty The name of the property - * @param pValue The property value - * - * @throws NoSuchMethodException if there's no write method for the - * given property - * @throws InvocationTargetException if invoking the write method failed - * @throws IllegalAccessException if the caller class has no access to the - * write method - */ - public static void setPropertyValue(Object pObject, String pProperty, Object pValue) - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - - // - // TODO: Support set(Object, Object)/put(Object, Object) methods - // of Collections! - // Handle lists and arrays with [] (index) operator - - Class paramType = pValue != null ? pValue.getClass() : Object.class; - - // Preserve references - Object obj = pObject; - String property = pProperty; - - // Recurse and find real parent if property contains a '.' - int dotIdx = property.indexOf('.'); - if (dotIdx >= 0) { - // Get real parent - obj = getPropertyValue(obj, property.substring(0, dotIdx)); - // Get the property of the parent - property = property.substring(dotIdx + 1); - } - - // Find method - Object[] params = {pValue}; - Method method = getMethodMayModifyParams(obj, "set" + StringUtil.capitalize(property), - new Class[] {paramType}, params); - - // Invoke it - method.invoke(obj, params); - } - - private static Method getMethodMayModifyParams(Object pObject, String pName, Class[] pParams, Object[] pValues) - throws NoSuchMethodException { - // NOTE: This method assumes pParams.length == 1 && pValues.length == 1 - - Method method = null; - Class paramType = pParams[0]; - - try { - method = pObject.getClass().getMethod(pName, pParams); - } - catch (NoSuchMethodException e) { - // No direct match - - // 1: If primitive wrapper, try unwrap conversion first - /*if (paramType.isPrimitive()) { // NOTE: Can't be primitive type - params[0] = ReflectUtil.wrapType(paramType); - } - else*/ if (ReflectUtil.isPrimitiveWrapper(paramType)) { - pParams[0] = ReflectUtil.unwrapType(paramType); - } - - try { - // If this does not throw an exception, it works - method = pObject.getClass().getMethod(pName, pParams); - } - catch (Throwable t) { - // Ignore - } - - // 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 exception, it works - method = pObject.getClass().getMethod(pName, pParams); - } - catch (Throwable t) { - // Ignore/Continue - continue; - } - - break; - } - } - - // 3: Try to find a different method with the same name, that has - // a parameter type we can convert to... - // NOTE: There's no ordering here.. - // TODO: Should we try to do that? What would the ordering be? - if (method == null) { - Method[] methods = pObject.getClass().getMethods(); - for (Method candidate : methods) { - if (Modifier.isPublic(candidate.getModifiers()) && candidate.getName().equals(pName) - && candidate.getReturnType() == Void.TYPE && candidate.getParameterTypes().length == 1) { - // NOTE: Assumes paramTypes.length == 1 - - Class type = candidate.getParameterTypes()[0]; - - try { - pValues[0] = convertValueToType(pValues[0], type); - } - catch (Throwable t) { - continue; - } - - // We were able to convert the parameter, let's try - method = candidate; - break; - } - } - } - - // Give up... - if (method == null) { - throw e; - } - } - return method; - } - - private static Object convertValueToType(Object pValue, Class pType) throws ConversionException { - if (pType.isPrimitive()) { - if (pType == Boolean.TYPE && pValue instanceof Boolean) { - return pValue; - } - else if (pType == Byte.TYPE && pValue instanceof Byte) { - return pValue; - } - else if (pType == Character.TYPE && pValue instanceof Character) { - return pValue; - } - else if (pType == Double.TYPE && pValue instanceof Double) { - return pValue; - } - else if (pType == Float.TYPE && pValue instanceof Float) { - return pValue; - } - else if (pType == Integer.TYPE && pValue instanceof Integer) { - return pValue; - } - else if (pType == Long.TYPE && pValue instanceof Long) { - return pValue; - } - else if (pType == Short.TYPE && pValue instanceof Short) { - return pValue; - } - } - - // 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(); - return converter.toObject((String) pValue, pType); - } - else if (pType == String.class) { - Converter converter = Converter.getInstance(); - return converter.toString(pValue); - } - else { - throw new ConversionException("Cannot convert " + pValue.getClass().getName() + " to " + pType.getName()); - } - } - - /** - * Creates an object from the given class' single argument constructor. - * - * @param pClass The class to create instance from - * @param pParam The parameters to the constructor - * - * @return The object created from the constructor. - * If the constructor could not be invoked for any reason, null is - * returned. - * - * @throws InvocationTargetException if the constructor failed - */ - // TODO: Move to ReflectUtil - public static T createInstance(Class pClass, Object pParam) - throws InvocationTargetException { - return createInstance(pClass, new Object[] {pParam}); - } - - /** - * Creates an object from the given class' constructor that matches - * the given paramaters. - * - * @param pClass The class to create instance from - * @param pParams The parameters to the constructor - * - * @return The object created from the constructor. - * If the constructor could not be invoked for any reason, null is - * returned. - * - * @throws InvocationTargetException if the constructor failed - */ - // TODO: Move to ReflectUtil - public static T createInstance(Class pClass, Object... pParams) - throws InvocationTargetException { - T value; - - try { - // Create param and argument arrays - Class[] paramTypes = null; - if (pParams != null && pParams.length > 0) { - paramTypes = new Class[pParams.length]; - for (int i = 0; i < pParams.length; i++) { - paramTypes[i] = pParams[i].getClass(); - } - } - - // Get constructor - Constructor constructor = pClass.getConstructor(paramTypes); - - // Invoke and create instance - value = constructor.newInstance(pParams); - } - /* All this to let InvocationTargetException pass on */ - catch (NoSuchMethodException nsme) { - return null; - } - catch (IllegalAccessException iae) { - return null; - } - catch (IllegalArgumentException iarge) { - return null; - } - catch (InstantiationException ie) { - return null; - } - catch (ExceptionInInitializerError err) { - return null; - } - - return value; - } - - /** - * Gets an object from any given static method, with the given parameter. - * - * @param pClass The class to invoke method on - * @param pMethod The name of the method to invoke - * @param pParam The parameter to the method - * - * @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. - * - * @throws InvocationTargetException if the invocation failed - */ - // TODO: Move to ReflectUtil - // TODO: Rename to invokeStatic? - public static Object invokeStaticMethod(Class pClass, String pMethod, Object pParam) - throws InvocationTargetException { - - return invokeStaticMethod(pClass, pMethod, new Object[] {pParam}); - } - - /** - * Gets an object from any given static method, with the given parameter. - * - * @param pClass The class to invoke method on - * @param pMethod The name of the method to invoke - * @param pParams The parameters to the method - * - * @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. - * - * @throws InvocationTargetException if the invocation failed - */ - // TODO: Move to ReflectUtil - // TODO: Rename to invokeStatic? - public static Object invokeStaticMethod(Class pClass, String pMethod, Object... pParams) - throws InvocationTargetException { - - Object value = null; - - try { - // Create param and argument arrays - Class[] paramTypes = new Class[pParams.length]; - for (int i = 0; i < pParams.length; i++) { - paramTypes[i] = pParams[i].getClass(); - } - - // 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[]) - Method method = pClass.getMethod(pMethod, paramTypes); - - // Invoke public static method - if (Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) { - value = method.invoke(null, pParams); - } - - } - /* All this to let InvocationTargetException pass on */ - catch (NoSuchMethodException nsme) { - return null; - } - catch (IllegalAccessException iae) { - return null; - } - catch (IllegalArgumentException iarge) { - return null; - } - - return value; - } - - /** - * Configures the bean according to the given mapping. - * For each {@code Map.Entry} in {@code Map.values()}, - * a method named - * {@code set + capitalize(entry.getKey())} is called on the bean, - * with {@code entry.getValue()} as its argument. - *

- * Properties that has no matching set-method in the bean, are simply - * discarded. - * - * @param pBean The bean to configure - * @param pMapping The mapping for the bean - * - * @throws NullPointerException if any of the parameters are null. - * @throws InvocationTargetException if an error occurs when invoking the - * setter-method. - */ - // TODO: Add a version that takes a ConfigurationErrorListener callback interface - // TODO: ...or a boolean pFailOnError parameter - // TODO: ...or return Exceptions as an array?! - // TODO: ...or something whatsoever that makes clients able to determine something's not right - public static void configure(final Object pBean, final Map pMapping) throws InvocationTargetException { - configure(pBean, pMapping, false); - } - - /** - * Configures the bean according to the given mapping. - * For each {@code Map.Entry} in {@code Map.values()}, - * a method named - * {@code set + capitalize(entry.getKey())} is called on the bean, - * with {@code entry.getValue()} as its argument. - *

- * Optionally, lisp-style names are allowed, and automatically converted - * to Java-style camel-case names. - *

- * Properties that has no matching set-method in the bean, are simply - * discarded. - * - * @see StringUtil#lispToCamel(String) - * - * @param pBean The bean to configure - * @param pMapping The mapping for the bean - * @param pLispToCamel Allow lisp-style names, and automatically convert - * them to Java-style camel-case. - * - * @throws NullPointerException if any of the parameters are null. - * @throws InvocationTargetException if an error occurs when invoking the - * setter-method. - */ - public static void configure(final Object pBean, final Map pMapping, final boolean pLispToCamel) throws InvocationTargetException { - // Loop over properties in mapping - for (final Map.Entry entry : pMapping.entrySet()) { - try { - // Configure each property in turn - final String property = StringUtil.valueOf(entry.getKey()); - try { - setPropertyValue(pBean, property, entry.getValue()); - } - 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()); - } - } - } - catch (NoSuchMethodException nsme) { - // This property was not configured - } - catch (IllegalAccessException iae) { - // This property was not configured - } - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.util.convert.ConversionException; +import com.twelvemonkeys.util.convert.Converter; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Map; + +/** + * A utility class with some useful bean-related functions. + *

+ * NOTE: This class is not considered part of the public API and may be changed without notice + *

+ * + * @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/BeanUtil.java#2 $ + */ +public final class BeanUtil { + + // Disallow creating objects of this type + private BeanUtil() { + } + + /** + * Gets a property value from the given object, using reflection. + * Now supports getting values from properties of properties + * (recursive). + * + * @param pObject The object to get the property from + * @param pProperty The name of the property + * + * @return A string containing the value of the given property, or {@code null} + * if it can not be found. + */ + public static Object getPropertyValue(Object pObject, String pProperty) { + // TODO: Remove System.err's... Create new Exception? Hmm.. + // TODO: Support get(Object) method of Collections! + // Handle lists and arrays with [] (index) operator + + if (pObject == null || pProperty == null || pProperty.length() < 1) { + return null; + } + + Class objClass = pObject.getClass(); + + Object result = pObject; + + // Method for method... + String subProp; + int begIdx = 0; + int endIdx = begIdx; + + while (begIdx < pProperty.length() && begIdx >= 0) { + + endIdx = pProperty.indexOf(".", endIdx + 1); + if (endIdx > 0) { + subProp = pProperty.substring(begIdx, endIdx); + begIdx = endIdx + 1; + } + else { + // The final property! + // If there's just the first-level property, subProp will be + // equal to property + subProp = pProperty.substring(begIdx); + begIdx = -1; + } + + // Check for "[" and "]" + Object[] param = null; + Class[] paramClass = new Class[0]; + + int begBracket; + if ((begBracket = subProp.indexOf("[")) > 0) { + // An error if there is no matching bracket + if (!subProp.endsWith("]")) { + return null; + } + + String between = subProp.substring(begBracket + 1, + subProp.length() - 1); + subProp = subProp.substring(0, begBracket); + + // If brackets exist, check type of argument between brackets + param = new Object[1]; + paramClass = new Class[1]; + + //try { + // TODO: isNumber returns true, even if too big for integer... + if (StringUtil.isNumber(between)) { + // We have a number + // Integer -> array subscript -> getXXX(int i) + try { + // Insert param and it's Class + param[0] = Integer.valueOf(between); + paramClass[0] = Integer.TYPE; // int.class + } + catch (NumberFormatException e) { + // ?? + // Probably too small or too large value.. + } + } + else { + //catch (NumberFormatException e) { + // Not a number... Try String + // String -> Hashtable key -> getXXX(String str) + // Insert param and it's Class + param[0] = between.toLowerCase(); + paramClass[0] = String.class; + } + } + + Method method; + String methodName = "get" + StringUtil.capitalize(subProp); + try { + // Try to get the "get" method for the given property + method = objClass.getMethod(methodName, paramClass); + } + catch (NoSuchMethodException e) { + System.err.print("No method named \"" + methodName + "()\""); + // The array might be of size 0... + if (paramClass.length > 0 && paramClass[0] != null) { + System.err.print(" with the parameter " + paramClass[0].getName()); + } + + System.err.println(" in class " + objClass.getName() + "!"); + return null; + } + + // If method for some reason should be null, give up + if (method == null) { + return null; + } + + try { + // We have a method, try to invoke it + // The resutling object will be either the property we are + // Looking for, or the parent + + // System.err.println("Trying " + objClass.getName() + "." + method.getName() + "(" + ((param != null && param.length > 0) ? param[0] : "") + ")"); + result = method.invoke(result, param); + } + catch (InvocationTargetException e) { + System.err.println("property=" + pProperty + " & result=" + result + " & param=" + Arrays.toString(param)); + e.getTargetException().printStackTrace(); + e.printStackTrace(); + return null; + } + catch (IllegalAccessException e) { + e.printStackTrace(); + return null; + } + catch (NullPointerException e) { + System.err.println(objClass.getName() + "." + method.getName() + "(" + ((paramClass.length > 0 && paramClass[0] != null) ? paramClass[0].getName() : "") + ")"); + e.printStackTrace(); + return null; + } + + if (result != null) { + // Get the class of the reulting object + objClass = result.getClass(); + } + else { + return null; + } + } // while + + return result; + } + + /** + * Sets the property value to an object using reflection. + * Supports setting values of properties that are properties of + * properties (recursive). + * + * @param pObject The object to get a property from + * @param pProperty The name of the property + * @param pValue The property value + * + * @throws NoSuchMethodException if there's no write method for the + * given property + * @throws InvocationTargetException if invoking the write method failed + * @throws IllegalAccessException if the caller class has no access to the + * write method + */ + public static void setPropertyValue(Object pObject, String pProperty, Object pValue) + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + + // + // TODO: Support set(Object, Object)/put(Object, Object) methods + // of Collections! + // Handle lists and arrays with [] (index) operator + + Class paramType = pValue != null ? pValue.getClass() : Object.class; + + // Preserve references + Object obj = pObject; + String property = pProperty; + + // Recurse and find real parent if property contains a '.' + int dotIdx = property.indexOf('.'); + if (dotIdx >= 0) { + // Get real parent + obj = getPropertyValue(obj, property.substring(0, dotIdx)); + // Get the property of the parent + property = property.substring(dotIdx + 1); + } + + // Find method + Object[] params = {pValue}; + Method method = getMethodMayModifyParams(obj, "set" + StringUtil.capitalize(property), + new Class[] {paramType}, params); + + // Invoke it + method.invoke(obj, params); + } + + private static Method getMethodMayModifyParams(Object pObject, String pName, Class[] pParams, Object[] pValues) + throws NoSuchMethodException { + // NOTE: This method assumes pParams.length == 1 && pValues.length == 1 + + Method method = null; + Class paramType = pParams[0]; + + try { + method = pObject.getClass().getMethod(pName, pParams); + } + catch (NoSuchMethodException e) { + // No direct match + + // 1: If primitive wrapper, try unwrap conversion first + /*if (paramType.isPrimitive()) { // NOTE: Can't be primitive type + params[0] = ReflectUtil.wrapType(paramType); + } + else*/ if (ReflectUtil.isPrimitiveWrapper(paramType)) { + pParams[0] = ReflectUtil.unwrapType(paramType); + } + + try { + // If this does not throw an exception, it works + method = pObject.getClass().getMethod(pName, pParams); + } + catch (Throwable t) { + // Ignore + } + + // 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 exception, it works + method = pObject.getClass().getMethod(pName, pParams); + } + catch (Throwable t) { + // Ignore/Continue + continue; + } + + break; + } + } + + // 3: Try to find a different method with the same name, that has + // a parameter type we can convert to... + // NOTE: There's no ordering here.. + // TODO: Should we try to do that? What would the ordering be? + if (method == null) { + Method[] methods = pObject.getClass().getMethods(); + for (Method candidate : methods) { + if (Modifier.isPublic(candidate.getModifiers()) && candidate.getName().equals(pName) + && candidate.getReturnType() == Void.TYPE && candidate.getParameterTypes().length == 1) { + // NOTE: Assumes paramTypes.length == 1 + + Class type = candidate.getParameterTypes()[0]; + + try { + pValues[0] = convertValueToType(pValues[0], type); + } + catch (Throwable t) { + continue; + } + + // We were able to convert the parameter, let's try + method = candidate; + break; + } + } + } + + // Give up... + if (method == null) { + throw e; + } + } + return method; + } + + private static Object convertValueToType(Object pValue, Class pType) throws ConversionException { + if (pType.isPrimitive()) { + if (pType == Boolean.TYPE && pValue instanceof Boolean) { + return pValue; + } + else if (pType == Byte.TYPE && pValue instanceof Byte) { + return pValue; + } + else if (pType == Character.TYPE && pValue instanceof Character) { + return pValue; + } + else if (pType == Double.TYPE && pValue instanceof Double) { + return pValue; + } + else if (pType == Float.TYPE && pValue instanceof Float) { + return pValue; + } + else if (pType == Integer.TYPE && pValue instanceof Integer) { + return pValue; + } + else if (pType == Long.TYPE && pValue instanceof Long) { + return pValue; + } + else if (pType == Short.TYPE && pValue instanceof Short) { + return pValue; + } + } + + // 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(); + return converter.toObject((String) pValue, pType); + } + else if (pType == String.class) { + Converter converter = Converter.getInstance(); + return converter.toString(pValue); + } + else { + throw new ConversionException("Cannot convert " + pValue.getClass().getName() + " to " + pType.getName()); + } + } + + /** + * Creates an object from the given class' single argument constructor. + * + * @param pClass The class to create instance from + * @param pParam The parameters to the constructor + * + * @return The object created from the constructor. + * If the constructor could not be invoked for any reason, null is + * returned. + * + * @throws InvocationTargetException if the constructor failed + */ + // TODO: Move to ReflectUtil + public static T createInstance(Class pClass, Object pParam) + throws InvocationTargetException { + return createInstance(pClass, new Object[] {pParam}); + } + + /** + * Creates an object from the given class' constructor that matches + * the given paramaters. + * + * @param pClass The class to create instance from + * @param pParams The parameters to the constructor + * + * @return The object created from the constructor. + * If the constructor could not be invoked for any reason, null is + * returned. + * + * @throws InvocationTargetException if the constructor failed + */ + // TODO: Move to ReflectUtil + public static T createInstance(Class pClass, Object... pParams) + throws InvocationTargetException { + T value; + + try { + // Create param and argument arrays + Class[] paramTypes = null; + if (pParams != null && pParams.length > 0) { + paramTypes = new Class[pParams.length]; + for (int i = 0; i < pParams.length; i++) { + paramTypes[i] = pParams[i].getClass(); + } + } + + // Get constructor + Constructor constructor = pClass.getConstructor(paramTypes); + + // Invoke and create instance + value = constructor.newInstance(pParams); + } + /* All this to let InvocationTargetException pass on */ + catch (NoSuchMethodException nsme) { + return null; + } + catch (IllegalAccessException iae) { + return null; + } + catch (IllegalArgumentException iarge) { + return null; + } + catch (InstantiationException ie) { + return null; + } + catch (ExceptionInInitializerError err) { + return null; + } + + return value; + } + + /** + * Gets an object from any given static method, with the given parameter. + * + * @param pClass The class to invoke method on + * @param pMethod The name of the method to invoke + * @param pParam The parameter to the method + * + * @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. + * + * @throws InvocationTargetException if the invocation failed + */ + // TODO: Move to ReflectUtil + // TODO: Rename to invokeStatic? + public static Object invokeStaticMethod(Class pClass, String pMethod, Object pParam) + throws InvocationTargetException { + + return invokeStaticMethod(pClass, pMethod, new Object[] {pParam}); + } + + /** + * Gets an object from any given static method, with the given parameter. + * + * @param pClass The class to invoke method on + * @param pMethod The name of the method to invoke + * @param pParams The parameters to the method + * + * @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. + * + * @throws InvocationTargetException if the invocation failed + */ + // TODO: Move to ReflectUtil + // TODO: Rename to invokeStatic? + public static Object invokeStaticMethod(Class pClass, String pMethod, Object... pParams) + throws InvocationTargetException { + + Object value = null; + + try { + // Create param and argument arrays + Class[] paramTypes = new Class[pParams.length]; + for (int i = 0; i < pParams.length; i++) { + paramTypes[i] = pParams[i].getClass(); + } + + // 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[]) + Method method = pClass.getMethod(pMethod, paramTypes); + + // Invoke public static method + if (Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) { + value = method.invoke(null, pParams); + } + + } + /* All this to let InvocationTargetException pass on */ + catch (NoSuchMethodException nsme) { + return null; + } + catch (IllegalAccessException iae) { + return null; + } + catch (IllegalArgumentException iarge) { + return null; + } + + return value; + } + + /** + * Configures the bean according to the given mapping. + * For each {@code Map.Entry} in {@code Map.values()}, + * a method named + * {@code set + capitalize(entry.getKey())} is called on the bean, + * with {@code entry.getValue()} as its argument. + *

+ * Properties that has no matching set-method in the bean, are simply + * discarded. + *

+ * + * @param pBean The bean to configure + * @param pMapping The mapping for the bean + * + * @throws NullPointerException if any of the parameters are null. + * @throws InvocationTargetException if an error occurs when invoking the + * setter-method. + */ + // TODO: Add a version that takes a ConfigurationErrorListener callback interface + // TODO: ...or a boolean pFailOnError parameter + // TODO: ...or return Exceptions as an array?! + // TODO: ...or something whatsoever that makes clients able to determine something's not right + public static void configure(final Object pBean, final Map pMapping) throws InvocationTargetException { + configure(pBean, pMapping, false); + } + + /** + * Configures the bean according to the given mapping. + * For each {@code Map.Entry} in {@code Map.values()}, + * a method named + * {@code set + capitalize(entry.getKey())} is called on the bean, + * with {@code entry.getValue()} as its argument. + *

+ * Optionally, lisp-style names are allowed, and automatically converted + * to Java-style camel-case names. + *

+ *

+ * Properties that has no matching set-method in the bean, are simply + * discarded. + *

+ * + * @see StringUtil#lispToCamel(String) + * + * @param pBean The bean to configure + * @param pMapping The mapping for the bean + * @param pLispToCamel Allow lisp-style names, and automatically convert + * them to Java-style camel-case. + * + * @throws NullPointerException if any of the parameters are null. + * @throws InvocationTargetException if an error occurs when invoking the + * setter-method. + */ + public static void configure(final Object pBean, final Map pMapping, final boolean pLispToCamel) throws InvocationTargetException { + // Loop over properties in mapping + for (final Map.Entry entry : pMapping.entrySet()) { + try { + // Configure each property in turn + final String property = StringUtil.valueOf(entry.getKey()); + try { + setPropertyValue(pBean, property, entry.getValue()); + } + 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()); + } + } + } + catch (NoSuchMethodException nsme) { + // This property was not configured + } + catch (IllegalAccessException iae) { + // This property was not configured + } + } + } +} 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 780a68dd..92e262e3 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 @@ -1,204 +1,203 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.util.Date; -import java.util.TimeZone; - -/** - * A utility class with useful date manipulation methods and constants. - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/DateUtil.java#1 $ - */ -public final class DateUtil { - - /** One second: 1000 milliseconds. */ - public static final long SECOND = 1000l; - - /** One minute: 60 seconds (60 000 milliseconds). */ - public static final long MINUTE = 60l * SECOND; - - /** - * One hour: 60 minutes (3 600 000 milliseconds). - * 60 minutes = 3 600 seconds = 3 600 000 milliseconds - */ - public static final long HOUR = 60l * MINUTE; - - /** - * One day: 24 hours (86 400 000 milliseconds). - * 24 hours = 1 440 minutes = 86 400 seconds = 86 400 000 milliseconds. - */ - public static final long DAY = 24l * HOUR; - - /** - * One calendar year: 365.2425 days (31556952000 milliseconds). - * 365.2425 days = 8765.82 hours = 525949.2 minutes = 31556952 seconds - * = 31556952000 milliseconds. - */ - public static final long CALENDAR_YEAR = 3652425l * 24l * 60l * 6l; - - private DateUtil() { - } - - /** - * Returns the time between the given start time and now (as defined by - * {@link System#currentTimeMillis()}). - * - * @param pStart the start time - * - * @return the time between the given start time and now. - */ - public static long delta(long pStart) { - return System.currentTimeMillis() - pStart; - } - - /** - * Returns the time between the given start time and now (as defined by - * {@link System#currentTimeMillis()}). - * - * @param pStart the start time - * - * @return the time between the given start time and now. - */ - public static long delta(Date pStart) { - return System.currentTimeMillis() - pStart.getTime(); - } - - /** - * Gets the current time, rounded down to the closest second. - * Equivalent to invoking - * {@code roundToSecond(System.currentTimeMillis())}. - * - * @return the current time, rounded to the closest second. - */ - public static long currentTimeSecond() { - return roundToSecond(System.currentTimeMillis()); - } - - /** - * Gets the current time, rounded down to the closest minute. - * Equivalent to invoking - * {@code roundToMinute(System.currentTimeMillis())}. - * - * @return the current time, rounded to the closest minute. - */ - public static long currentTimeMinute() { - return roundToMinute(System.currentTimeMillis()); - } - - /** - * Gets the current time, rounded down to the closest hour. - * Equivalent to invoking - * {@code roundToHour(System.currentTimeMillis())}. - * - * @return the current time, rounded to the closest hour. - */ - public static long currentTimeHour() { - return roundToHour(System.currentTimeMillis()); - } - - /** - * Gets the current time, rounded down to the closest day. - * Equivalent to invoking - * {@code roundToDay(System.currentTimeMillis())}. - * - * @return the current time, rounded to the closest day. - */ - public static long currentTimeDay() { - return roundToDay(System.currentTimeMillis()); - } - - /** - * Rounds the given time down to the closest second. - * - * @param pTime time - * @return the time rounded to the closest second. - */ - public static long roundToSecond(final long pTime) { - return (pTime / SECOND) * SECOND; - } - - /** - * Rounds the given time down to the closest minute. - * - * @param pTime time - * @return the time rounded to the closest minute. - */ - public static long roundToMinute(final long pTime) { - return (pTime / MINUTE) * MINUTE; - } - - /** - * Rounds the given time down to the closest hour, using the default timezone. - * - * @param pTime time - * @return the time rounded to the closest hour. - */ - public static long roundToHour(final long pTime) { - return roundToHour(pTime, TimeZone.getDefault()); - } - - /** - * Rounds the given time down to the closest hour, using the given timezone. - * - * @param pTime time - * @param pTimeZone the timezone to use when rounding - * @return the time rounded to the closest hour. - */ - public static long roundToHour(final long pTime, final TimeZone pTimeZone) { - int offset = pTimeZone.getOffset(pTime); - return ((pTime / HOUR) * HOUR) - offset; - } - - /** - * Rounds the given time down to the closest day, using the default timezone. - * - * @param pTime time - * @return the time rounded to the closest day. - */ - public static long roundToDay(final long pTime) { - return roundToDay(pTime, TimeZone.getDefault()); - } - - /** - * Rounds the given time down to the closest day, using the given timezone. - * - * @param pTime time - * @param pTimeZone the timezone to use when rounding - * @return the time rounded to the closest day. - */ - public static long roundToDay(final long pTime, final TimeZone pTimeZone) { - int offset = pTimeZone.getOffset(pTime); - return (((pTime + offset) / DAY) * DAY) - offset; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.util.Date; +import java.util.TimeZone; + +/** + * A utility class with useful date manipulation methods and constants. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/DateUtil.java#1 $ + */ +public final class DateUtil { + + /** One second: 1000 milliseconds. */ + public static final long SECOND = 1000l; + + /** One minute: 60 seconds (60 000 milliseconds). */ + public static final long MINUTE = 60l * SECOND; + + /** + * One hour: 60 minutes (3 600 000 milliseconds). + * 60 minutes = 3 600 seconds = 3 600 000 milliseconds + */ + public static final long HOUR = 60l * MINUTE; + + /** + * One day: 24 hours (86 400 000 milliseconds). + * 24 hours = 1 440 minutes = 86 400 seconds = 86 400 000 milliseconds. + */ + public static final long DAY = 24l * HOUR; + + /** + * One calendar year: 365.2425 days (31556952000 milliseconds). + * 365.2425 days = 8765.82 hours = 525949.2 minutes = 31556952 seconds + * = 31556952000 milliseconds. + */ + public static final long CALENDAR_YEAR = 3652425l * 24l * 60l * 6l; + + private DateUtil() { + } + + /** + * Returns the time between the given start time and now (as defined by + * {@link System#currentTimeMillis()}). + * + * @param pStart the start time + * + * @return the time between the given start time and now. + */ + public static long delta(long pStart) { + return System.currentTimeMillis() - pStart; + } + + /** + * Returns the time between the given start time and now (as defined by + * {@link System#currentTimeMillis()}). + * + * @param pStart the start time + * + * @return the time between the given start time and now. + */ + public static long delta(Date pStart) { + return System.currentTimeMillis() - pStart.getTime(); + } + + /** + * Gets the current time, rounded down to the closest second. + * Equivalent to invoking + * {@code roundToSecond(System.currentTimeMillis())}. + * + * @return the current time, rounded to the closest second. + */ + public static long currentTimeSecond() { + return roundToSecond(System.currentTimeMillis()); + } + + /** + * Gets the current time, rounded down to the closest minute. + * Equivalent to invoking + * {@code roundToMinute(System.currentTimeMillis())}. + * + * @return the current time, rounded to the closest minute. + */ + public static long currentTimeMinute() { + return roundToMinute(System.currentTimeMillis()); + } + + /** + * Gets the current time, rounded down to the closest hour. + * Equivalent to invoking + * {@code roundToHour(System.currentTimeMillis())}. + * + * @return the current time, rounded to the closest hour. + */ + public static long currentTimeHour() { + return roundToHour(System.currentTimeMillis()); + } + + /** + * Gets the current time, rounded down to the closest day. + * Equivalent to invoking + * {@code roundToDay(System.currentTimeMillis())}. + * + * @return the current time, rounded to the closest day. + */ + public static long currentTimeDay() { + return roundToDay(System.currentTimeMillis()); + } + + /** + * Rounds the given time down to the closest second. + * + * @param pTime time + * @return the time rounded to the closest second. + */ + public static long roundToSecond(final long pTime) { + return (pTime / SECOND) * SECOND; + } + + /** + * Rounds the given time down to the closest minute. + * + * @param pTime time + * @return the time rounded to the closest minute. + */ + public static long roundToMinute(final long pTime) { + return (pTime / MINUTE) * MINUTE; + } + + /** + * Rounds the given time down to the closest hour, using the default timezone. + * + * @param pTime time + * @return the time rounded to the closest hour. + */ + public static long roundToHour(final long pTime) { + return roundToHour(pTime, TimeZone.getDefault()); + } + + /** + * Rounds the given time down to the closest hour, using the given timezone. + * + * @param pTime time + * @param pTimeZone the timezone to use when rounding + * @return the time rounded to the closest hour. + */ + public static long roundToHour(final long pTime, final TimeZone pTimeZone) { + int offset = pTimeZone.getOffset(pTime); + return ((pTime / HOUR) * HOUR) - offset; + } + + /** + * Rounds the given time down to the closest day, using the default timezone. + * + * @param pTime time + * @return the time rounded to the closest day. + */ + public static long roundToDay(final long pTime) { + return roundToDay(pTime, TimeZone.getDefault()); + } + + /** + * Rounds the given time down to the closest day, using the given timezone. + * + * @param pTime time + * @param pTimeZone the timezone to use when rounding + * @return the time rounded to the closest day. + */ + public static long roundToDay(final long pTime, final TimeZone pTimeZone) { + int offset = pTimeZone.getOffset(pTime); + return (((pTime + offset) / DAY) * DAY) - offset; + } +} 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 f0ab7e1a..8dadc5d2 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 @@ -199,9 +199,10 @@ public final class Platform { /** * Enumeration of common System {@code Architecture}s. - *

+ *

* For {@link #Unknown unknown architectures}, {@code toString()} will return * the the same value as {@code System.getProperty("os.arch")}. + *

* * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Platform.java#1 $ @@ -228,9 +229,10 @@ public final class Platform { /** * Enumeration of common {@code OperatingSystem}s. - *

+ *

* For {@link #Unknown unknown operating systems}, {@code getName()} will return * the the same value as {@code System.getProperty("os.name")}. + *

* * @author Harald Kuhr * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Platform.java#1 $ diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/lang/ReflectUtil.java b/common/common-lang/src/main/java/com/twelvemonkeys/lang/ReflectUtil.java index 615672ab..aefd92a0 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/lang/ReflectUtil.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/lang/ReflectUtil.java @@ -1,139 +1,140 @@ -/* - * 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 of the copyright holder 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 HOLDER 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; - -/** - * Util class for various reflection-based operations. - *

- * NOTE: This class is not considered part of the public API and may be - * changed without notice - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ReflectUtil.java#1 $ - */ -public final class ReflectUtil { - - /** Don't allow instances */ - private ReflectUtil() {} - - /** - * Returns the primitive type for the given wrapper type. - * - * @param pType the wrapper type - * - * @return the primitive type - * - * @throws IllegalArgumentException if {@code pType} is not a primitive - * wrapper - */ - public static Class unwrapType(Class pType) { - if (pType == Boolean.class) { - return Boolean.TYPE; - } - else if (pType == Byte.class) { - return Byte.TYPE; - } - else if (pType == Character.class) { - return Character.TYPE; - } - else if (pType == Double.class) { - return Double.TYPE; - } - else if (pType == Float.class) { - return Float.TYPE; - } - else if (pType == Integer.class) { - return Integer.TYPE; - } - else if (pType == Long.class) { - return Long.TYPE; - } - else if (pType == Short.class) { - return Short.TYPE; - } - - throw new IllegalArgumentException("Not a primitive wrapper: " + pType); - } - - /** - * Returns the wrapper type for the given primitive type. - * - * @param pType the primitive tpye - * - * @return the wrapper type - * - * @throws IllegalArgumentException if {@code pType} is not a primitive - * type - */ - public static Class wrapType(Class pType) { - if (pType == Boolean.TYPE) { - return Boolean.class; - } - else if (pType == Byte.TYPE) { - return Byte.class; - } - else if (pType == Character.TYPE) { - return Character.class; - } - else if (pType == Double.TYPE) { - return Double.class; - } - else if (pType == Float.TYPE) { - return Float.class; - } - else if (pType == Integer.TYPE) { - return Integer.class; - } - else if (pType == Long.TYPE) { - return Long.class; - } - else if (pType == Short.TYPE) { - return Short.class; - } - - throw new IllegalArgumentException("Not a primitive type: " + pType); - } - - /** - * Returns {@code true} if the given type is a primitive wrapper. - * - * @param pType - * - * @return {@code true} if the given type is a primitive wrapper, otherwise - * {@code false} - */ - public static boolean isPrimitiveWrapper(Class pType) { - return pType == Boolean.class || pType == Byte.class - || pType == Character.class || pType == Double.class - || pType == Float.class || pType == Integer.class - || pType == Long.class || pType == Short.class; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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; + +/** + * Util class for various reflection-based operations. + *

+ * NOTE: This class is not considered part of the public API and may be + * changed without notice + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ReflectUtil.java#1 $ + */ +public final class ReflectUtil { + + /** Don't allow instances */ + private ReflectUtil() {} + + /** + * Returns the primitive type for the given wrapper type. + * + * @param pType the wrapper type + * + * @return the primitive type + * + * @throws IllegalArgumentException if {@code pType} is not a primitive + * wrapper + */ + public static Class unwrapType(Class pType) { + if (pType == Boolean.class) { + return Boolean.TYPE; + } + else if (pType == Byte.class) { + return Byte.TYPE; + } + else if (pType == Character.class) { + return Character.TYPE; + } + else if (pType == Double.class) { + return Double.TYPE; + } + else if (pType == Float.class) { + return Float.TYPE; + } + else if (pType == Integer.class) { + return Integer.TYPE; + } + else if (pType == Long.class) { + return Long.TYPE; + } + else if (pType == Short.class) { + return Short.TYPE; + } + + throw new IllegalArgumentException("Not a primitive wrapper: " + pType); + } + + /** + * Returns the wrapper type for the given primitive type. + * + * @param pType the primitive tpye + * + * @return the wrapper type + * + * @throws IllegalArgumentException if {@code pType} is not a primitive + * type + */ + public static Class wrapType(Class pType) { + if (pType == Boolean.TYPE) { + return Boolean.class; + } + else if (pType == Byte.TYPE) { + return Byte.class; + } + else if (pType == Character.TYPE) { + return Character.class; + } + else if (pType == Double.TYPE) { + return Double.class; + } + else if (pType == Float.TYPE) { + return Float.class; + } + else if (pType == Integer.TYPE) { + return Integer.class; + } + else if (pType == Long.TYPE) { + return Long.class; + } + else if (pType == Short.TYPE) { + return Short.class; + } + + throw new IllegalArgumentException("Not a primitive type: " + pType); + } + + /** + * Returns {@code true} if the given type is a primitive wrapper. + * + * @param pType + * + * @return {@code true} if the given type is a primitive wrapper, otherwise + * {@code false} + */ + public static boolean isPrimitiveWrapper(Class pType) { + return pType == Boolean.class || pType == Byte.class + || pType == Character.class || pType == Double.class + || pType == Float.class || pType == Integer.class + || pType == Long.class || pType == Short.class; + } +} 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 5b546646..69979657 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 @@ -1,2152 +1,2161 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.util.StringTokenIterator; - -import java.awt.*; -import java.io.UnsupportedEncodingException; -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.nio.charset.UnsupportedCharsetException; -import java.sql.Timestamp; -import java.text.DateFormat; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -/** - * A utility class with some useful string manipulation methods. - * - * @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/lang/StringUtil.java#2 $ - * @todo Consistency check: Method names, parameter sequence, Exceptions, - * return values, null-value handling and parameter names (cosmetics). - */ -public final class StringUtil { - - /** - * The default delimiter string, used by the {@code toXXXArray()} - * methods. - * Its value is {@code ", \t\n\r\f"}. - * - * - * @see #toStringArray(String) - * @see #toIntArray(String) - * @see #toLongArray(String) - * @see #toDoubleArray(String) - */ - public final static String DELIMITER_STRING = ", \t\n\r\f"; - - // Avoid constructor showing up in API doc - private StringUtil() { - } - - /** - * 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. - * - * @param pData the bytes to be decoded to characters - * @param pOffset the index of the first byte to decode - * @param pLength the number of bytes to decode - * @param pCharset the name of a supported character set - * @return a newly created string. - * @throws UnsupportedCharsetException - * - * @see String#String(byte[], int, int, String) - */ - public static String decode(final byte[] pData, final int pOffset, final int pLength, final String pCharset) { - try { - return new String(pData, pOffset, pLength, pCharset); - } - catch (UnsupportedEncodingException e) { - throw new UnsupportedCharsetException(pCharset); - } - } - - /** - * Returns the value of the given {@code Object}, as a {@code String}. - * Unlike String.valueOf, this method returns {@code null} - * instead of the {@code String} "null", if {@code null} is given as - * the argument. - * - * @param pObj the Object to find the {@code String} value of. - * @return the String value of the given object, or {@code null} if the - * {@code pObj} == {@code null}. - * @see String#valueOf(Object) - * @see String#toString() - */ - public static String valueOf(Object pObj) { - return ((pObj != null) ? pObj.toString() : null); - } - - /** - * Converts a string to uppercase. - * - * @param pString the string to convert - * @return the string converted to uppercase, or null if the argument was - * null. - */ - public static String toUpperCase(String pString) { - if (pString != null) { - return pString.toUpperCase(); - } - return null; - } - - /** - * Converts a string to lowercase. - * - * @param pString the string to convert - * @return the string converted to lowercase, or null if the argument was - * null. - */ - public static String toLowerCase(String pString) { - if (pString != null) { - return pString.toLowerCase(); - } - return null; - } - - /** - * Tests if a String is null, or contains nothing but white-space. - * - * @param pString The string to test - * @return true if the string is null or contains only whitespace, - * otherwise false. - */ - public static boolean isEmpty(String pString) { - return ((pString == null) || (pString.trim().length() == 0)); - } - - /** - * Tests a string array, to see if all items are null or an empty string. - * - * @param pStringArray The string array to check. - * @return true if the string array is null or only contains string items - * that are null or contain only whitespace, otherwise false. - */ - public static boolean isEmpty(String[] pStringArray) { - // No elements to test - if (pStringArray == null) { - return true; - } - - // Test all the elements - for (String string : pStringArray) { - if (!isEmpty(string)) { - return false; - } - } - - // All elements are empty - return true; - } - - /** - * Tests if a string contains another string. - * - * @param pContainer The string to test - * @param pLookFor The string to look for - * @return {@code true} if the container string is contains the string, and - * both parameters are non-{@code null}, otherwise {@code false}. - */ - public static boolean contains(String pContainer, String pLookFor) { - return ((pContainer != null) && (pLookFor != null) && (pContainer.indexOf(pLookFor) >= 0)); - } - - /** - * Tests if a string contains another string, ignoring case. - * - * @param pContainer The string to test - * @param pLookFor The string to look for - * @return {@code true} if the container string is contains the string, and - * both parameters are non-{@code null}, otherwise {@code false}. - * @see #contains(String,String) - */ - public static boolean containsIgnoreCase(String pContainer, String pLookFor) { - return indexOfIgnoreCase(pContainer, pLookFor, 0) >= 0; - } - - /** - * Tests if a string contains a specific character. - * - * @param pString The string to check. - * @param pChar The character to search for. - * @return true if the string contains the specific character. - */ - public static boolean contains(final String pString, final int pChar) { - return ((pString != null) && (pString.indexOf(pChar) >= 0)); - } - - /** - * Tests if a string contains a specific character, ignoring case. - * - * @param pString The string to check. - * @param pChar The character to search for. - * @return true if the string contains the specific character. - */ - public static boolean containsIgnoreCase(String pString, int pChar) { - return ((pString != null) - && ((pString.indexOf(Character.toLowerCase((char) pChar)) >= 0) - || (pString.indexOf(Character.toUpperCase((char) pChar)) >= 0))); - - // NOTE: I don't convert the string to uppercase, but instead test - // the string (potentially) two times, as this is more efficient for - // long strings (in most cases). - } - - /** - * Returns the index within this string of the first occurrence of the - * specified substring. - * - * @param pString The string to test - * @param pLookFor The string to look for - * @return if the string argument occurs as a substring within this object, - * then the index of the first character of the first such substring is - * returned; if it does not occur as a substring, -1 is returned. - * @see String#indexOf(String) - */ - public static int indexOfIgnoreCase(String pString, String pLookFor) { - return indexOfIgnoreCase(pString, pLookFor, 0); - } - - /** - * Returns the index within this string of the first occurrence of the - * specified substring, starting at the specified index. - * - * @param pString The string to test - * @param pLookFor The string to look for - * @param pPos The first index to test - * @return if the string argument occurs as a substring within this object, - * then the index of the first character of the first such substring is - * returned; if it does not occur as a substring, -1 is returned. - * @see String#indexOf(String,int) - */ - public static int indexOfIgnoreCase(String pString, String pLookFor, int pPos) { - if ((pString == null) || (pLookFor == null)) { - return -1; - } - if (pLookFor.length() == 0) { - return pPos;// All strings "contains" the empty string - } - if (pLookFor.length() > pString.length()) { - return -1;// Cannot contain string longer than itself - } - - // Get first char - char firstL = Character.toLowerCase(pLookFor.charAt(0)); - char firstU = Character.toUpperCase(pLookFor.charAt(0)); - int indexLower = 0; - int indexUpper = 0; - - for (int i = pPos; i <= (pString.length() - pLookFor.length()); i++) { - - // Peek for first char - indexLower = ((indexLower >= 0) && (indexLower <= i)) - ? pString.indexOf(firstL, i) - : indexLower; - indexUpper = ((indexUpper >= 0) && (indexUpper <= i)) - ? pString.indexOf(firstU, i) - : indexUpper; - if (indexLower < 0) { - if (indexUpper < 0) { - return -1;// First char not found - } - else { - i = indexUpper;// Only upper - } - } - else if (indexUpper < 0) { - i = indexLower;// Only lower - } - else { - - // Both found, select first occurence - i = (indexLower < indexUpper) - ? indexLower - : indexUpper; - } - - // Only one? - if (pLookFor.length() == 1) { - return i;// The only char found! - } - - // Test if we still have enough chars - else if (i > (pString.length() - pLookFor.length())) { - return -1; - } - - // Test if last char equals! (regionMatches is expensive) - else if ((pString.charAt(i + pLookFor.length() - 1) != Character.toLowerCase(pLookFor.charAt(pLookFor.length() - 1))) - && (pString.charAt(i + pLookFor.length() - 1) != Character.toUpperCase(pLookFor.charAt(pLookFor.length() - 1)))) { - continue;// Nope, try next - } - - // Test from second char, until second-last char - else if ((pLookFor.length() <= 2) || pString.regionMatches(true, i + 1, pLookFor, 1, pLookFor.length() - 2)) { - return i; - } - } - return -1; - } - - /** - * Returns the index within this string of the rightmost occurrence of the - * specified substring. The rightmost empty string "" is considered to - * occur at the index value {@code pString.length() - 1}. - * - * @param pString The string to test - * @param pLookFor The string to look for - * @return If the string argument occurs one or more times as a substring - * within this object at a starting index no greater than fromIndex, then - * the index of the first character of the last such substring is returned. - * If it does not occur as a substring starting at fromIndex or earlier, -1 - * is returned. - * @see String#lastIndexOf(String) - */ - public static int lastIndexOfIgnoreCase(String pString, String pLookFor) { - return lastIndexOfIgnoreCase(pString, pLookFor, pString != null ? pString.length() - 1 : -1); - } - - /** - * Returns the index within this string of the rightmost occurrence of the - * specified substring. The rightmost empty string "" is considered to - * occur at the index value {@code pPos} - * - * @param pString The string to test - * @param pLookFor The string to look for - * @param pPos The last index to test - * @return If the string argument occurs one or more times as a substring - * within this object at a starting index no greater than fromIndex, then - * the index of the first character of the last such substring is returned. - * If it does not occur as a substring starting at fromIndex or earlier, -1 - * is returned. - * @see String#lastIndexOf(String,int) - */ - public static int lastIndexOfIgnoreCase(String pString, String pLookFor, int pPos) { - if ((pString == null) || (pLookFor == null)) { - return -1; - } - if (pLookFor.length() == 0) { - return pPos;// All strings "contains" the empty string - } - if (pLookFor.length() > pString.length()) { - return -1;// Cannot contain string longer than itself - } - - // Get first char - char firstL = Character.toLowerCase(pLookFor.charAt(0)); - char firstU = Character.toUpperCase(pLookFor.charAt(0)); - int indexLower = pPos; - int indexUpper = pPos; - - for (int i = pPos; i >= 0; i--) { - - // Peek for first char - indexLower = ((indexLower >= 0) && (indexLower >= i)) - ? pString.lastIndexOf(firstL, i) - : indexLower; - indexUpper = ((indexUpper >= 0) && (indexUpper >= i)) - ? pString.lastIndexOf(firstU, i) - : indexUpper; - if (indexLower < 0) { - if (indexUpper < 0) { - return -1;// First char not found - } - else { - i = indexUpper;// Only upper - } - } - else if (indexUpper < 0) { - i = indexLower;// Only lower - } - else { - - // Both found, select last occurence - i = (indexLower > indexUpper) - ? indexLower - : indexUpper; - } - - // Only one? - if (pLookFor.length() == 1) { - return i;// The only char found! - } - - // Test if we still have enough chars - else if (i > (pString.length() - pLookFor.length())) { - //return -1; - continue; - } - - // Test if last char equals! (regionMatches is expensive) - else - if ((pString.charAt(i + pLookFor.length() - 1) != Character.toLowerCase(pLookFor.charAt(pLookFor.length() - 1))) - && (pString.charAt(i + pLookFor.length() - 1) != Character.toUpperCase(pLookFor.charAt(pLookFor.length() - 1)))) { - continue;// Nope, try next - } - - // Test from second char, until second-last char - else - if ((pLookFor.length() <= 2) || pString.regionMatches(true, i + 1, pLookFor, 1, pLookFor.length() - 2)) { - return i; - } - } - return -1; - } - - /** - * Returns the index within this string of the first occurrence of the - * specified character. - * - * @param pString The string to test - * @param pChar The character to look for - * @return if the string argument occurs as a substring within this object, - * then the index of the first character of the first such substring is - * returned; if it does not occur as a substring, -1 is returned. - * @see String#indexOf(int) - */ - public static int indexOfIgnoreCase(String pString, int pChar) { - return indexOfIgnoreCase(pString, pChar, 0); - } - - /** - * Returns the index within this string of the first occurrence of the - * specified character, starting at the specified index. - * - * @param pString The string to test - * @param pChar The character to look for - * @param pPos The first index to test - * @return if the string argument occurs as a substring within this object, - * then the index of the first character of the first such substring is - * returned; if it does not occur as a substring, -1 is returned. - * @see String#indexOf(int,int) - */ - public static int indexOfIgnoreCase(String pString, int pChar, int pPos) { - if ((pString == null)) { - return -1; - } - - // Get first char - char lower = Character.toLowerCase((char) pChar); - char upper = Character.toUpperCase((char) pChar); - int indexLower; - int indexUpper; - - // Test for char - indexLower = pString.indexOf(lower, pPos); - indexUpper = pString.indexOf(upper, pPos); - if (indexLower < 0) { - - /* if (indexUpper < 0) - return -1; // First char not found - else */ - return indexUpper;// Only upper - } - else if (indexUpper < 0) { - return indexLower;// Only lower - } - else { - - // Both found, select first occurence - return (indexLower < indexUpper) - ? indexLower - : indexUpper; - } - } - - /** - * Returns the index within this string of the last occurrence of the - * specified character. - * - * @param pString The string to test - * @param pChar The character to look for - * @return if the string argument occurs as a substring within this object, - * then the index of the first character of the first such substring is - * returned; if it does not occur as a substring, -1 is returned. - * @see String#lastIndexOf(int) - */ - public static int lastIndexOfIgnoreCase(String pString, int pChar) { - return lastIndexOfIgnoreCase(pString, pChar, pString != null ? pString.length() : -1); - } - - /** - * Returns the index within this string of the last occurrence of the - * specified character, searching backward starting at the specified index. - * - * @param pString The string to test - * @param pChar The character to look for - * @param pPos The last index to test - * @return if the string argument occurs as a substring within this object, - * then the index of the first character of the first such substring is - * returned; if it does not occur as a substring, -1 is returned. - * @see String#lastIndexOf(int,int) - */ - public static int lastIndexOfIgnoreCase(String pString, int pChar, int pPos) { - if ((pString == null)) { - return -1; - } - - // Get first char - char lower = Character.toLowerCase((char) pChar); - char upper = Character.toUpperCase((char) pChar); - int indexLower; - int indexUpper; - - // Test for char - indexLower = pString.lastIndexOf(lower, pPos); - indexUpper = pString.lastIndexOf(upper, pPos); - if (indexLower < 0) { - - /* if (indexUpper < 0) - return -1; // First char not found - else */ - return indexUpper;// Only upper - } - else if (indexUpper < 0) { - return indexLower;// Only lower - } - else { - - // Both found, select last occurence - return (indexLower > indexUpper) - ? indexLower - : indexUpper; - } - } - - /** - * Trims the argument string for whitespace on the left side only. - * - * @param pString the string to trim - * @return the string with no whitespace on the left, or {@code null} if - * the string argument is {@code null}. - * @see #rtrim - * @see String#trim() - */ - public static String ltrim(String pString) { - if ((pString == null) || (pString.length() == 0)) { - return pString;// Null or empty string - } - for (int i = 0; i < pString.length(); i++) { - if (!Character.isWhitespace(pString.charAt(i))) { - if (i == 0) { - return pString;// First char is not whitespace - } - else { - return pString.substring(i);// Return rest after whitespace - } - } - } - - // If all whitespace, return empty string - return ""; - } - - /** - * Trims the argument string for whitespace on the right side only. - * - * @param pString the string to trim - * @return the string with no whitespace on the right, or {@code null} if - * the string argument is {@code null}. - * @see #ltrim - * @see String#trim() - */ - public static String rtrim(String pString) { - if ((pString == null) || (pString.length() == 0)) { - return pString;// Null or empty string - } - for (int i = pString.length(); i > 0; i--) { - if (!Character.isWhitespace(pString.charAt(i - 1))) { - if (i == pString.length()) { - return pString;// First char is not whitespace - } - else { - return pString.substring(0, i);// Return before whitespace - } - } - } - - // If all whitespace, return empty string - return ""; - } - - /** - * Replaces a substring of a string with another string. All matches are - * replaced. - * - * @param pSource The source String - * @param pPattern The pattern to replace - * @param pReplace The new String to be inserted instead of the - * replace String - * @return The new String with the pattern replaced - */ - public static String replace(String pSource, String pPattern, String pReplace) { - if (pPattern.length() == 0) { - return pSource;// Special case: No pattern to replace - } - - int match; - int offset = 0; - StringBuilder result = new StringBuilder(); - - // Loop string, until last occurence of pattern, and replace - while ((match = pSource.indexOf(pPattern, offset)) != -1) { - // Append everything until pattern - result.append(pSource.substring(offset, match)); - // Append the replace string - result.append(pReplace); - offset = match + pPattern.length(); - } - - // Append rest of string and return - result.append(pSource.substring(offset)); - - return result.toString(); - } - - /** - * Replaces a substring of a string with another string, ignoring case. - * All matches are replaced. - * - * @param pSource The source String - * @param pPattern The pattern to replace - * @param pReplace The new String to be inserted instead of the - * replace String - * @return The new String with the pattern replaced - * @see #replace(String,String,String) - */ - public static String replaceIgnoreCase(String pSource, String pPattern, String pReplace) { - if (pPattern.length() == 0) { - return pSource;// Special case: No pattern to replace - } - int match; - int offset = 0; - StringBuilder result = new StringBuilder(); - - while ((match = indexOfIgnoreCase(pSource, pPattern, offset)) != -1) { - result.append(pSource.substring(offset, match)); - result.append(pReplace); - offset = match + pPattern.length(); - } - result.append(pSource.substring(offset)); - return result.toString(); - } - - /** - * Cuts a string between two words, before a sepcified length, if the - * string is longer than the maxium lenght. The string is optionally padded - * with the pad argument. The method assumes words to be separated by the - * space character (" "). - * Note that the maximum length argument is absolute, and will also include - * the length of the padding. - * - * @param pString The string to cut - * @param pMaxLen The maximum length before cutting - * @param pPad The string to append at the end, aftrer cutting - * @return The cutted string with padding, or the original string, if it - * was shorter than the max length. - * @see #pad(String,int,String,boolean) - */ - public static String cut(String pString, int pMaxLen, String pPad) { - if (pString == null) { - return null; - } - if (pPad == null) { - pPad = ""; - } - int len = pString.length(); - - if (len > pMaxLen) { - len = pString.lastIndexOf(' ', pMaxLen - pPad.length()); - } - else { - return pString; - } - return pString.substring(0, len) + pPad; - } - - /** - * Makes the Nth letter of a String uppercase. If the index is outside the - * the length of the argument string, the argument is simply returned. - * - * @param pString The string to capitalize - * @param pIndex The base-0 index of the char to capitalize. - * @return The capitalized string, or null, if a null argument was given. - */ - public static String capitalize(String pString, int pIndex) { - if (pIndex < 0) { - throw new IndexOutOfBoundsException("Negative index not allowed: " + pIndex); - } - if (pString == null || pString.length() <= pIndex) { - return pString; - } - - // This is the fastest method, according to my tests - - // Skip array duplication if allready capitalized - if (Character.isUpperCase(pString.charAt(pIndex))) { - return pString; - } - - // Convert to char array, capitalize and create new String - char[] charArray = pString.toCharArray(); - charArray[pIndex] = Character.toUpperCase(charArray[pIndex]); - return new String(charArray); - - /** - StringBuilder buf = new StringBuilder(pString); - buf.setCharAt(pIndex, Character.toUpperCase(buf.charAt(pIndex))); - return buf.toString(); - //*/ - - /** - return pString.substring(0, pIndex) - + Character.toUpperCase(pString.charAt(pIndex)) - + pString.substring(pIndex + 1); - //*/ - } - - /** - * Makes the first letter of a String uppercase. - * - * @param pString The string to capitalize - * @return The capitalized string, or null, if a null argument was given. - */ - public static String capitalize(String pString) { - return capitalize(pString, 0); - } - - /** - * Formats a number with leading zeroes, to a specified length. - * - * @param pNum The number to format - * @param pLen The number of digits - * @return A string containing the formatted number - * @throws IllegalArgumentException Thrown, if the number contains - * more digits than allowed by the length argument. - * @see #pad(String,int,String,boolean) - * @deprecated Use StringUtil.pad instead! - */ - - /*public*/ - static String formatNumber(long pNum, int pLen) throws IllegalArgumentException { - StringBuilder result = new StringBuilder(); - - if (pNum >= Math.pow(10, pLen)) { - throw new IllegalArgumentException("The number to format cannot contain more digits than the length argument specifies!"); - } - for (int i = pLen; i > 1; i--) { - if (pNum < Math.pow(10, i - 1)) { - result.append('0'); - } - else { - break; - } - } - result.append(pNum); - return result.toString(); - } - - /** - * String length check with simple concatenation of selected pad-string. - * E.g. a zip number from 123 to the correct 0123. - * - * @param pSource The source string. - * @param pRequiredLength The accurate length of the resulting string. - * @param pPadString The string for concatenation. - * @param pPrepend The location of fill-ins, prepend (true), - * or append (false) - * @return a concatenated string. - * @todo What if source is allready longer than required length? - * @todo Consistency with cut - * @see #cut(String,int,String) - */ - public static String pad(String pSource, int pRequiredLength, String pPadString, boolean pPrepend) { - if (pPadString == null || pPadString.length() == 0) { - throw new IllegalArgumentException("Pad string: \"" + pPadString + "\""); - } - - if (pSource.length() >= pRequiredLength) { - return pSource; - } - - // TODO: Benchmark the new version against the old one, to see if it's really faster - // Rewrite to first create pad - // - pad += pad; - until length is >= gap - // then append the pad and cut if too long - int gap = pRequiredLength - pSource.length(); - StringBuilder result = new StringBuilder(pPadString); - while (result.length() < gap) { - result.append(result); - } - - if (result.length() > gap) { - result.delete(gap, result.length()); - } - - return pPrepend ? result.append(pSource).toString() : result.insert(0, pSource).toString(); - - /* - StringBuilder result = new StringBuilder(pSource); - - // Concatenation until proper string length - while (result.length() < pRequiredLength) { - // Prepend or append - if (pPrepend) { // Front - result.insert(0, pPadString); - } - else { // Back - result.append(pPadString); - } - } - - // Truncate - if (result.length() > pRequiredLength) { - if (pPrepend) { - result.delete(0, result.length() - pRequiredLength); - } - else { - result.delete(pRequiredLength, result.length()); - } - } - return result.toString(); - */ - } - - /** - * Converts the string to a date, using the default date format. - * - * @param pString the string to convert - * @return the date - * @see DateFormat - * @see DateFormat#getInstance() - */ - public static Date toDate(String pString) { - // Default - return toDate(pString, DateFormat.getInstance()); - } - - /** - * Converts the string to a date, using the given format. - * - * @param pString the string to convert - * @param pFormat the date format - * @return the date - * @todo cache formats? - * @see java.text.SimpleDateFormat - * @see java.text.SimpleDateFormat#SimpleDateFormat(String) - */ - public static Date toDate(String pString, String pFormat) { - // Get the format from cache, or create new and insert - // Return new date - return toDate(pString, new SimpleDateFormat(pFormat)); - } - - /** - * Converts the string to a date, using the given format. - * - * @param pString the string to convert - * @param pFormat the date format - * @return the date - * @see SimpleDateFormat - * @see SimpleDateFormat#SimpleDateFormat(String) - * @see DateFormat - */ - public static Date toDate(final String pString, final DateFormat pFormat) { - try { - synchronized (pFormat) { - // Parse date using given format - return pFormat.parse(pString); - } - } - catch (ParseException pe) { - // Wrap in RuntimeException - throw new IllegalArgumentException(pe.getMessage()); - } - } - - /** - * Converts the string to a jdbc Timestamp, using the standard Timestamp - * escape format. - * - * @param pValue the value - * @return a new {@code Timestamp} - * @see java.sql.Timestamp - * @see java.sql.Timestamp#valueOf(String) - */ - public static Timestamp toTimestamp(final String pValue) { - // Parse date using default format - return Timestamp.valueOf(pValue); - } - - /** - * Converts a delimiter separated String to an array of Strings. - * - * @param pString The comma-separated string - * @param pDelimiters The delimiter string - * @return a {@code String} array containing the delimiter separated elements - */ - public static String[] toStringArray(String pString, String pDelimiters) { - if (isEmpty(pString)) { - return new String[0]; - } - - StringTokenIterator st = new StringTokenIterator(pString, pDelimiters); - List v = new ArrayList(); - - while (st.hasMoreElements()) { - v.add(st.nextToken()); - } - - return v.toArray(new String[v.size()]); - } - - /** - * Converts a comma-separated String to an array of Strings. - * - * @param pString The comma-separated string - * @return a {@code String} array containing the comma-separated elements - * @see #toStringArray(String,String) - */ - public static String[] toStringArray(String pString) { - return toStringArray(pString, DELIMITER_STRING); - } - - /** - * Converts a comma-separated String to an array of ints. - * - * @param pString The comma-separated string - * @param pDelimiters The delimiter string - * @param pBase The radix - * @return an {@code int} array - * @throws NumberFormatException if any of the elements are not parseable - * as an int - */ - public static int[] toIntArray(String pString, String pDelimiters, int pBase) { - if (isEmpty(pString)) { - return new int[0]; - } - - // Some room for improvement here... - String[] temp = toStringArray(pString, pDelimiters); - int[] array = new int[temp.length]; - - for (int i = 0; i < array.length; i++) { - array[i] = Integer.parseInt(temp[i], pBase); - } - return array; - } - - /** - * Converts a comma-separated String to an array of ints. - * - * @param pString The comma-separated string - * @return an {@code int} array - * @throws NumberFormatException if any of the elements are not parseable - * as an int - * @see #toStringArray(String,String) - * @see #DELIMITER_STRING - */ - public static int[] toIntArray(String pString) { - return toIntArray(pString, DELIMITER_STRING, 10); - } - - /** - * Converts a comma-separated String to an array of ints. - * - * @param pString The comma-separated string - * @param pDelimiters The delimiter string - * @return an {@code int} array - * @throws NumberFormatException if any of the elements are not parseable - * as an int - * @see #toIntArray(String,String) - */ - public static int[] toIntArray(String pString, String pDelimiters) { - return toIntArray(pString, pDelimiters, 10); - } - - /** - * Converts a comma-separated String to an array of longs. - * - * @param pString The comma-separated string - * @param pDelimiters The delimiter string - * @return a {@code long} array - * @throws NumberFormatException if any of the elements are not parseable - * as a long - */ - public static long[] toLongArray(String pString, String pDelimiters) { - if (isEmpty(pString)) { - return new long[0]; - } - - // Some room for improvement here... - String[] temp = toStringArray(pString, pDelimiters); - long[] array = new long[temp.length]; - - for (int i = 0; i < array.length; i++) { - array[i] = Long.parseLong(temp[i]); - } - return array; - } - - /** - * Converts a comma-separated String to an array of longs. - * - * @param pString The comma-separated string - * @return a {@code long} array - * @throws NumberFormatException if any of the elements are not parseable - * as a long - * @see #toStringArray(String,String) - * @see #DELIMITER_STRING - */ - public static long[] toLongArray(String pString) { - return toLongArray(pString, DELIMITER_STRING); - } - - /** - * Converts a comma-separated String to an array of doubles. - * - * @param pString The comma-separated string - * @param pDelimiters The delimiter string - * @return a {@code double} array - * @throws NumberFormatException if any of the elements are not parseable - * as a double - */ - public static double[] toDoubleArray(String pString, String pDelimiters) { - if (isEmpty(pString)) { - return new double[0]; - } - - // Some room for improvement here... - String[] temp = toStringArray(pString, pDelimiters); - double[] array = new double[temp.length]; - - for (int i = 0; i < array.length; i++) { - array[i] = Double.valueOf(temp[i]); - - // Double.parseDouble() is 1.2... - } - return array; - } - - /** - * Converts a comma-separated String to an array of doubles. - * - * @param pString The comma-separated string - * @return a {@code double} array - * @throws NumberFormatException if any of the elements are not parseable - * as a double - * @see #toDoubleArray(String,String) - * @see #DELIMITER_STRING - */ - public static double[] toDoubleArray(String pString) { - return toDoubleArray(pString, DELIMITER_STRING); - } - - /** - * Parses a string to a Color. - * The argument can be a color constant (static constant from - * {@link java.awt.Color java.awt.Color}), like {@code black} or - * {@code red}, or it can be HTML/CSS-style, on the format: - *
    - *
  • {@code #RRGGBB}, where RR, GG and BB means two digit - * hexadecimal for red, green and blue values respectively.
  • - *
  • {@code #AARRGGBB}, as above, with AA as alpha component.
  • - *
  • {@code #RGB}, where R, G and B means one digit - * hexadecimal for red, green and blue values respectively.
  • - *
  • {@code #ARGB}, as above, with A as alpha component.
  • - *
- * - * @param pString the string representation of the color - * @return the {@code Color} object, or {@code null} if the argument - * is {@code null} - * @throws IllegalArgumentException if the string does not map to a color. - * @see java.awt.Color - */ - public static Color toColor(String pString) { - // No string, no color - if (pString == null) { - return null; - } - - // #RRGGBB format - if (pString.charAt(0) == '#') { - int r = 0; - int g = 0; - int b = 0; - int a = -1;// alpha - - if (pString.length() >= 7) { - int idx = 1; - - // AA - if (pString.length() >= 9) { - a = Integer.parseInt(pString.substring(idx, idx + 2), 0x10); - idx += 2; - } - // RR GG BB - r = Integer.parseInt(pString.substring(idx, idx + 2), 0x10); - g = Integer.parseInt(pString.substring(idx + 2, idx + 4), 0x10); - b = Integer.parseInt(pString.substring(idx + 4, idx + 6), 0x10); - - } - else if (pString.length() >= 4) { - int idx = 1; - - // A - if (pString.length() >= 5) { - a = Integer.parseInt(pString.substring(idx, ++idx), 0x10) * 0x10; - } - // R G B - r = Integer.parseInt(pString.substring(idx, ++idx), 0x10) * 0x10; - g = Integer.parseInt(pString.substring(idx, ++idx), 0x10) * 0x10; - b = Integer.parseInt(pString.substring(idx, ++idx), 0x10) * 0x10; - } - if (a != -1) { - // With alpha - return new Color(r, g, b, a); - } - - // No alpha - return new Color(r, g, b); - } - - // Get color by name - try { - Class colorClass = Color.class; - Field field = null; - - // Workaround for stupidity in Color class constant field names - try { - field = colorClass.getField(pString); - } - catch (Exception e) { - // Don't care, this is just a workaround... - } - if (field == null) { - // NOTE: The toLowerCase() on the next line will lose darkGray - // and lightGray... - field = colorClass.getField(pString.toLowerCase()); - } - - // Only try to get public final fields - int mod = field.getModifiers(); - - if (Modifier.isPublic(mod) && Modifier.isStatic(mod)) { - return (Color) field.get(null); - } - } - catch (NoSuchFieldException nsfe) { - // No such color, throw illegal argument? - throw new IllegalArgumentException("No such color: " + pString); - } - catch (SecurityException se) { - // Can't access field, return null - } - catch (IllegalAccessException iae) { - // Can't happen, as the field must be public static - } - catch (IllegalArgumentException iar) { - // Can't happen, as the field must be static - } - - // This should never be reached, but you never know... ;-) - return null; - } - - /** - * Creates a HTML/CSS String representation of the given color. - * The HTML/CSS color format is defined as: - *
    - *
  • {@code #RRGGBB}, where RR, GG and BB means two digit - * hexadecimal for red, green and blue values respectively.
  • - *
  • {@code #AARRGGBB}, as above, with AA as alpha component.
  • - *
- *

- * Examlples: {@code toColorString(Color.red) == "#ff0000"}, - * {@code toColorString(new Color(0xcc, 0xcc, 0xcc)) == "#cccccc"}. - * - * @param pColor the color - * @return A String representation of the color on HTML/CSS form - * @todo Consider moving to ImageUtil? - */ - public static String toColorString(Color pColor) { - // Not a color... - if (pColor == null) { - return null; - } - - StringBuilder str = new StringBuilder(Integer.toHexString(pColor.getRGB())); - - // Make sure string is 8 chars - for (int i = str.length(); i < 8; i++) { - str.insert(0, '0'); - } - - // All opaque is default - if (str.charAt(0) == 'f' && str.charAt(1) == 'f') { - str.delete(0, 2); - } - - // Prepend hash - return str.insert(0, '#').toString(); - } - - /** - * Tests a string, to see if it is an number (element of Z). - * Valid integers are positive natural numbers (1, 2, 3, ...), - * their negatives (?1, ?2, ?3, ...) and the number zero. - *

- * Note that there is no guarantees made, that this number can be - * represented as either an int or a long. - * - * @param pString The string to check. - * @return true if the String is a natural number. - */ - public static boolean isNumber(String pString) { - if (isEmpty(pString)) { - return false; - } - - // Special case for first char, may be minus sign ('-') - char ch = pString.charAt(0); - if (!(ch == '-' || Character.isDigit(ch))) { - return false; - } - - // Test every char - for (int i = 1; i < pString.length(); i++) { - if (!Character.isDigit(pString.charAt(i))) { - return false; - } - } - - // All digits must be a natural number - return true; - } - - /* - * This version is benchmarked against toStringArray and found to be - * increasingly slower, the more elements the string contains. - * Kept here - */ - - /** - * Removes all occurences of a specific character in a string. - * This method is not design for efficiency! - *

- * - * @param pSource - * @param pSubstring - * @param pPosition - * @return the modified string. - */ - - /* - public static String removeChar(String pSourceString, final char pBadChar) { - - char[] sourceCharArray = pSourceString.toCharArray(); - List modifiedCharList = new Vector(sourceCharArray.length, 1); - - // Filter the string - for (int i = 0; i < sourceCharArray.length; i++) { - if (sourceCharArray[i] != pBadChar) { - modifiedCharList.add(new Character(sourceCharArray[i])); - } - } - - // Clean the character list - modifiedCharList = (List) CollectionUtil.purifyCollection((Collection) modifiedCharList); - - // Create new modified String - char[] modifiedCharArray = new char[modifiedCharList.size()]; - for (int i = 0; i < modifiedCharArray.length; i++) { - modifiedCharArray[i] = ((Character) modifiedCharList.get(i)).charValue(); - } - - return new String(modifiedCharArray); - } - */ - - /** - * - * This method is not design for efficiency! - *

- * @param pSourceString The String for modification. - * @param pBadChars The char array containing the characters to remove from the source string. - * @return the modified string. - * @-deprecated Not tested yet! - * - */ - - /* - public static String removeChars(String pSourceString, final char[] pBadChars) { - - char[] sourceCharArray = pSourceString.toCharArray(); - List modifiedCharList = new Vector(sourceCharArray.length, 1); - - Map badCharMap = new Hashtable(); - Character dummyChar = new Character('*'); - for (int i = 0; i < pBadChars.length; i++) { - badCharMap.put(new Character(pBadChars[i]), dummyChar); - } - - // Filter the string - for (int i = 0; i < sourceCharArray.length; i++) { - Character arrayChar = new Character(sourceCharArray[i]); - if (!badCharMap.containsKey(arrayChar)) { - modifiedCharList.add(new Character(sourceCharArray[i])); - } - } - - // Clean the character list - modifiedCharList = (List) CollectionUtil.purifyCollection((Collection) modifiedCharList); - - // Create new modified String - char[] modifiedCharArray = new char[modifiedCharList.size()]; - for (int i = 0; i < modifiedCharArray.length; i++) { - modifiedCharArray[i] = ((Character) modifiedCharList.get(i)).charValue(); - } - - return new String(modifiedCharArray); - - } - */ - - /** - * Ensures that a string includes a given substring at a given position. - *

- * Extends the string with a given string if it is not already there. - * E.g an URL "www.vg.no", to "http://www.vg.no". - * - * @param pSource The source string. - * @param pSubstring The substring to include. - * @param pPosition The location of the fill-in, the index starts with 0. - * @return the string, with the substring at the given location. - */ - static String ensureIncludesAt(String pSource, String pSubstring, int pPosition) { - StringBuilder newString = new StringBuilder(pSource); - - try { - String existingSubstring = pSource.substring(pPosition, pPosition + pSubstring.length()); - - if (!existingSubstring.equalsIgnoreCase(pSubstring)) { - newString.insert(pPosition, pSubstring); - } - } - catch (Exception e) { - // Do something!? - } - return newString.toString(); - } - - /** - * Ensures that a string does not include a given substring at a given - * position. - *

- * Removes a given substring from a string if it is there. - * E.g an URL "http://www.vg.no", to "www.vg.no". - * - * @param pSource The source string. - * @param pSubstring The substring to check and possibly remove. - * @param pPosition The location of possible substring removal, the index starts with 0. - * @return the string, without the substring at the given location. - */ - static String ensureExcludesAt(String pSource, String pSubstring, int pPosition) { - StringBuilder newString = new StringBuilder(pSource); - - try { - String existingString = pSource.substring(pPosition + 1, pPosition + pSubstring.length() + 1); - - if (!existingString.equalsIgnoreCase(pSubstring)) { - newString.delete(pPosition, pPosition + pSubstring.length()); - } - } - catch (Exception e) { - // Do something!? - } - return newString.toString(); - } - - /** - * Gets the first substring between the given string boundaries. - *

- * - * @param pSource The source string. - * @param pBeginBoundaryString The string that marks the beginning. - * @param pEndBoundaryString The string that marks the end. - * @param pOffset The index to start searching in the source - * string. If it is less than 0, the index will be set to 0. - * @return the substring demarcated by the given string boundaries or null - * if not both string boundaries are found. - */ - public static String substring(final String pSource, final String pBeginBoundaryString, final String pEndBoundaryString, - final int pOffset) { - // Check offset - int offset = (pOffset < 0) - ? 0 - : pOffset; - - // Find the start index - int startIndex = pSource.indexOf(pBeginBoundaryString, offset) + pBeginBoundaryString.length(); - - if (startIndex < 0) { - return null; - } - - // Find the end index - int endIndex = pSource.indexOf(pEndBoundaryString, startIndex); - - if (endIndex < 0) { - return null; - } - return pSource.substring(startIndex, endIndex); - } - - /** - * Removes the first substring demarcated by the given string boundaries. - *

- * - * @param pSource The source string. - * @param pBeginBoundaryChar The character that marks the beginning of the - * unwanted substring. - * @param pEndBoundaryChar The character that marks the end of the - * unwanted substring. - * @param pOffset The index to start searching in the source - * string. If it is less than 0, the index will be set to 0. - * @return the source string with all the demarcated substrings removed, - * included the demarcation characters. - * @deprecated this method actually removes all demarcated substring.. doesn't it? - */ - - /*public*/ - static String removeSubstring(final String pSource, final char pBeginBoundaryChar, final char pEndBoundaryChar, final int pOffset) { - StringBuilder filteredString = new StringBuilder(); - boolean insideDemarcatedArea = false; - char[] charArray = pSource.toCharArray(); - - for (char c : charArray) { - if (!insideDemarcatedArea) { - if (c == pBeginBoundaryChar) { - insideDemarcatedArea = true; - } - else { - filteredString.append(c); - } - } - else { - if (c == pEndBoundaryChar) { - insideDemarcatedArea = false; - } - } - } - return filteredString.toString(); - } - - /** - * Removes all substrings demarcated by the given string boundaries. - *

- * - * @param pSource The source string. - * @param pBeginBoundaryChar The character that marks the beginning of the unwanted substring. - * @param pEndBoundaryChar The character that marks the end of the unwanted substring. - * @return the source string with all the demarcated substrings removed, included the demarcation characters. - */ - /*public*/ - static String removeSubstrings(final String pSource, final char pBeginBoundaryChar, final char pEndBoundaryChar) { - StringBuilder filteredString = new StringBuilder(); - boolean insideDemarcatedArea = false; - char[] charArray = pSource.toCharArray(); - - for (char c : charArray) { - if (!insideDemarcatedArea) { - if (c == pBeginBoundaryChar) { - insideDemarcatedArea = true; - } - else { - filteredString.append(c); - } - } - else { - if (c == pEndBoundaryChar) { - insideDemarcatedArea = false; - } - } - } - return filteredString.toString(); - } - - /** - * Gets the first element of a {@code String} containing string elements delimited by a given delimiter. - * NB - Straightforward implementation! - *

- * - * @param pSource The source string. - * @param pDelimiter The delimiter used in the source string. - * @return The last string element. - * @todo This method should be re-implemented for more efficient execution. - */ - public static String getFirstElement(final String pSource, final String pDelimiter) { - if (pDelimiter == null) { - throw new IllegalArgumentException("delimiter == null"); - } - - if (StringUtil.isEmpty(pSource)) { - return pSource; - } - - int idx = pSource.indexOf(pDelimiter); - if (idx >= 0) { - return pSource.substring(0, idx); - } - return pSource; - } - - /** - * Gets the last element of a {@code String} containing string elements - * delimited by a given delimiter. - * NB - Straightforward implementation! - *

- * - * @param pSource The source string. - * @param pDelimiter The delimiter used in the source string. - * @return The last string element. - */ - public static String getLastElement(final String pSource, final String pDelimiter) { - if (pDelimiter == null) { - throw new IllegalArgumentException("delimiter == null"); - } - - if (StringUtil.isEmpty(pSource)) { - return pSource; - } - int idx = pSource.lastIndexOf(pDelimiter); - if (idx >= 0) { - return pSource.substring(idx + 1); - } - return pSource; - } - - /** - * Converts a string array to a string of comma-separated values. - * - * @param pStringArray the string array - * @return A string of comma-separated values - */ - public static String toCSVString(Object[] pStringArray) { - return toCSVString(pStringArray, ", "); - } - - /** - * Converts a string array to a string separated by the given delimiter. - * - * @param pStringArray the string array - * @param pDelimiterString the delimiter string - * @return string of delimiter separated values - * @throws IllegalArgumentException if {@code pDelimiterString == null} - */ - public static String toCSVString(Object[] pStringArray, String pDelimiterString) { - if (pStringArray == null) { - return ""; - } - if (pDelimiterString == null) { - throw new IllegalArgumentException("delimiter == null"); - } - - StringBuilder buffer = new StringBuilder(); - for (int i = 0; i < pStringArray.length; i++) { - if (i > 0) { - buffer.append(pDelimiterString); - } - - buffer.append(pStringArray[i]); - } - - return buffer.toString(); - } - - /** - * @param pObject the object - * @return a deep string representation of the given object - */ - public static String deepToString(Object pObject) { - return deepToString(pObject, false, 1); - } - - /** - * @param pObject the object - * @param pDepth the maximum depth - * @param pForceDeep {@code true} to force deep {@code toString}, even - * if object overrides toString - * @return a deep string representation of the given object - * @todo Array handling (print full type and length) - * @todo Register handlers for specific toDebugString handling? :-) - */ - public static String deepToString(Object pObject, boolean pForceDeep, int pDepth) { - // Null is null - if (pObject == null) { - return null; - } - - // Implements toString, use it as-is unless pForceDeep - if (!pForceDeep && !isIdentityToString(pObject)) { - return pObject.toString(); - } - - StringBuilder buffer = new StringBuilder(); - - if (pObject.getClass().isArray()) { - // Special array handling - Class componentClass = pObject.getClass(); - while (componentClass.isArray()) { - buffer.append('['); - buffer.append(Array.getLength(pObject)); - buffer.append(']'); - componentClass = componentClass.getComponentType(); - } - buffer.insert(0, componentClass); - buffer.append(" {hashCode="); - buffer.append(Integer.toHexString(pObject.hashCode())); - buffer.append("}"); - } - else { - // Append toString value only if overridden - if (isIdentityToString(pObject)) { - buffer.append(" {"); - } - else { - buffer.append(" {toString="); - buffer.append(pObject.toString()); - buffer.append(", "); - } - buffer.append("hashCode="); - buffer.append(Integer.toHexString(pObject.hashCode())); - // Loop through, and filter out any getters - Method[] methods = pObject.getClass().getMethods(); - for (Method method : methods) { - // Filter only public methods - if (Modifier.isPublic(method.getModifiers())) { - String methodName = method.getName(); - - // Find name of property - String name = null; - if (!methodName.equals("getClass") - && methodName.length() > 3 && methodName.startsWith("get") - && Character.isUpperCase(methodName.charAt(3))) { - name = methodName.substring(3); - } - else if (methodName.length() > 2 && methodName.startsWith("is") - && Character.isUpperCase(methodName.charAt(2))) { - name = methodName.substring(2); - } - - if (name != null) { - // If lowercase name, convert, else keep case - if (name.length() > 1 && Character.isLowerCase(name.charAt(1))) { - name = Character.toLowerCase(name.charAt(0)) + name.substring(1); - } - - Class[] paramTypes = method.getParameterTypes();// involves array copying... - boolean hasParams = (paramTypes != null && paramTypes.length > 0); - boolean isVoid = Void.TYPE.equals(method.getReturnType()); - - // Filter return type & parameters - if (!isVoid && !hasParams) { - try { - Object value = method.invoke(pObject); - buffer.append(", "); - buffer.append(name); - buffer.append('='); - if (pDepth != 0 && value != null && isIdentityToString(value)) { - buffer.append(deepToString(value, pForceDeep, pDepth > 0 ? pDepth - 1 : -1)); - } - else { - buffer.append(value); - } - } - catch (Exception e) { - // Next..! - } - } - } - } - } - buffer.append('}'); - - // Get toString from original object - buffer.insert(0, pObject.getClass().getName()); - } - - return buffer.toString(); - } - - /** - * Tests if the {@code toString} method of the given object is inherited - * from {@code Object}. - * - * @param pObject the object - * @return {@code true} if toString of class Object - */ - private static boolean isIdentityToString(Object pObject) { - try { - Method toString = pObject.getClass().getMethod("toString"); - if (toString.getDeclaringClass() == Object.class) { - return true; - } - } - catch (Exception ignore) { - // Ignore - } - - return false; - } - - /** - * Returns a string on the same format as {@code Object.toString()}. - * - * @param pObject the object - * @return the object as a {@code String} on the format of - * {@code Object.toString()} - */ - public static String identityToString(Object pObject) { - if (pObject == null) { - return null; - } - else { - return pObject.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(pObject)); - } - } - - /** - * Tells whether or not the given string string matches the given regular - * expression. - *

- * An invocation of this method of the form - * matches(str, regex) yields exactly the - * same result as the expression - *

- *

{@link Pattern}. - * {@link Pattern#matches(String, CharSequence) matches} - * (regex, str)
- * - * @param pString the string - * @param pRegex the regular expression to which this string is to be matched - * @return {@code true} if, and only if, this string matches the - * given regular expression - * @throws PatternSyntaxException if the regular expression's syntax is invalid - * @see Pattern - * @see String#matches(String) - */ - public boolean matches(String pString, String pRegex) throws PatternSyntaxException { - return Pattern.matches(pRegex, pString); - } - - /** - * Replaces the first substring of the given string that matches the given - * regular expression with the given pReplacement. - *

- * An invocation of this method of the form - * replaceFirst(str, regex, repl) - * yields exactly the same result as the expression - *

- *

- * {@link Pattern}.{@link Pattern#compile compile}(regex). - * {@link Pattern#matcher matcher}(str). - * {@link java.util.regex.Matcher#replaceFirst replaceFirst}(repl)
- * - * @param pString the string - * @param pRegex the regular expression to which this string is to be matched - * @param pReplacement the replacement text - * @return The resulting {@code String} - * @throws PatternSyntaxException if the regular expression's syntax is invalid - * @see Pattern - * @see java.util.regex.Matcher#replaceFirst(String) - */ - public String replaceFirst(String pString, String pRegex, String pReplacement) { - return Pattern.compile(pRegex).matcher(pString).replaceFirst(pReplacement); - } - - /** - * Replaces each substring of this string that matches the given - * regular expression with the given pReplacement. - *

- * An invocation of this method of the form - * replaceAll(str, pRegex, repl<) - * yields exactly the same result as the expression - *

- *

- * {@link Pattern}.{@link Pattern#compile compile}(pRegex). - * {@link Pattern#matcher matcher}(str{@code ). - * {@link java.util.regex.Matcher#replaceAll replaceAll}(}repl{@code )}
- * - * @param pString the string - * @param pRegex the regular expression to which this string is to be matched - * @param pReplacement the replacement string - * @return The resulting {@code String} - * @throws PatternSyntaxException if the regular expression's syntax is invalid - * @see Pattern - * @see String#replaceAll(String,String) - */ - public String replaceAll(String pString, String pRegex, String pReplacement) { - return Pattern.compile(pRegex).matcher(pString).replaceAll(pReplacement); - } - - /** - * Splits this string around matches of the given regular expression. - *

- * The array returned by this method contains each substring of this - * string that is terminated by another substring that matches the given - * expression or is terminated by the end of the string. The substrings in - * the array are in the order in which they occur in this string. If the - * expression does not match any part of the input then the resulting array - * has just one element, namely this string. - *

- * The {@code pLimit} parameter controls the number of times the - * pattern is applied and therefore affects the length of the resulting - * array. If the pLimit n is greater than zero then the pattern - * will be applied at most n - 1 times, the array's - * length will be no greater than n, and the array's last entry - * will contain all input beyond the last matched delimiter. If n - * is non-positive then the pattern will be applied as many times as - * possible and the array can have any length. If n is zero then - * the pattern will be applied as many times as possible, the array can - * have any length, and trailing empty strings will be discarded. - *

- * An invocation of this method of the form - * split(str, regex, n) - * yields the same result as the expression - *

- *

{@link Pattern}. - * {@link Pattern#compile compile}(regex). - * {@link Pattern#split(CharSequence,int) split}(str, n) - *
- * - * @param pString the string - * @param pRegex the delimiting regular expression - * @param pLimit the result threshold, as described above - * @return the array of strings computed by splitting this string - * around matches of the given regular expression - * @throws PatternSyntaxException - * if the regular expression's syntax is invalid - * @see Pattern - * @see String#split(String,int) - */ - public String[] split(String pString, String pRegex, int pLimit) { - return Pattern.compile(pRegex).split(pString, pLimit); - } - - /** - * Splits this string around matches of the given regular expression. - *

- * This method works as if by invoking the two-argument - * {@link #split(String,String,int) split} method with the given - * expression and a limit argument of zero. - * Trailing empty strings are therefore not included in the resulting array. - * - * @param pString the string - * @param pRegex the delimiting regular expression - * @return the array of strings computed by splitting this string - * around matches of the given regular expression - * @throws PatternSyntaxException if the regular expression's syntax is invalid - * @see Pattern - * @see String#split(String) - */ - public String[] split(String pString, String pRegex) { - return split(pString, pRegex, 0); - } - - /** - * Converts the input string - * from camel-style (Java in-fix) naming convention - * to Lisp-style naming convention (hyphen delimitted, all lower case). - * Other characters in the string are left untouched. - *

- * Eg. - * {@code "foo" => "foo"}, - * {@code "fooBar" => "foo-bar"}, - * {@code "myURL" => "my-url"}, - * {@code "HttpRequestWrapper" => "http-request-wrapper"} - * {@code "HttpURLConnection" => "http-url-connection"} - * {@code "my45Caliber" => "my-45-caliber"} - * {@code "allready-lisp" => "allready-lisp"} - * - * @param pString the camel-style input string - * @return the string converted to lisp-style naming convention - * @throws IllegalArgumentException if {@code pString == null} - * @see #lispToCamel(String) - */ - // TODO: RefactorMe! - public static String camelToLisp(final String pString) { - if (pString == null) { - throw new IllegalArgumentException("string == null"); - } - if (pString.length() == 0) { - return pString; - } - - StringBuilder buf = null; - int lastPos = 0; - boolean inCharSequence = false; - boolean inNumberSequence = false; - - // NOTE: Start at index 1, as first letter should never be hyphen - for (int i = 1; i < pString.length(); i++) { - char current = pString.charAt(i); - if (Character.isUpperCase(current)) { - // Init buffer if necessary - if (buf == null) { - buf = new StringBuilder(pString.length() + 3);// Allow for some growth - } - - if (inNumberSequence) { - // Sequence end - inNumberSequence = false; - - buf.append(pString.substring(lastPos, i)); - if (current != '-') { - buf.append('-'); - } - lastPos = i; - continue; - } - - // Treat multiple uppercase chars as single word - char previous = pString.charAt(i - 1); - if (i == lastPos || Character.isUpperCase(previous)) { - inCharSequence = true; - continue; - } - - // Append word - buf.append(pString.substring(lastPos, i).toLowerCase()); - if (previous != '-') { - buf.append('-'); - } - buf.append(Character.toLowerCase(current)); - - lastPos = i + 1; - } - else if (Character.isDigit(current)) { - // Init buffer if necessary - if (buf == null) { - buf = new StringBuilder(pString.length() + 3);// Allow for some growth - } - - if (inCharSequence) { - // Sequence end - inCharSequence = false; - - buf.append(pString.substring(lastPos, i).toLowerCase()); - if (current != '-') { - buf.append('-'); - } - lastPos = i; - continue; - } - - // Treat multiple digits as single word - char previous = pString.charAt(i - 1); - if (i == lastPos || Character.isDigit(previous)) { - inNumberSequence = true; - continue; - } - - // Append word - buf.append(pString.substring(lastPos, i).toLowerCase()); - if (previous != '-') { - buf.append('-'); - } - buf.append(Character.toLowerCase(current)); - - lastPos = i + 1; - } - else if (inNumberSequence) { - // Sequence end - inNumberSequence = false; - - buf.append(pString.substring(lastPos, i)); - if (current != '-') { - buf.append('-'); - } - lastPos = i; - } - else if (inCharSequence) { - // Sequence end - inCharSequence = false; - - // NOTE: Special treatment! Last upper case, is first char in - // next word, not last char in this word - buf.append(pString.substring(lastPos, i - 1).toLowerCase()); - if (current != '-') { - buf.append('-'); - } - lastPos = i - 1; - } - } - - if (buf != null) { - // Append the rest - buf.append(pString.substring(lastPos).toLowerCase()); - return buf.toString(); - } - else { - return Character.isUpperCase(pString.charAt(0)) ? pString.toLowerCase() : pString; - } - } - - /** - * Converts the input string - * from Lisp-style naming convention (hyphen delimitted, all lower case) - * to camel-style (Java in-fix) naming convention. - * Other characters in the string are left untouched. - *

- * Eg. - * {@code "foo" => "foo"}, - * {@code "foo-bar" => "fooBar"}, - * {@code "http-request-wrapper" => "httpRequestWrapper"} - * {@code "my-45-caliber" => "my45Caliber"} - * {@code "allreadyCamel" => "allreadyCamel"} - *

- * - * @param pString the lisp-style input string - * @return the string converted to camel-style - * @throws IllegalArgumentException if {@code pString == null} - * @see #lispToCamel(String,boolean) - * @see #camelToLisp(String) - */ - public static String lispToCamel(final String pString) { - return lispToCamel(pString, false); - } - - /** - * Converts the input string - * from Lisp-style naming convention (hyphen delimitted, all lower case) - * to camel-style (Java in-fix) naming convention. - * Other characters in the string are left untouched. - *

- * To create a string starting with a lower case letter - * (like Java variable names, etc), - * specify the {@code pFirstUpperCase} paramter to be {@code false}. - * Eg. - * {@code "foo" => "foo"}, - * {@code "foo-bar" => "fooBar"}, - * {@code "allreadyCamel" => "allreadyCamel"} - *

- * To create a string starting with an upper case letter - * (like Java class name, etc), - * specify the {@code pFirstUpperCase} paramter to be {@code true}. - * Eg. - * {@code "http-request-wrapper" => "HttpRequestWrapper"} - * {@code "my-45-caliber" => "My45Caliber"} - *

- * - * @param pString the lisp-style input string - * @param pFirstUpperCase {@code true} if the first char should be - * upper case - * @return the string converted to camel-style - * @throws IllegalArgumentException if {@code pString == null} - * @see #camelToLisp(String) - */ - public static String lispToCamel(final String pString, final boolean pFirstUpperCase) { - if (pString == null) { - throw new IllegalArgumentException("string == null"); - } - if (pString.length() == 0) { - return pString; - } - - StringBuilder buf = null; - int lastPos = 0; - - for (int i = 0; i < pString.length(); i++) { - char current = pString.charAt(i); - if (current == '-') { - - // Init buffer if necessary - if (buf == null) { - buf = new StringBuilder(pString.length() - 1);// Can't be larger - } - - // Append with upper case - if (lastPos != 0 || pFirstUpperCase) { - buf.append(Character.toUpperCase(pString.charAt(lastPos))); - lastPos++; - } - - buf.append(pString.substring(lastPos, i).toLowerCase()); - lastPos = i + 1; - } - } - - if (buf != null) { - buf.append(Character.toUpperCase(pString.charAt(lastPos))); - buf.append(pString.substring(lastPos + 1).toLowerCase()); - return buf.toString(); - } - else { - if (pFirstUpperCase && !Character.isUpperCase(pString.charAt(0))) { - return capitalize(pString, 0); - } - else - if (!pFirstUpperCase && Character.isUpperCase(pString.charAt(0))) { - return Character.toLowerCase(pString.charAt(0)) + pString.substring(1); - } - - return pString; - } - } - - public static String reverse(final String pString) { - final char[] chars = new char[pString.length()]; - pString.getChars(0, chars.length, chars, 0); - - for (int i = 0; i < chars.length / 2; i++) { - char temp = chars[i]; - chars[i] = chars[chars.length - 1 - i]; - chars[chars.length - 1 - i] = temp; - } - - return new String(chars); - } +/* + * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.util.StringTokenIterator; + +import java.awt.*; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.nio.charset.UnsupportedCharsetException; +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * A utility class with some useful string manipulation methods. + * + * @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/lang/StringUtil.java#2 $ + * return values, null-value handling and parameter names (cosmetics). + */ +// TODO: Consistency check: Method names, parameter sequence, Exceptions, +public final class StringUtil { + + /** + * The default delimiter string, used by the {@code toXXXArray()} + * methods. + * Its value is {@code ", \t\n\r\f"}. + * + * + * @see #toStringArray(String) + * @see #toIntArray(String) + * @see #toLongArray(String) + * @see #toDoubleArray(String) + */ + public final static String DELIMITER_STRING = ", \t\n\r\f"; + + // Avoid constructor showing up in API doc + private StringUtil() { + } + + /** + * 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. + * + * @param pData the bytes to be decoded to characters + * @param pOffset the index of the first byte to decode + * @param pLength the number of bytes to decode + * @param pCharset the name of a supported character set + * @return a newly created string. + * @throws UnsupportedCharsetException + * + * @see String#String(byte[], int, int, String) + */ + public static String decode(final byte[] pData, final int pOffset, final int pLength, final String pCharset) { + try { + return new String(pData, pOffset, pLength, pCharset); + } + catch (UnsupportedEncodingException e) { + throw new UnsupportedCharsetException(pCharset); + } + } + + /** + * Returns the value of the given {@code Object}, as a {@code String}. + * Unlike String.valueOf, this method returns {@code null} + * instead of the {@code String} "null", if {@code null} is given as + * the argument. + * + * @param pObj the Object to find the {@code String} value of. + * @return the String value of the given object, or {@code null} if the + * {@code pObj} == {@code null}. + * @see String#valueOf(Object) + * @see String#toString() + */ + public static String valueOf(Object pObj) { + return ((pObj != null) ? pObj.toString() : null); + } + + /** + * Converts a string to uppercase. + * + * @param pString the string to convert + * @return the string converted to uppercase, or null if the argument was + * null. + */ + public static String toUpperCase(String pString) { + if (pString != null) { + return pString.toUpperCase(); + } + return null; + } + + /** + * Converts a string to lowercase. + * + * @param pString the string to convert + * @return the string converted to lowercase, or null if the argument was + * null. + */ + public static String toLowerCase(String pString) { + if (pString != null) { + return pString.toLowerCase(); + } + return null; + } + + /** + * Tests if a String is null, or contains nothing but white-space. + * + * @param pString The string to test + * @return true if the string is null or contains only whitespace, + * otherwise false. + */ + public static boolean isEmpty(String pString) { + return ((pString == null) || (pString.trim().length() == 0)); + } + + /** + * Tests a string array, to see if all items are null or an empty string. + * + * @param pStringArray The string array to check. + * @return true if the string array is null or only contains string items + * that are null or contain only whitespace, otherwise false. + */ + public static boolean isEmpty(String[] pStringArray) { + // No elements to test + if (pStringArray == null) { + return true; + } + + // Test all the elements + for (String string : pStringArray) { + if (!isEmpty(string)) { + return false; + } + } + + // All elements are empty + return true; + } + + /** + * Tests if a string contains another string. + * + * @param pContainer The string to test + * @param pLookFor The string to look for + * @return {@code true} if the container string is contains the string, and + * both parameters are non-{@code null}, otherwise {@code false}. + */ + public static boolean contains(String pContainer, String pLookFor) { + return ((pContainer != null) && (pLookFor != null) && (pContainer.indexOf(pLookFor) >= 0)); + } + + /** + * Tests if a string contains another string, ignoring case. + * + * @param pContainer The string to test + * @param pLookFor The string to look for + * @return {@code true} if the container string is contains the string, and + * both parameters are non-{@code null}, otherwise {@code false}. + * @see #contains(String,String) + */ + public static boolean containsIgnoreCase(String pContainer, String pLookFor) { + return indexOfIgnoreCase(pContainer, pLookFor, 0) >= 0; + } + + /** + * Tests if a string contains a specific character. + * + * @param pString The string to check. + * @param pChar The character to search for. + * @return true if the string contains the specific character. + */ + public static boolean contains(final String pString, final int pChar) { + return ((pString != null) && (pString.indexOf(pChar) >= 0)); + } + + /** + * Tests if a string contains a specific character, ignoring case. + * + * @param pString The string to check. + * @param pChar The character to search for. + * @return true if the string contains the specific character. + */ + public static boolean containsIgnoreCase(String pString, int pChar) { + return ((pString != null) + && ((pString.indexOf(Character.toLowerCase((char) pChar)) >= 0) + || (pString.indexOf(Character.toUpperCase((char) pChar)) >= 0))); + + // NOTE: I don't convert the string to uppercase, but instead test + // the string (potentially) two times, as this is more efficient for + // long strings (in most cases). + } + + /** + * Returns the index within this string of the first occurrence of the + * specified substring. + * + * @param pString The string to test + * @param pLookFor The string to look for + * @return if the string argument occurs as a substring within this object, + * then the index of the first character of the first such substring is + * returned; if it does not occur as a substring, -1 is returned. + * @see String#indexOf(String) + */ + public static int indexOfIgnoreCase(String pString, String pLookFor) { + return indexOfIgnoreCase(pString, pLookFor, 0); + } + + /** + * Returns the index within this string of the first occurrence of the + * specified substring, starting at the specified index. + * + * @param pString The string to test + * @param pLookFor The string to look for + * @param pPos The first index to test + * @return if the string argument occurs as a substring within this object, + * then the index of the first character of the first such substring is + * returned; if it does not occur as a substring, -1 is returned. + * @see String#indexOf(String,int) + */ + public static int indexOfIgnoreCase(String pString, String pLookFor, int pPos) { + if ((pString == null) || (pLookFor == null)) { + return -1; + } + if (pLookFor.length() == 0) { + return pPos;// All strings "contains" the empty string + } + if (pLookFor.length() > pString.length()) { + return -1;// Cannot contain string longer than itself + } + + // Get first char + char firstL = Character.toLowerCase(pLookFor.charAt(0)); + char firstU = Character.toUpperCase(pLookFor.charAt(0)); + int indexLower = 0; + int indexUpper = 0; + + for (int i = pPos; i <= (pString.length() - pLookFor.length()); i++) { + + // Peek for first char + indexLower = ((indexLower >= 0) && (indexLower <= i)) + ? pString.indexOf(firstL, i) + : indexLower; + indexUpper = ((indexUpper >= 0) && (indexUpper <= i)) + ? pString.indexOf(firstU, i) + : indexUpper; + if (indexLower < 0) { + if (indexUpper < 0) { + return -1;// First char not found + } + else { + i = indexUpper;// Only upper + } + } + else if (indexUpper < 0) { + i = indexLower;// Only lower + } + else { + + // Both found, select first occurence + i = (indexLower < indexUpper) + ? indexLower + : indexUpper; + } + + // Only one? + if (pLookFor.length() == 1) { + return i;// The only char found! + } + + // Test if we still have enough chars + else if (i > (pString.length() - pLookFor.length())) { + return -1; + } + + // Test if last char equals! (regionMatches is expensive) + else if ((pString.charAt(i + pLookFor.length() - 1) != Character.toLowerCase(pLookFor.charAt(pLookFor.length() - 1))) + && (pString.charAt(i + pLookFor.length() - 1) != Character.toUpperCase(pLookFor.charAt(pLookFor.length() - 1)))) { + continue;// Nope, try next + } + + // Test from second char, until second-last char + else if ((pLookFor.length() <= 2) || pString.regionMatches(true, i + 1, pLookFor, 1, pLookFor.length() - 2)) { + return i; + } + } + return -1; + } + + /** + * Returns the index within this string of the rightmost occurrence of the + * specified substring. The rightmost empty string "" is considered to + * occur at the index value {@code pString.length() - 1}. + * + * @param pString The string to test + * @param pLookFor The string to look for + * @return If the string argument occurs one or more times as a substring + * within this object at a starting index no greater than fromIndex, then + * the index of the first character of the last such substring is returned. + * If it does not occur as a substring starting at fromIndex or earlier, -1 + * is returned. + * @see String#lastIndexOf(String) + */ + public static int lastIndexOfIgnoreCase(String pString, String pLookFor) { + return lastIndexOfIgnoreCase(pString, pLookFor, pString != null ? pString.length() - 1 : -1); + } + + /** + * Returns the index within this string of the rightmost occurrence of the + * specified substring. The rightmost empty string "" is considered to + * occur at the index value {@code pPos} + * + * @param pString The string to test + * @param pLookFor The string to look for + * @param pPos The last index to test + * @return If the string argument occurs one or more times as a substring + * within this object at a starting index no greater than fromIndex, then + * the index of the first character of the last such substring is returned. + * If it does not occur as a substring starting at fromIndex or earlier, -1 + * is returned. + * @see String#lastIndexOf(String,int) + */ + public static int lastIndexOfIgnoreCase(String pString, String pLookFor, int pPos) { + if ((pString == null) || (pLookFor == null)) { + return -1; + } + if (pLookFor.length() == 0) { + return pPos;// All strings "contains" the empty string + } + if (pLookFor.length() > pString.length()) { + return -1;// Cannot contain string longer than itself + } + + // Get first char + char firstL = Character.toLowerCase(pLookFor.charAt(0)); + char firstU = Character.toUpperCase(pLookFor.charAt(0)); + int indexLower = pPos; + int indexUpper = pPos; + + for (int i = pPos; i >= 0; i--) { + + // Peek for first char + indexLower = ((indexLower >= 0) && (indexLower >= i)) + ? pString.lastIndexOf(firstL, i) + : indexLower; + indexUpper = ((indexUpper >= 0) && (indexUpper >= i)) + ? pString.lastIndexOf(firstU, i) + : indexUpper; + if (indexLower < 0) { + if (indexUpper < 0) { + return -1;// First char not found + } + else { + i = indexUpper;// Only upper + } + } + else if (indexUpper < 0) { + i = indexLower;// Only lower + } + else { + + // Both found, select last occurence + i = (indexLower > indexUpper) + ? indexLower + : indexUpper; + } + + // Only one? + if (pLookFor.length() == 1) { + return i;// The only char found! + } + + // Test if we still have enough chars + else if (i > (pString.length() - pLookFor.length())) { + //return -1; + continue; + } + + // Test if last char equals! (regionMatches is expensive) + else + if ((pString.charAt(i + pLookFor.length() - 1) != Character.toLowerCase(pLookFor.charAt(pLookFor.length() - 1))) + && (pString.charAt(i + pLookFor.length() - 1) != Character.toUpperCase(pLookFor.charAt(pLookFor.length() - 1)))) { + continue;// Nope, try next + } + + // Test from second char, until second-last char + else + if ((pLookFor.length() <= 2) || pString.regionMatches(true, i + 1, pLookFor, 1, pLookFor.length() - 2)) { + return i; + } + } + return -1; + } + + /** + * Returns the index within this string of the first occurrence of the + * specified character. + * + * @param pString The string to test + * @param pChar The character to look for + * @return if the string argument occurs as a substring within this object, + * then the index of the first character of the first such substring is + * returned; if it does not occur as a substring, -1 is returned. + * @see String#indexOf(int) + */ + public static int indexOfIgnoreCase(String pString, int pChar) { + return indexOfIgnoreCase(pString, pChar, 0); + } + + /** + * Returns the index within this string of the first occurrence of the + * specified character, starting at the specified index. + * + * @param pString The string to test + * @param pChar The character to look for + * @param pPos The first index to test + * @return if the string argument occurs as a substring within this object, + * then the index of the first character of the first such substring is + * returned; if it does not occur as a substring, -1 is returned. + * @see String#indexOf(int,int) + */ + public static int indexOfIgnoreCase(String pString, int pChar, int pPos) { + if ((pString == null)) { + return -1; + } + + // Get first char + char lower = Character.toLowerCase((char) pChar); + char upper = Character.toUpperCase((char) pChar); + int indexLower; + int indexUpper; + + // Test for char + indexLower = pString.indexOf(lower, pPos); + indexUpper = pString.indexOf(upper, pPos); + if (indexLower < 0) { + + /* if (indexUpper < 0) + return -1; // First char not found + else */ + return indexUpper;// Only upper + } + else if (indexUpper < 0) { + return indexLower;// Only lower + } + else { + + // Both found, select first occurence + return (indexLower < indexUpper) + ? indexLower + : indexUpper; + } + } + + /** + * Returns the index within this string of the last occurrence of the + * specified character. + * + * @param pString The string to test + * @param pChar The character to look for + * @return if the string argument occurs as a substring within this object, + * then the index of the first character of the first such substring is + * returned; if it does not occur as a substring, -1 is returned. + * @see String#lastIndexOf(int) + */ + public static int lastIndexOfIgnoreCase(String pString, int pChar) { + return lastIndexOfIgnoreCase(pString, pChar, pString != null ? pString.length() : -1); + } + + /** + * Returns the index within this string of the last occurrence of the + * specified character, searching backward starting at the specified index. + * + * @param pString The string to test + * @param pChar The character to look for + * @param pPos The last index to test + * @return if the string argument occurs as a substring within this object, + * then the index of the first character of the first such substring is + * returned; if it does not occur as a substring, -1 is returned. + * @see String#lastIndexOf(int,int) + */ + public static int lastIndexOfIgnoreCase(String pString, int pChar, int pPos) { + if ((pString == null)) { + return -1; + } + + // Get first char + char lower = Character.toLowerCase((char) pChar); + char upper = Character.toUpperCase((char) pChar); + int indexLower; + int indexUpper; + + // Test for char + indexLower = pString.lastIndexOf(lower, pPos); + indexUpper = pString.lastIndexOf(upper, pPos); + if (indexLower < 0) { + + /* if (indexUpper < 0) + return -1; // First char not found + else */ + return indexUpper;// Only upper + } + else if (indexUpper < 0) { + return indexLower;// Only lower + } + else { + + // Both found, select last occurence + return (indexLower > indexUpper) + ? indexLower + : indexUpper; + } + } + + /** + * Trims the argument string for whitespace on the left side only. + * + * @param pString the string to trim + * @return the string with no whitespace on the left, or {@code null} if + * the string argument is {@code null}. + * @see #rtrim + * @see String#trim() + */ + public static String ltrim(String pString) { + if ((pString == null) || (pString.length() == 0)) { + return pString;// Null or empty string + } + for (int i = 0; i < pString.length(); i++) { + if (!Character.isWhitespace(pString.charAt(i))) { + if (i == 0) { + return pString;// First char is not whitespace + } + else { + return pString.substring(i);// Return rest after whitespace + } + } + } + + // If all whitespace, return empty string + return ""; + } + + /** + * Trims the argument string for whitespace on the right side only. + * + * @param pString the string to trim + * @return the string with no whitespace on the right, or {@code null} if + * the string argument is {@code null}. + * @see #ltrim + * @see String#trim() + */ + public static String rtrim(String pString) { + if ((pString == null) || (pString.length() == 0)) { + return pString;// Null or empty string + } + for (int i = pString.length(); i > 0; i--) { + if (!Character.isWhitespace(pString.charAt(i - 1))) { + if (i == pString.length()) { + return pString;// First char is not whitespace + } + else { + return pString.substring(0, i);// Return before whitespace + } + } + } + + // If all whitespace, return empty string + return ""; + } + + /** + * Replaces a substring of a string with another string. All matches are + * replaced. + * + * @param pSource The source String + * @param pPattern The pattern to replace + * @param pReplace The new String to be inserted instead of the + * replace String + * @return The new String with the pattern replaced + */ + public static String replace(String pSource, String pPattern, String pReplace) { + if (pPattern.length() == 0) { + return pSource;// Special case: No pattern to replace + } + + int match; + int offset = 0; + StringBuilder result = new StringBuilder(); + + // Loop string, until last occurence of pattern, and replace + while ((match = pSource.indexOf(pPattern, offset)) != -1) { + // Append everything until pattern + result.append(pSource.substring(offset, match)); + // Append the replace string + result.append(pReplace); + offset = match + pPattern.length(); + } + + // Append rest of string and return + result.append(pSource.substring(offset)); + + return result.toString(); + } + + /** + * Replaces a substring of a string with another string, ignoring case. + * All matches are replaced. + * + * @param pSource The source String + * @param pPattern The pattern to replace + * @param pReplace The new String to be inserted instead of the + * replace String + * @return The new String with the pattern replaced + * @see #replace(String,String,String) + */ + public static String replaceIgnoreCase(String pSource, String pPattern, String pReplace) { + if (pPattern.length() == 0) { + return pSource;// Special case: No pattern to replace + } + int match; + int offset = 0; + StringBuilder result = new StringBuilder(); + + while ((match = indexOfIgnoreCase(pSource, pPattern, offset)) != -1) { + result.append(pSource.substring(offset, match)); + result.append(pReplace); + offset = match + pPattern.length(); + } + result.append(pSource.substring(offset)); + return result.toString(); + } + + /** + * Cuts a string between two words, before a sepcified length, if the + * string is longer than the maxium lenght. The string is optionally padded + * with the pad argument. The method assumes words to be separated by the + * space character (" "). + * Note that the maximum length argument is absolute, and will also include + * the length of the padding. + * + * @param pString The string to cut + * @param pMaxLen The maximum length before cutting + * @param pPad The string to append at the end, aftrer cutting + * @return The cutted string with padding, or the original string, if it + * was shorter than the max length. + * @see #pad(String,int,String,boolean) + */ + public static String cut(String pString, int pMaxLen, String pPad) { + if (pString == null) { + return null; + } + if (pPad == null) { + pPad = ""; + } + int len = pString.length(); + + if (len > pMaxLen) { + len = pString.lastIndexOf(' ', pMaxLen - pPad.length()); + } + else { + return pString; + } + return pString.substring(0, len) + pPad; + } + + /** + * Makes the Nth letter of a String uppercase. If the index is outside the + * the length of the argument string, the argument is simply returned. + * + * @param pString The string to capitalize + * @param pIndex The base-0 index of the char to capitalize. + * @return The capitalized string, or null, if a null argument was given. + */ + public static String capitalize(String pString, int pIndex) { + if (pIndex < 0) { + throw new IndexOutOfBoundsException("Negative index not allowed: " + pIndex); + } + if (pString == null || pString.length() <= pIndex) { + return pString; + } + + // This is the fastest method, according to my tests + + // Skip array duplication if allready capitalized + if (Character.isUpperCase(pString.charAt(pIndex))) { + return pString; + } + + // Convert to char array, capitalize and create new String + char[] charArray = pString.toCharArray(); + charArray[pIndex] = Character.toUpperCase(charArray[pIndex]); + return new String(charArray); + + /** + StringBuilder buf = new StringBuilder(pString); + buf.setCharAt(pIndex, Character.toUpperCase(buf.charAt(pIndex))); + return buf.toString(); + //*/ + + /** + return pString.substring(0, pIndex) + + Character.toUpperCase(pString.charAt(pIndex)) + + pString.substring(pIndex + 1); + //*/ + } + + /** + * Makes the first letter of a String uppercase. + * + * @param pString The string to capitalize + * @return The capitalized string, or null, if a null argument was given. + */ + public static String capitalize(String pString) { + return capitalize(pString, 0); + } + + /** + * Formats a number with leading zeroes, to a specified length. + * + * @param pNum The number to format + * @param pLen The number of digits + * @return A string containing the formatted number + * @throws IllegalArgumentException Thrown, if the number contains + * more digits than allowed by the length argument. + * @see #pad(String,int,String,boolean) + * @deprecated Use StringUtil.pad instead! + */ + + /*public*/ + static String formatNumber(long pNum, int pLen) throws IllegalArgumentException { + StringBuilder result = new StringBuilder(); + + if (pNum >= Math.pow(10, pLen)) { + throw new IllegalArgumentException("The number to format cannot contain more digits than the length argument specifies!"); + } + for (int i = pLen; i > 1; i--) { + if (pNum < Math.pow(10, i - 1)) { + result.append('0'); + } + else { + break; + } + } + result.append(pNum); + return result.toString(); + } + + /** + * String length check with simple concatenation of selected pad-string. + * E.g. a zip number from 123 to the correct 0123. + * + * @param pSource The source string. + * @param pRequiredLength The accurate length of the resulting string. + * @param pPadString The string for concatenation. + * @param pPrepend The location of fill-ins, prepend (true), + * or append (false) + * @return a concatenated string. + * @see #cut(String,int,String) + */ + // TODO: What if source is allready longer than required length? + // TODO: Consistency with cut + public static String pad(String pSource, int pRequiredLength, String pPadString, boolean pPrepend) { + if (pPadString == null || pPadString.length() == 0) { + throw new IllegalArgumentException("Pad string: \"" + pPadString + "\""); + } + + if (pSource.length() >= pRequiredLength) { + return pSource; + } + + // TODO: Benchmark the new version against the old one, to see if it's really faster + // Rewrite to first create pad + // - pad += pad; - until length is >= gap + // then append the pad and cut if too long + int gap = pRequiredLength - pSource.length(); + StringBuilder result = new StringBuilder(pPadString); + while (result.length() < gap) { + result.append(result); + } + + if (result.length() > gap) { + result.delete(gap, result.length()); + } + + return pPrepend ? result.append(pSource).toString() : result.insert(0, pSource).toString(); + + /* + StringBuilder result = new StringBuilder(pSource); + + // Concatenation until proper string length + while (result.length() < pRequiredLength) { + // Prepend or append + if (pPrepend) { // Front + result.insert(0, pPadString); + } + else { // Back + result.append(pPadString); + } + } + + // Truncate + if (result.length() > pRequiredLength) { + if (pPrepend) { + result.delete(0, result.length() - pRequiredLength); + } + else { + result.delete(pRequiredLength, result.length()); + } + } + return result.toString(); + */ + } + + /** + * Converts the string to a date, using the default date format. + * + * @param pString the string to convert + * @return the date + * @see DateFormat + * @see DateFormat#getInstance() + */ + public static Date toDate(String pString) { + // Default + return toDate(pString, DateFormat.getInstance()); + } + + /** + * Converts the string to a date, using the given format. + * + * @param pString the string to convert + * @param pFormat the date format + * @return the date + * + * @see java.text.SimpleDateFormat + * @see java.text.SimpleDateFormat#SimpleDateFormat(String) + */ + // TODO: cache formats? + public static Date toDate(String pString, String pFormat) { + // Get the format from cache, or create new and insert + // Return new date + return toDate(pString, new SimpleDateFormat(pFormat)); + } + + /** + * Converts the string to a date, using the given format. + * + * @param pString the string to convert + * @param pFormat the date format + * @return the date + * @see SimpleDateFormat + * @see SimpleDateFormat#SimpleDateFormat(String) + * @see DateFormat + */ + public static Date toDate(final String pString, final DateFormat pFormat) { + try { + synchronized (pFormat) { + // Parse date using given format + return pFormat.parse(pString); + } + } + catch (ParseException pe) { + // Wrap in RuntimeException + throw new IllegalArgumentException(pe.getMessage()); + } + } + + /** + * Converts the string to a jdbc Timestamp, using the standard Timestamp + * escape format. + * + * @param pValue the value + * @return a new {@code Timestamp} + * @see java.sql.Timestamp + * @see java.sql.Timestamp#valueOf(String) + */ + public static Timestamp toTimestamp(final String pValue) { + // Parse date using default format + return Timestamp.valueOf(pValue); + } + + /** + * Converts a delimiter separated String to an array of Strings. + * + * @param pString The comma-separated string + * @param pDelimiters The delimiter string + * @return a {@code String} array containing the delimiter separated elements + */ + public static String[] toStringArray(String pString, String pDelimiters) { + if (isEmpty(pString)) { + return new String[0]; + } + + StringTokenIterator st = new StringTokenIterator(pString, pDelimiters); + List v = new ArrayList(); + + while (st.hasMoreElements()) { + v.add(st.nextToken()); + } + + return v.toArray(new String[v.size()]); + } + + /** + * Converts a comma-separated String to an array of Strings. + * + * @param pString The comma-separated string + * @return a {@code String} array containing the comma-separated elements + * @see #toStringArray(String,String) + */ + public static String[] toStringArray(String pString) { + return toStringArray(pString, DELIMITER_STRING); + } + + /** + * Converts a comma-separated String to an array of ints. + * + * @param pString The comma-separated string + * @param pDelimiters The delimiter string + * @param pBase The radix + * @return an {@code int} array + * @throws NumberFormatException if any of the elements are not parseable + * as an int + */ + public static int[] toIntArray(String pString, String pDelimiters, int pBase) { + if (isEmpty(pString)) { + return new int[0]; + } + + // Some room for improvement here... + String[] temp = toStringArray(pString, pDelimiters); + int[] array = new int[temp.length]; + + for (int i = 0; i < array.length; i++) { + array[i] = Integer.parseInt(temp[i], pBase); + } + return array; + } + + /** + * Converts a comma-separated String to an array of ints. + * + * @param pString The comma-separated string + * @return an {@code int} array + * @throws NumberFormatException if any of the elements are not parseable + * as an int + * @see #toStringArray(String,String) + * @see #DELIMITER_STRING + */ + public static int[] toIntArray(String pString) { + return toIntArray(pString, DELIMITER_STRING, 10); + } + + /** + * Converts a comma-separated String to an array of ints. + * + * @param pString The comma-separated string + * @param pDelimiters The delimiter string + * @return an {@code int} array + * @throws NumberFormatException if any of the elements are not parseable + * as an int + * @see #toIntArray(String,String) + */ + public static int[] toIntArray(String pString, String pDelimiters) { + return toIntArray(pString, pDelimiters, 10); + } + + /** + * Converts a comma-separated String to an array of longs. + * + * @param pString The comma-separated string + * @param pDelimiters The delimiter string + * @return a {@code long} array + * @throws NumberFormatException if any of the elements are not parseable + * as a long + */ + public static long[] toLongArray(String pString, String pDelimiters) { + if (isEmpty(pString)) { + return new long[0]; + } + + // Some room for improvement here... + String[] temp = toStringArray(pString, pDelimiters); + long[] array = new long[temp.length]; + + for (int i = 0; i < array.length; i++) { + array[i] = Long.parseLong(temp[i]); + } + return array; + } + + /** + * Converts a comma-separated String to an array of longs. + * + * @param pString The comma-separated string + * @return a {@code long} array + * @throws NumberFormatException if any of the elements are not parseable + * as a long + * @see #toStringArray(String,String) + * @see #DELIMITER_STRING + */ + public static long[] toLongArray(String pString) { + return toLongArray(pString, DELIMITER_STRING); + } + + /** + * Converts a comma-separated String to an array of doubles. + * + * @param pString The comma-separated string + * @param pDelimiters The delimiter string + * @return a {@code double} array + * @throws NumberFormatException if any of the elements are not parseable + * as a double + */ + public static double[] toDoubleArray(String pString, String pDelimiters) { + if (isEmpty(pString)) { + return new double[0]; + } + + // Some room for improvement here... + String[] temp = toStringArray(pString, pDelimiters); + double[] array = new double[temp.length]; + + for (int i = 0; i < array.length; i++) { + array[i] = Double.valueOf(temp[i]); + + // Double.parseDouble() is 1.2... + } + return array; + } + + /** + * Converts a comma-separated String to an array of doubles. + * + * @param pString The comma-separated string + * @return a {@code double} array + * @throws NumberFormatException if any of the elements are not parseable + * as a double + * @see #toDoubleArray(String,String) + * @see #DELIMITER_STRING + */ + public static double[] toDoubleArray(String pString) { + return toDoubleArray(pString, DELIMITER_STRING); + } + + /** + * Parses a string to a Color. + * The argument can be a color constant (static constant from + * {@link java.awt.Color java.awt.Color}), like {@code black} or + * {@code red}, or it can be HTML/CSS-style, on the format: + *

    + *
  • {@code #RRGGBB}, where RR, GG and BB means two digit + * hexadecimal for red, green and blue values respectively.
  • + *
  • {@code #AARRGGBB}, as above, with AA as alpha component.
  • + *
  • {@code #RGB}, where R, G and B means one digit + * hexadecimal for red, green and blue values respectively.
  • + *
  • {@code #ARGB}, as above, with A as alpha component.
  • + *
+ * + * @param pString the string representation of the color + * @return the {@code Color} object, or {@code null} if the argument + * is {@code null} + * @throws IllegalArgumentException if the string does not map to a color. + * @see java.awt.Color + */ + public static Color toColor(String pString) { + // No string, no color + if (pString == null) { + return null; + } + + // #RRGGBB format + if (pString.charAt(0) == '#') { + int r = 0; + int g = 0; + int b = 0; + int a = -1;// alpha + + if (pString.length() >= 7) { + int idx = 1; + + // AA + if (pString.length() >= 9) { + a = Integer.parseInt(pString.substring(idx, idx + 2), 0x10); + idx += 2; + } + // RR GG BB + r = Integer.parseInt(pString.substring(idx, idx + 2), 0x10); + g = Integer.parseInt(pString.substring(idx + 2, idx + 4), 0x10); + b = Integer.parseInt(pString.substring(idx + 4, idx + 6), 0x10); + + } + else if (pString.length() >= 4) { + int idx = 1; + + // A + if (pString.length() >= 5) { + a = Integer.parseInt(pString.substring(idx, ++idx), 0x10) * 0x10; + } + // R G B + r = Integer.parseInt(pString.substring(idx, ++idx), 0x10) * 0x10; + g = Integer.parseInt(pString.substring(idx, ++idx), 0x10) * 0x10; + b = Integer.parseInt(pString.substring(idx, ++idx), 0x10) * 0x10; + } + if (a != -1) { + // With alpha + return new Color(r, g, b, a); + } + + // No alpha + return new Color(r, g, b); + } + + // Get color by name + try { + Class colorClass = Color.class; + Field field = null; + + // Workaround for stupidity in Color class constant field names + try { + field = colorClass.getField(pString); + } + catch (Exception e) { + // Don't care, this is just a workaround... + } + if (field == null) { + // NOTE: The toLowerCase() on the next line will lose darkGray + // and lightGray... + field = colorClass.getField(pString.toLowerCase()); + } + + // Only try to get public final fields + int mod = field.getModifiers(); + + if (Modifier.isPublic(mod) && Modifier.isStatic(mod)) { + return (Color) field.get(null); + } + } + catch (NoSuchFieldException nsfe) { + // No such color, throw illegal argument? + throw new IllegalArgumentException("No such color: " + pString); + } + catch (SecurityException se) { + // Can't access field, return null + } + catch (IllegalAccessException iae) { + // Can't happen, as the field must be public static + } + catch (IllegalArgumentException iar) { + // Can't happen, as the field must be static + } + + // This should never be reached, but you never know... ;-) + return null; + } + + /** + * Creates a HTML/CSS String representation of the given color. + * The HTML/CSS color format is defined as: + *
    + *
  • {@code #RRGGBB}, where RR, GG and BB means two digit + * hexadecimal for red, green and blue values respectively.
  • + *
  • {@code #AARRGGBB}, as above, with AA as alpha component.
  • + *
+ *

+ * Examlples: {@code toColorString(Color.red) == "#ff0000"}, + * {@code toColorString(new Color(0xcc, 0xcc, 0xcc)) == "#cccccc"}. + *

+ * + * @param pColor the color + * @return A String representation of the color on HTML/CSS form + */ + // TODO: Consider moving to ImageUtil? + public static String toColorString(Color pColor) { + // Not a color... + if (pColor == null) { + return null; + } + + StringBuilder str = new StringBuilder(Integer.toHexString(pColor.getRGB())); + + // Make sure string is 8 chars + for (int i = str.length(); i < 8; i++) { + str.insert(0, '0'); + } + + // All opaque is default + if (str.charAt(0) == 'f' && str.charAt(1) == 'f') { + str.delete(0, 2); + } + + // Prepend hash + return str.insert(0, '#').toString(); + } + + /** + * Tests a string, to see if it is an number (element of Z). + * Valid integers are positive natural numbers (1, 2, 3, ...), + * their negatives (?1, ?2, ?3, ...) and the number zero. + *

+ * Note that there is no guarantees made, that this number can be + * represented as either an int or a long. + *

+ * + * @param pString The string to check. + * @return true if the String is a natural number. + */ + public static boolean isNumber(String pString) { + if (isEmpty(pString)) { + return false; + } + + // Special case for first char, may be minus sign ('-') + char ch = pString.charAt(0); + if (!(ch == '-' || Character.isDigit(ch))) { + return false; + } + + // Test every char + for (int i = 1; i < pString.length(); i++) { + if (!Character.isDigit(pString.charAt(i))) { + return false; + } + } + + // All digits must be a natural number + return true; + } + + /* + * This version is benchmarked against toStringArray and found to be + * increasingly slower, the more elements the string contains. + * Kept here + */ + + /** + * Removes all occurences of a specific character in a string. + * This method is not design for efficiency! + *

+ * + * @param pSource + * @param pSubstring + * @param pPosition + * @return the modified string. + */ + + /* + public static String removeChar(String pSourceString, final char pBadChar) { + + char[] sourceCharArray = pSourceString.toCharArray(); + List modifiedCharList = new Vector(sourceCharArray.length, 1); + + // Filter the string + for (int i = 0; i < sourceCharArray.length; i++) { + if (sourceCharArray[i] != pBadChar) { + modifiedCharList.add(new Character(sourceCharArray[i])); + } + } + + // Clean the character list + modifiedCharList = (List) CollectionUtil.purifyCollection((Collection) modifiedCharList); + + // Create new modified String + char[] modifiedCharArray = new char[modifiedCharList.size()]; + for (int i = 0; i < modifiedCharArray.length; i++) { + modifiedCharArray[i] = ((Character) modifiedCharList.get(i)).charValue(); + } + + return new String(modifiedCharArray); + } + */ + + /** + * + * This method is not design for efficiency! + *

+ * @param pSourceString The String for modification. + * @param pBadChars The char array containing the characters to remove from the source string. + * @return the modified string. + * @-deprecated Not tested yet! + * + */ + + /* + public static String removeChars(String pSourceString, final char[] pBadChars) { + + char[] sourceCharArray = pSourceString.toCharArray(); + List modifiedCharList = new Vector(sourceCharArray.length, 1); + + Map badCharMap = new Hashtable(); + Character dummyChar = new Character('*'); + for (int i = 0; i < pBadChars.length; i++) { + badCharMap.put(new Character(pBadChars[i]), dummyChar); + } + + // Filter the string + for (int i = 0; i < sourceCharArray.length; i++) { + Character arrayChar = new Character(sourceCharArray[i]); + if (!badCharMap.containsKey(arrayChar)) { + modifiedCharList.add(new Character(sourceCharArray[i])); + } + } + + // Clean the character list + modifiedCharList = (List) CollectionUtil.purifyCollection((Collection) modifiedCharList); + + // Create new modified String + char[] modifiedCharArray = new char[modifiedCharList.size()]; + for (int i = 0; i < modifiedCharArray.length; i++) { + modifiedCharArray[i] = ((Character) modifiedCharList.get(i)).charValue(); + } + + return new String(modifiedCharArray); + + } + */ + + /** + * Ensures that a string includes a given substring at a given position. + *

+ * Extends the string with a given string if it is not already there. + * E.g an URL "www.vg.no", to "http://www.vg.no". + *

+ * + * @param pSource The source string. + * @param pSubstring The substring to include. + * @param pPosition The location of the fill-in, the index starts with 0. + * @return the string, with the substring at the given location. + */ + static String ensureIncludesAt(String pSource, String pSubstring, int pPosition) { + StringBuilder newString = new StringBuilder(pSource); + + try { + String existingSubstring = pSource.substring(pPosition, pPosition + pSubstring.length()); + + if (!existingSubstring.equalsIgnoreCase(pSubstring)) { + newString.insert(pPosition, pSubstring); + } + } + catch (Exception e) { + // Do something!? + } + return newString.toString(); + } + + /** + * Ensures that a string does not include a given substring at a given + * position. + *

+ * Removes a given substring from a string if it is there. + * E.g an URL "http://www.vg.no", to "www.vg.no". + *

+ * + * @param pSource The source string. + * @param pSubstring The substring to check and possibly remove. + * @param pPosition The location of possible substring removal, the index starts with 0. + * @return the string, without the substring at the given location. + */ + static String ensureExcludesAt(String pSource, String pSubstring, int pPosition) { + StringBuilder newString = new StringBuilder(pSource); + + try { + String existingString = pSource.substring(pPosition + 1, pPosition + pSubstring.length() + 1); + + if (!existingString.equalsIgnoreCase(pSubstring)) { + newString.delete(pPosition, pPosition + pSubstring.length()); + } + } + catch (Exception e) { + // Do something!? + } + return newString.toString(); + } + + /** + * Gets the first substring between the given string boundaries. + * + * @param pSource The source string. + * @param pBeginBoundaryString The string that marks the beginning. + * @param pEndBoundaryString The string that marks the end. + * @param pOffset The index to start searching in the source + * string. If it is less than 0, the index will be set to 0. + * @return the substring demarcated by the given string boundaries or null + * if not both string boundaries are found. + */ + public static String substring(final String pSource, final String pBeginBoundaryString, final String pEndBoundaryString, + final int pOffset) { + // Check offset + int offset = (pOffset < 0) + ? 0 + : pOffset; + + // Find the start index + int startIndex = pSource.indexOf(pBeginBoundaryString, offset) + pBeginBoundaryString.length(); + + if (startIndex < 0) { + return null; + } + + // Find the end index + int endIndex = pSource.indexOf(pEndBoundaryString, startIndex); + + if (endIndex < 0) { + return null; + } + return pSource.substring(startIndex, endIndex); + } + + /** + * Removes the first substring demarcated by the given string boundaries. + * + * @param pSource The source string. + * @param pBeginBoundaryChar The character that marks the beginning of the + * unwanted substring. + * @param pEndBoundaryChar The character that marks the end of the + * unwanted substring. + * @param pOffset The index to start searching in the source + * string. If it is less than 0, the index will be set to 0. + * @return the source string with all the demarcated substrings removed, + * included the demarcation characters. + * @deprecated this method actually removes all demarcated substring.. doesn't it? + */ + + /*public*/ + static String removeSubstring(final String pSource, final char pBeginBoundaryChar, final char pEndBoundaryChar, final int pOffset) { + StringBuilder filteredString = new StringBuilder(); + boolean insideDemarcatedArea = false; + char[] charArray = pSource.toCharArray(); + + for (char c : charArray) { + if (!insideDemarcatedArea) { + if (c == pBeginBoundaryChar) { + insideDemarcatedArea = true; + } + else { + filteredString.append(c); + } + } + else { + if (c == pEndBoundaryChar) { + insideDemarcatedArea = false; + } + } + } + return filteredString.toString(); + } + + /** + * Removes all substrings demarcated by the given string boundaries. + * + * @param pSource The source string. + * @param pBeginBoundaryChar The character that marks the beginning of the unwanted substring. + * @param pEndBoundaryChar The character that marks the end of the unwanted substring. + * @return the source string with all the demarcated substrings removed, included the demarcation characters. + */ + /*public*/ + static String removeSubstrings(final String pSource, final char pBeginBoundaryChar, final char pEndBoundaryChar) { + StringBuilder filteredString = new StringBuilder(); + boolean insideDemarcatedArea = false; + char[] charArray = pSource.toCharArray(); + + for (char c : charArray) { + if (!insideDemarcatedArea) { + if (c == pBeginBoundaryChar) { + insideDemarcatedArea = true; + } + else { + filteredString.append(c); + } + } + else { + if (c == pEndBoundaryChar) { + insideDemarcatedArea = false; + } + } + } + return filteredString.toString(); + } + + /** + * Gets the first element of a {@code String} containing string elements delimited by a given delimiter. + * NB - Straightforward implementation! + * + * @param pSource The source string. + * @param pDelimiter The delimiter used in the source string. + * @return The last string element. + */ + // TODO: This method should be re-implemented for more efficient execution. + public static String getFirstElement(final String pSource, final String pDelimiter) { + if (pDelimiter == null) { + throw new IllegalArgumentException("delimiter == null"); + } + + if (StringUtil.isEmpty(pSource)) { + return pSource; + } + + int idx = pSource.indexOf(pDelimiter); + if (idx >= 0) { + return pSource.substring(0, idx); + } + return pSource; + } + + /** + * Gets the last element of a {@code String} containing string elements + * delimited by a given delimiter. + * NB - Straightforward implementation! + * + * @param pSource The source string. + * @param pDelimiter The delimiter used in the source string. + * @return The last string element. + */ + public static String getLastElement(final String pSource, final String pDelimiter) { + if (pDelimiter == null) { + throw new IllegalArgumentException("delimiter == null"); + } + + if (StringUtil.isEmpty(pSource)) { + return pSource; + } + int idx = pSource.lastIndexOf(pDelimiter); + if (idx >= 0) { + return pSource.substring(idx + 1); + } + return pSource; + } + + /** + * Converts a string array to a string of comma-separated values. + * + * @param pStringArray the string array + * @return A string of comma-separated values + */ + public static String toCSVString(Object[] pStringArray) { + return toCSVString(pStringArray, ", "); + } + + /** + * Converts a string array to a string separated by the given delimiter. + * + * @param pStringArray the string array + * @param pDelimiterString the delimiter string + * @return string of delimiter separated values + * @throws IllegalArgumentException if {@code pDelimiterString == null} + */ + public static String toCSVString(Object[] pStringArray, String pDelimiterString) { + if (pStringArray == null) { + return ""; + } + if (pDelimiterString == null) { + throw new IllegalArgumentException("delimiter == null"); + } + + StringBuilder buffer = new StringBuilder(); + for (int i = 0; i < pStringArray.length; i++) { + if (i > 0) { + buffer.append(pDelimiterString); + } + + buffer.append(pStringArray[i]); + } + + return buffer.toString(); + } + + /** + * @param pObject the object + * @return a deep string representation of the given object + */ + public static String deepToString(Object pObject) { + return deepToString(pObject, false, 1); + } + + /** + * @param pObject the object + * @param pDepth the maximum depth + * @param pForceDeep {@code true} to force deep {@code toString}, even + * if object overrides toString + * @return a deep string representation of the given object + */ + // TODO: Array handling (print full type and length) + // TODO: Register handlers for specific toDebugString handling? :-) + public static String deepToString(Object pObject, boolean pForceDeep, int pDepth) { + // Null is null + if (pObject == null) { + return null; + } + + // Implements toString, use it as-is unless pForceDeep + if (!pForceDeep && !isIdentityToString(pObject)) { + return pObject.toString(); + } + + StringBuilder buffer = new StringBuilder(); + + if (pObject.getClass().isArray()) { + // Special array handling + Class componentClass = pObject.getClass(); + while (componentClass.isArray()) { + buffer.append('['); + buffer.append(Array.getLength(pObject)); + buffer.append(']'); + componentClass = componentClass.getComponentType(); + } + buffer.insert(0, componentClass); + buffer.append(" {hashCode="); + buffer.append(Integer.toHexString(pObject.hashCode())); + buffer.append("}"); + } + else { + // Append toString value only if overridden + if (isIdentityToString(pObject)) { + buffer.append(" {"); + } + else { + buffer.append(" {toString="); + buffer.append(pObject.toString()); + buffer.append(", "); + } + buffer.append("hashCode="); + buffer.append(Integer.toHexString(pObject.hashCode())); + // Loop through, and filter out any getters + Method[] methods = pObject.getClass().getMethods(); + for (Method method : methods) { + // Filter only public methods + if (Modifier.isPublic(method.getModifiers())) { + String methodName = method.getName(); + + // Find name of property + String name = null; + if (!methodName.equals("getClass") + && methodName.length() > 3 && methodName.startsWith("get") + && Character.isUpperCase(methodName.charAt(3))) { + name = methodName.substring(3); + } + else if (methodName.length() > 2 && methodName.startsWith("is") + && Character.isUpperCase(methodName.charAt(2))) { + name = methodName.substring(2); + } + + if (name != null) { + // If lowercase name, convert, else keep case + if (name.length() > 1 && Character.isLowerCase(name.charAt(1))) { + name = Character.toLowerCase(name.charAt(0)) + name.substring(1); + } + + Class[] paramTypes = method.getParameterTypes();// involves array copying... + boolean hasParams = (paramTypes != null && paramTypes.length > 0); + boolean isVoid = Void.TYPE.equals(method.getReturnType()); + + // Filter return type & parameters + if (!isVoid && !hasParams) { + try { + Object value = method.invoke(pObject); + buffer.append(", "); + buffer.append(name); + buffer.append('='); + if (pDepth != 0 && value != null && isIdentityToString(value)) { + buffer.append(deepToString(value, pForceDeep, pDepth > 0 ? pDepth - 1 : -1)); + } + else { + buffer.append(value); + } + } + catch (Exception e) { + // Next..! + } + } + } + } + } + buffer.append('}'); + + // Get toString from original object + buffer.insert(0, pObject.getClass().getName()); + } + + return buffer.toString(); + } + + /** + * Tests if the {@code toString} method of the given object is inherited + * from {@code Object}. + * + * @param pObject the object + * @return {@code true} if toString of class Object + */ + private static boolean isIdentityToString(Object pObject) { + try { + Method toString = pObject.getClass().getMethod("toString"); + if (toString.getDeclaringClass() == Object.class) { + return true; + } + } + catch (Exception ignore) { + // Ignore + } + + return false; + } + + /** + * Returns a string on the same format as {@code Object.toString()}. + * + * @param pObject the object + * @return the object as a {@code String} on the format of + * {@code Object.toString()} + */ + public static String identityToString(Object pObject) { + if (pObject == null) { + return null; + } + else { + return pObject.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(pObject)); + } + } + + /** + * Tells whether or not the given string string matches the given regular + * expression. + *

+ * An invocation of this method of the form + * matches(str, regex) yields exactly the + * same result as the expression + *

+ *
{@link Pattern}. + * {@link Pattern#matches(String, CharSequence) matches} + * (regex, str)
+ * + * @param pString the string + * @param pRegex the regular expression to which this string is to be matched + * @return {@code true} if, and only if, this string matches the + * given regular expression + * @throws PatternSyntaxException if the regular expression's syntax is invalid + * @see Pattern + * @see String#matches(String) + */ + public boolean matches(String pString, String pRegex) throws PatternSyntaxException { + return Pattern.matches(pRegex, pString); + } + + /** + * Replaces the first substring of the given string that matches the given + * regular expression with the given pReplacement. + *

+ * An invocation of this method of the form + * + * replaceFirst(str, regex, repl) + * + * yields exactly the same result as the expression: + *

+ *
+ * {@link Pattern}.{@link Pattern#compile(String) compile}(regex). + * {@link Pattern#matcher matcher}(str). + * {@link java.util.regex.Matcher#replaceFirst replaceFirst}(repl) + *
+ * + * @param pString the string + * @param pRegex the regular expression to which this string is to be matched + * @param pReplacement the replacement text + * @return The resulting {@code String} + * @throws PatternSyntaxException if the regular expression's syntax is invalid + * @see Pattern + * @see java.util.regex.Matcher#replaceFirst(String) + */ + public String replaceFirst(String pString, String pRegex, String pReplacement) { + return Pattern.compile(pRegex).matcher(pString).replaceFirst(pReplacement); + } + + /** + * Replaces each substring of this string that matches the given + * regular expression with the given pReplacement. + *

+ * An invocation of this method of the form + * replaceAll(str, pRegex, repl) + * yields exactly the same result as the expression + *

+ *
+ * {@link Pattern}.{@link Pattern#compile(String) compile}(pRegex). + * {@link Pattern#matcher matcher}(str{@code ). + * {@link java.util.regex.Matcher#replaceAll replaceAll}(}repl{@code )} + *
+ * + * @param pString the string + * @param pRegex the regular expression to which this string is to be matched + * @param pReplacement the replacement string + * @return The resulting {@code String} + * @throws PatternSyntaxException if the regular expression's syntax is invalid + * @see Pattern + * @see String#replaceAll(String,String) + */ + public String replaceAll(String pString, String pRegex, String pReplacement) { + return Pattern.compile(pRegex).matcher(pString).replaceAll(pReplacement); + } + + /** + * Splits this string around matches of the given regular expression. + *

+ * The array returned by this method contains each substring of this + * string that is terminated by another substring that matches the given + * expression or is terminated by the end of the string. The substrings in + * the array are in the order in which they occur in this string. If the + * expression does not match any part of the input then the resulting array + * has just one element, namely this string. + *

+ *

+ * The {@code pLimit} parameter controls the number of times the + * pattern is applied and therefore affects the length of the resulting + * array. If the pLimit n is greater than zero then the pattern + * will be applied at most n - 1 times, the array's + * length will be no greater than n, and the array's last entry + * will contain all input beyond the last matched delimiter. If n + * is non-positive then the pattern will be applied as many times as + * possible and the array can have any length. If n is zero then + * the pattern will be applied as many times as possible, the array can + * have any length, and trailing empty strings will be discarded. + *

+ *

+ * An invocation of this method of the form + * split(str, regex, n) + * yields the same result as the expression: + *

+ *
{@link Pattern}. + * {@link Pattern#compile(String) compile}(regex). + * {@link Pattern#split(CharSequence,int) split}(str, n) + *
+ * + * @param pString the string + * @param pRegex the delimiting regular expression + * @param pLimit the result threshold, as described above + * @return the array of strings computed by splitting this string + * around matches of the given regular expression + * @throws PatternSyntaxException + * if the regular expression's syntax is invalid + * @see Pattern + * @see String#split(String,int) + */ + public String[] split(String pString, String pRegex, int pLimit) { + return Pattern.compile(pRegex).split(pString, pLimit); + } + + /** + * Splits this string around matches of the given regular expression. + *

+ * This method works as if by invoking the two-argument + * {@link #split(String,String,int) split} method with the given + * expression and a limit argument of zero. + * Trailing empty strings are therefore not included in the resulting array. + *

+ * + * @param pString the string + * @param pRegex the delimiting regular expression + * @return the array of strings computed by splitting this string + * around matches of the given regular expression + * @throws PatternSyntaxException if the regular expression's syntax is invalid + * @see Pattern + * @see String#split(String) + */ + public String[] split(String pString, String pRegex) { + return split(pString, pRegex, 0); + } + + /** + * Converts the input string + * from camel-style (Java in-fix) naming convention + * to Lisp-style naming convention (hyphen delimitted, all lower case). + * Other characters in the string are left untouched. + *

+ * Eg. + * {@code "foo" => "foo"}, + * {@code "fooBar" => "foo-bar"}, + * {@code "myURL" => "my-url"}, + * {@code "HttpRequestWrapper" => "http-request-wrapper"} + * {@code "HttpURLConnection" => "http-url-connection"} + * {@code "my45Caliber" => "my-45-caliber"} + * {@code "allready-lisp" => "allready-lisp"} + *

+ * + * @param pString the camel-style input string + * @return the string converted to lisp-style naming convention + * @throws IllegalArgumentException if {@code pString == null} + * @see #lispToCamel(String) + */ + // TODO: RefactorMe! + public static String camelToLisp(final String pString) { + if (pString == null) { + throw new IllegalArgumentException("string == null"); + } + if (pString.length() == 0) { + return pString; + } + + StringBuilder buf = null; + int lastPos = 0; + boolean inCharSequence = false; + boolean inNumberSequence = false; + + // NOTE: Start at index 1, as first letter should never be hyphen + for (int i = 1; i < pString.length(); i++) { + char current = pString.charAt(i); + if (Character.isUpperCase(current)) { + // Init buffer if necessary + if (buf == null) { + buf = new StringBuilder(pString.length() + 3);// Allow for some growth + } + + if (inNumberSequence) { + // Sequence end + inNumberSequence = false; + + buf.append(pString.substring(lastPos, i)); + if (current != '-') { + buf.append('-'); + } + lastPos = i; + continue; + } + + // Treat multiple uppercase chars as single word + char previous = pString.charAt(i - 1); + if (i == lastPos || Character.isUpperCase(previous)) { + inCharSequence = true; + continue; + } + + // Append word + buf.append(pString.substring(lastPos, i).toLowerCase()); + if (previous != '-') { + buf.append('-'); + } + buf.append(Character.toLowerCase(current)); + + lastPos = i + 1; + } + else if (Character.isDigit(current)) { + // Init buffer if necessary + if (buf == null) { + buf = new StringBuilder(pString.length() + 3);// Allow for some growth + } + + if (inCharSequence) { + // Sequence end + inCharSequence = false; + + buf.append(pString.substring(lastPos, i).toLowerCase()); + if (current != '-') { + buf.append('-'); + } + lastPos = i; + continue; + } + + // Treat multiple digits as single word + char previous = pString.charAt(i - 1); + if (i == lastPos || Character.isDigit(previous)) { + inNumberSequence = true; + continue; + } + + // Append word + buf.append(pString.substring(lastPos, i).toLowerCase()); + if (previous != '-') { + buf.append('-'); + } + buf.append(Character.toLowerCase(current)); + + lastPos = i + 1; + } + else if (inNumberSequence) { + // Sequence end + inNumberSequence = false; + + buf.append(pString.substring(lastPos, i)); + if (current != '-') { + buf.append('-'); + } + lastPos = i; + } + else if (inCharSequence) { + // Sequence end + inCharSequence = false; + + // NOTE: Special treatment! Last upper case, is first char in + // next word, not last char in this word + buf.append(pString.substring(lastPos, i - 1).toLowerCase()); + if (current != '-') { + buf.append('-'); + } + lastPos = i - 1; + } + } + + if (buf != null) { + // Append the rest + buf.append(pString.substring(lastPos).toLowerCase()); + return buf.toString(); + } + else { + return Character.isUpperCase(pString.charAt(0)) ? pString.toLowerCase() : pString; + } + } + + /** + * Converts the input string + * from Lisp-style naming convention (hyphen delimitted, all lower case) + * to camel-style (Java in-fix) naming convention. + * Other characters in the string are left untouched. + *

+ * Eg. + * {@code "foo" => "foo"}, + * {@code "foo-bar" => "fooBar"}, + * {@code "http-request-wrapper" => "httpRequestWrapper"} + * {@code "my-45-caliber" => "my45Caliber"} + * {@code "allreadyCamel" => "allreadyCamel"} + *

+ * + * @param pString the lisp-style input string + * @return the string converted to camel-style + * @throws IllegalArgumentException if {@code pString == null} + * @see #lispToCamel(String,boolean) + * @see #camelToLisp(String) + */ + public static String lispToCamel(final String pString) { + return lispToCamel(pString, false); + } + + /** + * Converts the input string + * from Lisp-style naming convention (hyphen delimitted, all lower case) + * to camel-style (Java in-fix) naming convention. + * Other characters in the string are left untouched. + *

+ * To create a string starting with a lower case letter + * (like Java variable names, etc), + * specify the {@code pFirstUpperCase} paramter to be {@code false}. + * Eg. + * {@code "foo" => "foo"}, + * {@code "foo-bar" => "fooBar"}, + * {@code "allreadyCamel" => "allreadyCamel"} + *

+ *

+ * To create a string starting with an upper case letter + * (like Java class name, etc), + * specify the {@code pFirstUpperCase} paramter to be {@code true}. + * Eg. + * {@code "http-request-wrapper" => "HttpRequestWrapper"} + * {@code "my-12-monkeys" => "My12Monkeys"} + *

+ * + * @param pString the lisp-style input string + * @param pFirstUpperCase {@code true} if the first char should be + * upper case + * @return the string converted to camel-style + * @throws IllegalArgumentException if {@code pString == null} + * @see #camelToLisp(String) + */ + public static String lispToCamel(final String pString, final boolean pFirstUpperCase) { + if (pString == null) { + throw new IllegalArgumentException("string == null"); + } + if (pString.length() == 0) { + return pString; + } + + StringBuilder buf = null; + int lastPos = 0; + + for (int i = 0; i < pString.length(); i++) { + char current = pString.charAt(i); + if (current == '-') { + + // Init buffer if necessary + if (buf == null) { + buf = new StringBuilder(pString.length() - 1);// Can't be larger + } + + // Append with upper case + if (lastPos != 0 || pFirstUpperCase) { + buf.append(Character.toUpperCase(pString.charAt(lastPos))); + lastPos++; + } + + buf.append(pString.substring(lastPos, i).toLowerCase()); + lastPos = i + 1; + } + } + + if (buf != null) { + buf.append(Character.toUpperCase(pString.charAt(lastPos))); + buf.append(pString.substring(lastPos + 1).toLowerCase()); + return buf.toString(); + } + else { + if (pFirstUpperCase && !Character.isUpperCase(pString.charAt(0))) { + return capitalize(pString, 0); + } + else + if (!pFirstUpperCase && Character.isUpperCase(pString.charAt(0))) { + return Character.toLowerCase(pString.charAt(0)) + pString.substring(1); + } + + return pString; + } + } + + public static String reverse(final String pString) { + final char[] chars = new char[pString.length()]; + pString.getChars(0, chars.length, chars, 0); + + for (int i = 0; i < chars.length / 2; i++) { + char temp = chars[i]; + chars[i] = chars[chars.length - 1 - i]; + chars[chars.length - 1 - i] = temp; + } + + return new String(chars); + } } \ No newline at end of file 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 6a6069a0..d824624b 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 @@ -1,688 +1,687 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.io.*; -import java.lang.reflect.Array; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.security.AccessController; -import java.security.PrivilegedAction; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; - -/** - * A utility class with some useful system-related functions. - *

- * NOTE: This class is not considered part of the public API and may be - * changed without notice - * - * @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/SystemUtil.java#3 $ - * - */ -public final class SystemUtil { - /** {@code ".xml"} */ - public static String XML_PROPERTIES = ".xml"; - /** {@code ".properties"} */ - public static String STD_PROPERTIES = ".properties"; - - // Disallow creating objects of this type - private SystemUtil() { - } - - /** This class marks an inputstream as containing XML, does nothing */ - private static class XMLPropertiesInputStream extends FilterInputStream { - public XMLPropertiesInputStream(InputStream pIS) { - super(pIS); - } - } - - /** - * Gets the named resource as a stream from the given Class' Classoader. - * If the pGuessSuffix parameter is true, the method will try to append - * typical properties file suffixes, such as ".properties" or ".xml". - * - * @param pClassLoader the class loader to use - * @param pName name of the resource - * @param pGuessSuffix guess suffix - * - * @return an input stream reading from the resource - */ - private static InputStream getResourceAsStream(ClassLoader pClassLoader, String pName, boolean pGuessSuffix) { - InputStream is; - - if (!pGuessSuffix) { - is = pClassLoader.getResourceAsStream(pName); - - // If XML, wrap stream - if (is != null && pName.endsWith(XML_PROPERTIES)) { - is = new XMLPropertiesInputStream(is); - } - } - else { - // Try normal properties - is = pClassLoader.getResourceAsStream(pName + STD_PROPERTIES); - - // Try XML - if (is == null) { - is = pClassLoader.getResourceAsStream(pName + XML_PROPERTIES); - - // Wrap stream - if (is != null) { - is = new XMLPropertiesInputStream(is); - } - } - } - - // Return stream - return is; - } - - /** - * Gets the named file as a stream from the current directory. - * If the pGuessSuffix parameter is true, the method will try to append - * typical properties file suffixes, such as ".properties" or ".xml". - * - * @param pName name of the resource - * @param pGuessSuffix guess suffix - * - * @return an input stream reading from the resource - */ - private static InputStream getFileAsStream(String pName, boolean pGuessSuffix) { - InputStream is = null; - File propertiesFile; - - try { - if (!pGuessSuffix) { - // Get file - propertiesFile = new File(pName); - - if (propertiesFile.exists()) { - is = new FileInputStream(propertiesFile); - - // If XML, wrap stream - if (pName.endsWith(XML_PROPERTIES)) { - is = new XMLPropertiesInputStream(is); - } - } - } - else { - // Try normal properties - propertiesFile = new File(pName + STD_PROPERTIES); - - if (propertiesFile.exists()) { - is = new FileInputStream(propertiesFile); - } - else { - // Try XML - propertiesFile = new File(pName + XML_PROPERTIES); - - if (propertiesFile.exists()) { - // Wrap stream - is = new XMLPropertiesInputStream(new FileInputStream(propertiesFile)); - } - } - } - } - catch (FileNotFoundException fnf) { - // Should not happen, as we always test that the file .exists() - // before creating InputStream - // assert false; - } - - return is; - } - - /** - * Utility method for loading a named properties-file for a class. - *

- * The properties-file is loaded through either: - *

    - *
  1. The given class' class loader (from classpath)
  2. - *
  3. Or, the system class loader (from classpath)
  4. - *
  5. Or, if it cannot be found in the classpath, an attempt to read from - * the current directory (or full path if given).
  6. - *
- *

- * Both normal java.util.Properties and com.twelvemonkeys.util.XMLProperties - * are supported (XML-properties must have ".xml" as its file extension). - * - * @param pClass The class to load properties for. If this parameter is - * {@code null}, the method will work exactly as - * {@link #loadProperties(String)} - * @param pName The name of the properties-file. If this parameter is - * {@code null}, the method will work exactly as - * {@link #loadProperties(Class)} - * - * @return A Properties mapping read from the given file or for the given - * class. - * - * @throws NullPointerException if both {@code pName} and - * {@code pClass} paramters are {@code null} - * @throws IOException if an error occurs during load. - * @throws FileNotFoundException if no properties-file could be found. - * - * @see #loadProperties(String) - * @see #loadProperties(Class) - * @see java.lang.ClassLoader#getResourceAsStream - * @see java.lang.ClassLoader#getSystemResourceAsStream - * - * @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 - { - // Convert to name the classloader understands - String name = !StringUtil.isEmpty(pName) ? pName : pClass.getName().replace('.', '/'); - - // Should we try to guess suffix? - boolean guessSuffix = (pName == null || pName.indexOf('.') < 0); - - InputStream is; - - // TODO: WHAT IF MULTIPLE RESOURCES EXISTS?! - // Try loading resource through the current class' classloader - 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) ? - // "XML-properties" : "Normal .properties") - // + " 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) { - // Nothing to do - //System.out.println(((is instanceof XMLPropertiesInputStream) ? - // "XML-properties" : "Normal .properties") - // + " from System ClassLoader"); - } - // All failed, try loading from file - else if ((is = getFileAsStream(name, guessSuffix)) != null) { - //System.out.println(((is instanceof XMLPropertiesInputStream) ? - // "XML-properties" : "Normal .properties") - // + " from System ClassLoader"); - } - else { - if (guessSuffix) { - // TODO: file extension iterator or something... - throw new FileNotFoundException(name + ".properties or " + name + ".xml"); - } - else { - throw new FileNotFoundException(name); - } - } - - // We have inputstream now, load... - try { - return loadProperties(is); - } - finally { - // NOTE: If is == null, a FileNotFoundException must have been thrown above - try { - is.close(); - } - catch (IOException ioe) { - // Not critical... - } - } - } - - /** - * Utility method for loading a properties-file for a given class. - * The properties are searched for on the form - * "com/package/ClassName.properties" or - * "com/package/ClassName.xml". - *

- * The properties-file is loaded through either: - *

    - *
  1. The given class' class loader (from classpath)
  2. - *
  3. Or, the system class loader (from classpath)
  4. - *
  5. Or, if it cannot be found in the classpath, an attempt to read from - * the current directory (or full path if given).
  6. - *
- *

- * Both normal java.util.Properties and com.twelvemonkeys.util.XMLProperties - * are supported (XML-properties must have ".xml" as its file extension). - * - * @param pClass The class to load properties for - * @return A Properties mapping for the given class. - * - * @throws NullPointerException if the {@code pClass} paramters is - * {@code null} - * @throws IOException if an error occurs during load. - * @throws FileNotFoundException if no properties-file could be found. - * - * @see #loadProperties(String) - * @see #loadProperties(Class, String) - * @see java.lang.ClassLoader#getResourceAsStream - * @see java.lang.ClassLoader#getSystemResourceAsStream - * - */ - public static Properties loadProperties(Class pClass) throws IOException { - return loadProperties(pClass, null); - } - - /** - * Utility method for loading a named properties-file. - *

- * The properties-file is loaded through either: - *

    - *
  1. The system class loader (from classpath)
  2. - *
  3. Or, if it cannot be found in the classpath, an attempt to read from - * the current directory.
  4. - *
- *

- * Both normal java.util.Properties and com.twelvemonkeys.util.XMLProperties - * are supported (XML-properties must have ".xml" as its file extension). - * - * @param pName The name of the properties-file. - * @return A Properties mapping read from the given file. - * - * @throws NullPointerException if the {@code pName} paramters is - * {@code null} - * @throws IOException if an error occurs during load. - * @throws FileNotFoundException if no properties-file could be found. - * - * @see #loadProperties(Class) - * @see #loadProperties(Class, String) - * @see java.lang.ClassLoader#getSystemResourceAsStream - * - */ - public static Properties loadProperties(String pName) throws IOException { - return loadProperties(null, pName); - } - - /* - * Utility method for loading a properties-file. - *

- * The properties files may also be contained in a zip/jar-file named - * by the {@code com.twelvemonkeys.util.Config} system property (use "java -D" - * to override). Default is "config.zip" in the current directory. - * - * @param pName The name of the file to loaded - * @return A Properties mapping for the given class. If no properties- - * file was found, an empty Properties object is returned. - * - */ - /* - public static Properties loadProperties(String pName) throws IOException { - // Use XML? - boolean useXML = pName.endsWith(XML_PROPERTIES) ? true : false; - - InputStream is = null; - - File file = new File(pName); - - String configName = System.getProperty("com.twelvemonkeys.util.Config"); - File configArchive = new File(!StringUtil.isEmpty(configName) - ? configName : DEFAULT_CONFIG); - - // Get input stream to the file containing the properties - if (file.exists()) { - // Try reading from file, normal way - is = new FileInputStream(file); - } - else if (configArchive.exists()) { - // Try reading properties from zip-file - ZipFile zip = new ZipFile(configArchive); - ZipEntry ze = zip.getEntry(pName); - if (ze != null) { - is = zip.getInputStream(ze); - } - - } - - // Do the loading - try { - // Load the properties - return loadProperties(is, useXML); - } - finally { - // Try closing the archive to free resources - if (is != null) { - try { - is.close(); - } - catch (IOException ioe) { - // Not critical... - } - } - } - - } - */ - - /** - * Returns a Properties, loaded from the given inputstream. If the given - * inputstream is null, then an empty Properties object is returned. - * - * @param pInput the inputstream to read from - * - * @return a Properties object read from the given stream, or an empty - * Properties mapping, if the stream is null. - * - * @throws IOException if an error occurred when reading from the input - * stream. - * - */ - private static Properties loadProperties(InputStream pInput) - throws IOException { - - if (pInput == null) { - throw new IllegalArgumentException("InputStream == null!"); - } - - Properties mapping = new Properties(); - /*if (pInput instanceof XMLPropertiesInputStream) { - mapping = new XMLProperties(); - } - else { - mapping = new Properties(); - }*/ - - // Load the properties - mapping.load(pInput); - - return mapping; - } - - @SuppressWarnings({"SuspiciousSystemArraycopy"}) - public static Object clone(final Cloneable pObject) throws CloneNotSupportedException { - if (pObject == null) { - return null; // Null is clonable.. Easy. ;-) - } - - // All arrays does have a clone method, but it's invisible for reflection... - // By luck, multi-dimensional primitive arrays are instances of Object[] - if (pObject instanceof Object[]) { - return ((Object[]) pObject).clone(); - } - else if (pObject.getClass().isArray()) { - // One-dimensional primitive array, cloned manually - int lenght = Array.getLength(pObject); - Object clone = Array.newInstance(pObject.getClass().getComponentType(), lenght); - System.arraycopy(pObject, 0, clone, 0, lenght); - return clone; - } - - try { - // Find the clone method - Method clone = null; - Class clazz = pObject.getClass(); - do { - try { - clone = clazz.getDeclaredMethod("clone"); - break; // Found, or throws exception above - } - catch (NoSuchMethodException ignore) { - // Ignore - } - } - while ((clazz = clazz.getSuperclass()) != null); - - // NOTE: This should never happen - if (clone == null) { - throw new CloneNotSupportedException(pObject.getClass().getName()); - } - - // Override access if needed - if (!clone.isAccessible()) { - clone.setAccessible(true); - } - - // Invoke clone method on original object - return clone.invoke(pObject); - } - catch (SecurityException e) { - CloneNotSupportedException cns = new CloneNotSupportedException(pObject.getClass().getName()); - cns.initCause(e); - throw cns; - } - catch (IllegalAccessException e) { - throw new CloneNotSupportedException(pObject.getClass().getName()); - } - catch (InvocationTargetException e) { - if (e.getTargetException() instanceof CloneNotSupportedException) { - throw (CloneNotSupportedException) e.getTargetException(); - } - else if (e.getTargetException() instanceof RuntimeException) { - throw (RuntimeException) e.getTargetException(); - } - else if (e.getTargetException() instanceof Error) { - throw (Error) e.getTargetException(); - } - - throw new CloneNotSupportedException(pObject.getClass().getName()); - } - } - -// public static void loadLibrary(String pLibrary) { -// NativeLoader.loadLibrary(pLibrary); -// } -// -// public static void loadLibrary(String pLibrary, ClassLoader pLoader) { -// NativeLoader.loadLibrary(pLibrary, pLoader); -// } - - public static void main(String[] args) throws CloneNotSupportedException { - - System.out.println("clone: " + args.clone().length + " (" + args.length + ")"); - System.out.println("copy: " + ((String[]) clone(args)).length + " (" + args.length + ")"); - - int[] ints = {1,2,3}; - int[] copies = (int[]) clone(ints); - System.out.println("Copies: " + copies.length + " (" + ints.length + ")"); - - int[][] intsToo = {{1}, {2,3}, {4,5,6}}; - int[][] copiesToo = (int[][]) clone(intsToo); - System.out.println("Copies: " + copiesToo.length + " (" + intsToo.length + ")"); - System.out.println("Copies0: " + copiesToo[0].length + " (" + intsToo[0].length + ")"); - System.out.println("Copies1: " + copiesToo[1].length + " (" + intsToo[1].length + ")"); - System.out.println("Copies2: " + copiesToo[2].length + " (" + intsToo[2].length + ")"); - - Map map = new HashMap(); - - for (String arg : args) { - map.put(arg, arg); - } - - Map copy = (Map) clone((Cloneable) map); - - System.out.println("Map : " + map); - System.out.println("Copy: " + copy); - - /* - SecurityManager sm = System.getSecurityManager(); - - try { - System.setSecurityManager(new SecurityManager() { - public void checkPermission(Permission perm) { - if (perm.getName().equals("suppressAccessChecks")) { - throw new SecurityException(); - } - //super.checkPermission(perm); - } - }); - */ - - Cloneable cloneable = new Cloneable() {}; // No public clone method - Cloneable clone = (Cloneable) clone(cloneable); - - System.out.println("cloneable: " + cloneable); - System.out.println("clone: " + clone); - - /* - } - finally { - System.setSecurityManager(sm); - } - */ - - AccessController.doPrivileged(new PrivilegedAction() { - public Void run() { - return null; - } - }, AccessController.getContext()); - - //String string = args.length > 0 ? args[0] : "jaffa"; - //clone(string); - } - - /** - * Tests if a named class is generally available. - * If a class is considered available, a call to - * {@code Class.forName(pClassName)} will not result in an exception. - * - * @param pClassName the class name to test - * @return {@code true} if available - */ - public static boolean isClassAvailable(String pClassName) { - return isClassAvailable(pClassName, (ClassLoader) null); - } - - /** - * Tests if a named class is available from another class. - * If a class is considered available, a call to - * {@code Class.forName(pClassName, true, pFromClass.getClassLoader())} - * will not result in an exception. - * - * @param pClassName the class name to test - * @param pFromClass the class to test from - * @return {@code true} if available - */ - public static boolean isClassAvailable(String pClassName, Class pFromClass) { - ClassLoader loader = pFromClass != null ? pFromClass.getClassLoader() : null; - return isClassAvailable(pClassName, loader); - } - - private static boolean isClassAvailable(String pClassName, ClassLoader pLoader) { - try { - // TODO: Sometimes init is not needed, but need to find a way to know... - getClass(pClassName, true, pLoader); - return true; - } - catch (SecurityException ignore) { - // Ignore - } - catch (ClassNotFoundException ignore) { - // Ignore - } - catch (LinkageError ignore) { - // Ignore - } - - return false; - } - - public static boolean isFieldAvailable(final String pClassName, final String pFieldName) { - return isFieldAvailable(pClassName, pFieldName, (ClassLoader) null); - } - - public static boolean isFieldAvailable(final String pClassName, final String pFieldName, final Class pFromClass) { - ClassLoader loader = pFromClass != null ? pFromClass.getClassLoader() : null; - return isFieldAvailable(pClassName, pFieldName, loader); - } - - private static boolean isFieldAvailable(final String pClassName, final String pFieldName, final ClassLoader pLoader) { - try { - Class cl = getClass(pClassName, false, pLoader); - - Field field = cl.getField(pFieldName); - if (field != null) { - return true; - } - } - catch (ClassNotFoundException ignore) { - // Ignore - } - catch (LinkageError ignore) { - // Ignore - } - catch (NoSuchFieldException ignore) { - // Ignore - } - return false; - } - - public static boolean isMethodAvailable(String pClassName, String pMethodName) { - // Finds void only - return isMethodAvailable(pClassName, pMethodName, null, (ClassLoader) null); - } - - public static boolean isMethodAvailable(String pClassName, String pMethodName, Class[] pParams) { - return isMethodAvailable(pClassName, pMethodName, pParams, (ClassLoader) null); - } - - public static boolean isMethodAvailable(String pClassName, String pMethodName, Class[] pParams, Class pFromClass) { - ClassLoader loader = pFromClass != null ? pFromClass.getClassLoader() : null; - return isMethodAvailable(pClassName, pMethodName, pParams, loader); - } - - private static boolean isMethodAvailable(String pClassName, String pMethodName, Class[] pParams, ClassLoader pLoader) { - try { - Class cl = getClass(pClassName, false, pLoader); - - Method method = cl.getMethod(pMethodName, pParams); - if (method != null) { - return true; - } - } - catch (ClassNotFoundException ignore) { - // Ignore - } - catch (LinkageError ignore) { - // Ignore - } - catch (NoSuchMethodException ignore) { - // Ignore - } - return false; - } - - private static Class getClass(String pClassName, boolean pInitialize, ClassLoader pLoader) throws ClassNotFoundException { - // 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(); - - return Class.forName(pClassName, pInitialize, loader); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.io.*; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * A utility class with some useful system-related functions. + *

+ * NOTE: This class is not considered part of the public API and may be + * changed without notice + *

+ * + * @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/SystemUtil.java#3 $ + * + */ +public final class SystemUtil { + /** {@code ".xml"} */ + public static String XML_PROPERTIES = ".xml"; + /** {@code ".properties"} */ + public static String STD_PROPERTIES = ".properties"; + + // Disallow creating objects of this type + private SystemUtil() { + } + + /** This class marks an inputstream as containing XML, does nothing */ + private static class XMLPropertiesInputStream extends FilterInputStream { + public XMLPropertiesInputStream(InputStream pIS) { + super(pIS); + } + } + + /** + * Gets the named resource as a stream from the given Class' Classoader. + * If the pGuessSuffix parameter is true, the method will try to append + * typical properties file suffixes, such as ".properties" or ".xml". + * + * @param pClassLoader the class loader to use + * @param pName name of the resource + * @param pGuessSuffix guess suffix + * + * @return an input stream reading from the resource + */ + private static InputStream getResourceAsStream(ClassLoader pClassLoader, String pName, boolean pGuessSuffix) { + InputStream is; + + if (!pGuessSuffix) { + is = pClassLoader.getResourceAsStream(pName); + + // If XML, wrap stream + if (is != null && pName.endsWith(XML_PROPERTIES)) { + is = new XMLPropertiesInputStream(is); + } + } + else { + // Try normal properties + is = pClassLoader.getResourceAsStream(pName + STD_PROPERTIES); + + // Try XML + if (is == null) { + is = pClassLoader.getResourceAsStream(pName + XML_PROPERTIES); + + // Wrap stream + if (is != null) { + is = new XMLPropertiesInputStream(is); + } + } + } + + // Return stream + return is; + } + + /** + * Gets the named file as a stream from the current directory. + * If the pGuessSuffix parameter is true, the method will try to append + * typical properties file suffixes, such as ".properties" or ".xml". + * + * @param pName name of the resource + * @param pGuessSuffix guess suffix + * + * @return an input stream reading from the resource + */ + private static InputStream getFileAsStream(String pName, boolean pGuessSuffix) { + InputStream is = null; + File propertiesFile; + + try { + if (!pGuessSuffix) { + // Get file + propertiesFile = new File(pName); + + if (propertiesFile.exists()) { + is = new FileInputStream(propertiesFile); + + // If XML, wrap stream + if (pName.endsWith(XML_PROPERTIES)) { + is = new XMLPropertiesInputStream(is); + } + } + } + else { + // Try normal properties + propertiesFile = new File(pName + STD_PROPERTIES); + + if (propertiesFile.exists()) { + is = new FileInputStream(propertiesFile); + } + else { + // Try XML + propertiesFile = new File(pName + XML_PROPERTIES); + + if (propertiesFile.exists()) { + // Wrap stream + is = new XMLPropertiesInputStream(new FileInputStream(propertiesFile)); + } + } + } + } + catch (FileNotFoundException fnf) { + // Should not happen, as we always test that the file .exists() + // before creating InputStream + // assert false; + } + + return is; + } + + /** + * Utility method for loading a named properties-file for a class. + *

+ * The properties-file is loaded through either: + *

    + *
  1. The given class' class loader (from classpath)
  2. + *
  3. Or, the system class loader (from classpath)
  4. + *
  5. Or, if it cannot be found in the classpath, an attempt to read from + * the current directory (or full path if given).
  6. + *
+ *

+ * Both normal java.util.Properties and com.twelvemonkeys.util.XMLProperties + * are supported (XML-properties must have ".xml" as its file extension). + * + * @param pClass The class to load properties for. If this parameter is + * {@code null}, the method will work exactly as + * {@link #loadProperties(String)} + * @param pName The name of the properties-file. If this parameter is + * {@code null}, the method will work exactly as + * {@link #loadProperties(Class)} + * + * @return A Properties mapping read from the given file or for the given + * class. + * + * @throws NullPointerException if both {@code pName} and + * {@code pClass} paramters are {@code null} + * @throws IOException if an error occurs during load. + * @throws FileNotFoundException if no properties-file could be found. + * + * @see #loadProperties(String) + * @see #loadProperties(Class) + * @see java.lang.ClassLoader#getResourceAsStream + * @see java.lang.ClassLoader#getSystemResourceAsStream + */ + // 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 { + // Convert to name the classloader understands + String name = !StringUtil.isEmpty(pName) ? pName : pClass.getName().replace('.', '/'); + + // Should we try to guess suffix? + boolean guessSuffix = (pName == null || pName.indexOf('.') < 0); + + InputStream is; + + // TODO: WHAT IF MULTIPLE RESOURCES EXISTS?! + // Try loading resource through the current class' classloader + 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) ? + // "XML-properties" : "Normal .properties") + // + " 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) { + // Nothing to do + //System.out.println(((is instanceof XMLPropertiesInputStream) ? + // "XML-properties" : "Normal .properties") + // + " from System ClassLoader"); + } + // All failed, try loading from file + else if ((is = getFileAsStream(name, guessSuffix)) != null) { + //System.out.println(((is instanceof XMLPropertiesInputStream) ? + // "XML-properties" : "Normal .properties") + // + " from System ClassLoader"); + } + else { + if (guessSuffix) { + // TODO: file extension iterator or something... + throw new FileNotFoundException(name + ".properties or " + name + ".xml"); + } + else { + throw new FileNotFoundException(name); + } + } + + // We have inputstream now, load... + try { + return loadProperties(is); + } + finally { + // NOTE: If is == null, a FileNotFoundException must have been thrown above + try { + is.close(); + } + catch (IOException ioe) { + // Not critical... + } + } + } + + /** + * Utility method for loading a properties-file for a given class. + * The properties are searched for on the form + * "com/package/ClassName.properties" or + * "com/package/ClassName.xml". + *

+ * The properties-file is loaded through either: + *

    + *
  1. The given class' class loader (from classpath)
  2. + *
  3. Or, the system class loader (from classpath)
  4. + *
  5. Or, if it cannot be found in the classpath, an attempt to read from + * the current directory (or full path if given).
  6. + *
+ *

+ * Both normal java.util.Properties and com.twelvemonkeys.util.XMLProperties + * are supported (XML-properties must have ".xml" as its file extension). + * + * @param pClass The class to load properties for + * @return A Properties mapping for the given class. + * + * @throws NullPointerException if the {@code pClass} paramters is + * {@code null} + * @throws IOException if an error occurs during load. + * @throws FileNotFoundException if no properties-file could be found. + * + * @see #loadProperties(String) + * @see #loadProperties(Class, String) + * @see java.lang.ClassLoader#getResourceAsStream + * @see java.lang.ClassLoader#getSystemResourceAsStream + * + */ + public static Properties loadProperties(Class pClass) throws IOException { + return loadProperties(pClass, null); + } + + /** + * Utility method for loading a named properties-file. + *

+ * The properties-file is loaded through either: + *

    + *
  1. The system class loader (from classpath)
  2. + *
  3. Or, if it cannot be found in the classpath, an attempt to read from + * the current directory.
  4. + *
+ *

+ * Both normal java.util.Properties and com.twelvemonkeys.util.XMLProperties + * are supported (XML-properties must have ".xml" as its file extension). + * + * @param pName The name of the properties-file. + * @return A Properties mapping read from the given file. + * + * @throws NullPointerException if the {@code pName} paramters is + * {@code null} + * @throws IOException if an error occurs during load. + * @throws FileNotFoundException if no properties-file could be found. + * + * @see #loadProperties(Class) + * @see #loadProperties(Class, String) + * @see java.lang.ClassLoader#getSystemResourceAsStream + * + */ + public static Properties loadProperties(String pName) throws IOException { + return loadProperties(null, pName); + } + + /* + * Utility method for loading a properties-file. + *

+ * The properties files may also be contained in a zip/jar-file named + * by the {@code com.twelvemonkeys.util.Config} system property (use "java -D" + * to override). Default is "config.zip" in the current directory. + * + * @param pName The name of the file to loaded + * @return A Properties mapping for the given class. If no properties- + * file was found, an empty Properties object is returned. + * + */ + /* + public static Properties loadProperties(String pName) throws IOException { + // Use XML? + boolean useXML = pName.endsWith(XML_PROPERTIES) ? true : false; + + InputStream is = null; + + File file = new File(pName); + + String configName = System.getProperty("com.twelvemonkeys.util.Config"); + File configArchive = new File(!StringUtil.isEmpty(configName) + ? configName : DEFAULT_CONFIG); + + // Get input stream to the file containing the properties + if (file.exists()) { + // Try reading from file, normal way + is = new FileInputStream(file); + } + else if (configArchive.exists()) { + // Try reading properties from zip-file + ZipFile zip = new ZipFile(configArchive); + ZipEntry ze = zip.getEntry(pName); + if (ze != null) { + is = zip.getInputStream(ze); + } + + } + + // Do the loading + try { + // Load the properties + return loadProperties(is, useXML); + } + finally { + // Try closing the archive to free resources + if (is != null) { + try { + is.close(); + } + catch (IOException ioe) { + // Not critical... + } + } + } + + } + */ + + /** + * Returns a Properties, loaded from the given inputstream. If the given + * inputstream is null, then an empty Properties object is returned. + * + * @param pInput the inputstream to read from + * + * @return a Properties object read from the given stream, or an empty + * Properties mapping, if the stream is null. + * + * @throws IOException if an error occurred when reading from the input + * stream. + * + */ + private static Properties loadProperties(InputStream pInput) + throws IOException { + + if (pInput == null) { + throw new IllegalArgumentException("InputStream == null!"); + } + + Properties mapping = new Properties(); + /*if (pInput instanceof XMLPropertiesInputStream) { + mapping = new XMLProperties(); + } + else { + mapping = new Properties(); + }*/ + + // Load the properties + mapping.load(pInput); + + return mapping; + } + + @SuppressWarnings({"SuspiciousSystemArraycopy"}) + public static Object clone(final Cloneable pObject) throws CloneNotSupportedException { + if (pObject == null) { + return null; // Null is clonable.. Easy. ;-) + } + + // All arrays does have a clone method, but it's invisible for reflection... + // By luck, multi-dimensional primitive arrays are instances of Object[] + if (pObject instanceof Object[]) { + return ((Object[]) pObject).clone(); + } + else if (pObject.getClass().isArray()) { + // One-dimensional primitive array, cloned manually + int lenght = Array.getLength(pObject); + Object clone = Array.newInstance(pObject.getClass().getComponentType(), lenght); + System.arraycopy(pObject, 0, clone, 0, lenght); + return clone; + } + + try { + // Find the clone method + Method clone = null; + Class clazz = pObject.getClass(); + do { + try { + clone = clazz.getDeclaredMethod("clone"); + break; // Found, or throws exception above + } + catch (NoSuchMethodException ignore) { + // Ignore + } + } + while ((clazz = clazz.getSuperclass()) != null); + + // NOTE: This should never happen + if (clone == null) { + throw new CloneNotSupportedException(pObject.getClass().getName()); + } + + // Override access if needed + if (!clone.isAccessible()) { + clone.setAccessible(true); + } + + // Invoke clone method on original object + return clone.invoke(pObject); + } + catch (SecurityException e) { + CloneNotSupportedException cns = new CloneNotSupportedException(pObject.getClass().getName()); + cns.initCause(e); + throw cns; + } + catch (IllegalAccessException e) { + throw new CloneNotSupportedException(pObject.getClass().getName()); + } + catch (InvocationTargetException e) { + if (e.getTargetException() instanceof CloneNotSupportedException) { + throw (CloneNotSupportedException) e.getTargetException(); + } + else if (e.getTargetException() instanceof RuntimeException) { + throw (RuntimeException) e.getTargetException(); + } + else if (e.getTargetException() instanceof Error) { + throw (Error) e.getTargetException(); + } + + throw new CloneNotSupportedException(pObject.getClass().getName()); + } + } + +// public static void loadLibrary(String pLibrary) { +// NativeLoader.loadLibrary(pLibrary); +// } +// +// public static void loadLibrary(String pLibrary, ClassLoader pLoader) { +// NativeLoader.loadLibrary(pLibrary, pLoader); +// } + + public static void main(String[] args) throws CloneNotSupportedException { + + System.out.println("clone: " + args.clone().length + " (" + args.length + ")"); + System.out.println("copy: " + ((String[]) clone(args)).length + " (" + args.length + ")"); + + int[] ints = {1,2,3}; + int[] copies = (int[]) clone(ints); + System.out.println("Copies: " + copies.length + " (" + ints.length + ")"); + + int[][] intsToo = {{1}, {2,3}, {4,5,6}}; + int[][] copiesToo = (int[][]) clone(intsToo); + System.out.println("Copies: " + copiesToo.length + " (" + intsToo.length + ")"); + System.out.println("Copies0: " + copiesToo[0].length + " (" + intsToo[0].length + ")"); + System.out.println("Copies1: " + copiesToo[1].length + " (" + intsToo[1].length + ")"); + System.out.println("Copies2: " + copiesToo[2].length + " (" + intsToo[2].length + ")"); + + Map map = new HashMap(); + + for (String arg : args) { + map.put(arg, arg); + } + + Map copy = (Map) clone((Cloneable) map); + + System.out.println("Map : " + map); + System.out.println("Copy: " + copy); + + /* + SecurityManager sm = System.getSecurityManager(); + + try { + System.setSecurityManager(new SecurityManager() { + public void checkPermission(Permission perm) { + if (perm.getName().equals("suppressAccessChecks")) { + throw new SecurityException(); + } + //super.checkPermission(perm); + } + }); + */ + + Cloneable cloneable = new Cloneable() {}; // No public clone method + Cloneable clone = (Cloneable) clone(cloneable); + + System.out.println("cloneable: " + cloneable); + System.out.println("clone: " + clone); + + /* + } + finally { + System.setSecurityManager(sm); + } + */ + + AccessController.doPrivileged(new PrivilegedAction() { + public Void run() { + return null; + } + }, AccessController.getContext()); + + //String string = args.length > 0 ? args[0] : "jaffa"; + //clone(string); + } + + /** + * Tests if a named class is generally available. + * If a class is considered available, a call to + * {@code Class.forName(pClassName)} will not result in an exception. + * + * @param pClassName the class name to test + * @return {@code true} if available + */ + public static boolean isClassAvailable(String pClassName) { + return isClassAvailable(pClassName, (ClassLoader) null); + } + + /** + * Tests if a named class is available from another class. + * If a class is considered available, a call to + * {@code Class.forName(pClassName, true, pFromClass.getClassLoader())} + * will not result in an exception. + * + * @param pClassName the class name to test + * @param pFromClass the class to test from + * @return {@code true} if available + */ + public static boolean isClassAvailable(String pClassName, Class pFromClass) { + ClassLoader loader = pFromClass != null ? pFromClass.getClassLoader() : null; + return isClassAvailable(pClassName, loader); + } + + private static boolean isClassAvailable(String pClassName, ClassLoader pLoader) { + try { + // TODO: Sometimes init is not needed, but need to find a way to know... + getClass(pClassName, true, pLoader); + return true; + } + catch (SecurityException ignore) { + // Ignore + } + catch (ClassNotFoundException ignore) { + // Ignore + } + catch (LinkageError ignore) { + // Ignore + } + + return false; + } + + public static boolean isFieldAvailable(final String pClassName, final String pFieldName) { + return isFieldAvailable(pClassName, pFieldName, (ClassLoader) null); + } + + public static boolean isFieldAvailable(final String pClassName, final String pFieldName, final Class pFromClass) { + ClassLoader loader = pFromClass != null ? pFromClass.getClassLoader() : null; + return isFieldAvailable(pClassName, pFieldName, loader); + } + + private static boolean isFieldAvailable(final String pClassName, final String pFieldName, final ClassLoader pLoader) { + try { + Class cl = getClass(pClassName, false, pLoader); + + Field field = cl.getField(pFieldName); + if (field != null) { + return true; + } + } + catch (ClassNotFoundException ignore) { + // Ignore + } + catch (LinkageError ignore) { + // Ignore + } + catch (NoSuchFieldException ignore) { + // Ignore + } + return false; + } + + public static boolean isMethodAvailable(String pClassName, String pMethodName) { + // Finds void only + return isMethodAvailable(pClassName, pMethodName, null, (ClassLoader) null); + } + + public static boolean isMethodAvailable(String pClassName, String pMethodName, Class[] pParams) { + return isMethodAvailable(pClassName, pMethodName, pParams, (ClassLoader) null); + } + + public static boolean isMethodAvailable(String pClassName, String pMethodName, Class[] pParams, Class pFromClass) { + ClassLoader loader = pFromClass != null ? pFromClass.getClassLoader() : null; + return isMethodAvailable(pClassName, pMethodName, pParams, loader); + } + + private static boolean isMethodAvailable(String pClassName, String pMethodName, Class[] pParams, ClassLoader pLoader) { + try { + Class cl = getClass(pClassName, false, pLoader); + + Method method = cl.getMethod(pMethodName, pParams); + if (method != null) { + return true; + } + } + catch (ClassNotFoundException ignore) { + // Ignore + } + catch (LinkageError ignore) { + // Ignore + } + catch (NoSuchMethodException ignore) { + // Ignore + } + return false; + } + + private static Class getClass(String pClassName, boolean pInitialize, ClassLoader pLoader) throws ClassNotFoundException { + // 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(); + + return Class.forName(pClassName, pInitialize, loader); + } +} 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 450081d4..6c61981d 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 @@ -36,10 +36,11 @@ import java.util.Map; /** * Kind of like {@code org.apache.commons.lang.Validate}. Just smarter. ;-) - *

+ *

* 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. + *

* * @author Harald Kuhr * @author last modified by $Author: haku $ 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 4d551e1f..a354be08 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 @@ -1,402 +1,404 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.io.Serializable; -import java.util.*; - -/** - * AbstractDecoratedMap - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java#2 $ - */ -// TODO: The generics in this class looks suspicious.. -abstract class AbstractDecoratedMap extends AbstractMap implements Map, Serializable, Cloneable { - protected Map> entries; - protected transient volatile int modCount; - - 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}. - */ - public AbstractDecoratedMap() { - this(new HashMap>(), null); - } - - /** - * Creates a {@code Map} backed by a {@code HashMap}, containing all - * key/value mappings from the given {@code Map}. - *

- * This is constructor is here to comply with the reccomendations for - * "standard" constructors in the {@code Map} interface. - * - * @see #AbstractDecoratedMap(java.util.Map, java.util.Map) - * - * @param pContents the map whose mappings are to be placed in this map. - * May be {@code null}. - */ - public AbstractDecoratedMap(Map pContents) { - this(new HashMap>(), pContents); - } - - /** - * Creates a {@code Map} backed by the given backing-{@code Map}, - * containing all key/value mappings from the given contents-{@code Map}. - *

- * NOTE: The backing map is structuraly cahnged, and it should NOT be - * accessed directly, after the wrapped map is created. - * - * @param pBacking the backing map of this map. Must be either empty, or - * the same map as {@code pContents}. - * @param pContents the map whose mappings are to be placed in this map. - * May be {@code null}. - * - * @throws IllegalArgumentException if {@code pBacking} is {@code null} - * or if {@code pBacking} differs from {@code pContent} and is not empty. - */ - public AbstractDecoratedMap(Map> pBacking, Map pContents) { - if (pBacking == null) { - throw new IllegalArgumentException("backing == null"); - } - - Entry[] entries = null; - if (pBacking == pContents) { - // NOTE: Special treatment to avoid ClassCastExceptions - Set> es = pContents.entrySet(); - //noinspection unchecked - entries = new Entry[es.size()]; - entries = es.toArray(entries); - pContents = null; - pBacking.clear(); - } - else if (!pBacking.isEmpty()) { - throw new IllegalArgumentException("backing must be empty"); - } - - this.entries = pBacking; - init(); - - if (pContents != null) { - putAll(pContents); - } - else if (entries != null) { - // Reinsert entries, this time wrapped - for (Entry entry : entries) { - put(entry.getKey(), entry.getValue()); - } - } - } - - /** - * Default implementation, does nothing. - */ - protected void init() { - } - - public int size() { - return entries.size(); - } - - public void clear() { - entries.clear(); - modCount++; - init(); - } - - public boolean isEmpty() { - return entries.isEmpty(); - } - - public boolean containsKey(Object pKey) { - return entries.containsKey(pKey); - } - - /** - * Returns {@code true} if this map maps one or more keys to the - * specified pValue. More formally, returns {@code true} if and only if - * this map contains at least one mapping to a pValue {@code v} such that - * {@code (pValue==null ? v==null : pValue.equals(v))}. - *

- * This implementation requires time linear in the map size for this - * operation. - * - * @param pValue pValue whose presence in this map is to be tested. - * @return {@code true} if this map maps one or more keys to the - * specified pValue. - */ - public boolean containsValue(Object pValue) { - for (V value : values()) { - if (value == pValue || (value != null && value.equals(pValue))) { - return true; - } - } - - return false; - } - - public Collection values() { - Collection values = this.values; - return values != null ? values : (this.values = new Values()); - } - - public Set> entrySet() { - Set> es = entrySet; - return es != null ? es : (entrySet = new EntrySet()); - } - - public Set keySet() { - Set ks = keySet; - return ks != null ? ks : (keySet = new KeySet()); - } - - /** - * Returns a shallow copy of this {@code AbstractMap} instance: the keys - * and values themselves are not cloned. - * - * @return a shallow copy of this map. - */ - protected Object clone() throws CloneNotSupportedException { - AbstractDecoratedMap map = (AbstractDecoratedMap) super.clone(); - - map.values = null; - map.entrySet = null; - map.keySet = null; - - // TODO: Implement: Need to clone the backing map... - - return map; - } - - // Subclass overrides these to alter behavior of views' iterator() method - protected abstract Iterator newKeyIterator(); - - protected abstract Iterator newValueIterator(); - - protected abstract Iterator> newEntryIterator(); - - // TODO: Implement these (get/put/remove)? - public abstract V get(Object pKey); - - public abstract V remove(Object pKey); - - public abstract V put(K pKey, V pValue); - - /*protected*/ Entry createEntry(K pKey, V pValue) { - return new BasicEntry(pKey, pValue); - } - - /*protected*/ Entry getEntry(K pKey) { - return entries.get(pKey); - } - - /** - * Removes the given entry from the Map. - * - * @param pEntry the entry to be removed - * - * @return the removed entry, or {@code null} if nothing was removed. - */ - protected Entry removeEntry(Entry pEntry) { - if (pEntry == null) { - return null; - } - - // Find candidate entry for this key - Entry candidate = getEntry(pEntry.getKey()); - if (candidate == pEntry || (candidate != null && candidate.equals(pEntry))) { - // Remove - remove(pEntry.getKey()); - return pEntry; - } - return null; - } - - protected class Values extends AbstractCollection { - public Iterator iterator() { - return newValueIterator(); - } - - public int size() { - return AbstractDecoratedMap.this.size(); - } - - public boolean contains(Object o) { - return containsValue(o); - } - - public void clear() { - AbstractDecoratedMap.this.clear(); - } - } - - protected class EntrySet extends AbstractSet> { - public Iterator> iterator() { - return newEntryIterator(); - } - - public boolean contains(Object o) { - if (!(o instanceof Entry)) - return false; - Entry e = (Entry) o; - - //noinspection SuspiciousMethodCalls - Entry candidate = entries.get(e.getKey()); - return candidate != null && candidate.equals(e); - } - - public boolean remove(Object o) { - if (!(o instanceof Entry)) { - return false; - } - - /* - // 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) entries.get(key); - - // Same entry? - if (entry != null && entry.equals(o)) { - return AbstractWrappedMap.this.remove(key) != null; - } - - return false; - */ - - //noinspection unchecked - return AbstractDecoratedMap.this.removeEntry((Entry) o) != null; - } - - public int size() { - return AbstractDecoratedMap.this.size(); - } - - public void clear() { - AbstractDecoratedMap.this.clear(); - } - } - - protected class KeySet extends AbstractSet { - public Iterator iterator() { - return newKeyIterator(); - } - public int size() { - return AbstractDecoratedMap.this.size(); - } - public boolean contains(Object o) { - return containsKey(o); - } - public boolean remove(Object o) { - return AbstractDecoratedMap.this.remove(o) != null; - } - public void clear() { - AbstractDecoratedMap.this.clear(); - } - } - - /** - * A simple Map.Entry implementaton. - */ - static class BasicEntry implements Entry, Serializable { - K mKey; - V mValue; - - BasicEntry(K pKey, V pValue) { - mKey = pKey; - mValue = pValue; - } - - /** - * Default implementation does nothing. - * - * @param pMap the map that is accessed - */ - protected void recordAccess(Map pMap) { - } - - /** - * Default implementation does nothing. - * @param pMap the map that is removed from - */ - protected void recordRemoval(Map pMap) { - } - - public V getValue() { - return mValue; - } - - public V setValue(V pValue) { - V oldValue = mValue; - mValue = pValue; - return oldValue; - } - - public K getKey() { - return mKey; - } - - public boolean equals(Object pOther) { - if (!(pOther instanceof Map.Entry)) { - return false; - } - - Map.Entry entry = (Map.Entry) pOther; - - Object k1 = mKey; - Object k2 = entry.getKey(); - - if (k1 == k2 || (k1 != null && k1.equals(k2))) { - Object v1 = mValue; - Object v2 = entry.getValue(); - - if (v1 == v2 || (v1 != null && v1.equals(v2))) { - return true; - } - } - - return false; - } - - public int hashCode() { - return (mKey == null ? 0 : mKey.hashCode()) ^ - (mValue == null ? 0 : mValue.hashCode()); - } - - public String toString() { - return getKey() + "=" + getValue(); - } - } +/* + * 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 of the copyright holder 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 HOLDER 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.io.Serializable; +import java.util.*; + +/** + * AbstractDecoratedMap + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java#2 $ + */ +// TODO: The generics in this class looks suspicious.. +abstract class AbstractDecoratedMap extends AbstractMap implements Map, Serializable, Cloneable { + protected Map> entries; + protected transient volatile int modCount; + + 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}. + */ + public AbstractDecoratedMap() { + this(new HashMap>(), null); + } + + /** + * Creates a {@code Map} backed by a {@code HashMap}, containing all + * key/value mappings from the given {@code Map}. + *

+ * This is constructor is here to comply with the reccomendations for + * "standard" constructors in the {@code Map} interface. + *

+ * + * @see #AbstractDecoratedMap(java.util.Map, java.util.Map) + * + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null}. + */ + public AbstractDecoratedMap(Map pContents) { + this(new HashMap>(), pContents); + } + + /** + * Creates a {@code Map} backed by the given backing-{@code Map}, + * containing all key/value mappings from the given contents-{@code Map}. + *

+ * NOTE: The backing map is structuraly cahnged, and it should NOT be + * accessed directly, after the wrapped map is created. + *

+ * + * @param pBacking the backing map of this map. Must be either empty, or + * the same map as {@code pContents}. + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null}. + * + * @throws IllegalArgumentException if {@code pBacking} is {@code null} + * or if {@code pBacking} differs from {@code pContent} and is not empty. + */ + public AbstractDecoratedMap(Map> pBacking, Map pContents) { + if (pBacking == null) { + throw new IllegalArgumentException("backing == null"); + } + + Entry[] entries = null; + if (pBacking == pContents) { + // NOTE: Special treatment to avoid ClassCastExceptions + Set> es = pContents.entrySet(); + //noinspection unchecked + entries = new Entry[es.size()]; + entries = es.toArray(entries); + pContents = null; + pBacking.clear(); + } + else if (!pBacking.isEmpty()) { + throw new IllegalArgumentException("backing must be empty"); + } + + this.entries = pBacking; + init(); + + if (pContents != null) { + putAll(pContents); + } + else if (entries != null) { + // Reinsert entries, this time wrapped + for (Entry entry : entries) { + put(entry.getKey(), entry.getValue()); + } + } + } + + /** + * Default implementation, does nothing. + */ + protected void init() { + } + + public int size() { + return entries.size(); + } + + public void clear() { + entries.clear(); + modCount++; + init(); + } + + public boolean isEmpty() { + return entries.isEmpty(); + } + + public boolean containsKey(Object pKey) { + return entries.containsKey(pKey); + } + + /** + * Returns {@code true} if this map maps one or more keys to the + * specified pValue. More formally, returns {@code true} if and only if + * this map contains at least one mapping to a pValue {@code v} such that + * {@code (pValue==null ? v==null : pValue.equals(v))}. + *

+ * This implementation requires time linear in the map size for this + * operation. + *

+ * + * @param pValue pValue whose presence in this map is to be tested. + * @return {@code true} if this map maps one or more keys to the + * specified pValue. + */ + public boolean containsValue(Object pValue) { + for (V value : values()) { + if (value == pValue || (value != null && value.equals(pValue))) { + return true; + } + } + + return false; + } + + public Collection values() { + Collection values = this.values; + return values != null ? values : (this.values = new Values()); + } + + public Set> entrySet() { + Set> es = entrySet; + return es != null ? es : (entrySet = new EntrySet()); + } + + public Set keySet() { + Set ks = keySet; + return ks != null ? ks : (keySet = new KeySet()); + } + + /** + * Returns a shallow copy of this {@code AbstractMap} instance: the keys + * and values themselves are not cloned. + * + * @return a shallow copy of this map. + */ + protected Object clone() throws CloneNotSupportedException { + AbstractDecoratedMap map = (AbstractDecoratedMap) super.clone(); + + map.values = null; + map.entrySet = null; + map.keySet = null; + + // TODO: Implement: Need to clone the backing map... + + return map; + } + + // Subclass overrides these to alter behavior of views' iterator() method + protected abstract Iterator newKeyIterator(); + + protected abstract Iterator newValueIterator(); + + protected abstract Iterator> newEntryIterator(); + + // TODO: Implement these (get/put/remove)? + public abstract V get(Object pKey); + + public abstract V remove(Object pKey); + + public abstract V put(K pKey, V pValue); + + /*protected*/ Entry createEntry(K pKey, V pValue) { + return new BasicEntry(pKey, pValue); + } + + /*protected*/ Entry getEntry(K pKey) { + return entries.get(pKey); + } + + /** + * Removes the given entry from the Map. + * + * @param pEntry the entry to be removed + * + * @return the removed entry, or {@code null} if nothing was removed. + */ + protected Entry removeEntry(Entry pEntry) { + if (pEntry == null) { + return null; + } + + // Find candidate entry for this key + Entry candidate = getEntry(pEntry.getKey()); + if (candidate == pEntry || (candidate != null && candidate.equals(pEntry))) { + // Remove + remove(pEntry.getKey()); + return pEntry; + } + return null; + } + + protected class Values extends AbstractCollection { + public Iterator iterator() { + return newValueIterator(); + } + + public int size() { + return AbstractDecoratedMap.this.size(); + } + + public boolean contains(Object o) { + return containsValue(o); + } + + public void clear() { + AbstractDecoratedMap.this.clear(); + } + } + + protected class EntrySet extends AbstractSet> { + public Iterator> iterator() { + return newEntryIterator(); + } + + public boolean contains(Object o) { + if (!(o instanceof Entry)) + return false; + Entry e = (Entry) o; + + //noinspection SuspiciousMethodCalls + Entry candidate = entries.get(e.getKey()); + return candidate != null && candidate.equals(e); + } + + public boolean remove(Object o) { + if (!(o instanceof Entry)) { + return false; + } + + /* + // 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) entries.get(key); + + // Same entry? + if (entry != null && entry.equals(o)) { + return AbstractWrappedMap.this.remove(key) != null; + } + + return false; + */ + + //noinspection unchecked + return AbstractDecoratedMap.this.removeEntry((Entry) o) != null; + } + + public int size() { + return AbstractDecoratedMap.this.size(); + } + + public void clear() { + AbstractDecoratedMap.this.clear(); + } + } + + protected class KeySet extends AbstractSet { + public Iterator iterator() { + return newKeyIterator(); + } + public int size() { + return AbstractDecoratedMap.this.size(); + } + public boolean contains(Object o) { + return containsKey(o); + } + public boolean remove(Object o) { + return AbstractDecoratedMap.this.remove(o) != null; + } + public void clear() { + AbstractDecoratedMap.this.clear(); + } + } + + /** + * A simple Map.Entry implementaton. + */ + static class BasicEntry implements Entry, Serializable { + K mKey; + V mValue; + + BasicEntry(K pKey, V pValue) { + mKey = pKey; + mValue = pValue; + } + + /** + * Default implementation does nothing. + * + * @param pMap the map that is accessed + */ + protected void recordAccess(Map pMap) { + } + + /** + * Default implementation does nothing. + * @param pMap the map that is removed from + */ + protected void recordRemoval(Map pMap) { + } + + public V getValue() { + return mValue; + } + + public V setValue(V pValue) { + V oldValue = mValue; + mValue = pValue; + return oldValue; + } + + public K getKey() { + return mKey; + } + + public boolean equals(Object pOther) { + if (!(pOther instanceof Map.Entry)) { + return false; + } + + Map.Entry entry = (Map.Entry) pOther; + + Object k1 = mKey; + Object k2 = entry.getKey(); + + if (k1 == k2 || (k1 != null && k1.equals(k2))) { + Object v1 = mValue; + Object v2 = entry.getValue(); + + if (v1 == v2 || (v1 != null && v1.equals(v2))) { + return true; + } + } + + return false; + } + + public int hashCode() { + return (mKey == null ? 0 : mKey.hashCode()) ^ + (mValue == null ? 0 : mValue.hashCode()); + } + + public String toString() { + return getKey() + "=" + getValue(); + } + } } \ No newline at end of file diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java index aaebbe4e..bd4e284c 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java @@ -1,89 +1,88 @@ -/* - * 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 of the copyright holder 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 HOLDER 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; - -/** - * Abstract base class for {@code TokenIterator}s to extend. - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java#1 $ - */ -public abstract class AbstractTokenIterator implements TokenIterator { - - /** - * Not supported. - * - * @throws UnsupportedOperationException {@code remove} is not supported by - * this Iterator. - */ - public void remove() { - // TODO: This is not difficult: - // - Convert String to StringBuilder in constructor - // - delete(pos, next.lenght()) - // - Add toString() method - // BUT: Would it ever be useful? :-) - - throw new UnsupportedOperationException("remove"); - } - - public final boolean hasMoreTokens() { - return hasNext(); - } - - /** - * Returns the next element in the iteration as a {@code String}. - * This implementation simply returns {@code (String) next()}. - * - * @return the next element in the iteration. - * @exception java.util.NoSuchElementException iteration has no more elements. - * @see #next() - */ - public final String nextToken() { - return next(); - } - - /** - * This implementation simply returns {@code hasNext()}. - * @see #hasNext() - */ - public final boolean hasMoreElements() { - return hasNext(); - } - - /** - * This implementation simply returns {@code next()}. - * @see #next() - */ - public final String nextElement() { - return next(); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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; + +/** + * Abstract base class for {@code TokenIterator}s to extend. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java#1 $ + */ +public abstract class AbstractTokenIterator implements TokenIterator { + + /** + * Not supported. + * + * @throws UnsupportedOperationException {@code remove} is not supported by + * this Iterator. + */ + public void remove() { + // TODO: This is not difficult: + // - Convert String to StringBuilder in constructor + // - delete(pos, next.lenght()) + // - Add toString() method + // BUT: Would it ever be useful? :-) + + throw new UnsupportedOperationException("remove"); + } + + public final boolean hasMoreTokens() { + return hasNext(); + } + + /** + * Returns the next element in the iteration as a {@code String}. + * This implementation simply returns {@code (String) next()}. + * + * @return the next element in the iteration. + * @exception java.util.NoSuchElementException iteration has no more elements. + * @see #next() + */ + public final String nextToken() { + return next(); + } + + /** + * This implementation simply returns {@code hasNext()}. + * @see #hasNext() + */ + public final boolean hasMoreElements() { + return hasNext(); + } + + /** + * This implementation simply returns {@code next()}. + * @see #next() + */ + public final String nextElement() { + return next(); + } +} 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 906c3391..6dd0edbe 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 @@ -1,247 +1,248 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.IndexedPropertyDescriptor; -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; -import java.io.Serializable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.*; - -/** - * A {@code Map} adapter for a Java Bean. - *

- * Ruthlessly stolen from - * initDescriptors(Object pBean) throws IntrospectionException { - final Set descriptors = new HashSet(); - - PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(pBean.getClass()).getPropertyDescriptors(); - for (PropertyDescriptor descriptor : propertyDescriptors) { - // Skip Object.getClass(), as you probably never want it - if ("class".equals(descriptor.getName()) && descriptor.getPropertyType() == Class.class) { - continue; - } - - // Only support simple setter/getters. - if (!(descriptor instanceof IndexedPropertyDescriptor)) { - descriptors.add(descriptor); - } - } - - return Collections.unmodifiableSet(descriptors); - } - - public Set> entrySet() { - return new BeanSet(); - } - - public Object get(final Object pKey) { - return super.get(pKey); - } - - public Object put(final String pKey, final Object pValue) { - checkKey(pKey); - - for (Entry entry : entrySet()) { - if (entry.getKey().equals(pKey)) { - return entry.setValue(pValue); - } - } - - return null; - } - - public Object remove(final Object pKey) { - return super.remove(checkKey(pKey)); - } - - public int size() { - return descriptors.size(); - } - - private String checkKey(final Object pKey) { - if (pKey == null) { - throw new IllegalArgumentException("key == null"); - } - // NB - the cast forces CCE if key is the wrong type. - final String name = (String) pKey; - - if (!containsKey(name)) { - throw new IllegalArgumentException("Bad key: " + pKey); - } - - return name; - } - - private Object readResolve() throws IntrospectionException { - // Initialize the property descriptors - descriptors = initDescriptors(bean); - return this; - } - - private class BeanSet extends AbstractSet> { - public Iterator> iterator() { - return new BeanIterator(descriptors.iterator()); - } - - public int size() { - return descriptors.size(); - } - } - - private class BeanIterator implements Iterator> { - private final Iterator mIterator; - - public BeanIterator(final Iterator pIterator) { - mIterator = pIterator; - } - - public boolean hasNext() { - return mIterator.hasNext(); - } - - public BeanEntry next() { - return new BeanEntry(mIterator.next()); - } - - public void remove() { - mIterator.remove(); - } - } - - private class BeanEntry implements Entry { - private final PropertyDescriptor mDescriptor; - - public BeanEntry(final PropertyDescriptor pDescriptor) { - this.mDescriptor = pDescriptor; - } - - public String getKey() { - return mDescriptor.getName(); - } - - public Object getValue() { - return unwrap(new Wrapped() { - public Object run() throws IllegalAccessException, InvocationTargetException { - final Method method = mDescriptor.getReadMethod(); - // A write-only bean. - if (method == null) { - throw new UnsupportedOperationException("No getter: " + mDescriptor.getName()); - } - - return method.invoke(bean); - } - }); - } - - public Object setValue(final Object pValue) { - return unwrap(new Wrapped() { - public Object run() throws IllegalAccessException, InvocationTargetException { - final Method method = mDescriptor.getWriteMethod(); - // A read-only bean. - if (method == null) { - throw new UnsupportedOperationException("No write method for property: " + mDescriptor.getName()); - } - - final Object old = getValue(); - method.invoke(bean, pValue); - return old; - } - }); - } - - public boolean equals(Object pOther) { - if (!(pOther instanceof Map.Entry)) { - return false; - } - - Map.Entry entry = (Map.Entry) pOther; - - Object k1 = getKey(); - Object k2 = entry.getKey(); - - if (k1 == k2 || (k1 != null && k1.equals(k2))) { - Object v1 = getValue(); - Object v2 = entry.getValue(); - - if (v1 == v2 || (v1 != null && v1.equals(v2))) { - return true; - } - } - - return false; - } - - public int hashCode() { - return (getKey() == null ? 0 : getKey().hashCode()) ^ - (getValue() == null ? 0 : getValue().hashCode()); - } - - public String toString() { - return getKey() + "=" + getValue(); - } - } - - private static interface Wrapped { - Object run() throws IllegalAccessException, InvocationTargetException; - } - - private static Object unwrap(final Wrapped wrapped) { - try { - return wrapped.run(); - } - catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - catch (final InvocationTargetException e) { - // Javadocs for setValue indicate cast is ok. - throw (RuntimeException) e.getCause(); - } - } +/* + * 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 of the copyright holder 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 HOLDER 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.IndexedPropertyDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; + +/** + * A {@code Map} adapter for a Java Bean. + *

+ * Ruthlessly stolen from + * Binkley's Blog + *

+ */ +public final class BeanMap extends AbstractMap implements Serializable, Cloneable { + private final Object bean; + private transient Set descriptors; + + public BeanMap(Object pBean) throws IntrospectionException { + if (pBean == null) { + throw new IllegalArgumentException("bean == null"); + } + + bean = pBean; + descriptors = initDescriptors(pBean); + } + + private static Set initDescriptors(Object pBean) throws IntrospectionException { + final Set descriptors = new HashSet(); + + PropertyDescriptor[] propertyDescriptors = Introspector.getBeanInfo(pBean.getClass()).getPropertyDescriptors(); + for (PropertyDescriptor descriptor : propertyDescriptors) { + // Skip Object.getClass(), as you probably never want it + if ("class".equals(descriptor.getName()) && descriptor.getPropertyType() == Class.class) { + continue; + } + + // Only support simple setter/getters. + if (!(descriptor instanceof IndexedPropertyDescriptor)) { + descriptors.add(descriptor); + } + } + + return Collections.unmodifiableSet(descriptors); + } + + public Set> entrySet() { + return new BeanSet(); + } + + public Object get(final Object pKey) { + return super.get(pKey); + } + + public Object put(final String pKey, final Object pValue) { + checkKey(pKey); + + for (Entry entry : entrySet()) { + if (entry.getKey().equals(pKey)) { + return entry.setValue(pValue); + } + } + + return null; + } + + public Object remove(final Object pKey) { + return super.remove(checkKey(pKey)); + } + + public int size() { + return descriptors.size(); + } + + private String checkKey(final Object pKey) { + if (pKey == null) { + throw new IllegalArgumentException("key == null"); + } + // NB - the cast forces CCE if key is the wrong type. + final String name = (String) pKey; + + if (!containsKey(name)) { + throw new IllegalArgumentException("Bad key: " + pKey); + } + + return name; + } + + private Object readResolve() throws IntrospectionException { + // Initialize the property descriptors + descriptors = initDescriptors(bean); + return this; + } + + private class BeanSet extends AbstractSet> { + public Iterator> iterator() { + return new BeanIterator(descriptors.iterator()); + } + + public int size() { + return descriptors.size(); + } + } + + private class BeanIterator implements Iterator> { + private final Iterator mIterator; + + public BeanIterator(final Iterator pIterator) { + mIterator = pIterator; + } + + public boolean hasNext() { + return mIterator.hasNext(); + } + + public BeanEntry next() { + return new BeanEntry(mIterator.next()); + } + + public void remove() { + mIterator.remove(); + } + } + + private class BeanEntry implements Entry { + private final PropertyDescriptor mDescriptor; + + public BeanEntry(final PropertyDescriptor pDescriptor) { + this.mDescriptor = pDescriptor; + } + + public String getKey() { + return mDescriptor.getName(); + } + + public Object getValue() { + return unwrap(new Wrapped() { + public Object run() throws IllegalAccessException, InvocationTargetException { + final Method method = mDescriptor.getReadMethod(); + // A write-only bean. + if (method == null) { + throw new UnsupportedOperationException("No getter: " + mDescriptor.getName()); + } + + return method.invoke(bean); + } + }); + } + + public Object setValue(final Object pValue) { + return unwrap(new Wrapped() { + public Object run() throws IllegalAccessException, InvocationTargetException { + final Method method = mDescriptor.getWriteMethod(); + // A read-only bean. + if (method == null) { + throw new UnsupportedOperationException("No write method for property: " + mDescriptor.getName()); + } + + final Object old = getValue(); + method.invoke(bean, pValue); + return old; + } + }); + } + + public boolean equals(Object pOther) { + if (!(pOther instanceof Map.Entry)) { + return false; + } + + Map.Entry entry = (Map.Entry) pOther; + + Object k1 = getKey(); + Object k2 = entry.getKey(); + + if (k1 == k2 || (k1 != null && k1.equals(k2))) { + Object v1 = getValue(); + Object v2 = entry.getValue(); + + if (v1 == v2 || (v1 != null && v1.equals(v2))) { + return true; + } + } + + return false; + } + + public int hashCode() { + return (getKey() == null ? 0 : getKey().hashCode()) ^ + (getValue() == null ? 0 : getValue().hashCode()); + } + + public String toString() { + return getKey() + "=" + getValue(); + } + } + + private static interface Wrapped { + Object run() throws IllegalAccessException, InvocationTargetException; + } + + private static Object unwrap(final Wrapped wrapped) { + try { + return wrapped.run(); + } + catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + catch (final InvocationTargetException e) { + // Javadocs for setValue indicate cast is ok. + throw (RuntimeException) e.getCause(); + } + } } \ No newline at end of file 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 3b492a61..b85b892a 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 @@ -1,635 +1,637 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.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: com/twelvemonkeys/util/CollectionUtil.java#3 $ - * @see Collections - * @see Arrays - */ -public final class CollectionUtil { - - /** - * Testing only. - * - * @param pArgs command line arguents - */ - @SuppressWarnings({"UnusedDeclaration", "UnusedAssignment", "unchecked"}) - public static void main(String[] pArgs) { - int howMany = 1000; - - if (pArgs.length > 0) { - howMany = Integer.parseInt(pArgs[0]); - } - long start; - long end; - - /* - int[] intArr1 = new int[] { - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 - }; - int[] intArr2 = new int[] { - 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 - }; - start = System.currentTimeMillis(); - - for (int i = 0; i < howMany; i++) { - intArr1 = (int[]) mergeArrays(intArr1, 0, intArr1.length, intArr2, 0, intArr2.length); - - } - end = System.currentTimeMillis(); - - System.out.println("mergeArrays: " + howMany + " * " + intArr2.length + " ints took " + (end - start) + " milliseconds (" + intArr1.length - + ")"); - */ - - //////////////////////////////// - String[] stringArr1 = new String[]{ - "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" - }; - - /* - String[] stringArr2 = new String[] { - "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" - }; - - start = System.currentTimeMillis(); - for (int i = 0; i < howMany; i++) { - stringArr1 = (String[]) mergeArrays(stringArr1, 0, stringArr1.length, stringArr2, 0, stringArr2.length); - - } - end = System.currentTimeMillis(); - System.out.println("mergeArrays: " + howMany + " * " + stringArr2.length + " Strings took " + (end - start) + " milliseconds (" - + stringArr1.length + ")"); - - - start = System.currentTimeMillis(); - while (intArr1.length > stringArr2.length) { - intArr1 = (int[]) subArray(intArr1, 0, intArr1.length - stringArr2.length); - - } - end = System.currentTimeMillis(); - - System.out.println("subArray: " + howMany + " * " + intArr2.length + " ints took " + (end - start) + " milliseconds (" + intArr1.length - + ")"); - - start = System.currentTimeMillis(); - while (stringArr1.length > stringArr2.length) { - stringArr1 = (String[]) subArray(stringArr1, stringArr2.length); - - } - end = System.currentTimeMillis(); - System.out.println("subArray: " + howMany + " * " + stringArr2.length + " Strings took " + (end - start) + " milliseconds (" - + stringArr1.length + ")"); - - */ - stringArr1 = new String[]{ - "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", - "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" - }; - System.out.println("\nFilterIterators:\n"); - List list = Arrays.asList(stringArr1); - Iterator iter = new FilterIterator(list.iterator(), new FilterIterator.Filter() { - - public boolean accept(Object pElement) { - return ((String) pElement).length() > 5; - } - }); - - while (iter.hasNext()) { - String str = (String) iter.next(); - - System.out.println(str + " has more than 5 letters!"); - } - iter = new FilterIterator(list.iterator(), new FilterIterator.Filter() { - - public boolean accept(Object pElement) { - return ((String) pElement).length() <= 5; - } - }); - - while (iter.hasNext()) { - String str = (String) iter.next(); - - System.out.println(str + " has less than, or exactly 5 letters!"); - } - start = System.currentTimeMillis(); - - for (int i = 0; i < howMany; i++) { - iter = new FilterIterator(list.iterator(), new FilterIterator.Filter() { - - public boolean accept(Object pElement) { - return ((String) pElement).length() <= 5; - } - }); - while (iter.hasNext()) { - iter.next(); - System.out.print(""); - } - } - -// end = System.currentTimeMillis(); -// System.out.println("Time: " + (end - start) + " ms"); -// System.out.println("\nClosureCollection:\n"); -// forEach(list, new Closure() { -// -// public void execute(Object pElement) { -// -// String str = (String) pElement; -// -// if (str.length() > 5) { -// System.out.println(str + " has more than 5 letters!"); -// } -// else { -// System.out.println(str + " has less than, or exactly 5 letters!"); -// } -// } -// }); -// start = System.currentTimeMillis(); -// for (int i = 0; i < howMany; i++) { -// forEach(list, new Closure() { -// -// public void execute(Object pElement) { -// -// String str = (String) pElement; -// -// if (str.length() <= 5) { -// System.out.print(""); -// } -// } -// }); -// } -// end = System.currentTimeMillis(); -// System.out.println("Time: " + (end - start) + " ms"); - } - - // Disallow creating objects of this type - private CollectionUtil() {} - - /** - * Merges two arrays into a new array. Elements from array1 and array2 will - * be copied into a new array, that has array1.length + array2.length - * elements. - * - * @param pArray1 First array - * @param pArray2 Second array, must be compatible with (assignable from) - * the first array - * @return A new array, containing the values of array1 and array2. The - * array (wrapped as an object), will have the length of array1 + - * array2, and can be safely cast to the type of the array1 - * parameter. - * @see #mergeArrays(Object,int,int,Object,int,int) - * @see java.lang.System#arraycopy(Object,int,Object,int,int) - */ - public static Object mergeArrays(Object pArray1, Object pArray2) { - return mergeArrays(pArray1, 0, Array.getLength(pArray1), pArray2, 0, Array.getLength(pArray2)); - } - - /** - * Merges two arrays into a new array. Elements from pArray1 and pArray2 will - * be copied into a new array, that has pLength1 + pLength2 elements. - * - * @param pArray1 First array - * @param pOffset1 the offset into the first array - * @param pLength1 the number of elements to copy from the first array - * @param pArray2 Second array, must be compatible with (assignable from) - * the first array - * @param pOffset2 the offset into the second array - * @param pLength2 the number of elements to copy from the second array - * @return A new array, containing the values of pArray1 and pArray2. The - * array (wrapped as an object), will have the length of pArray1 + - * pArray2, and can be safely cast to the type of the pArray1 - * parameter. - * @see java.lang.System#arraycopy(Object,int,Object,int,int) - */ - @SuppressWarnings({"SuspiciousSystemArraycopy"}) - public static Object mergeArrays(Object pArray1, int pOffset1, int pLength1, Object pArray2, int pOffset2, int pLength2) { - Class class1 = pArray1.getClass(); - Class type = class1.getComponentType(); - - // Create new array of the new length - Object array = Array.newInstance(type, pLength1 + pLength2); - - System.arraycopy(pArray1, pOffset1, array, 0, pLength1); - System.arraycopy(pArray2, pOffset2, array, pLength1, pLength2); - return array; - } - - /** - * 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} or - * if {@code pArray} is not an array. - * @throws ArrayIndexOutOfBoundsException if {@code pStart} < 0 - */ - public static Object subArray(Object pArray, int pStart) { - 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} 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} or - * if {@code pArray} is not an array. - * @throws ArrayIndexOutOfBoundsException if {@code pStart} < 0 - */ - @SuppressWarnings({"SuspiciousSystemArraycopy"}) - public static Object subArray(Object pArray, int pStart, int pLength) { - Validate.notNull(pArray, "array"); - - // Get component type - Class type; - - // Sanity check start index - if (pStart < 0) { - throw new ArrayIndexOutOfBoundsException(pStart + " < 0"); - } - // Check if argument is array - else if ((type = pArray.getClass().getComponentType()) == null) { - // NOTE: No need to test class.isArray(), really - throw new IllegalArgumentException("Not an array: " + pArray); - } - - // Store original length - int originalLength = Array.getLength(pArray); - - // Find new length, stay within bounds - int newLength = (pLength < 0) - ? Math.max(0, originalLength - pStart) - : Math.min(pLength, Math.max(0, originalLength - pStart)); - - // Store result - Object result; - - if (newLength < originalLength) { - // Create sub array & copy into - result = Array.newInstance(type, newLength); - System.arraycopy(pArray, pStart, result, 0, newLength); - } - else { - // Just return original array - // NOTE: This can ONLY happen if pStart == 0 - result = pArray; - } - - // Return - 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(); - } - - public T next() { - return pEnum.nextElement(); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - - /** - * Adds all elements of the iterator to the collection. - * - * @param pCollection the collection - * @param pIterator the elements to add - * - * @throws UnsupportedOperationException if {@code add} is not supported by - * 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 {@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. - */ - public static void addAll(Collection pCollection, Iterator pIterator) { - while (pIterator.hasNext()) { - pCollection.add(pIterator.next()); - } - } - - // Is there a use case where Arrays.asList(pArray).iterator() can't ne used? - /** - * Creates a thin {@link Iterator} wrapper around an array. - * - * @param pArray the array to iterate - * @return a new {@link ListIterator} - * @throws IllegalArgumentException if {@code pArray} is {@code null}, - * {@code pStart < 0}, or - * {@code pLength > pArray.length - pStart} - */ - public static ListIterator iterator(final E[] pArray) { - return iterator(pArray, 0, notNull(pArray).length); - } - - /** - * Creates a thin {@link Iterator} wrapper around an array. - * - * @param pArray the array to iterate - * @param pStart the offset into the array - * @param pLength the number of elements to include in the iterator - * @return a new {@link ListIterator} - * @throws IllegalArgumentException if {@code pArray} is {@code null}, - * {@code pStart < 0}, or - * {@code pLength > pArray.length - pStart} - */ - public static ListIterator iterator(final E[] pArray, final int pStart, final int pLength) { - return new ArrayIterator(pArray, pStart, pLength); - } - - /** - * Creates an inverted mapping of the key/value pairs in the given map. - * - * @param pSource the source map - * @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 duplicates. - * - * @see #invert(java.util.Map, java.util.Map, DuplicateHandler) - */ - public static Map invert(Map pSource) { - return invert(pSource, null, null); - } - - /** - * Creates an inverted mapping of the key/value pairs in the given map. - * Optionally, a duplicate handler may be specified, to resolve duplicate keys in the result map. - * - * @param pSource the source map - * @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 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, - * or if source map contains duplicate values and {@code pHandler == null}. - */ - // TODO: Create a better duplicate handler, that takes Entries as parameters and returns an Entry - public static Map invert(Map pSource, Map pResult, DuplicateHandler pHandler) { - if (pSource == null) { - throw new IllegalArgumentException("source == null"); - } - - Map result = pResult; - if (result == null) { - try { - //noinspection unchecked - result = pSource.getClass().newInstance(); - } - catch (InstantiationException e) { - // Handled below - } - catch (IllegalAccessException e) { - // Handled below - } - - if (result == null) { - throw new IllegalArgumentException("result == null and source class " + pSource.getClass() + " cannot be instantiated."); - } - } - - // Copy entries into result map, inversed - Set> entries = pSource.entrySet(); - for (Map.Entry entry : entries) { - V newKey = entry.getValue(); - K newValue = entry.getKey(); - - // Handle dupliates - if (result.containsKey(newKey)) { - if (pHandler != null) { - newValue = pHandler.resolve(result.get(newKey), newValue); - } - else { - throw new IllegalArgumentException("Result would include duplicate keys, but no DuplicateHandler specified."); - } - } - - result.put(newKey, newValue); - } - - return result; - } - - public static Comparator reverseOrder(final Comparator pOriginal) { - return new ReverseComparator(pOriginal); - } - - private static class ReverseComparator implements Comparator { - private final Comparator comparator; - - public ReverseComparator(final Comparator pComparator) { - comparator = notNull(pComparator); - } - - - public int compare(T pLeft, T pRight) { - int result = comparator.compare(pLeft, pRight); - - // We can't simply return -result, as -Integer.MIN_VALUE == Integer.MIN_VALUE. - return -(result | (result >>> 1)); - } - } - - @SuppressWarnings({"unchecked", "UnusedDeclaration"}) - static , E> T generify(final Iterator pIterator, final Class pElementType) { - return (T) pIterator; - } - - @SuppressWarnings({"unchecked", "UnusedDeclaration"}) - static , E> T generify(final Collection pCollection, final Class pElementType) { - return (T) pCollection; - } - - @SuppressWarnings({"unchecked", "UnusedDeclaration"}) - static , K, V> T generify(final Map pMap, final Class pKeyType, final Class pValueType) { - return (T) pMap; - } - - @SuppressWarnings({"unchecked"}) - static , E> T generify2(Collection pCollection) { - return (T) pCollection; - } - - private static class ArrayIterator implements ListIterator { - private int next; - private final int start; - private final int length; - private final E[] array; - - 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 next < length + start; - } - - public E next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - - try { - return array[next++]; - } - catch (ArrayIndexOutOfBoundsException e) { - NoSuchElementException nse = new NoSuchElementException(e.getMessage()); - nse.initCause(e); - throw nse; - } - } - - public void remove() { - throw new UnsupportedOperationException(); - } - - public void add(E pElement) { - throw new UnsupportedOperationException(); - } - - public boolean hasPrevious() { - return next > start; - } - - public int nextIndex() { - return next - start; - } - - public E previous() { - if (!hasPrevious()) { - throw new NoSuchElementException(); - } - - try { - return array[--next]; - } - catch (ArrayIndexOutOfBoundsException e) { - NoSuchElementException nse = new NoSuchElementException(e.getMessage()); - nse.initCause(e); - throw nse; - } - } - - public int previousIndex() { - return nextIndex() - 1; - } - - public void set(E pElement) { - array[next - 1] = pElement; - } - } +/* + * 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 of the copyright holder 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 HOLDER 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.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: com/twelvemonkeys/util/CollectionUtil.java#3 $ + * @see Collections + * @see Arrays + */ +public final class CollectionUtil { + + /** + * Testing only. + * + * @param pArgs command line arguents + */ + @SuppressWarnings({"UnusedDeclaration", "UnusedAssignment", "unchecked"}) + public static void main(String[] pArgs) { + int howMany = 1000; + + if (pArgs.length > 0) { + howMany = Integer.parseInt(pArgs[0]); + } + long start; + long end; + + /* + int[] intArr1 = new int[] { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + }; + int[] intArr2 = new int[] { + 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 + }; + start = System.currentTimeMillis(); + + for (int i = 0; i < howMany; i++) { + intArr1 = (int[]) mergeArrays(intArr1, 0, intArr1.length, intArr2, 0, intArr2.length); + + } + end = System.currentTimeMillis(); + + System.out.println("mergeArrays: " + howMany + " * " + intArr2.length + " ints took " + (end - start) + " milliseconds (" + intArr1.length + + ")"); + */ + + //////////////////////////////// + String[] stringArr1 = new String[]{ + "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" + }; + + /* + String[] stringArr2 = new String[] { + "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" + }; + + start = System.currentTimeMillis(); + for (int i = 0; i < howMany; i++) { + stringArr1 = (String[]) mergeArrays(stringArr1, 0, stringArr1.length, stringArr2, 0, stringArr2.length); + + } + end = System.currentTimeMillis(); + System.out.println("mergeArrays: " + howMany + " * " + stringArr2.length + " Strings took " + (end - start) + " milliseconds (" + + stringArr1.length + ")"); + + + start = System.currentTimeMillis(); + while (intArr1.length > stringArr2.length) { + intArr1 = (int[]) subArray(intArr1, 0, intArr1.length - stringArr2.length); + + } + end = System.currentTimeMillis(); + + System.out.println("subArray: " + howMany + " * " + intArr2.length + " ints took " + (end - start) + " milliseconds (" + intArr1.length + + ")"); + + start = System.currentTimeMillis(); + while (stringArr1.length > stringArr2.length) { + stringArr1 = (String[]) subArray(stringArr1, stringArr2.length); + + } + end = System.currentTimeMillis(); + System.out.println("subArray: " + howMany + " * " + stringArr2.length + " Strings took " + (end - start) + " milliseconds (" + + stringArr1.length + ")"); + + */ + stringArr1 = new String[]{ + "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", + "fifteen", "sixteen", "seventeen", "eighteen", "nineteen" + }; + System.out.println("\nFilterIterators:\n"); + List list = Arrays.asList(stringArr1); + Iterator iter = new FilterIterator(list.iterator(), new FilterIterator.Filter() { + + public boolean accept(Object pElement) { + return ((String) pElement).length() > 5; + } + }); + + while (iter.hasNext()) { + String str = (String) iter.next(); + + System.out.println(str + " has more than 5 letters!"); + } + iter = new FilterIterator(list.iterator(), new FilterIterator.Filter() { + + public boolean accept(Object pElement) { + return ((String) pElement).length() <= 5; + } + }); + + while (iter.hasNext()) { + String str = (String) iter.next(); + + System.out.println(str + " has less than, or exactly 5 letters!"); + } + start = System.currentTimeMillis(); + + for (int i = 0; i < howMany; i++) { + iter = new FilterIterator(list.iterator(), new FilterIterator.Filter() { + + public boolean accept(Object pElement) { + return ((String) pElement).length() <= 5; + } + }); + while (iter.hasNext()) { + iter.next(); + System.out.print(""); + } + } + +// end = System.currentTimeMillis(); +// System.out.println("Time: " + (end - start) + " ms"); +// System.out.println("\nClosureCollection:\n"); +// forEach(list, new Closure() { +// +// public void execute(Object pElement) { +// +// String str = (String) pElement; +// +// if (str.length() > 5) { +// System.out.println(str + " has more than 5 letters!"); +// } +// else { +// System.out.println(str + " has less than, or exactly 5 letters!"); +// } +// } +// }); +// start = System.currentTimeMillis(); +// for (int i = 0; i < howMany; i++) { +// forEach(list, new Closure() { +// +// public void execute(Object pElement) { +// +// String str = (String) pElement; +// +// if (str.length() <= 5) { +// System.out.print(""); +// } +// } +// }); +// } +// end = System.currentTimeMillis(); +// System.out.println("Time: " + (end - start) + " ms"); + } + + // Disallow creating objects of this type + private CollectionUtil() {} + + /** + * Merges two arrays into a new array. Elements from array1 and array2 will + * be copied into a new array, that has array1.length + array2.length + * elements. + * + * @param pArray1 First array + * @param pArray2 Second array, must be compatible with (assignable from) + * the first array + * @return A new array, containing the values of array1 and array2. The + * array (wrapped as an object), will have the length of array1 + + * array2, and can be safely cast to the type of the array1 + * parameter. + * @see #mergeArrays(Object,int,int,Object,int,int) + * @see java.lang.System#arraycopy(Object,int,Object,int,int) + */ + public static Object mergeArrays(Object pArray1, Object pArray2) { + return mergeArrays(pArray1, 0, Array.getLength(pArray1), pArray2, 0, Array.getLength(pArray2)); + } + + /** + * Merges two arrays into a new array. Elements from pArray1 and pArray2 will + * be copied into a new array, that has pLength1 + pLength2 elements. + * + * @param pArray1 First array + * @param pOffset1 the offset into the first array + * @param pLength1 the number of elements to copy from the first array + * @param pArray2 Second array, must be compatible with (assignable from) + * the first array + * @param pOffset2 the offset into the second array + * @param pLength2 the number of elements to copy from the second array + * @return A new array, containing the values of pArray1 and pArray2. The + * array (wrapped as an object), will have the length of pArray1 + + * pArray2, and can be safely cast to the type of the pArray1 + * parameter. + * @see java.lang.System#arraycopy(Object,int,Object,int,int) + */ + @SuppressWarnings({"SuspiciousSystemArraycopy"}) + public static Object mergeArrays(Object pArray1, int pOffset1, int pLength1, Object pArray2, int pOffset2, int pLength2) { + Class class1 = pArray1.getClass(); + Class type = class1.getComponentType(); + + // Create new array of the new length + Object array = Array.newInstance(type, pLength1 + pLength2); + + System.arraycopy(pArray1, pOffset1, array, 0, pLength1); + System.arraycopy(pArray2, pOffset2, array, pLength1, pLength2); + return array; + } + + /** + * 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} or + * if {@code pArray} is not an array. + * @throws ArrayIndexOutOfBoundsException if {@code pStart} < 0 + */ + public static Object subArray(Object pArray, int pStart) { + 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 the type of 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. + * + * @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} 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} or + * if {@code pArray} is not an array. + * @throws ArrayIndexOutOfBoundsException if {@code pStart} < 0 + */ + @SuppressWarnings({"SuspiciousSystemArraycopy"}) + public static Object subArray(Object pArray, int pStart, int pLength) { + Validate.notNull(pArray, "array"); + + // Get component type + Class type; + + // Sanity check start index + if (pStart < 0) { + throw new ArrayIndexOutOfBoundsException(pStart + " < 0"); + } + // Check if argument is array + else if ((type = pArray.getClass().getComponentType()) == null) { + // NOTE: No need to test class.isArray(), really + throw new IllegalArgumentException("Not an array: " + pArray); + } + + // Store original length + int originalLength = Array.getLength(pArray); + + // Find new length, stay within bounds + int newLength = (pLength < 0) + ? Math.max(0, originalLength - pStart) + : Math.min(pLength, Math.max(0, originalLength - pStart)); + + // Store result + Object result; + + if (newLength < originalLength) { + // Create sub array & copy into + result = Array.newInstance(type, newLength); + System.arraycopy(pArray, pStart, result, 0, newLength); + } + else { + // Just return original array + // NOTE: This can ONLY happen if pStart == 0 + result = pArray; + } + + // Return + 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 the type of 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, + * 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(); + } + + public T next() { + return pEnum.nextElement(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + /** + * Adds all elements of the iterator to the collection. + * + * @param pCollection the collection + * @param pIterator the elements to add + * + * @throws UnsupportedOperationException if {@code add} is not supported by + * 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 {@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. + */ + public static void addAll(Collection pCollection, Iterator pIterator) { + while (pIterator.hasNext()) { + pCollection.add(pIterator.next()); + } + } + + // Is there a use case where Arrays.asList(pArray).iterator() can't ne used? + /** + * Creates a thin {@link Iterator} wrapper around an array. + * + * @param pArray the array to iterate + * @return a new {@link ListIterator} + * @throws IllegalArgumentException if {@code pArray} is {@code null}, + * {@code pStart < 0}, or + * {@code pLength > pArray.length - pStart} + */ + public static ListIterator iterator(final E[] pArray) { + return iterator(pArray, 0, notNull(pArray).length); + } + + /** + * Creates a thin {@link Iterator} wrapper around an array. + * + * @param pArray the array to iterate + * @param pStart the offset into the array + * @param pLength the number of elements to include in the iterator + * @return a new {@link ListIterator} + * @throws IllegalArgumentException if {@code pArray} is {@code null}, + * {@code pStart < 0}, or + * {@code pLength > pArray.length - pStart} + */ + public static ListIterator iterator(final E[] pArray, final int pStart, final int pLength) { + return new ArrayIterator(pArray, pStart, pLength); + } + + /** + * Creates an inverted mapping of the key/value pairs in the given map. + * + * @param pSource the source map + * @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 duplicates. + * + * @see #invert(java.util.Map, java.util.Map, DuplicateHandler) + */ + public static Map invert(Map pSource) { + return invert(pSource, null, null); + } + + /** + * Creates an inverted mapping of the key/value pairs in the given map. + * Optionally, a duplicate handler may be specified, to resolve duplicate keys in the result map. + * + * @param pSource the source map + * @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 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, + * or if source map contains duplicate values and {@code pHandler == null}. + */ + // TODO: Create a better duplicate handler, that takes Entries as parameters and returns an Entry + public static Map invert(Map pSource, Map pResult, DuplicateHandler pHandler) { + if (pSource == null) { + throw new IllegalArgumentException("source == null"); + } + + Map result = pResult; + if (result == null) { + try { + //noinspection unchecked + result = pSource.getClass().newInstance(); + } + catch (InstantiationException e) { + // Handled below + } + catch (IllegalAccessException e) { + // Handled below + } + + if (result == null) { + throw new IllegalArgumentException("result == null and source class " + pSource.getClass() + " cannot be instantiated."); + } + } + + // Copy entries into result map, inversed + Set> entries = pSource.entrySet(); + for (Map.Entry entry : entries) { + V newKey = entry.getValue(); + K newValue = entry.getKey(); + + // Handle dupliates + if (result.containsKey(newKey)) { + if (pHandler != null) { + newValue = pHandler.resolve(result.get(newKey), newValue); + } + else { + throw new IllegalArgumentException("Result would include duplicate keys, but no DuplicateHandler specified."); + } + } + + result.put(newKey, newValue); + } + + return result; + } + + public static Comparator reverseOrder(final Comparator pOriginal) { + return new ReverseComparator(pOriginal); + } + + private static class ReverseComparator implements Comparator { + private final Comparator comparator; + + public ReverseComparator(final Comparator pComparator) { + comparator = notNull(pComparator); + } + + + public int compare(T pLeft, T pRight) { + int result = comparator.compare(pLeft, pRight); + + // We can't simply return -result, as -Integer.MIN_VALUE == Integer.MIN_VALUE. + return -(result | (result >>> 1)); + } + } + + @SuppressWarnings({"unchecked", "UnusedDeclaration"}) + static , E> T generify(final Iterator pIterator, final Class pElementType) { + return (T) pIterator; + } + + @SuppressWarnings({"unchecked", "UnusedDeclaration"}) + static , E> T generify(final Collection pCollection, final Class pElementType) { + return (T) pCollection; + } + + @SuppressWarnings({"unchecked", "UnusedDeclaration"}) + static , K, V> T generify(final Map pMap, final Class pKeyType, final Class pValueType) { + return (T) pMap; + } + + @SuppressWarnings({"unchecked"}) + static , E> T generify2(Collection pCollection) { + return (T) pCollection; + } + + private static class ArrayIterator implements ListIterator { + private int next; + private final int start; + private final int length; + private final E[] array; + + 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 next < length + start; + } + + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + try { + return array[next++]; + } + catch (ArrayIndexOutOfBoundsException e) { + NoSuchElementException nse = new NoSuchElementException(e.getMessage()); + nse.initCause(e); + throw nse; + } + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public void add(E pElement) { + throw new UnsupportedOperationException(); + } + + public boolean hasPrevious() { + return next > start; + } + + public int nextIndex() { + return next - start; + } + + public E previous() { + if (!hasPrevious()) { + throw new NoSuchElementException(); + } + + try { + return array[--next]; + } + catch (ArrayIndexOutOfBoundsException e) { + NoSuchElementException nse = new NoSuchElementException(e.getMessage()); + nse.initCause(e); + throw nse; + } + } + + public int previousIndex() { + return nextIndex() - 1; + } + + public void set(E pElement) { + array[next - 1] = pElement; + } + } } \ No newline at end of file diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/DuplicateHandler.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/DuplicateHandler.java index f3f56d28..eab84167 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/DuplicateHandler.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/DuplicateHandler.java @@ -1,153 +1,152 @@ -/* - * 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 of the copyright holder 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 HOLDER 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; - -/** - * DuplicateHandler - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/DuplicateHandler.java#2 $ - */ -public interface DuplicateHandler { - - /** - * Resolves duplicates according to a certain strategy. - * - * @param pOld the old value - * @param pNew the new value - * - * @return the resolved value. - * - * @throws IllegalArgumentException is the arguments cannot be resolved for - * some reason. - */ - public T resolve(T pOld, T pNew); - - /** - * Will use the first (old) value. Any new values will be discarded. - * - * @see CollectionUtil#invert(java.util.Map, java.util.Map, DuplicateHandler) - */ - public final static DuplicateHandler USE_FIRST_VALUE = new DuplicateHandler() { - /** - * Returns {@code pOld}. - * - * @param pOld the old value - * @param pNew the new value - * - * @return {@code pOld} - */ - public Object resolve(Object pOld, Object pNew) { - return pOld; - } - }; - - /** - * Will use the last (new) value. Any old values will be discarded - * (overwritten). - * - * @see CollectionUtil#invert(java.util.Map, java.util.Map, DuplicateHandler) - */ - public final static DuplicateHandler USE_LAST_VALUE = new DuplicateHandler() { - /** - * Returns {@code pNew}. - * - * @param pOld the old value - * @param pNew the new value - * - * @return {@code pNew} - */ - public Object resolve(Object pOld, Object pNew) { - return pNew; - } - }; - - /** - * Converts duplicats to an {@code Object} array. - * - * @see CollectionUtil#invert(java.util.Map, java.util.Map, DuplicateHandler) - */ - public final static DuplicateHandler DUPLICATES_AS_ARRAY = new DuplicateHandler() { - /** - * Returns an {@code Object} array, containing {@code pNew} as its - * last element. - * - * @param pOld the old value - * @param pNew the new value - * - * @return an {@code Object} array, containing {@code pNew} as its - * last element. - */ - public Object resolve(Object pOld, Object pNew) { - Object[] result; - - if (pOld instanceof Object[]) { - Object[] old = ((Object[]) pOld); - result = new Object[old.length + 1]; - System.arraycopy(old, 0, result, 0, old.length); - result[old.length] = pNew; - } - else { - result = new Object[] {pOld, pNew}; - } - - return result; - } - }; - - /** - * Converts duplicates to a comma-separated {@code String}. - * Note that all values should allready be {@code String}s if using this - * handler. - * - * @see CollectionUtil#invert(java.util.Map, java.util.Map, DuplicateHandler) - */ - public final static DuplicateHandler DUPLICATES_AS_CSV = new DuplicateHandler() { - /** - * Returns a comma-separated {@code String}, with the string - * representation of {@code pNew} as the last element. - * - * @param pOld the old value - * @param pNew the new value - * - * @return a comma-separated {@code String}, with the string - * representation of {@code pNew} as the last element. - */ - public String resolve(String pOld, String pNew) { - StringBuilder result = new StringBuilder(String.valueOf(pOld)); - result.append(','); - result.append(pNew); - - return result.toString(); - } - }; -} +/* + * 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 of the copyright holder 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 HOLDER 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; + +/** + * DuplicateHandler + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/DuplicateHandler.java#2 $ + */ +public interface DuplicateHandler { + + /** + * Resolves duplicates according to a certain strategy. + * + * @param pOld the old value + * @param pNew the new value + * + * @return the resolved value. + * + * @throws IllegalArgumentException is the arguments cannot be resolved for + * some reason. + */ + public T resolve(T pOld, T pNew); + + /** + * Will use the first (old) value. Any new values will be discarded. + * + * @see CollectionUtil#invert(java.util.Map, java.util.Map, DuplicateHandler) + */ + public final static DuplicateHandler USE_FIRST_VALUE = new DuplicateHandler() { + /** + * Returns {@code pOld}. + * + * @param pOld the old value + * @param pNew the new value + * + * @return {@code pOld} + */ + public Object resolve(Object pOld, Object pNew) { + return pOld; + } + }; + + /** + * Will use the last (new) value. Any old values will be discarded + * (overwritten). + * + * @see CollectionUtil#invert(java.util.Map, java.util.Map, DuplicateHandler) + */ + public final static DuplicateHandler USE_LAST_VALUE = new DuplicateHandler() { + /** + * Returns {@code pNew}. + * + * @param pOld the old value + * @param pNew the new value + * + * @return {@code pNew} + */ + public Object resolve(Object pOld, Object pNew) { + return pNew; + } + }; + + /** + * Converts duplicats to an {@code Object} array. + * + * @see CollectionUtil#invert(java.util.Map, java.util.Map, DuplicateHandler) + */ + public final static DuplicateHandler DUPLICATES_AS_ARRAY = new DuplicateHandler() { + /** + * Returns an {@code Object} array, containing {@code pNew} as its + * last element. + * + * @param pOld the old value + * @param pNew the new value + * + * @return an {@code Object} array, containing {@code pNew} as its + * last element. + */ + public Object resolve(Object pOld, Object pNew) { + Object[] result; + + if (pOld instanceof Object[]) { + Object[] old = ((Object[]) pOld); + result = new Object[old.length + 1]; + System.arraycopy(old, 0, result, 0, old.length); + result[old.length] = pNew; + } + else { + result = new Object[] {pOld, pNew}; + } + + return result; + } + }; + + /** + * Converts duplicates to a comma-separated {@code String}. + * Note that all values should allready be {@code String}s if using this + * handler. + * + * @see CollectionUtil#invert(java.util.Map, java.util.Map, DuplicateHandler) + */ + public final static DuplicateHandler DUPLICATES_AS_CSV = new DuplicateHandler() { + /** + * Returns a comma-separated {@code String}, with the string + * representation of {@code pNew} as the last element. + * + * @param pOld the old value + * @param pNew the new value + * + * @return a comma-separated {@code String}, with the string + * representation of {@code pNew} as the last element. + */ + public String resolve(String pOld, String pNew) { + StringBuilder result = new StringBuilder(String.valueOf(pOld)); + result.append(','); + result.append(pNew); + + return result.toString(); + } + }; +} 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 7ca3df1a..3b3fbec2 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 @@ -1,153 +1,154 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.util.Iterator; -import java.util.NoSuchElementException; - -/** - * Wraps (decorates) an {@code Iterator} with extra functionality, to allow - * element filtering. Each - * element is filtered against the given {@code Filter}, and only elements - * that are {@code accept}ed are returned by the {@code next} method. - *

- * The optional {@code remove} operation is implemented, but may throw - * {@code UnsupportedOperationException} if the underlying iterator does not - * support the remove operation. - * - * @see FilterIterator.Filter - * - * @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/FilterIterator.java#1 $ - */ -public class FilterIterator implements Iterator { - - protected final Filter filter; - protected final Iterator iterator; - - private E next = null; - private E current = null; - - /** - * Creates a {@code FilterIterator} that wraps the {@code Iterator}. Each - * element is filtered against the given {@code Filter}, and only elements - * that are {@code accept}ed are returned by the {@code next} method. - * - * @param pIterator the iterator to filter - * @param pFilter the filter - * @see FilterIterator.Filter - */ - public FilterIterator(final Iterator pIterator, final Filter pFilter) { - if (pIterator == null) { - throw new IllegalArgumentException("iterator == null"); - } - if (pFilter == null) { - throw new IllegalArgumentException("filter == null"); - } - - iterator = pIterator; - filter = pFilter; - } - - /** - * Returns {@code true} if the iteration has more elements. (In other - * words, returns {@code true} if {@code next} would return an element - * rather than throwing an exception.) - * - * @return {@code true} if the iterator has more elements. - * @see FilterIterator.Filter#accept - */ - public boolean hasNext() { - while (next == null && iterator.hasNext()) { - E element = iterator.next(); - - if (filter.accept(element)) { - next = element; - break; - } - } - - return next != null; - } - - /** - * Returns the next element in the iteration. - * - * @return the next element in the iteration. - * @see FilterIterator.Filter#accept - */ - public E next() { - if (hasNext()) { - current = next; - - // Make sure we advance next time - next = null; - return current; - } - else { - throw new NoSuchElementException("Iteration has no more elements."); - } - } - - /** - * Removes from the underlying collection the last element returned by the - * iterator (optional operation). This method can be called only once per - * call to {@code next}. The behavior of an iterator is unspecified if - * the underlying collection is modified while the iteration is in - * progress in any way other than by calling this method. - */ - public void remove() { - if (current != null) { - iterator.remove(); - } - else { - throw new IllegalStateException("Iteration has no current element."); - } - } - - /** - * Used to tests whether or not an element fulfills certain criteria, and - * hence should be accepted by the FilterIterator instance. - */ - public static interface Filter { - - /** - * Tests whether or not the element fulfills certain criteria, and hence - * should be accepted. - * - * @param pElement the element to test - * @return {@code true} if the object is accepted, otherwise - * {@code false} - */ - public boolean accept(E pElement); - } +/* + * 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 of the copyright holder 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 HOLDER 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.util.Iterator; +import java.util.NoSuchElementException; + +/** + * Wraps (decorates) an {@code Iterator} with extra functionality, to allow + * element filtering. Each + * element is filtered against the given {@code Filter}, and only elements + * that are {@code accept}ed are returned by the {@code next} method. + *

+ * The optional {@code remove} operation is implemented, but may throw + * {@code UnsupportedOperationException} if the underlying iterator does not + * support the remove operation. + *

+ * + * @see FilterIterator.Filter + * + * @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/FilterIterator.java#1 $ + */ +public class FilterIterator implements Iterator { + + protected final Filter filter; + protected final Iterator iterator; + + private E next = null; + private E current = null; + + /** + * Creates a {@code FilterIterator} that wraps the {@code Iterator}. Each + * element is filtered against the given {@code Filter}, and only elements + * that are {@code accept}ed are returned by the {@code next} method. + * + * @param pIterator the iterator to filter + * @param pFilter the filter + * @see FilterIterator.Filter + */ + public FilterIterator(final Iterator pIterator, final Filter pFilter) { + if (pIterator == null) { + throw new IllegalArgumentException("iterator == null"); + } + if (pFilter == null) { + throw new IllegalArgumentException("filter == null"); + } + + iterator = pIterator; + filter = pFilter; + } + + /** + * Returns {@code true} if the iteration has more elements. (In other + * words, returns {@code true} if {@code next} would return an element + * rather than throwing an exception.) + * + * @return {@code true} if the iterator has more elements. + * @see FilterIterator.Filter#accept + */ + public boolean hasNext() { + while (next == null && iterator.hasNext()) { + E element = iterator.next(); + + if (filter.accept(element)) { + next = element; + break; + } + } + + return next != null; + } + + /** + * Returns the next element in the iteration. + * + * @return the next element in the iteration. + * @see FilterIterator.Filter#accept + */ + public E next() { + if (hasNext()) { + current = next; + + // Make sure we advance next time + next = null; + return current; + } + else { + throw new NoSuchElementException("Iteration has no more elements."); + } + } + + /** + * Removes from the underlying collection the last element returned by the + * iterator (optional operation). This method can be called only once per + * call to {@code next}. The behavior of an iterator is unspecified if + * the underlying collection is modified while the iteration is in + * progress in any way other than by calling this method. + */ + public void remove() { + if (current != null) { + iterator.remove(); + } + else { + throw new IllegalStateException("Iteration has no current element."); + } + } + + /** + * Used to tests whether or not an element fulfills certain criteria, and + * hence should be accepted by the FilterIterator instance. + */ + public static interface Filter { + + /** + * Tests whether or not the element fulfills certain criteria, and hence + * should be accepted. + * + * @param pElement the element to test + * @return {@code true} if the object is accepted, otherwise + * {@code false} + */ + public boolean accept(E pElement); + } } \ No newline at end of file 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 b81d36fa..035f06bb 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 @@ -1,178 +1,180 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.io.Serializable; -import java.util.Iterator; -import java.util.Map; - -/** - * A {@code Map} decorator that makes the mappings in the backing map - * case insensitive - * (this is implemented by converting all keys to uppercase), - * if the keys used are {@code Strings}. If the keys - * used are not {@code String}s, it wil work as a normal - * {@code java.util.Map}. - *

- * - * @see java.util.Map - * - * @author Harald Kuhr - */ -public class IgnoreCaseMap extends AbstractDecoratedMap implements Serializable, Cloneable { - - /** - * Constructs a new empty {@code Map}. - * The backing map will be a {@link java.util.HashMap} - */ - public IgnoreCaseMap() { - super(); - } - - /** - * Constructs a new {@code Map} with the same key-value mappings as the - * given {@code Map}. - * The backing map will be a {@link java.util.HashMap} - *

- * NOTE: As the keys in the given map parameter will be converted to - * uppercase (if they are strings), any duplicate key/value pair where - * {@code key instanceof String && key.equalsIgnoreCase(otherKey)} - * is true, will be lost. - * - * @param pMap the map whose mappings are to be placed in this map. - */ - public IgnoreCaseMap(Map pMap) { - super(pMap); - } - - /** - * Constructs a new {@code Map} with the same key-value mappings as the - * given {@code Map}. - *

- * NOTE: The backing map is structuraly cahnged, and it should NOT be - * accessed directly, after the wrapped map is created. - *

- * NOTE: As the keys in the given map parameter will be converted to - * uppercase (if they are strings), any duplicate key/value pair where - * {@code key instanceof String && key.equalsIgnoreCase(otherKey)} - * is true, will be lost. - * - * @param pBacking the backing map of this map. Must be either empty, or - * the same map as {@code pContents}. - * @param pContents the map whose mappings are to be placed in this map. - * May be {@code null} - * - * @throws IllegalArgumentException if {@code pBacking} is {@code null} - * @throws IllegalArgumentException if {@code pBacking} differs from - * {@code pContent} and is not empty. - */ - public IgnoreCaseMap(Map pBacking, Map pContents) { - super(pBacking, pContents); - } - - /** - * Maps the specified key to the specified value in this map. - * Note: If the key used is a string, the key will not be case-sensitive. - * - * @param pKey the map key. - * @param pValue the value. - * @return the previous value of the specified key in this map, - * or null if it did not have one. - */ - public V put(String pKey, V pValue) { - String key = (String) toUpper(pKey); - return unwrap(entries.put(key, new BasicEntry(key, pValue))); - } - - private V unwrap(Entry pEntry) { - return pEntry != null ? pEntry.getValue() : null; - } - - /** - * Returns the value to which the specified key is mapped in this - * map. - * Note: If the key used is a string, the key will not be case-sensitive. - * - * @param pKey a key in the map - * @return the value to which the key is mapped in this map; null if - * the key is not mapped to any value in this map. - */ - public V get(Object pKey) { - return unwrap(entries.get(toUpper(pKey))); - } - - /** - * Removes the key (and its corresponding value) from this map. This - * method does nothing if the key is not in the map. - * Note: If the key used is a string, the key will not be case-sensitive. - * - * @param pKey the key that needs to be removed. - * @return the value to which the key had been mapped in this map, - * or null if the key did not have a mapping. - */ - public V remove(Object pKey) { - return unwrap(entries.remove(toUpper(pKey))); - } - - /** - * Tests if the specified object is a key in this map. - * Note: If the key used is a string, the key will not be case-sensitive. - * - * @param pKey possible key. - * @return true if and only if the specified object is a key in this - * map, as determined by the equals method; false otherwise. - */ - public boolean containsKey(Object pKey) { - return entries.containsKey(toUpper(pKey)); - } - - /** - * Converts the parameter to uppercase, if it's a String. - */ - protected static Object toUpper(final Object pObject) { - if (pObject instanceof String) { - return ((String) pObject).toUpperCase(); - } - return pObject; - } - - protected Iterator> newEntryIterator() { - return (Iterator) entries.entrySet().iterator(); - } - - protected Iterator newKeyIterator() { - return entries.keySet().iterator(); - } - - protected Iterator newValueIterator() { - return (Iterator) entries.values().iterator(); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.io.Serializable; +import java.util.Iterator; +import java.util.Map; + +/** + * A {@code Map} decorator that makes the mappings in the backing map + * case insensitive + * (this is implemented by converting all keys to uppercase), + * if the keys used are {@code Strings}. If the keys + * used are not {@code String}s, it wil work as a normal + * {@code java.util.Map}. + * + * @see java.util.Map + * + * @author Harald Kuhr + */ +public class IgnoreCaseMap extends AbstractDecoratedMap implements Serializable, Cloneable { + + /** + * Constructs a new empty {@code Map}. + * The backing map will be a {@link java.util.HashMap} + */ + public IgnoreCaseMap() { + super(); + } + + /** + * Constructs a new {@code Map} with the same key-value mappings as the + * given {@code Map}. + * The backing map will be a {@link java.util.HashMap} + *

+ * NOTE: As the keys in the given map parameter will be converted to + * uppercase (if they are strings), any duplicate key/value pair where + * {@code key instanceof String && key.equalsIgnoreCase(otherKey)} + * is true, will be lost. + *

+ * + * @param pMap the map whose mappings are to be placed in this map. + */ + public IgnoreCaseMap(Map pMap) { + super(pMap); + } + + /** + * Constructs a new {@code Map} with the same key-value mappings as the + * given {@code Map}. + *

+ * NOTE: The backing map is structuraly cahnged, and it should NOT be + * accessed directly, after the wrapped map is created. + *

+ *

+ * NOTE: As the keys in the given map parameter will be converted to + * uppercase (if they are strings), any duplicate key/value pair where + * {@code key instanceof String && key.equalsIgnoreCase(otherKey)} + * is true, will be lost. + *

+ * + * @param pBacking the backing map of this map. Must be either empty, or + * the same map as {@code pContents}. + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null} + * + * @throws IllegalArgumentException if {@code pBacking} is {@code null} + * @throws IllegalArgumentException if {@code pBacking} differs from + * {@code pContent} and is not empty. + */ + public IgnoreCaseMap(Map pBacking, Map pContents) { + super(pBacking, pContents); + } + + /** + * Maps the specified key to the specified value in this map. + * Note: If the key used is a string, the key will not be case-sensitive. + * + * @param pKey the map key. + * @param pValue the value. + * @return the previous value of the specified key in this map, + * or null if it did not have one. + */ + public V put(String pKey, V pValue) { + String key = (String) toUpper(pKey); + return unwrap(entries.put(key, new BasicEntry(key, pValue))); + } + + private V unwrap(Entry pEntry) { + return pEntry != null ? pEntry.getValue() : null; + } + + /** + * Returns the value to which the specified key is mapped in this + * map. + * Note: If the key used is a string, the key will not be case-sensitive. + * + * @param pKey a key in the map + * @return the value to which the key is mapped in this map; null if + * the key is not mapped to any value in this map. + */ + public V get(Object pKey) { + return unwrap(entries.get(toUpper(pKey))); + } + + /** + * Removes the key (and its corresponding value) from this map. This + * method does nothing if the key is not in the map. + * Note: If the key used is a string, the key will not be case-sensitive. + * + * @param pKey the key that needs to be removed. + * @return the value to which the key had been mapped in this map, + * or null if the key did not have a mapping. + */ + public V remove(Object pKey) { + return unwrap(entries.remove(toUpper(pKey))); + } + + /** + * Tests if the specified object is a key in this map. + * Note: If the key used is a string, the key will not be case-sensitive. + * + * @param pKey possible key. + * @return true if and only if the specified object is a key in this + * map, as determined by the equals method; false otherwise. + */ + public boolean containsKey(Object pKey) { + return entries.containsKey(toUpper(pKey)); + } + + /** + * Converts the parameter to uppercase, if it's a String. + */ + protected static Object toUpper(final Object pObject) { + if (pObject instanceof String) { + return ((String) pObject).toUpperCase(); + } + return pObject; + } + + protected Iterator> newEntryIterator() { + return (Iterator) entries.entrySet().iterator(); + } + + protected Iterator newKeyIterator() { + return entries.keySet().iterator(); + } + + protected Iterator newValueIterator() { + return (Iterator) entries.values().iterator(); + } +} 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 2cb7df05..5910b35c 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 @@ -1,468 +1,466 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.io.Serializable; -import java.util.*; - -/** - * Generic map and linked list implementation of the {@code Map} interface, - * with predictable iteration order. - *

- * Resembles {@code LinkedHashMap} from JDK 1.4+, but is backed by a generic - * {@code Map}, rather than implementing a particular algoritm. - *

- * This linked list defines the iteration ordering, which is normally the order - * in which keys were inserted into the map (insertion-order). - * Note that insertion order is not affected if a key is re-inserted - * into the map (a key {@code k} is reinserted into a map {@code m} if - * {@code m.put(k, v)} is invoked when {@code m.containsKey(k)} would return - * {@code true} immediately prior to the invocation). - *

- * A special {@link #LinkedMap(boolean) constructor} is provided to create a - * linked hash map whose order of iteration is the order in which its entries - * were last accessed, from least-recently accessed to most-recently - * (access-order). - * This kind of map is well-suited to building LRU caches. - * Invoking the {@code put} or {@code get} method results in an access to the - * corresponding entry (assuming it exists after the invocation completes). - * The {@code putAll} method generates one entry access for each mapping in - * the specified map, in the order that key-value mappings are provided by the - * specified map's entry set iterator. - * No other methods generate entry accesses. - * In particular, operations on collection-views do not affect the order of - * iteration of the backing map. - *

- * The {@link #removeEldestEntry(Map.Entry)} method may be overridden to impose - * a policy for removing stale mappings automatically when new mappings are - * added to the map. - * - * @author inspired by LinkedHashMap from JDK 1.4+, by Josh Bloch - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedMap.java#1 $ - * - * @see LinkedHashMap - * @see LRUMap - */ -public class LinkedMap extends AbstractDecoratedMap implements Serializable { - - transient LinkedEntry head; - protected final boolean accessOrder; - - /** - * Creates a {@code LinkedMap} backed by a {@code HashMap}, with default - * (insertion) order. - */ - public LinkedMap() { - this(null, false); - } - - /** - * Creates a {@code LinkedMap} backed by a {@code HashMap}, with the - * given order. - * - * @param pAccessOrder if {@code true}, ordering will be "least recently - * accessed item" to "latest accessed item", otherwise "first inserted item" - * to "latest inserted item". - */ - public LinkedMap(boolean pAccessOrder) { - this(null, pAccessOrder); - } - - /** - * Creates a {@code LinkedMap} backed by a {@code HashMap}, with key/value - * pairs copied from {@code pContents} and default (insertion) order. - * - * @param pContents the map whose mappings are to be placed in this map. - * May be {@code null}. - */ - public LinkedMap(Map pContents) { - this(pContents, false); - } - - /** - * Creates a {@code LinkedMap} backed by a {@code HashMap}, with key/value - * pairs copied from {@code pContents} and the given order. - * - * @param pContents the map whose mappings are to be placed in this map. - * May be {@code null}. - * @param pAccessOrder if {@code true}, ordering will be "least recently - * accessed item" to "latest accessed item", otherwise "first inserted item" - * to "latest inserted item". - */ - public LinkedMap(Map pContents, boolean pAccessOrder) { - super(pContents); - accessOrder = pAccessOrder; - } - - /** - * Creates a {@code LinkedMap} backed by the given map, with key/value - * pairs copied from {@code pContents} and default (insertion) order. - * - * @param pBacking the backing map of this map. Must be either empty, or - * the same map as {@code pContents}. - * @param pContents the map whose mappings are to be placed in this map. - * May be {@code null}. - */ - public LinkedMap(Map> pBacking, Map pContents) { - this(pBacking, pContents, false); - } - - /** - * Creates a {@code LinkedMap} backed by the given map, with key/value - * pairs copied from {@code pContents} and the given order. - * - * @param pBacking the backing map of this map. Must be either empty, or - * the same map as {@code pContents}. - * @param pContents the map whose mappings are to be placed in this map. - * May be {@code null}. - * @param pAccessOrder if {@code true}, ordering will be "least recently - * accessed item" to "latest accessed item", otherwise "first inserted item" - * to "latest inserted item". - */ - public LinkedMap(Map> pBacking, Map pContents, boolean pAccessOrder) { - super(pBacking, pContents); - accessOrder = pAccessOrder; - } - - protected void init() { - head = new LinkedEntry(null, null, null) { - void addBefore(LinkedEntry pExisting) { - throw new Error(); - } - void remove() { - throw new Error(); - } - public void recordAccess(Map pMap) { - throw new Error(); - } - public void recordRemoval(Map pMap) { - throw new Error(); - } - public void recordRemoval() { - throw new Error(); - } - public V getValue() { - throw new Error(); - } - public V setValue(V pValue) { - throw new Error(); - } - public K getKey() { - throw new Error(); - } - public String toString() { - return "head"; - } - }; - head.previous = head.next = head; - } - - public boolean containsValue(Object pValue) { - // Overridden to take advantage of faster iterator - if (pValue == null) { - for (LinkedEntry e = head.next; e != head; e = e.next) { - if (e.mValue == null) { - return true; - } - } - } else { - for (LinkedEntry e = head.next; e != head; e = e.next) { - if (pValue.equals(e.mValue)) { - return true; - } - } - } - return false; - } - - protected Iterator newKeyIterator() { - return new KeyIterator(); - } - - protected Iterator newValueIterator() { - return new ValueIterator(); - } - - protected Iterator> newEntryIterator() { - return new EntryIterator(); - } - - private abstract class LinkedMapIterator implements Iterator { - LinkedEntry mNextEntry = head.next; - LinkedEntry mLastReturned = null; - - /** - * The modCount value that the iterator believes that the backing - * List should have. If this expectation is violated, the iterator - * has detected concurrent modification. - */ - int mExpectedModCount = modCount; - - public boolean hasNext() { - return mNextEntry != head; - } - - public void remove() { - if (mLastReturned == null) { - throw new IllegalStateException(); - } - - if (modCount != mExpectedModCount) { - throw new ConcurrentModificationException(); - } - - LinkedMap.this.remove(mLastReturned.mKey); - mLastReturned = null; - - mExpectedModCount = modCount; - } - - LinkedEntry nextEntry() { - if (modCount != mExpectedModCount) { - throw new ConcurrentModificationException(); - } - - if (mNextEntry == head) { - throw new NoSuchElementException(); - } - - LinkedEntry e = mLastReturned = mNextEntry; - mNextEntry = e.next; - - return e; - } - } - - private class KeyIterator extends LinkedMap.LinkedMapIterator { - public K next() { - return nextEntry().mKey; - } - } - - private class ValueIterator extends LinkedMap.LinkedMapIterator { - public V next() { - return nextEntry().mValue; - } - } - - private class EntryIterator extends LinkedMap.LinkedMapIterator> { - public Entry next() { - return nextEntry(); - } - } - - public V get(Object pKey) { - LinkedEntry entry = (LinkedEntry) entries.get(pKey); - - if (entry != null) { - entry.recordAccess(this); - return entry.mValue; - } - - return null; - } - - public V remove(Object pKey) { - LinkedEntry entry = (LinkedEntry) entries.remove(pKey); - - if (entry != null) { - entry.remove(); - modCount++; - - return entry.mValue; - } - return null; - } - - public V put(K pKey, V pValue) { - 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 = head.next; - if (removeEldestEntry(eldest)) { - removeEntry(eldest); - } - - entry = createEntry(pKey, pValue); - entry.addBefore(head); - - entries.put(pKey, entry); - } - else { - oldValue = entry.mValue; - - entry.mValue = pValue; - entry.recordAccess(this); - } - - modCount++; - - return oldValue; - } - - /** - * Creates a new {@code LinkedEntry}. - * - * @param pKey the key - * @param pValue the value - * @return a new LinkedEntry - */ - /*protected*/ LinkedEntry createEntry(K pKey, V pValue) { - return new LinkedEntry(pKey, pValue, null); - } - - /** - * @todo - * - * @return a copy of this map, with the same order and same key/value pairs. - */ - public Object clone() throws CloneNotSupportedException { - LinkedMap map; - - map = (LinkedMap) super.clone(); - - // TODO: The rest of the work is PROBABLY handled by - // AbstractDecoratedMap, but need to verify that. - - return map; - } - - /** - * Returns {@code true} if this map should remove its eldest entry. - * This method is invoked by {@code put} and {@code putAll} after - * inserting a new entry into the map. It provides the implementer - * with the opportunity to remove the eldest entry each time a new one - * is added. This is useful if the map represents a cache: it allows - * the map to reduce memory consumption by deleting stale entries. - * - *

Sample use: this override will allow the map to grow up to 100 - * entries and then delete the eldest entry each time a new entry is - * added, maintaining a steady state of 100 entries. - *

-     *     private static final int MAX_ENTRIES = 100;
-     *
-     *     protected boolean removeEldestEntry(Map.Entry eldest) {
-     *        return size() > MAX_ENTRIES;
-     *     }
-     * 
- * - *

This method typically does not modify the map in any way, - * instead allowing the map to modify itself as directed by its - * return value. It is permitted for this method to modify - * the map directly, but if it does so, it must return - * {@code false} (indicating that the map should not attempt any - * further modification). The effects of returning {@code true} - * after modifying the map from within this method are unspecified. - * - *

This implementation merely returns {@code false} (so that this - * map acts like a normal map - the eldest element is never removed). - * - * @param pEldest The least recently inserted entry in the map, or if - * this is an access-ordered map, the least recently accessed - * entry. This is the entry that will be removed it this - * method returns {@code true}. If the map was empty prior - * to the {@code put} or {@code putAll} invocation resulting - * in this invocation, this will be the entry that was just - * inserted; in other words, if the map contains a single - * entry, the eldest entry is also the newest. - * @return {@code true} if the eldest entry should be removed - * from the map; {@code false} if it should be retained. - */ - protected boolean removeEldestEntry(Entry pEldest) { - return false; - } - - /** - * Linked list implementation of {@code Map.Entry}. - */ - protected static class LinkedEntry extends BasicEntry implements Serializable { - LinkedEntry previous; - LinkedEntry next; - - LinkedEntry(K pKey, V pValue, LinkedEntry pNext) { - super(pKey, pValue); - - next = pNext; - } - - /** - * Adds this entry before the given entry (which must be an existing - * entry) in the list. - * - * @param pExisting the entry to add before - */ - void addBefore(LinkedEntry pExisting) { - next = pExisting; - previous = pExisting.previous; - - previous.next = this; - next.previous = this; - } - - /** - * Removes this entry from the linked list. - */ - void remove() { - previous.next = next; - next.previous = previous; - } - - /** - * If the entry is part of an access ordered list, moves the entry to - * the end of the list. - * - * @param pMap the map to record access for - */ - protected void recordAccess(Map pMap) { - LinkedMap linkedMap = (LinkedMap) pMap; - if (linkedMap.accessOrder) { - linkedMap.modCount++; - remove(); - addBefore(linkedMap.head); - } - } - - /** - * Removes this entry from the linked list. - * - * @param pMap the map to record removal from - */ - protected void recordRemoval(Map pMap) { - // TODO: Is this REALLY correct? - remove(); - } - } +/* + * 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 of the copyright holder 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 HOLDER 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.io.Serializable; +import java.util.*; + +/** + * Generic map and linked list implementation of the {@code Map} interface, + * with predictable iteration order. + *

+ * Resembles {@code LinkedHashMap} from JDK 1.4+, but is backed by a generic + * {@code Map}, rather than implementing a particular algoritm. + *

+ * This linked list defines the iteration ordering, which is normally the order + * in which keys were inserted into the map (insertion-order). + * Note that insertion order is not affected if a key is re-inserted + * into the map (a key {@code k} is reinserted into a map {@code m} if + * {@code m.put(k, v)} is invoked when {@code m.containsKey(k)} would return + * {@code true} immediately prior to the invocation). + *

+ * A special {@link #LinkedMap(boolean) constructor} is provided to create a + * linked hash map whose order of iteration is the order in which its entries + * were last accessed, from least-recently accessed to most-recently + * (access-order). + * This kind of map is well-suited to building LRU caches. + * Invoking the {@code put} or {@code get} method results in an access to the + * corresponding entry (assuming it exists after the invocation completes). + * The {@code putAll} method generates one entry access for each mapping in + * the specified map, in the order that key-value mappings are provided by the + * specified map's entry set iterator. + * No other methods generate entry accesses. + * In particular, operations on collection-views do not affect the order of + * iteration of the backing map. + *

+ * The {@link #removeEldestEntry(Map.Entry)} method may be overridden to impose + * a policy for removing stale mappings automatically when new mappings are + * added to the map. + * + * @author inspired by LinkedHashMap from JDK 1.4+, by Josh Bloch + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedMap.java#1 $ + * + * @see LinkedHashMap + * @see LRUMap + */ +public class LinkedMap extends AbstractDecoratedMap implements Serializable { + + transient LinkedEntry head; + protected final boolean accessOrder; + + /** + * Creates a {@code LinkedMap} backed by a {@code HashMap}, with default + * (insertion) order. + */ + public LinkedMap() { + this(null, false); + } + + /** + * Creates a {@code LinkedMap} backed by a {@code HashMap}, with the + * given order. + * + * @param pAccessOrder if {@code true}, ordering will be "least recently + * accessed item" to "latest accessed item", otherwise "first inserted item" + * to "latest inserted item". + */ + public LinkedMap(boolean pAccessOrder) { + this(null, pAccessOrder); + } + + /** + * Creates a {@code LinkedMap} backed by a {@code HashMap}, with key/value + * pairs copied from {@code pContents} and default (insertion) order. + * + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null}. + */ + public LinkedMap(Map pContents) { + this(pContents, false); + } + + /** + * Creates a {@code LinkedMap} backed by a {@code HashMap}, with key/value + * pairs copied from {@code pContents} and the given order. + * + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null}. + * @param pAccessOrder if {@code true}, ordering will be "least recently + * accessed item" to "latest accessed item", otherwise "first inserted item" + * to "latest inserted item". + */ + public LinkedMap(Map pContents, boolean pAccessOrder) { + super(pContents); + accessOrder = pAccessOrder; + } + + /** + * Creates a {@code LinkedMap} backed by the given map, with key/value + * pairs copied from {@code pContents} and default (insertion) order. + * + * @param pBacking the backing map of this map. Must be either empty, or + * the same map as {@code pContents}. + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null}. + */ + public LinkedMap(Map> pBacking, Map pContents) { + this(pBacking, pContents, false); + } + + /** + * Creates a {@code LinkedMap} backed by the given map, with key/value + * pairs copied from {@code pContents} and the given order. + * + * @param pBacking the backing map of this map. Must be either empty, or + * the same map as {@code pContents}. + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null}. + * @param pAccessOrder if {@code true}, ordering will be "least recently + * accessed item" to "latest accessed item", otherwise "first inserted item" + * to "latest inserted item". + */ + public LinkedMap(Map> pBacking, Map pContents, boolean pAccessOrder) { + super(pBacking, pContents); + accessOrder = pAccessOrder; + } + + protected void init() { + head = new LinkedEntry(null, null, null) { + void addBefore(LinkedEntry pExisting) { + throw new Error(); + } + void remove() { + throw new Error(); + } + public void recordAccess(Map pMap) { + throw new Error(); + } + public void recordRemoval(Map pMap) { + throw new Error(); + } + public void recordRemoval() { + throw new Error(); + } + public V getValue() { + throw new Error(); + } + public V setValue(V pValue) { + throw new Error(); + } + public K getKey() { + throw new Error(); + } + public String toString() { + return "head"; + } + }; + head.previous = head.next = head; + } + + public boolean containsValue(Object pValue) { + // Overridden to take advantage of faster iterator + if (pValue == null) { + for (LinkedEntry e = head.next; e != head; e = e.next) { + if (e.mValue == null) { + return true; + } + } + } else { + for (LinkedEntry e = head.next; e != head; e = e.next) { + if (pValue.equals(e.mValue)) { + return true; + } + } + } + return false; + } + + protected Iterator newKeyIterator() { + return new KeyIterator(); + } + + protected Iterator newValueIterator() { + return new ValueIterator(); + } + + protected Iterator> newEntryIterator() { + return new EntryIterator(); + } + + private abstract class LinkedMapIterator implements Iterator { + LinkedEntry mNextEntry = head.next; + LinkedEntry mLastReturned = null; + + /** + * The modCount value that the iterator believes that the backing + * List should have. If this expectation is violated, the iterator + * has detected concurrent modification. + */ + int mExpectedModCount = modCount; + + public boolean hasNext() { + return mNextEntry != head; + } + + public void remove() { + if (mLastReturned == null) { + throw new IllegalStateException(); + } + + if (modCount != mExpectedModCount) { + throw new ConcurrentModificationException(); + } + + LinkedMap.this.remove(mLastReturned.mKey); + mLastReturned = null; + + mExpectedModCount = modCount; + } + + LinkedEntry nextEntry() { + if (modCount != mExpectedModCount) { + throw new ConcurrentModificationException(); + } + + if (mNextEntry == head) { + throw new NoSuchElementException(); + } + + LinkedEntry e = mLastReturned = mNextEntry; + mNextEntry = e.next; + + return e; + } + } + + private class KeyIterator extends LinkedMap.LinkedMapIterator { + public K next() { + return nextEntry().mKey; + } + } + + private class ValueIterator extends LinkedMap.LinkedMapIterator { + public V next() { + return nextEntry().mValue; + } + } + + private class EntryIterator extends LinkedMap.LinkedMapIterator> { + public Entry next() { + return nextEntry(); + } + } + + public V get(Object pKey) { + LinkedEntry entry = (LinkedEntry) entries.get(pKey); + + if (entry != null) { + entry.recordAccess(this); + return entry.mValue; + } + + return null; + } + + public V remove(Object pKey) { + LinkedEntry entry = (LinkedEntry) entries.remove(pKey); + + if (entry != null) { + entry.remove(); + modCount++; + + return entry.mValue; + } + return null; + } + + public V put(K pKey, V pValue) { + 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 = head.next; + if (removeEldestEntry(eldest)) { + removeEntry(eldest); + } + + entry = createEntry(pKey, pValue); + entry.addBefore(head); + + entries.put(pKey, entry); + } + else { + oldValue = entry.mValue; + + entry.mValue = pValue; + entry.recordAccess(this); + } + + modCount++; + + return oldValue; + } + + /** + * Creates a new {@code LinkedEntry}. + * + * @param pKey the key + * @param pValue the value + * @return a new LinkedEntry + */ + /*protected*/ LinkedEntry createEntry(K pKey, V pValue) { + return new LinkedEntry(pKey, pValue, null); + } + + /** + * @return a copy of this map, with the same order and same key/value pairs. + */ + public Object clone() throws CloneNotSupportedException { + LinkedMap map; + + map = (LinkedMap) super.clone(); + + // TODO: The rest of the work is PROBABLY handled by + // AbstractDecoratedMap, but need to verify that. + + return map; + } + + /** + * Returns {@code true} if this map should remove its eldest entry. + * This method is invoked by {@code put} and {@code putAll} after + * inserting a new entry into the map. It provides the implementer + * with the opportunity to remove the eldest entry each time a new one + * is added. This is useful if the map represents a cache: it allows + * the map to reduce memory consumption by deleting stale entries. + * + *

Sample use: this override will allow the map to grow up to 100 + * entries and then delete the eldest entry each time a new entry is + * added, maintaining a steady state of 100 entries. + *

+     *     private static final int MAX_ENTRIES = 100;
+     *
+     *     protected boolean removeEldestEntry(Map.Entry eldest) {
+     *        return size() > MAX_ENTRIES;
+     *     }
+     * 
+ * + *

This method typically does not modify the map in any way, + * instead allowing the map to modify itself as directed by its + * return value. It is permitted for this method to modify + * the map directly, but if it does so, it must return + * {@code false} (indicating that the map should not attempt any + * further modification). The effects of returning {@code true} + * after modifying the map from within this method are unspecified. + * + *

This implementation merely returns {@code false} (so that this + * map acts like a normal map - the eldest element is never removed). + * + * @param pEldest The least recently inserted entry in the map, or if + * this is an access-ordered map, the least recently accessed + * entry. This is the entry that will be removed it this + * method returns {@code true}. If the map was empty prior + * to the {@code put} or {@code putAll} invocation resulting + * in this invocation, this will be the entry that was just + * inserted; in other words, if the map contains a single + * entry, the eldest entry is also the newest. + * @return {@code true} if the eldest entry should be removed + * from the map; {@code false} if it should be retained. + */ + protected boolean removeEldestEntry(Entry pEldest) { + return false; + } + + /** + * Linked list implementation of {@code Map.Entry}. + */ + protected static class LinkedEntry extends BasicEntry implements Serializable { + LinkedEntry previous; + LinkedEntry next; + + LinkedEntry(K pKey, V pValue, LinkedEntry pNext) { + super(pKey, pValue); + + next = pNext; + } + + /** + * Adds this entry before the given entry (which must be an existing + * entry) in the list. + * + * @param pExisting the entry to add before + */ + void addBefore(LinkedEntry pExisting) { + next = pExisting; + previous = pExisting.previous; + + previous.next = this; + next.previous = this; + } + + /** + * Removes this entry from the linked list. + */ + void remove() { + previous.next = next; + next.previous = previous; + } + + /** + * If the entry is part of an access ordered list, moves the entry to + * the end of the list. + * + * @param pMap the map to record access for + */ + protected void recordAccess(Map pMap) { + LinkedMap linkedMap = (LinkedMap) pMap; + if (linkedMap.accessOrder) { + linkedMap.modCount++; + remove(); + addBefore(linkedMap.head); + } + } + + /** + * Removes this entry from the linked list. + * + * @param pMap the map to record removal from + */ + protected void recordRemoval(Map pMap) { + // TODO: Is this REALLY correct? + remove(); + } + } } \ No newline at end of file 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 18d6385f..302e8646 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 @@ -1,84 +1,85 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.io.Serializable; -import java.util.*; - -/** - * Generic map and linked list implementation of the {@code Set} interface, - * with predictable iteration order. - *

- * Resembles {@code LinkedHashSet} from JDK 1.4+, but is backed by a generic - * {@code LinkedMap}, rather than implementing a particular algoritm. - *

- * @see LinkedMap - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedSet.java#1 $ - */ -public class LinkedSet extends AbstractSet implements Set, Cloneable, Serializable { - - private final static Object DUMMY = new Object(); - - private final Map map; - - public LinkedSet() { - map = new LinkedMap(); - } - - public LinkedSet(Collection pCollection) { - this(); - addAll(pCollection); - } - - public boolean addAll(Collection pCollection) { - boolean changed = false; - for (E value : pCollection) { - if (add(value) && !changed) { - changed = true; - } - } - return changed; - } - - public boolean add(E pValue) { - return map.put(pValue, DUMMY) == null; - } - - public int size() { - return map.size(); - } - - public Iterator iterator() { - return map.keySet().iterator(); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.io.Serializable; +import java.util.*; + +/** + * Generic map and linked list implementation of the {@code Set} interface, + * with predictable iteration order. + *

+ * Resembles {@code LinkedHashSet} from JDK 1.4+, but is backed by a generic + * {@code LinkedMap}, rather than implementing a particular algoritm. + *

+ * + * @see LinkedMap + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedSet.java#1 $ + */ +public class LinkedSet extends AbstractSet implements Set, Cloneable, Serializable { + + private final static Object DUMMY = new Object(); + + private final Map map; + + public LinkedSet() { + map = new LinkedMap(); + } + + public LinkedSet(Collection pCollection) { + this(); + addAll(pCollection); + } + + public boolean addAll(Collection pCollection) { + boolean changed = false; + for (E value : pCollection) { + if (add(value) && !changed) { + changed = true; + } + } + return changed; + } + + public boolean add(E pValue) { + return map.put(pValue, DUMMY) == null; + } + + public int size() { + return map.size(); + } + + public Iterator 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 8eaec564..d9f8dca1 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 @@ -1,122 +1,123 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.io.Serializable; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.Set; - -/** - * 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, - * as the {@code put} and {@code putAll} methods will not modify the map. - * Instances of this class will always be an empty map. - * - * @author Harald Kuhr - * @version $Id: com/twelvemonkeys/util/NullMap.java#2 $ - */ -public final class NullMap implements Map, Serializable { - public final int size() { - return 0; - } - - public final void clear() { - } - - public final boolean isEmpty() { - return true; - } - - public final boolean containsKey(Object pKey) { - return false; - } - - public final boolean containsValue(Object pValue) { - return false; - } - - public final Collection values() { - return Collections.emptyList(); - } - - public final void putAll(Map pMap) { - } - - public final Set> entrySet() { - return Collections.emptySet(); - } - - public final Set keySet() { - return Collections.emptySet(); - } - - public final V get(Object pKey) { - return null; - } - - public final V remove(Object pKey) { - return null; - } - - public final V put(Object pKey, Object pValue) { - return null; - } - - /** - * Tests the given object for equality (wether it is also an empty - * {@code Map}). - * This is consistent with the standard {@code Map} implementations of the - * Java Collections Framework. - * - * @param pOther the object to compare with - * @return {@code true} if {@code pOther} is an empty {@code Map}, - * otherwise {@code false} - */ - public boolean equals(Object pOther) { - return (pOther instanceof Map) && ((Map) pOther).isEmpty(); - } - - /** - * Returns the {@code hashCode} of the empty map, {@code 0}. - * This is consistent with the standard {@code Map} implementations of the - * Java Collections Framework. - * - * @return {@code 0}, always - */ - public int hashCode() { - return 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 of the copyright holder 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 HOLDER 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.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * 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, + * as the {@code put} and {@code putAll} methods will not modify the map. + * Instances of this class will always be an empty map. + *

+ * + * @author Harald Kuhr + * @version $Id: com/twelvemonkeys/util/NullMap.java#2 $ + */ +public final class NullMap implements Map, Serializable { + public final int size() { + return 0; + } + + public final void clear() { + } + + public final boolean isEmpty() { + return true; + } + + public final boolean containsKey(Object pKey) { + return false; + } + + public final boolean containsValue(Object pValue) { + return false; + } + + public final Collection values() { + return Collections.emptyList(); + } + + public final void putAll(Map pMap) { + } + + public final Set> entrySet() { + return Collections.emptySet(); + } + + public final Set keySet() { + return Collections.emptySet(); + } + + public final V get(Object pKey) { + return null; + } + + public final V remove(Object pKey) { + return null; + } + + public final V put(Object pKey, Object pValue) { + return null; + } + + /** + * Tests the given object for equality (wether it is also an empty + * {@code Map}). + * This is consistent with the standard {@code Map} implementations of the + * Java Collections Framework. + * + * @param pOther the object to compare with + * @return {@code true} if {@code pOther} is an empty {@code Map}, + * otherwise {@code false} + */ + public boolean equals(Object pOther) { + return (pOther instanceof Map) && ((Map) pOther).isEmpty(); + } + + /** + * Returns the {@code hashCode} of the empty map, {@code 0}. + * This is consistent with the standard {@code Map} implementations of the + * Java Collections Framework. + * + * @return {@code 0}, always + */ + public int hashCode() { + return 0; + } } \ No newline at end of file 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 9c01fb18..97f8f04e 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 @@ -1,183 +1,183 @@ -/* - * 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 of the copyright holder 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 HOLDER 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; - -/** - * Utility class for storing times in a simple way. The internal time is stored - * as an int, counting seconds. - * - * @author Harald Kuhr - * @todo Milliseconds! - */ -public class Time { - - private int time = -1; - public final static int SECONDS_IN_MINUTE = 60; - - /** - * Creates a new time with 0 seconds, 0 minutes. - */ - public Time() { - this(0); - } - - /** - * Creates a new time with the given time (in seconds). - */ - public Time(int pTime) { - setTime(pTime); - } - - /** - * Sets the full time in seconds - */ - public void setTime(int pTime) { - if (pTime < 0) { - throw new IllegalArgumentException("Time argument must be 0 or positive!"); - } - time = pTime; - } - - /** - * Gets the full time in seconds. - */ - public int getTime() { - return time; - } - - /** - * Gets the full time in milliseconds, for use in creating dates or - * similar. - * - * @see java.util.Date#setTime(long) - */ - public long getTimeInMillis() { - return (long) time * 1000L; - } - - /** - * Sets the seconds part of the time. Note, if the seconds argument is 60 - * or greater, the value will "wrap", and increase the minutes also. - * - * @param pSeconds an integer that should be between 0 and 59. - */ - public void setSeconds(int pSeconds) { - time = getMinutes() * SECONDS_IN_MINUTE + pSeconds; - } - - /** - * Gets the seconds part of the time. - * - * @return an integer between 0 and 59 - */ - public int getSeconds() { - return time % SECONDS_IN_MINUTE; - } - - /** - * Sets the minutes part of the time. - * - * @param pMinutes an integer - */ - public void setMinutes(int pMinutes) { - time = pMinutes * SECONDS_IN_MINUTE + getSeconds(); - } - - /** - * Gets the minutes part of the time. - * - * @return an integer - */ - public int getMinutes() { - return time / SECONDS_IN_MINUTE; - } - - /** - * Creates a string representation of the time object. - * The string is returned on the form m:ss, - * where m is variable digits minutes and ss is two digits seconds. - * - * @return a string representation of the time object - * @see #toString(String) - */ - public String toString() { - return "" + getMinutes() + ":" - + (getSeconds() < 10 ? "0" : "") + getSeconds(); - } - - /** - * Creates a string representation of the time object. - * The string returned is on the format of the formatstring. - *
- *
m (or any multiple of m's) - *
the minutes part (padded with 0's, if number has less digits than - * the number of m's) - * m -> 0,1,...,59,60,61,... - * mm -> 00,01,...,59,60,61,... - *
s or ss - *
the seconds part (padded with 0's, if number has less digits than - * the number of s's) - * s -> 0,1,...,59 - * ss -> 00,01,...,59 - *
S - *
all seconds (including the ones above 59) - *
- * - * @param pFormatStr the format where - * @return a string representation of the time object - * @throws NumberFormatException - * @see TimeFormat#format(Time) - * @see #parseTime(String) - * @deprecated - */ - public String toString(String pFormatStr) { - TimeFormat tf = new TimeFormat(pFormatStr); - - return tf.format(this); - } - - /** - * Creates a string representation of the time object. - * The string is returned on the form m:ss, - * where m is variable digits minutes and ss is two digits seconds. - * - * @return a string representation of the time object - * @throws NumberFormatException - * @see TimeFormat#parse(String) - * @see #toString(String) - * @deprecated - */ - public static Time parseTime(String pStr) { - TimeFormat tf = TimeFormat.getInstance(); - - return tf.parse(pStr); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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; + +/** + * Utility class for storing times in a simple way. The internal time is stored + * as an int, counting seconds. + * + * @author Harald Kuhr + */ +// TODO: Milliseconds! +public class Time { + + private int time = -1; + public final static int SECONDS_IN_MINUTE = 60; + + /** + * Creates a new time with 0 seconds, 0 minutes. + */ + public Time() { + this(0); + } + + /** + * Creates a new time with the given time (in seconds). + */ + public Time(int pTime) { + setTime(pTime); + } + + /** + * Sets the full time in seconds + */ + public void setTime(int pTime) { + if (pTime < 0) { + throw new IllegalArgumentException("Time argument must be 0 or positive!"); + } + time = pTime; + } + + /** + * Gets the full time in seconds. + */ + public int getTime() { + return time; + } + + /** + * Gets the full time in milliseconds, for use in creating dates or + * similar. + * + * @see java.util.Date#setTime(long) + */ + public long getTimeInMillis() { + return (long) time * 1000L; + } + + /** + * Sets the seconds part of the time. Note, if the seconds argument is 60 + * or greater, the value will "wrap", and increase the minutes also. + * + * @param pSeconds an integer that should be between 0 and 59. + */ + public void setSeconds(int pSeconds) { + time = getMinutes() * SECONDS_IN_MINUTE + pSeconds; + } + + /** + * Gets the seconds part of the time. + * + * @return an integer between 0 and 59 + */ + public int getSeconds() { + return time % SECONDS_IN_MINUTE; + } + + /** + * Sets the minutes part of the time. + * + * @param pMinutes an integer + */ + public void setMinutes(int pMinutes) { + time = pMinutes * SECONDS_IN_MINUTE + getSeconds(); + } + + /** + * Gets the minutes part of the time. + * + * @return an integer + */ + public int getMinutes() { + return time / SECONDS_IN_MINUTE; + } + + /** + * Creates a string representation of the time object. + * The string is returned on the form m:ss, + * where m is variable digits minutes and ss is two digits seconds. + * + * @return a string representation of the time object + * @see #toString(String) + */ + public String toString() { + return "" + getMinutes() + ":" + + (getSeconds() < 10 ? "0" : "") + getSeconds(); + } + + /** + * Creates a string representation of the time object. + * The string returned is on the format of the formatstring. + *
+ *
m (or any multiple of m's) + *
the minutes part (padded with 0's, if number has less digits than + * the number of m's) + * m -> 0,1,...,59,60,61,... + * mm -> 00,01,...,59,60,61,... + *
s or ss + *
the seconds part (padded with 0's, if number has less digits than + * the number of s's) + * s -> 0,1,...,59 + * ss -> 00,01,...,59 + *
S + *
all seconds (including the ones above 59) + *
+ * + * @param pFormatStr the format where + * @return a string representation of the time object + * @throws NumberFormatException + * @see TimeFormat#format(Time) + * @see #parseTime(String) + * @deprecated + */ + public String toString(String pFormatStr) { + TimeFormat tf = new TimeFormat(pFormatStr); + + return tf.format(this); + } + + /** + * Creates a string representation of the time object. + * The string is returned on the form m:ss, + * where m is variable digits minutes and ss is two digits seconds. + * + * @return a string representation of the time object + * @throws NumberFormatException + * @see TimeFormat#parse(String) + * @see #toString(String) + * @deprecated + */ + public static Time parseTime(String pStr) { + TimeFormat tf = TimeFormat.getInstance(); + + return tf.parse(pStr); + } +} 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 ac725c31..663cea5a 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 @@ -1,452 +1,452 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.text.FieldPosition; -import java.text.Format; -import java.text.ParsePosition; -import java.util.StringTokenizer; -import java.util.Vector; - -/** - * Format for converting and parsing time. - *

- * The format is expressed in a string as follows: - *

- *
m (or any multiple of m's) - *
the minutes part (padded with 0's, if number has less digits than - * the number of m's) - * m -> 0,1,...,59,60,61,... - * mm -> 00,01,...,59,60,61,... - *
s or ss - *
the seconds part (padded with 0's, if number has less digits than - * the number of s's) - * s -> 0,1,...,59 - * ss -> 00,01,...,59 - *
S - *
all seconds (including the ones above 59) - *
- *

- * May not handle all cases, and formats... ;-) - * Safest is: Always delimiters between the minutes (m) and seconds (s) part. - *

- * TODO: - * Move to com.twelvemonkeys.text? - * Milliseconds! - * Fix bugs. - * Known bugs: - *

- * The last character in the formatString is not escaped, while it should be. - * The first character after an escaped character is escaped while is shouldn't - * be. - *

- * This is not a 100% compatible implementation of a java.text.Format. - * - * @see com.twelvemonkeys.util.Time - * - * @author Harald Kuhr - */ -public class TimeFormat extends Format { - final static String MINUTE = "m"; - final static String SECOND = "s"; - final static String TIME = "S"; - final static String ESCAPE = "\\"; - - /** - * The default time format - */ - - private final static TimeFormat DEFAULT_FORMAT = new TimeFormat("m:ss"); - protected String formatString = null; - - /** - * Main method for testing ONLY - */ - - static void main(String[] argv) { - Time time = null; - TimeFormat in = null; - TimeFormat out = null; - - if (argv.length >= 3) { - System.out.println("Creating out TimeFormat: \"" + argv[2] + "\""); - out = new TimeFormat(argv[2]); - } - - if (argv.length >= 2) { - System.out.println("Creating in TimeFormat: \"" + argv[1] + "\""); - in = new TimeFormat(argv[1]); - } - else { - System.out.println("Using default format for in"); - in = DEFAULT_FORMAT; - } - - if (out == null) - out = in; - - if (argv.length >= 1) { - System.out.println("Parsing: \"" + argv[0] + "\" with format \"" - + in.formatString + "\""); - time = in.parse(argv[0]); - } - else - time = new Time(); - - System.out.println("Time is \"" + out.format(time) + - "\" according to format \"" + out.formatString + "\""); - } - - - /** - * The formatter array. - */ - - protected TimeFormatter[] formatter; - - /** - * Creates a new TimeFormat with the given formatString, - */ - - public TimeFormat(String pStr) { - formatString = pStr; - - Vector formatter = new Vector(); - StringTokenizer tok = new StringTokenizer(pStr, "\\msS", true); - - String previous = null; - String current = null; - int previousCount = 0; - - while (tok.hasMoreElements()) { - current = tok.nextToken(); - - if (previous != null && previous.equals(ESCAPE)) { - // Handle escaping of s, S or m - current = ((current != null) ? current : "") - + (tok.hasMoreElements() ? tok.nextToken() : ""); - previous = null; - previousCount = 0; - } - - // Skip over first, - // or if current is the same, increase count, and try again - if (previous == null || previous.equals(current)) { - previousCount++; - previous = current; - } - else { - // Create new formatter for each part - if (previous.equals(MINUTE)) - formatter.add(new MinutesFormatter(previousCount)); - else if (previous.equals(SECOND)) - formatter.add(new SecondsFormatter(previousCount)); - else if (previous.equals(TIME)) - formatter.add(new SecondsFormatter(-1)); - else - formatter.add(new TextFormatter(previous)); - - previousCount = 1; - previous = current; - - } - } - - // Add new formatter for last part - if (previous != null) { - if (previous.equals(MINUTE)) - formatter.add(new MinutesFormatter(previousCount)); - else if (previous.equals(SECOND)) - formatter.add(new SecondsFormatter(previousCount)); - else if (previous.equals(TIME)) - formatter.add(new SecondsFormatter(-1)); - else - formatter.add(new TextFormatter(previous)); - } - - // Debug - /* - for (int i = 0; i < formatter.size(); i++) { - System.out.println("Formatter " + formatter.get(i).getClass() - + ": length=" + ((TimeFormatter) formatter.get(i)).digits); - } - */ - this.formatter = (TimeFormatter[]) - formatter.toArray(new TimeFormatter[formatter.size()]); - - } - - /** - * DUMMY IMPLEMENTATION!! - * Not locale specific. - */ - - public static TimeFormat getInstance() { - return DEFAULT_FORMAT; - } - - /** DUMMY IMPLEMENTATION!! */ - /* Not locale specific - public static TimeFormat getInstance(Locale pLocale) { - return DEFAULT_FORMAT; - } - */ - - /** DUMMY IMPLEMENTATION!! */ - /* Not locale specific - public static Locale[] getAvailableLocales() { - return new Locale[] {Locale.getDefault()}; - } - */ - - /** Gets the format string. */ - public String getFormatString() { - return formatString; - } - - /** DUMMY IMPLEMENTATION!! */ - public StringBuffer format(Object pObj, StringBuffer pToAppendTo, - FieldPosition pPos) { - if (!(pObj instanceof Time)) { - throw new IllegalArgumentException("Must be instance of " + Time.class); - } - - return pToAppendTo.append(format(pObj)); - } - - /** - * Formats the the given time, using this format. - */ - - public String format(Time pTime) { - StringBuilder buf = new StringBuilder(); - for (int i = 0; i < formatter.length; i++) { - buf.append(formatter[i].format(pTime)); - } - return buf.toString(); - } - - /** DUMMY IMPLEMENTATION!! */ - public Object parseObject(String pStr, ParsePosition pStatus) { - Time t = parse(pStr); - - pStatus.setIndex(pStr.length()); // Not 100% - - return t; - } - - /** - * Parses a Time, according to this format. - *

- * Will bug on some formats. It's safest to always use delimiters between - * the minutes (m) and seconds (s) part. - * - */ - public Time parse(String pStr) { - Time time = new Time(); - - int sec = 0; - int min = 0; - int pos = 0; - int skip = 0; - - boolean onlyUseSeconds = false; - - for (int i = 0; (i < formatter.length) - && (pos + skip < pStr.length()) ; i++) { - // Go to next offset - pos += skip; - - if (formatter[i] instanceof MinutesFormatter) { - // Parse MINUTES - if ((i + 1) < formatter.length - && formatter[i + 1] instanceof TextFormatter) { - // Skip until next format element - 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) >= formatter.length) { - // Skip until end of string - skip = pStr.length(); - } - else { - // Hope this is correct... - skip = formatter[i].digits; - } - - // May be first char - if (skip > pos) - min = Integer.parseInt(pStr.substring(pos, skip)); - } - else if (formatter[i] instanceof SecondsFormatter) { - // Parse SECONDS - if (formatter[i].digits == -1) { - // Only seconds (or full TIME) - if ((i + 1) < formatter.length - && formatter[i + 1] instanceof TextFormatter) { - // Skip until next format element - skip = pStr.indexOf(((TextFormatter) formatter[i + 1]).text, pos); - - } - else if ((i + 1) >= formatter.length) { - // Skip until end of string - skip = pStr.length(); - } - else { - // Cannot possibly know how long? - skip = 0; - continue; - } - - // Get seconds - sec = Integer.parseInt(pStr.substring(pos, skip)); - // System.out.println("Only seconds: " + sec); - - onlyUseSeconds = true; - break; - } - else { - // Normal SECONDS - if ((i + 1) < formatter.length - && formatter[i + 1] instanceof TextFormatter) { - // Skip until next format element - skip = pStr.indexOf(((TextFormatter) formatter[i + 1]).text, pos); - - } - else if ((i + 1) >= formatter.length) { - // Skip until end of string - skip = pStr.length(); - } - else { - skip = formatter[i].digits; - } - // Get seconds - sec = Integer.parseInt(pStr.substring(pos, skip)); - } - } - else if (formatter[i] instanceof TextFormatter) { - skip = formatter[i].digits; - } - - } - - // Set the minutes part if we should - if (!onlyUseSeconds) - time.setMinutes(min); - - // Set the seconds part - time.setSeconds(sec); - - return time; - } -} - -/** - * The base class of TimeFormatters - */ -abstract class TimeFormatter { - int digits = 0; - - abstract String format(Time t); -} - -/** - * Formats the seconds part of the Time - */ -class SecondsFormatter extends TimeFormatter { - - SecondsFormatter(int pDigits) { - digits = pDigits; - } - - String format(Time t) { - // Negative number of digits, means all seconds, no padding - if (digits < 0) { - return Integer.toString(t.getTime()); - } - - // 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(), digits); - return StringUtil.pad("" + t.getSeconds(), digits, "0", true); - } -} - -/** - * Formats the minutes part of the Time - */ -class MinutesFormatter extends TimeFormatter { - - MinutesFormatter(int pDigits) { - digits = pDigits; - } - - String format(Time t) { - // 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(), digits); - return StringUtil.pad("" + t.getMinutes(), digits, "0", true); - } -} - -/** - * Formats text constant part of the Time - */ -class TextFormatter extends TimeFormatter { - String text = null; - - TextFormatter(String pText) { - text = pText; - - // Just to be able to skip over - if (pText != null) { - digits = pText.length(); - } - } - - String format(Time t) { - // Simply return the text - return text; - } - -} +/* + * 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 of the copyright holder 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 HOLDER 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.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.util.StringTokenizer; +import java.util.Vector; + +/** + * Format for converting and parsing time. + *

+ * The format is expressed in a string as follows: + *

+ *
m (or any multiple of m's) + *
the minutes part (padded with 0's, if number has less digits than + * the number of m's) + * m -> 0,1,...,59,60,61,... + * mm -> 00,01,...,59,60,61,... + *
s or ss + *
the seconds part (padded with 0's, if number has less digits than + * the number of s's) + * s -> 0,1,...,59 + * ss -> 00,01,...,59 + *
S + *
all seconds (including the ones above 59) + *
+ *

+ * May not handle all cases, and formats... ;-) + * Safest is: Always delimiters between the minutes (m) and seconds (s) part. + *

+ * Known bugs: + *

+ * The last character in the formatString is not escaped, while it should be. + * The first character after an escaped character is escaped while is shouldn't + * be. + *

+ * This is not a 100% compatible implementation of a java.text.Format. + * + * @see com.twelvemonkeys.util.Time + * + * @author Harald Kuhr + */ +// TODO: +// Move to com.twelvemonkeys.text? +// Milliseconds! +// Fix bugs. +public class TimeFormat extends Format { + final static String MINUTE = "m"; + final static String SECOND = "s"; + final static String TIME = "S"; + final static String ESCAPE = "\\"; + + /** + * The default time format + */ + + private final static TimeFormat DEFAULT_FORMAT = new TimeFormat("m:ss"); + protected String formatString = null; + + /** + * Main method for testing ONLY + */ + + static void main(String[] argv) { + Time time = null; + TimeFormat in = null; + TimeFormat out = null; + + if (argv.length >= 3) { + System.out.println("Creating out TimeFormat: \"" + argv[2] + "\""); + out = new TimeFormat(argv[2]); + } + + if (argv.length >= 2) { + System.out.println("Creating in TimeFormat: \"" + argv[1] + "\""); + in = new TimeFormat(argv[1]); + } + else { + System.out.println("Using default format for in"); + in = DEFAULT_FORMAT; + } + + if (out == null) + out = in; + + if (argv.length >= 1) { + System.out.println("Parsing: \"" + argv[0] + "\" with format \"" + + in.formatString + "\""); + time = in.parse(argv[0]); + } + else + time = new Time(); + + System.out.println("Time is \"" + out.format(time) + + "\" according to format \"" + out.formatString + "\""); + } + + + /** + * The formatter array. + */ + + protected TimeFormatter[] formatter; + + /** + * Creates a new TimeFormat with the given formatString, + */ + + public TimeFormat(String pStr) { + formatString = pStr; + + Vector formatter = new Vector(); + StringTokenizer tok = new StringTokenizer(pStr, "\\msS", true); + + String previous = null; + String current = null; + int previousCount = 0; + + while (tok.hasMoreElements()) { + current = tok.nextToken(); + + if (previous != null && previous.equals(ESCAPE)) { + // Handle escaping of s, S or m + current = ((current != null) ? current : "") + + (tok.hasMoreElements() ? tok.nextToken() : ""); + previous = null; + previousCount = 0; + } + + // Skip over first, + // or if current is the same, increase count, and try again + if (previous == null || previous.equals(current)) { + previousCount++; + previous = current; + } + else { + // Create new formatter for each part + if (previous.equals(MINUTE)) + formatter.add(new MinutesFormatter(previousCount)); + else if (previous.equals(SECOND)) + formatter.add(new SecondsFormatter(previousCount)); + else if (previous.equals(TIME)) + formatter.add(new SecondsFormatter(-1)); + else + formatter.add(new TextFormatter(previous)); + + previousCount = 1; + previous = current; + + } + } + + // Add new formatter for last part + if (previous != null) { + if (previous.equals(MINUTE)) + formatter.add(new MinutesFormatter(previousCount)); + else if (previous.equals(SECOND)) + formatter.add(new SecondsFormatter(previousCount)); + else if (previous.equals(TIME)) + formatter.add(new SecondsFormatter(-1)); + else + formatter.add(new TextFormatter(previous)); + } + + // Debug + /* + for (int i = 0; i < formatter.size(); i++) { + System.out.println("Formatter " + formatter.get(i).getClass() + + ": length=" + ((TimeFormatter) formatter.get(i)).digits); + } + */ + this.formatter = (TimeFormatter[]) + formatter.toArray(new TimeFormatter[formatter.size()]); + + } + + /** + * DUMMY IMPLEMENTATION!! + * Not locale specific. + */ + + public static TimeFormat getInstance() { + return DEFAULT_FORMAT; + } + + /** DUMMY IMPLEMENTATION!! */ + /* Not locale specific + public static TimeFormat getInstance(Locale pLocale) { + return DEFAULT_FORMAT; + } + */ + + /** DUMMY IMPLEMENTATION!! */ + /* Not locale specific + public static Locale[] getAvailableLocales() { + return new Locale[] {Locale.getDefault()}; + } + */ + + /** Gets the format string. */ + public String getFormatString() { + return formatString; + } + + /** DUMMY IMPLEMENTATION!! */ + public StringBuffer format(Object pObj, StringBuffer pToAppendTo, + FieldPosition pPos) { + if (!(pObj instanceof Time)) { + throw new IllegalArgumentException("Must be instance of " + Time.class); + } + + return pToAppendTo.append(format(pObj)); + } + + /** + * Formats the the given time, using this format. + */ + + public String format(Time pTime) { + StringBuilder buf = new StringBuilder(); + for (int i = 0; i < formatter.length; i++) { + buf.append(formatter[i].format(pTime)); + } + return buf.toString(); + } + + /** DUMMY IMPLEMENTATION!! */ + public Object parseObject(String pStr, ParsePosition pStatus) { + Time t = parse(pStr); + + pStatus.setIndex(pStr.length()); // Not 100% + + return t; + } + + /** + * Parses a Time, according to this format. + *

+ * Will bug on some formats. It's safest to always use delimiters between + * the minutes (m) and seconds (s) part. + * + */ + public Time parse(String pStr) { + Time time = new Time(); + + int sec = 0; + int min = 0; + int pos = 0; + int skip = 0; + + boolean onlyUseSeconds = false; + + for (int i = 0; (i < formatter.length) + && (pos + skip < pStr.length()) ; i++) { + // Go to next offset + pos += skip; + + if (formatter[i] instanceof MinutesFormatter) { + // Parse MINUTES + if ((i + 1) < formatter.length + && formatter[i + 1] instanceof TextFormatter) { + // Skip until next format element + 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) >= formatter.length) { + // Skip until end of string + skip = pStr.length(); + } + else { + // Hope this is correct... + skip = formatter[i].digits; + } + + // May be first char + if (skip > pos) + min = Integer.parseInt(pStr.substring(pos, skip)); + } + else if (formatter[i] instanceof SecondsFormatter) { + // Parse SECONDS + if (formatter[i].digits == -1) { + // Only seconds (or full TIME) + if ((i + 1) < formatter.length + && formatter[i + 1] instanceof TextFormatter) { + // Skip until next format element + skip = pStr.indexOf(((TextFormatter) formatter[i + 1]).text, pos); + + } + else if ((i + 1) >= formatter.length) { + // Skip until end of string + skip = pStr.length(); + } + else { + // Cannot possibly know how long? + skip = 0; + continue; + } + + // Get seconds + sec = Integer.parseInt(pStr.substring(pos, skip)); + // System.out.println("Only seconds: " + sec); + + onlyUseSeconds = true; + break; + } + else { + // Normal SECONDS + if ((i + 1) < formatter.length + && formatter[i + 1] instanceof TextFormatter) { + // Skip until next format element + skip = pStr.indexOf(((TextFormatter) formatter[i + 1]).text, pos); + + } + else if ((i + 1) >= formatter.length) { + // Skip until end of string + skip = pStr.length(); + } + else { + skip = formatter[i].digits; + } + // Get seconds + sec = Integer.parseInt(pStr.substring(pos, skip)); + } + } + else if (formatter[i] instanceof TextFormatter) { + skip = formatter[i].digits; + } + + } + + // Set the minutes part if we should + if (!onlyUseSeconds) + time.setMinutes(min); + + // Set the seconds part + time.setSeconds(sec); + + return time; + } +} + +/** + * The base class of TimeFormatters + */ +abstract class TimeFormatter { + int digits = 0; + + abstract String format(Time t); +} + +/** + * Formats the seconds part of the Time + */ +class SecondsFormatter extends TimeFormatter { + + SecondsFormatter(int pDigits) { + digits = pDigits; + } + + String format(Time t) { + // Negative number of digits, means all seconds, no padding + if (digits < 0) { + return Integer.toString(t.getTime()); + } + + // 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(), digits); + return StringUtil.pad("" + t.getSeconds(), digits, "0", true); + } +} + +/** + * Formats the minutes part of the Time + */ +class MinutesFormatter extends TimeFormatter { + + MinutesFormatter(int pDigits) { + digits = pDigits; + } + + String format(Time t) { + // 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(), digits); + return StringUtil.pad("" + t.getMinutes(), digits, "0", true); + } +} + +/** + * Formats text constant part of the Time + */ +class TextFormatter extends TimeFormatter { + String text = null; + + TextFormatter(String pText) { + text = pText; + + // Just to be able to skip over + if (pText != null) { + digits = pText.length(); + } + } + + String format(Time t) { + // Simply return the text + 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 2b259dde..b3199f56 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 @@ -1,451 +1,453 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.io.Serializable; -import java.util.*; - -/** - * A {@code Map} implementation that removes (exipres) its elements after - * a given period. The map is by default backed by a {@link java.util.HashMap}, - * or can be instantiated with any given {@code Map} as backing. - *

- * Notes to consider when using this map: - *

    - *
  • Elements may not expire on the exact millisecond as expected.
  • - *
  • The value returned by the {@code size()} method of the map, or any of - * its collection views, may not represent - * the exact number of entries in the map at any given time.
  • - *
  • Elements in this map may expire at any time - * (but never between invocations of {@code Iterator.hasNext()} - * and {@code Iterator.next()} or {@code Iterator.remove()}, - * when iterating the collection views).
  • - *
- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeoutMap.java#2 $ - * - * @todo Consider have this Map extend LinkedMap.. That way the removeExpired - * method only have to run from the first element, until it finds an element - * that should not expire, as elements are in insertion order. - * and next expiry time would be the time of the first element. - * @todo Consider running the removeExpiredEntries method in a separate (deamon) thread - * @todo - or document why it is not such a good idea. - */ -public class TimeoutMap extends AbstractDecoratedMap implements ExpiringMap, Serializable, Cloneable { - /** - * Expiry time - */ - protected long expiryTime = 60000L; // 1 minute - - ////////////////////// - private volatile long nextExpiryTime; - ////////////////////// - - /** - * Creates a {@code TimeoutMap} with the default expiry time of 1 minute. - * This {@code TimeoutMap} will be backed by a new {@code HashMap} instance. - *

- * This is constructor is here to comply with the reccomendations for - * "standard" constructors in the {@code Map} interface. - * - * @see #TimeoutMap(long) - */ - public TimeoutMap() { - super(); - } - - /** - * Creates a {@code TimeoutMap} containing the same elements as the given map - * with the default expiry time of 1 minute. - * This {@code TimeoutMap} will be backed by a new {@code HashMap} instance, - * and not the map passed in as a paramter. - *

- * This is constructor is here to comply with the reccomendations for - * "standard" constructors in the {@code Map} interface. - * - * @param pContents the map whose mappings are to be placed in this map. - * May be {@code null}. - * @see #TimeoutMap(java.util.Map, Map, long) - * @see java.util.Map - */ - public TimeoutMap(Map pContents) { - super(pContents); - } - - /** - * Creates a {@code TimeoutMap} with the given expiry time (milliseconds). - * This {@code TimeoutMap} will be backed by a new {@code HashMap} instance. - * - * @param pExpiryTime the expiry time (time to live) for elements in this map - */ - public TimeoutMap(long pExpiryTime) { - this(); - expiryTime = pExpiryTime; - } - - /** - * Creates a {@code TimeoutMap} with the given expiry time (milliseconds). - * This {@code TimeoutMap} will be backed by the given {@code Map}. - *

- * Note that structurally modifying the backing map directly (not - * through this map or its collection views), is not allowed, and will - * produce undeterministic exceptions. - * - * @param pBacking the map that will be used as backing. - * @param pContents the map whose mappings are to be placed in this map. - * May be {@code null}. - * @param pExpiryTime the expiry time (time to live) for elements in this map - */ - public TimeoutMap(Map> pBacking, Map pContents, long pExpiryTime) { - super(pBacking, pContents); - expiryTime = pExpiryTime; - } - - /** - * Gets the maximum time any value will be kept in the map, before it expires. - * - * @return the expiry time - */ - public long getExpiryTime() { - return expiryTime; - } - - /** - * Sets the maximum time any value will be kept in the map, before it expires. - * Removes any items that are older than the specified time. - * - * @param pExpiryTime the expiry time (time to live) for elements in this map - */ - public void setExpiryTime(long pExpiryTime) { - long oldEexpiryTime = expiryTime; - - expiryTime = pExpiryTime; - - if (expiryTime < oldEexpiryTime) { - // Expire now - nextExpiryTime = 0; - removeExpiredEntries(); - } - } - - /** - * Returns the number of key-value mappings in this map. If the - * map contains more than {@code Integer.MAX_VALUE} elements, returns - * {@code Integer.MAX_VALUE}. - * - * @return the number of key-value mappings in this map. - */ - public int size() { - removeExpiredEntries(); - return entries.size(); - } - - /** - * Returns {@code true} if this map contains no key-value mappings. - * - * @return {@code true} if this map contains no key-value mappings. - */ - public boolean isEmpty() { - return (size() <= 0); - } - - /** - * Returns {@code true} if this map contains a mapping for the specified - * pKey. - * - * @param pKey pKey whose presence in this map is to be tested. - * @return {@code true} if this map contains a mapping for the specified - * pKey. - */ - public boolean containsKey(Object pKey) { - removeExpiredEntries(); - return entries.containsKey(pKey); - } - - /** - * Returns the value to which this map maps the specified pKey. Returns - * {@code null} if the map contains no mapping for this pKey. A return - * value of {@code null} does not necessarily indicate that the - * map contains no mapping for the pKey; it's also possible that the map - * explicitly maps the pKey to {@code null}. The {@code containsKey} - * operation may be used to distinguish these two cases. - * - * @param pKey pKey whose associated value is to be returned. - * @return the value to which this map maps the specified pKey, or - * {@code null} if the map contains no mapping for this pKey. - * @see #containsKey(java.lang.Object) - */ - public V get(Object pKey) { - TimedEntry entry = (TimedEntry) entries.get(pKey); - - if (entry == null) { - return null; - } - else if (entry.isExpired()) { - //noinspection SuspiciousMethodCalls - entries.remove(pKey); - processRemoved(entry); - return null; - } - return entry.getValue(); - } - - /** - * Associates the specified pValue with the specified pKey in this map - * (optional operation). If the map previously contained a mapping for - * this pKey, the old pValue is replaced. - * - * @param pKey pKey with which the specified pValue is to be associated. - * @param pValue pValue to be associated with the specified pKey. - * @return previous pValue associated with specified pKey, or {@code null} - * if there was no mapping for pKey. A {@code null} return can - * also indicate that the map previously associated {@code null} - * with the specified pKey, if the implementation supports - * {@code null} values. - */ - public V put(K pKey, V pValue) { - TimedEntry entry = (TimedEntry) entries.get(pKey); - V oldValue; - - if (entry == null) { - oldValue = null; - - entry = createEntry(pKey, pValue); - - entries.put(pKey, entry); - } - else { - oldValue = entry.mValue; - entry.setValue(pValue); - entry.recordAccess(this); - } - - // Need to remove expired objects every now and then - // We do it in the put method, to avoid resource leaks over time. - removeExpiredEntries(); - modCount++; - - return oldValue; - } - - /** - * Removes the mapping for this pKey from this map if present (optional - * operation). - * - * @param pKey pKey whose mapping is to be removed from the map. - * @return previous value associated with specified pKey, or {@code null} - * if there was no mapping for pKey. A {@code null} return can - * also indicate that the map previously associated {@code null} - * with the specified pKey, if the implementation supports - * {@code null} values. - */ - public V remove(Object pKey) { - TimedEntry entry = (TimedEntry) entries.remove(pKey); - return (entry != null) ? entry.getValue() : null; - } - - /** - * Removes all mappings from this map. - */ - public void clear() { - entries.clear(); // Finally something straightforward.. :-) - init(); - } - - /*protected*/ TimedEntry createEntry(K pKey, V pValue) { - return new TimedEntry(pKey, pValue); - } - - /** - * Removes any expired mappings. - * - */ - protected void removeExpiredEntries() { - // Remove any expired elements - long now = System.currentTimeMillis(); - if (now > nextExpiryTime) { - removeExpiredEntriesSynced(now); - } - } - - /** - * Okay, I guess this do resemble DCL... - * - * @todo Write some exhausting multi-threaded unit-tests. - * - * @param pTime now - */ - private synchronized void removeExpiredEntriesSynced(long pTime) { - if (pTime > nextExpiryTime) { - //// - long next = Long.MAX_VALUE; - nextExpiryTime = next; // Avoid multiple runs... - for (Iterator> iterator = new EntryIterator(); iterator.hasNext();) { - TimedEntry entry = (TimedEntry) iterator.next(); - //// - long expires = entry.expires(); - if (expires < next) { - next = expires; - } - //// - } - //// - nextExpiryTime = next; - } - } - - public Collection values() { - removeExpiredEntries(); - return super.values(); - } - - public Set> entrySet() { - removeExpiredEntries(); - return super.entrySet(); - } - - public Set keySet() { - removeExpiredEntries(); - return super.keySet(); - } - - // Subclass overrides these to alter behavior of views' iterator() method - protected Iterator newKeyIterator() { - return new KeyIterator(); - } - - protected Iterator newValueIterator() { - return new ValueIterator(); - } - - protected Iterator> newEntryIterator() { - return new EntryIterator(); - } - - public void processRemoved(Entry pRemoved) { - } - - /** - * Note: Iterating through this iterator will remove any expired values. - */ - private abstract class TimeoutMapIterator implements Iterator { - Iterator>> mIterator = entries.entrySet().iterator(); - BasicEntry mNext; - long mNow = System.currentTimeMillis(); - - public void remove() { - mNext = null; // advance - mIterator.remove(); - } - - public boolean hasNext() { - if (mNext != null) { - return true; // Never expires between hasNext and next/remove! - } - - while (mNext == null && mIterator.hasNext()) { - Entry> entry = mIterator.next(); - TimedEntry timed = (TimedEntry) entry.getValue(); - - if (timed.isExpiredBy(mNow)) { - // Remove from map, and continue - mIterator.remove(); - processRemoved(timed); - } - else { - // Go with this entry - mNext = timed; - return true; - } - } - - return false; - } - - BasicEntry nextEntry() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - - BasicEntry entry = mNext; - mNext = null; // advance - return entry; - } - } - - private class KeyIterator extends TimeoutMapIterator { - public K next() { - return nextEntry().mKey; - } - } - - private class ValueIterator extends TimeoutMapIterator { - public V next() { - return nextEntry().mValue; - } - } - - private class EntryIterator extends TimeoutMapIterator> { - public Entry next() { - return nextEntry(); - } - } - - /** - * Keeps track of timed objects - */ - private class TimedEntry extends BasicEntry { - private long mTimestamp; - - TimedEntry(K pKey, V pValue) { - super(pKey, pValue); - mTimestamp = System.currentTimeMillis(); - } - - public V setValue(V pValue) { - mTimestamp = System.currentTimeMillis(); - return super.setValue(pValue); - } - - final boolean isExpired() { - return isExpiredBy(System.currentTimeMillis()); - } - - final boolean isExpiredBy(final long pTime) { - return pTime > expires(); - } - - final long expires() { - return mTimestamp + expiryTime; - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.io.Serializable; +import java.util.*; + +/** + * A {@code Map} implementation that removes (exipres) its elements after + * a given period. The map is by default backed by a {@link java.util.HashMap}, + * or can be instantiated with any given {@code Map} as backing. + *

+ * Notes to consider when using this map: + *

+ *
    + *
  • Elements may not expire on the exact millisecond as expected.
  • + *
  • The value returned by the {@code size()} method of the map, or any of + * its collection views, may not represent + * the exact number of entries in the map at any given time.
  • + *
  • Elements in this map may expire at any time + * (but never between invocations of {@code Iterator.hasNext()} + * and {@code Iterator.next()} or {@code Iterator.remove()}, + * when iterating the collection views).
  • + *
+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeoutMap.java#2 $ + */ + // TODO: Consider have this Map extend LinkedMap.. That way the removeExpired + // method only have to run from the first element, until it finds an element + // that should not expire, as elements are in insertion order. + // and next expiry time would be the time of the first element. + // TODO: Consider running the removeExpiredEntries method in a separate (deamon) thread + // TODO: - or document why it is not such a good idea. +public class TimeoutMap extends AbstractDecoratedMap implements ExpiringMap, Serializable, Cloneable { + /** + * Expiry time + */ + protected long expiryTime = 60000L; // 1 minute + + ////////////////////// + private volatile long nextExpiryTime; + ////////////////////// + + /** + * Creates a {@code TimeoutMap} with the default expiry time of 1 minute. + * This {@code TimeoutMap} will be backed by a new {@code HashMap} instance. + *

+ * This is constructor is here to comply with the recommendations for + * "standard" constructors in the {@code Map} interface. + *

+ * + * @see #TimeoutMap(long) + */ + public TimeoutMap() { + super(); + } + + /** + * Creates a {@code TimeoutMap} containing the same elements as the given map + * with the default expiry time of 1 minute. + * This {@code TimeoutMap} will be backed by a new {@code HashMap} instance, + * and not the map passed in as a paramter. + *

+ * This is constructor is here to comply with the recommendations for + * "standard" constructors in the {@code Map} interface. + *

+ * + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null}. + * @see #TimeoutMap(java.util.Map, Map, long) + * @see java.util.Map + */ + public TimeoutMap(Map pContents) { + super(pContents); + } + + /** + * Creates a {@code TimeoutMap} with the given expiry time (milliseconds). + * This {@code TimeoutMap} will be backed by a new {@code HashMap} instance. + * + * @param pExpiryTime the expiry time (time to live) for elements in this map + */ + public TimeoutMap(long pExpiryTime) { + this(); + expiryTime = pExpiryTime; + } + + /** + * Creates a {@code TimeoutMap} with the given expiry time (milliseconds). + * This {@code TimeoutMap} will be backed by the given {@code Map}. + *

+ * Note that structurally modifying the backing map directly (not + * through this map or its collection views), is not allowed, and will + * produce undeterministic exceptions. + *

+ * + * @param pBacking the map that will be used as backing. + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null}. + * @param pExpiryTime the expiry time (time to live) for elements in this map + */ + public TimeoutMap(Map> pBacking, Map pContents, long pExpiryTime) { + super(pBacking, pContents); + expiryTime = pExpiryTime; + } + + /** + * Gets the maximum time any value will be kept in the map, before it expires. + * + * @return the expiry time + */ + public long getExpiryTime() { + return expiryTime; + } + + /** + * Sets the maximum time any value will be kept in the map, before it expires. + * Removes any items that are older than the specified time. + * + * @param pExpiryTime the expiry time (time to live) for elements in this map + */ + public void setExpiryTime(long pExpiryTime) { + long oldEexpiryTime = expiryTime; + + expiryTime = pExpiryTime; + + if (expiryTime < oldEexpiryTime) { + // Expire now + nextExpiryTime = 0; + removeExpiredEntries(); + } + } + + /** + * Returns the number of key-value mappings in this map. If the + * map contains more than {@code Integer.MAX_VALUE} elements, returns + * {@code Integer.MAX_VALUE}. + * + * @return the number of key-value mappings in this map. + */ + public int size() { + removeExpiredEntries(); + return entries.size(); + } + + /** + * Returns {@code true} if this map contains no key-value mappings. + * + * @return {@code true} if this map contains no key-value mappings. + */ + public boolean isEmpty() { + return (size() <= 0); + } + + /** + * Returns {@code true} if this map contains a mapping for the specified + * pKey. + * + * @param pKey pKey whose presence in this map is to be tested. + * @return {@code true} if this map contains a mapping for the specified + * pKey. + */ + public boolean containsKey(Object pKey) { + removeExpiredEntries(); + return entries.containsKey(pKey); + } + + /** + * Returns the value to which this map maps the specified pKey. Returns + * {@code null} if the map contains no mapping for this pKey. A return + * value of {@code null} does not necessarily indicate that the + * map contains no mapping for the pKey; it's also possible that the map + * explicitly maps the pKey to {@code null}. The {@code containsKey} + * operation may be used to distinguish these two cases. + * + * @param pKey pKey whose associated value is to be returned. + * @return the value to which this map maps the specified pKey, or + * {@code null} if the map contains no mapping for this pKey. + * @see #containsKey(java.lang.Object) + */ + public V get(Object pKey) { + TimedEntry entry = (TimedEntry) entries.get(pKey); + + if (entry == null) { + return null; + } + else if (entry.isExpired()) { + //noinspection SuspiciousMethodCalls + entries.remove(pKey); + processRemoved(entry); + return null; + } + return entry.getValue(); + } + + /** + * Associates the specified pValue with the specified pKey in this map + * (optional operation). If the map previously contained a mapping for + * this pKey, the old pValue is replaced. + * + * @param pKey pKey with which the specified pValue is to be associated. + * @param pValue pValue to be associated with the specified pKey. + * @return previous pValue associated with specified pKey, or {@code null} + * if there was no mapping for pKey. A {@code null} return can + * also indicate that the map previously associated {@code null} + * with the specified pKey, if the implementation supports + * {@code null} values. + */ + public V put(K pKey, V pValue) { + TimedEntry entry = (TimedEntry) entries.get(pKey); + V oldValue; + + if (entry == null) { + oldValue = null; + + entry = createEntry(pKey, pValue); + + entries.put(pKey, entry); + } + else { + oldValue = entry.mValue; + entry.setValue(pValue); + entry.recordAccess(this); + } + + // Need to remove expired objects every now and then + // We do it in the put method, to avoid resource leaks over time. + removeExpiredEntries(); + modCount++; + + return oldValue; + } + + /** + * Removes the mapping for this pKey from this map if present (optional + * operation). + * + * @param pKey pKey whose mapping is to be removed from the map. + * @return previous value associated with specified pKey, or {@code null} + * if there was no mapping for pKey. A {@code null} return can + * also indicate that the map previously associated {@code null} + * with the specified pKey, if the implementation supports + * {@code null} values. + */ + public V remove(Object pKey) { + TimedEntry entry = (TimedEntry) entries.remove(pKey); + return (entry != null) ? entry.getValue() : null; + } + + /** + * Removes all mappings from this map. + */ + public void clear() { + entries.clear(); // Finally something straightforward.. :-) + init(); + } + + /*protected*/ TimedEntry createEntry(K pKey, V pValue) { + return new TimedEntry(pKey, pValue); + } + + /** + * Removes any expired mappings. + * + */ + protected void removeExpiredEntries() { + // Remove any expired elements + long now = System.currentTimeMillis(); + if (now > nextExpiryTime) { + removeExpiredEntriesSynced(now); + } + } + + /** + * Okay, I guess this do resemble DCL... + * + * @param pTime now + */ + // TODO: Write some exhausting multi-threaded unit-tests. + private synchronized void removeExpiredEntriesSynced(long pTime) { + if (pTime > nextExpiryTime) { + //// + long next = Long.MAX_VALUE; + nextExpiryTime = next; // Avoid multiple runs... + for (Iterator> iterator = new EntryIterator(); iterator.hasNext();) { + TimedEntry entry = (TimedEntry) iterator.next(); + //// + long expires = entry.expires(); + if (expires < next) { + next = expires; + } + //// + } + //// + nextExpiryTime = next; + } + } + + public Collection values() { + removeExpiredEntries(); + return super.values(); + } + + public Set> entrySet() { + removeExpiredEntries(); + return super.entrySet(); + } + + public Set keySet() { + removeExpiredEntries(); + return super.keySet(); + } + + // Subclass overrides these to alter behavior of views' iterator() method + protected Iterator newKeyIterator() { + return new KeyIterator(); + } + + protected Iterator newValueIterator() { + return new ValueIterator(); + } + + protected Iterator> newEntryIterator() { + return new EntryIterator(); + } + + public void processRemoved(Entry pRemoved) { + } + + /** + * Note: Iterating through this iterator will remove any expired values. + */ + private abstract class TimeoutMapIterator implements Iterator { + Iterator>> mIterator = entries.entrySet().iterator(); + BasicEntry mNext; + long mNow = System.currentTimeMillis(); + + public void remove() { + mNext = null; // advance + mIterator.remove(); + } + + public boolean hasNext() { + if (mNext != null) { + return true; // Never expires between hasNext and next/remove! + } + + while (mNext == null && mIterator.hasNext()) { + Entry> entry = mIterator.next(); + TimedEntry timed = (TimedEntry) entry.getValue(); + + if (timed.isExpiredBy(mNow)) { + // Remove from map, and continue + mIterator.remove(); + processRemoved(timed); + } + else { + // Go with this entry + mNext = timed; + return true; + } + } + + return false; + } + + BasicEntry nextEntry() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + BasicEntry entry = mNext; + mNext = null; // advance + return entry; + } + } + + private class KeyIterator extends TimeoutMapIterator { + public K next() { + return nextEntry().mKey; + } + } + + private class ValueIterator extends TimeoutMapIterator { + public V next() { + return nextEntry().mValue; + } + } + + private class EntryIterator extends TimeoutMapIterator> { + public Entry next() { + return nextEntry(); + } + } + + /** + * Keeps track of timed objects + */ + private class TimedEntry extends BasicEntry { + private long mTimestamp; + + TimedEntry(K pKey, V pValue) { + super(pKey, pValue); + mTimestamp = System.currentTimeMillis(); + } + + public V setValue(V pValue) { + mTimestamp = System.currentTimeMillis(); + return super.setValue(pValue); + } + + final boolean isExpired() { + return isExpiredBy(System.currentTimeMillis()); + } + + final boolean isExpiredBy(final long pTime) { + return pTime > expires(); + } + + final long expires() { + return mTimestamp + expiryTime; + } + } +} diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/TokenIterator.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/TokenIterator.java index 45f2f7af..a2aa2438 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/TokenIterator.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/TokenIterator.java @@ -1,59 +1,58 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.util.Enumeration; -import java.util.Iterator; - -/** - * TokenIterator, Iterator-based replacement for StringTokenizer. - *

- * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TokenIterator.java#1 $ - */ -public interface TokenIterator extends Iterator, Enumeration { - boolean hasMoreTokens(); - - /** - * Returns the next element in the iteration as a {@code String}. - * - * @return the next element in the iteration. - * @exception java.util.NoSuchElementException iteration has no more elements. - */ - String nextToken(); - - /** - * Resets this iterator. - * - */ - void reset(); -} +/* + * 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 of the copyright holder 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 HOLDER 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.util.Enumeration; +import java.util.Iterator; + +/** + * TokenIterator, Iterator-based replacement for StringTokenizer. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TokenIterator.java#1 $ + */ +public interface TokenIterator extends Iterator, Enumeration { + boolean hasMoreTokens(); + + /** + * Returns the next element in the iteration as a {@code String}. + * + * @return the next element in the iteration. + * @exception java.util.NoSuchElementException iteration has no more elements. + */ + String nextToken(); + + /** + * Resets this iterator. + * + */ + void reset(); +} 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 4da27644..8f6b09c5 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 @@ -1,196 +1,198 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.convert; - -import com.twelvemonkeys.util.Time; - -import java.util.Date; -import java.util.Hashtable; -import java.util.Map; - -/** - * The converter (singleton). Converts strings to objects and back. - * This is the entry point to the converter framework. - *

- * By default, converters for {@link com.twelvemonkeys.util.Time}, {@link Date} - * and {@link Object} - * (the {@link DefaultConverter}) are registered by this class' static - * initializer. You might remove them using the - * {@code unregisterConverter} method. - * - * @see #registerConverter(Class, PropertyConverter) - * @see #unregisterConverter(Class) - * - * @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/Converter.java#1 $ - */ -// TODO: Get rid of singleton stuff -// Can probably be a pure static class, but is that a good idea? -// Maybe have BeanUtil act as a "proxy", and hide this class all together? -// 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 final Converter sInstance = new ConverterImpl(); // Thread safe & EASY - - /** The converters Map */ - protected final Map converters = new Hashtable(); - - // Register our predefined converters - static { - PropertyConverter defaultConverter = new DefaultConverter(); - registerConverter(Object.class, defaultConverter); - registerConverter(Boolean.TYPE, defaultConverter); - - PropertyConverter numberConverter = new NumberConverter(); - registerConverter(Number.class, numberConverter); - registerConverter(Byte.TYPE, numberConverter); - registerConverter(Double.TYPE, numberConverter); - registerConverter(Float.TYPE, numberConverter); - registerConverter(Integer.TYPE, numberConverter); - registerConverter(Long.TYPE, numberConverter); - registerConverter(Short.TYPE, numberConverter); - - registerConverter(Date.class, new DateConverter()); - registerConverter(Time.class, new TimeConverter()); - } - - /** - * Creates a Converter. - */ - protected Converter() { - } - - /** - * Gets the Converter instance. - * - * @return the converter instance - */ - public static Converter getInstance() { - return sInstance; - } - - /** - * Registers a converter for a given type. - * This converter will also be used for all subclasses, unless a more - * specific version is registered. - *

- * By default, converters for {@link com.twelvemonkeys.util.Time}, {@link Date} - * and {@link Object} - * (the {@link DefaultConverter}) are registered by this class' static - * initializer. You might remove them using the - * {@code unregisterConverter} method. - * - * @param pType the (super) type to register a converter for - * @param pConverter the converter - * - * @see #unregisterConverter(Class) - */ - public static void registerConverter(final Class pType, final PropertyConverter pConverter) { - getInstance().converters.put(pType, pConverter); - } - - /** - * Un-registers a converter for a given type. That is, making it unavailable - * for the converter framework, and making it (potentially) available for - * garbage collection. - * - * @param pType the (super) type to remove converter for - * - * @see #registerConverter(Class,PropertyConverter) - */ - @SuppressWarnings("UnusedDeclaration") - public static void unregisterConverter(final Class pType) { - getInstance().converters.remove(pType); - } - - /** - * Converts the string to an object of the given type. - * - * @param pString the string to convert - * @param pType the type to convert to - * - * @return the object created from the given string. - * - * @throws ConversionException if the string cannot be converted for any - * reason. - */ - public Object toObject(final String pString, final Class pType) throws ConversionException { - return toObject(pString, pType, null); - } - - /** - * Converts the string to an object of the given type, parsing after the - * given format. - * - * @param pString the string to convert - * @param pType the type to convert to - * @param pFormat the (optional) conversion format - * - * @return the object created from the given string. - * - * @throws ConversionException if the string cannot be converted for any - * reason. - */ - public abstract Object toObject(String pString, Class pType, String pFormat) - throws ConversionException; - - /** - * Converts the object to a string, using {@code object.toString()} - * - * @param pObject the object to convert. - * - * @return the string representation of the object, on the correct format. - * - * @throws ConversionException if the object cannot be converted to a - * string for any reason. - */ - public String toString(final Object pObject) throws ConversionException { - return toString(pObject, null); - } - - /** - * Converts the object to a string, using {@code object.toString()} - * - * @param pObject the object to convert. - * @param pFormat the (optional) conversion format - * - * @return the string representation of the object, on the correct format. - * - * @throws ConversionException if the object cannot be converted to a - * string for any reason. - */ - public abstract String toString(Object pObject, String pFormat) - throws ConversionException; -} +/* + * 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 of the copyright holder 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 HOLDER 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.convert; + +import com.twelvemonkeys.util.Time; + +import java.util.Date; +import java.util.Hashtable; +import java.util.Map; + +/** + * The converter (singleton). Converts strings to objects and back. + * This is the entry point to the converter framework. + *

+ * By default, converters for {@link com.twelvemonkeys.util.Time}, {@link Date} + * and {@link Object} + * (the {@link DefaultConverter}) are registered by this class' static + * initializer. You might remove them using the + * {@code unregisterConverter} method. + *

+ * + * @see #registerConverter(Class, PropertyConverter) + * @see #unregisterConverter(Class) + * + * @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/Converter.java#1 $ + */ +// TODO: Get rid of singleton stuff +// Can probably be a pure static class, but is that a good idea? +// Maybe have BeanUtil act as a "proxy", and hide this class all together? +// 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 final Converter sInstance = new ConverterImpl(); // Thread safe & EASY + + /** The converters Map */ + protected final Map converters = new Hashtable(); + + // Register our predefined converters + static { + PropertyConverter defaultConverter = new DefaultConverter(); + registerConverter(Object.class, defaultConverter); + registerConverter(Boolean.TYPE, defaultConverter); + + PropertyConverter numberConverter = new NumberConverter(); + registerConverter(Number.class, numberConverter); + registerConverter(Byte.TYPE, numberConverter); + registerConverter(Double.TYPE, numberConverter); + registerConverter(Float.TYPE, numberConverter); + registerConverter(Integer.TYPE, numberConverter); + registerConverter(Long.TYPE, numberConverter); + registerConverter(Short.TYPE, numberConverter); + + registerConverter(Date.class, new DateConverter()); + registerConverter(Time.class, new TimeConverter()); + } + + /** + * Creates a Converter. + */ + protected Converter() { + } + + /** + * Gets the Converter instance. + * + * @return the converter instance + */ + public static Converter getInstance() { + return sInstance; + } + + /** + * Registers a converter for a given type. + * This converter will also be used for all subclasses, unless a more + * specific version is registered. + *

+ * By default, converters for {@link com.twelvemonkeys.util.Time}, {@link Date} + * and {@link Object} + * (the {@link DefaultConverter}) are registered by this class' static + * initializer. You might remove them using the + * {@code unregisterConverter} method. + *

+ * + * @param pType the (super) type to register a converter for + * @param pConverter the converter + * + * @see #unregisterConverter(Class) + */ + public static void registerConverter(final Class pType, final PropertyConverter pConverter) { + getInstance().converters.put(pType, pConverter); + } + + /** + * Un-registers a converter for a given type. That is, making it unavailable + * for the converter framework, and making it (potentially) available for + * garbage collection. + * + * @param pType the (super) type to remove converter for + * + * @see #registerConverter(Class,PropertyConverter) + */ + @SuppressWarnings("UnusedDeclaration") + public static void unregisterConverter(final Class pType) { + getInstance().converters.remove(pType); + } + + /** + * Converts the string to an object of the given type. + * + * @param pString the string to convert + * @param pType the type to convert to + * + * @return the object created from the given string. + * + * @throws ConversionException if the string cannot be converted for any + * reason. + */ + public Object toObject(final String pString, final Class pType) throws ConversionException { + return toObject(pString, pType, null); + } + + /** + * Converts the string to an object of the given type, parsing after the + * given format. + * + * @param pString the string to convert + * @param pType the type to convert to + * @param pFormat the (optional) conversion format + * + * @return the object created from the given string. + * + * @throws ConversionException if the string cannot be converted for any + * reason. + */ + public abstract Object toObject(String pString, Class pType, String pFormat) + throws ConversionException; + + /** + * Converts the object to a string, using {@code object.toString()} + * + * @param pObject the object to convert. + * + * @return the string representation of the object, on the correct format. + * + * @throws ConversionException if the object cannot be converted to a + * string for any reason. + */ + public String toString(final Object pObject) throws ConversionException { + return toString(pObject, null); + } + + /** + * Converts the object to a string, using {@code object.toString()} + * + * @param pObject the object to convert. + * @param pFormat the (optional) conversion format + * + * @return the string representation of the object, on the correct format. + * + * @throws ConversionException if the object cannot be converted to a + * string for any reason. + */ + public abstract String toString(Object pObject, String pFormat) + throws ConversionException; +} 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 71e7ff77..13ca4dcb 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 @@ -1,157 +1,158 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.convert; - -import com.twelvemonkeys.lang.BeanUtil; -import com.twelvemonkeys.lang.StringUtil; - -import java.lang.reflect.InvocationTargetException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; - -/** - * Converts strings to dates and back. - *

- * This class has a static cache of {@code DateFormats}, to avoid - * creation and parsing of date 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/DateConverter.java#2 $ - */ -public class DateConverter extends NumberConverter { - - /** Creates a {@code DateConverter} */ - public DateConverter() { - } - - /** - * Converts the string to a date, using the given format for parsing. - * - * @param pString the string to convert. - * @param pType the type to convert to. {@code java.util.Date} and - * subclasses allowed. - * @param pFormat the format used for parsing. Must be a legal - * {@code SimpleDateFormat} format, or {@code null} which will use the - * default format. - * - * @return the object created from the given string. May safely be typecast - * to {@code java.util.Date} - * - * @see Date - * @see java.text.DateFormat - * - * @throws ConversionException - */ - public Object toObject(String pString, Class pType, String pFormat) throws ConversionException { - if (StringUtil.isEmpty(pString)) - return null; - - try { - DateFormat format; - - if (pFormat == null) { - // Use system default format, using default locale - format = DateFormat.getDateTimeInstance(); - } - else { - // Get format from cache - format = getDateFormat(pFormat); - } - - Date date = StringUtil.toDate(pString, format); - - // Allow for conversion to Date subclasses (ie. java.sql.*) - if (pType != Date.class) { - try { - date = (Date) BeanUtil.createInstance(pType, new Long(date.getTime())); - } - catch (ClassCastException e) { - throw new TypeMismathException(pType); - } - catch (InvocationTargetException e) { - throw new ConversionException(e); - } - } - - return date; - } - catch (RuntimeException rte) { - throw new ConversionException(rte); - } - } - - /** - * Converts the object to a string, using the given format - * - * @param pObject the object to convert. - * @param pFormat the format used for conversion. Must be a legal - * {@code SimpleDateFormat} format, or {@code null} which will use the - * default format. - * - * @return the string representation of the object, on the correct format. - * - * @throws ConversionException if the object is not a subclass of - * {@code java.util.Date} - * - * @see Date - * @see java.text.DateFormat - */ - public String toString(Object pObject, String pFormat) throws ConversionException { - if (pObject == null) - return null; - - if (!(pObject instanceof Date)) { - throw new TypeMismathException(pObject.getClass()); - } - - try { - // Convert to string, default way - if (StringUtil.isEmpty(pFormat)) { - return DateFormat.getDateTimeInstance().format(pObject); - } - - // Convert to string, using format - DateFormat format = getDateFormat(pFormat); - - return format.format(pObject); - } - catch (RuntimeException rte) { - throw new ConversionException(rte); - } - } - - private DateFormat getDateFormat(String pFormat) { - return (DateFormat) getFormat(SimpleDateFormat.class, pFormat, Locale.US); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.convert; + +import com.twelvemonkeys.lang.BeanUtil; +import com.twelvemonkeys.lang.StringUtil; + +import java.lang.reflect.InvocationTargetException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Converts strings to dates and back. + *

+ * This class has a static cache of {@code DateFormats}, to avoid + * creation and parsing of date 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/DateConverter.java#2 $ + */ +public class DateConverter extends NumberConverter { + + /** Creates a {@code DateConverter} */ + public DateConverter() { + } + + /** + * Converts the string to a date, using the given format for parsing. + * + * @param pString the string to convert. + * @param pType the type to convert to. {@code java.util.Date} and + * subclasses allowed. + * @param pFormat the format used for parsing. Must be a legal + * {@code SimpleDateFormat} format, or {@code null} which will use the + * default format. + * + * @return the object created from the given string. May safely be typecast + * to {@code java.util.Date} + * + * @see Date + * @see java.text.DateFormat + * + * @throws ConversionException + */ + public Object toObject(String pString, Class pType, String pFormat) throws ConversionException { + if (StringUtil.isEmpty(pString)) + return null; + + try { + DateFormat format; + + if (pFormat == null) { + // Use system default format, using default locale + format = DateFormat.getDateTimeInstance(); + } + else { + // Get format from cache + format = getDateFormat(pFormat); + } + + Date date = StringUtil.toDate(pString, format); + + // Allow for conversion to Date subclasses (ie. java.sql.*) + if (pType != Date.class) { + try { + date = (Date) BeanUtil.createInstance(pType, new Long(date.getTime())); + } + catch (ClassCastException e) { + throw new TypeMismathException(pType); + } + catch (InvocationTargetException e) { + throw new ConversionException(e); + } + } + + return date; + } + catch (RuntimeException rte) { + throw new ConversionException(rte); + } + } + + /** + * Converts the object to a string, using the given format + * + * @param pObject the object to convert. + * @param pFormat the format used for conversion. Must be a legal + * {@code SimpleDateFormat} format, or {@code null} which will use the + * default format. + * + * @return the string representation of the object, on the correct format. + * + * @throws ConversionException if the object is not a subclass of + * {@code java.util.Date} + * + * @see Date + * @see java.text.DateFormat + */ + public String toString(Object pObject, String pFormat) throws ConversionException { + if (pObject == null) + return null; + + if (!(pObject instanceof Date)) { + throw new TypeMismathException(pObject.getClass()); + } + + try { + // Convert to string, default way + if (StringUtil.isEmpty(pFormat)) { + return DateFormat.getDateTimeInstance().format(pObject); + } + + // Convert to string, using format + DateFormat format = getDateFormat(pFormat); + + return format.format(pObject); + } + catch (RuntimeException rte) { + throw new ConversionException(rte); + } + } + + private DateFormat getDateFormat(String 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 6ae3544b..86f6d838 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 @@ -1,268 +1,269 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.convert; - -import com.twelvemonkeys.lang.BeanUtil; -import com.twelvemonkeys.lang.StringUtil; - -import java.lang.reflect.Array; -import java.lang.reflect.InvocationTargetException; - -/** - * Converts strings to objects and back. - *

- * This converter first tries to create an object, using the class' single - * string argument constructor ({@code <type>(String)}) if found, - * otherwise, an attempt to call - * the class' static {@code valueOf(String)} method. If both fails, a - * {@link ConversionException} is thrown. - * - * @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/DefaultConverter.java#2 $ - * - */ -public final class DefaultConverter implements PropertyConverter { - - /** - * Creates a {@code DefaultConverter}. - */ - public DefaultConverter() { - } - - /** - * Converts the string to an object of the given type. - * - * @param pString the string to convert - * @param pType the type to convert to - * @param pFormat ignored. - * - * @return the object created from the given string. - * - * @throws ConversionException if the type is null, or if the string cannot - * be converted into the given type, using a string constructor or static - * {@code valueOf} method. - */ - public Object toObject(final String pString, final Class pType, final String pFormat) throws ConversionException { - if (pString == null) { - return null; - } - - if (pType == null) { - 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 = unBoxType(pType); - - try { - // Try to create instance from (String) - Object value = BeanUtil.createInstance(type, pString); - - if (value == null) { - // createInstance failed for some reason - // Try to invoke the static method valueOf(String) - value = BeanUtil.invokeStaticMethod(type, "valueOf", pString); - - if (value == null) { - // If the value is still null, well, then I cannot help... - throw new ConversionException(String.format( - "Could not convert String to %1$s: No constructor %1$s(String) or static %1$s.valueOf(String) method found!", - type.getName() - )); - } - } - - return value; - } - catch (InvocationTargetException ite) { - throw new ConversionException(ite.getTargetException()); - } - catch (ConversionException ce) { - throw ce; - } - catch (RuntimeException rte) { - throw new ConversionException(rte); - } - } - - private Object toArray(final String pString, final Class pType, final 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()}. - * - * @param pObject the object to convert. - * @param pFormat ignored. - * - * @return the string representation of the object, or {@code null} if {@code pObject == null} - */ - public String toString(final Object pObject, final String pFormat) - throws ConversionException { - - try { - 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(final 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; - } - - 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; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.convert; + +import com.twelvemonkeys.lang.BeanUtil; +import com.twelvemonkeys.lang.StringUtil; + +import java.lang.reflect.Array; +import java.lang.reflect.InvocationTargetException; + +/** + * Converts strings to objects and back. + *

+ * This converter first tries to create an object, using the class' single + * string argument constructor ({@code <type>(String)}) if found, + * otherwise, an attempt to call + * the class' static {@code valueOf(String)} method. If both fails, a + * {@link ConversionException} is thrown. + *

+ * + * @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/DefaultConverter.java#2 $ + * + */ +public final class DefaultConverter implements PropertyConverter { + + /** + * Creates a {@code DefaultConverter}. + */ + public DefaultConverter() { + } + + /** + * Converts the string to an object of the given type. + * + * @param pString the string to convert + * @param pType the type to convert to + * @param pFormat ignored. + * + * @return the object created from the given string. + * + * @throws ConversionException if the type is null, or if the string cannot + * be converted into the given type, using a string constructor or static + * {@code valueOf} method. + */ + public Object toObject(final String pString, final Class pType, final String pFormat) throws ConversionException { + if (pString == null) { + return null; + } + + if (pType == null) { + 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 = unBoxType(pType); + + try { + // Try to create instance from (String) + Object value = BeanUtil.createInstance(type, pString); + + if (value == null) { + // createInstance failed for some reason + // Try to invoke the static method valueOf(String) + value = BeanUtil.invokeStaticMethod(type, "valueOf", pString); + + if (value == null) { + // If the value is still null, well, then I cannot help... + throw new ConversionException(String.format( + "Could not convert String to %1$s: No constructor %1$s(String) or static %1$s.valueOf(String) method found!", + type.getName() + )); + } + } + + return value; + } + catch (InvocationTargetException ite) { + throw new ConversionException(ite.getTargetException()); + } + catch (ConversionException ce) { + throw ce; + } + catch (RuntimeException rte) { + throw new ConversionException(rte); + } + } + + private Object toArray(final String pString, final Class pType, final 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()}. + * + * @param pObject the object to convert. + * @param pFormat ignored. + * + * @return the string representation of the object, or {@code null} if {@code pObject == null} + */ + public String toString(final Object pObject, final String pFormat) + throws ConversionException { + + try { + 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(final 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; + } + + 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; + } +} 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 2d9ae304..6c2d75fb 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 @@ -1,210 +1,211 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.convert; - -import com.twelvemonkeys.lang.BeanUtil; -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.util.LRUHashMap; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.text.*; -import java.util.Arrays; -import java.util.Locale; -import java.util.Map; - -/** - * Converts strings to numbers and back. - *

- * This class has a static cache of {@code NumberFormats}, to avoid - * 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-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); - private static final Map sFormats = new LRUHashMap(50); - - public NumberConverter() { - } - - /** - * Converts the string to a number, using the given format for parsing. - * - * @param pString the string to convert. - * @param pType the type to convert to. PropertyConverter - * implementations may choose to ignore this parameter. - * @param pFormat the format used for parsing. PropertyConverter - * implementations may choose to ignore this parameter. Also, - * implementations that require a parser format, should provide a default - * format, and allow {@code null} as the format argument. - * - * @return the object created from the given string. May safely be typecast - * to {@code java.lang.Number} or the class of the {@code type} parameter. - * - * @see Number - * @see java.text.NumberFormat - * - * @throws ConversionException - */ - public Object toObject(final String pString, final Class pType, final String pFormat) throws ConversionException { - if (StringUtil.isEmpty(pString)) { - return null; - } - - try { - if (pType.equals(BigInteger.class)) { - return new BigInteger(pString); // No format? - } - if (pType.equals(BigDecimal.class)) { - return new BigDecimal(pString); // No format? - } - - NumberFormat format; - - if (pFormat == null) { - // Use system default format, using default locale - format = sDefaultFormat; - } - else { - // Get format from cache - format = getNumberFormat(pFormat); - } - - Number num; - synchronized (format) { - num = format.parse(pString); - } - - if (pType == Integer.TYPE || pType == Integer.class) { - return num.intValue(); - } - else if (pType == Long.TYPE || pType == Long.class) { - return num.longValue(); - } - else if (pType == Double.TYPE || pType == Double.class) { - return num.doubleValue(); - } - else if (pType == Float.TYPE || pType == Float.class) { - return num.floatValue(); - } - else if (pType == Byte.TYPE || pType == Byte.class) { - return num.byteValue(); - } - else if (pType == Short.TYPE || pType == Short.class) { - return num.shortValue(); - } - - return num; - } - catch (ParseException pe) { - throw new ConversionException(pe); - } - catch (RuntimeException rte) { - throw new ConversionException(rte); - } - } - - /** - * Converts the object to a string, using the given format - * - * @param pObject the object to convert. - * @param pFormat the format used for parsing. PropertyConverter - * implementations may choose to ignore this parameter. Also, - * implementations that require a parser format, should provide a default - * format, and allow {@code null} as the format argument. - * - * @return the string representation of the object, on the correct format. - * - * @throws ConversionException if the object is not a subclass of {@link java.lang.Number} - */ - public String toString(final Object pObject, final String pFormat) - throws ConversionException { - - if (pObject == null) { - return null; - } - - if (!(pObject instanceof Number)) { - throw new TypeMismathException(pObject.getClass()); - } - - try { - // Convert to string, default way - if (StringUtil.isEmpty(pFormat)) { - return sDefaultFormat.format(pObject); - } - - // Convert to string, using format - NumberFormat format = getNumberFormat(pFormat); - - synchronized (format) { - return format.format(pObject); - } - } - catch (RuntimeException rte) { - throw new ConversionException(rte); - } - } - - private NumberFormat getNumberFormat(String pFormat) { - return (NumberFormat) getFormat(DecimalFormat.class, pFormat, SYMBOLS); - } - - protected final Format getFormat(Class pFormatterClass, Object... pFormat) { - // Try to get format from cache - synchronized (sFormats) { - String key = pFormatterClass.getName() + ":" + Arrays.toString(pFormat); - Format format = sFormats.get(key); - - if (format == null) { - // If not found, create... - try { - format = (Format) BeanUtil.createInstance(pFormatterClass, pFormat); - } - catch (Exception e) { - e.printStackTrace(); - return null; - } - - // ...and store in cache - sFormats.put(key, format); - } - - return format; - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.convert; + +import com.twelvemonkeys.lang.BeanUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.util.LRUHashMap; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.*; +import java.util.Arrays; +import java.util.Locale; +import java.util.Map; + +/** + * Converts strings to numbers and back. + *

+ * This class has a static cache of {@code NumberFormats}, to avoid + * 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-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); + private static final Map sFormats = new LRUHashMap(50); + + public NumberConverter() { + } + + /** + * Converts the string to a number, using the given format for parsing. + * + * @param pString the string to convert. + * @param pType the type to convert to. PropertyConverter + * implementations may choose to ignore this parameter. + * @param pFormat the format used for parsing. PropertyConverter + * implementations may choose to ignore this parameter. Also, + * implementations that require a parser format, should provide a default + * format, and allow {@code null} as the format argument. + * + * @return the object created from the given string. May safely be typecast + * to {@code java.lang.Number} or the class of the {@code type} parameter. + * + * @see Number + * @see java.text.NumberFormat + * + * @throws ConversionException + */ + public Object toObject(final String pString, final Class pType, final String pFormat) throws ConversionException { + if (StringUtil.isEmpty(pString)) { + return null; + } + + try { + if (pType.equals(BigInteger.class)) { + return new BigInteger(pString); // No format? + } + if (pType.equals(BigDecimal.class)) { + return new BigDecimal(pString); // No format? + } + + NumberFormat format; + + if (pFormat == null) { + // Use system default format, using default locale + format = sDefaultFormat; + } + else { + // Get format from cache + format = getNumberFormat(pFormat); + } + + Number num; + synchronized (format) { + num = format.parse(pString); + } + + if (pType == Integer.TYPE || pType == Integer.class) { + return num.intValue(); + } + else if (pType == Long.TYPE || pType == Long.class) { + return num.longValue(); + } + else if (pType == Double.TYPE || pType == Double.class) { + return num.doubleValue(); + } + else if (pType == Float.TYPE || pType == Float.class) { + return num.floatValue(); + } + else if (pType == Byte.TYPE || pType == Byte.class) { + return num.byteValue(); + } + else if (pType == Short.TYPE || pType == Short.class) { + return num.shortValue(); + } + + return num; + } + catch (ParseException pe) { + throw new ConversionException(pe); + } + catch (RuntimeException rte) { + throw new ConversionException(rte); + } + } + + /** + * Converts the object to a string, using the given format + * + * @param pObject the object to convert. + * @param pFormat the format used for parsing. PropertyConverter + * implementations may choose to ignore this parameter. Also, + * implementations that require a parser format, should provide a default + * format, and allow {@code null} as the format argument. + * + * @return the string representation of the object, on the correct format. + * + * @throws ConversionException if the object is not a subclass of {@link java.lang.Number} + */ + public String toString(final Object pObject, final String pFormat) + throws ConversionException { + + if (pObject == null) { + return null; + } + + if (!(pObject instanceof Number)) { + throw new TypeMismathException(pObject.getClass()); + } + + try { + // Convert to string, default way + if (StringUtil.isEmpty(pFormat)) { + return sDefaultFormat.format(pObject); + } + + // Convert to string, using format + NumberFormat format = getNumberFormat(pFormat); + + synchronized (format) { + return format.format(pObject); + } + } + catch (RuntimeException rte) { + throw new ConversionException(rte); + } + } + + private NumberFormat getNumberFormat(String pFormat) { + return (NumberFormat) getFormat(DecimalFormat.class, pFormat, SYMBOLS); + } + + protected final Format getFormat(Class pFormatterClass, Object... pFormat) { + // Try to get format from cache + synchronized (sFormats) { + String key = pFormatterClass.getName() + ":" + Arrays.toString(pFormat); + Format format = sFormats.get(key); + + if (format == null) { + // If not found, create... + try { + format = (Format) BeanUtil.createInstance(pFormatterClass, pFormat); + } + catch (Exception e) { + e.printStackTrace(); + return null; + } + + // ...and store in cache + sFormats.put(key, format); + } + + return format; + } + } +} diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/TimeConverter.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/TimeConverter.java index d1f7234e..0dd69ec6 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/TimeConverter.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/convert/TimeConverter.java @@ -1,138 +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 of the copyright holder 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 HOLDER 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.convert; - -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.util.Time; -import com.twelvemonkeys.util.TimeFormat; - -/** - * Converts strings to times and back. - *

- * This class has a static cache of {@code TimeFormats}, to avoid creation and - * parsing of timeformats 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/TimeConverter.java#1 $ - */ -public class TimeConverter extends NumberConverter { - - public TimeConverter() { - } - - /** - * Converts the string to a time, using the given format for parsing. - * - * @param pString the string to convert. - * @param pType the type to convert to. PropertyConverter - * implementations may choose to ignore this parameter. - * @param pFormat the format used for parsing. PropertyConverter - * implementations may choose to ignore this parameter. Also, - * implementations that require a parser format, should provide a default - * format, and allow {@code null} as the format argument. - * - * @return the object created from the given string. May safely be typecast - * to {@code com.twelvemonkeys.util.Time} - * - * @see com.twelvemonkeys.util.Time - * @see com.twelvemonkeys.util.TimeFormat - * - * @throws ConversionException - */ - public Object toObject(String pString, Class pType, String pFormat) - throws ConversionException { - if (StringUtil.isEmpty(pString)) - return null; - - TimeFormat format; - - try { - if (pFormat == null) { - // Use system default format - format = TimeFormat.getInstance(); - } - else { - // Get format from cache - format = getTimeFormat(pFormat); - } - - return format.parse(pString); - } - catch (RuntimeException rte) { - throw new ConversionException(rte); - } - - } - - /** - * Converts the object to a string, using the given format - * - * @param pObject the object to convert. - * @param pFormat the format used for parsing. PropertyConverter - * implementations may choose to ignore this parameter. Also, - * implementations that require a parser format, should provide a default - * format, and allow {@code null} as the format argument. - * - * @return the string representation of the object, on the correct format. - * - * @throws ConversionException if the object is not a subclass of - * {@code com.twelvemonkeys.util.Time} - * - * @see com.twelvemonkeys.util.Time - * @see com.twelvemonkeys.util.TimeFormat - */ - public String toString(Object pObject, String pFormat) - throws ConversionException { - if (pObject == null) - return null; - - if (!(pObject instanceof com.twelvemonkeys.util.Time)) - throw new TypeMismathException(pObject.getClass()); - - try { - // Convert to string, default way - if (StringUtil.isEmpty(pFormat)) - return pObject.toString(); - - // Convert to string, using format - TimeFormat format = getTimeFormat(pFormat); - return format.format((Time) pObject); - } - catch (RuntimeException rte) { - throw new ConversionException(rte); - } - } - - private TimeFormat getTimeFormat(String pFormat) { - return (TimeFormat) getFormat(TimeFormat.class, pFormat); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.convert; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.util.Time; +import com.twelvemonkeys.util.TimeFormat; + +/** + * Converts strings to times and back. + *

+ * This class has a static cache of {@code TimeFormats}, to avoid creation and + * parsing of timeformats 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/TimeConverter.java#1 $ + */ +public class TimeConverter extends NumberConverter { + + public TimeConverter() { + } + + /** + * Converts the string to a time, using the given format for parsing. + * + * @param pString the string to convert. + * @param pType the type to convert to. PropertyConverter + * implementations may choose to ignore this parameter. + * @param pFormat the format used for parsing. PropertyConverter + * implementations may choose to ignore this parameter. Also, + * implementations that require a parser format, should provide a default + * format, and allow {@code null} as the format argument. + * + * @return the object created from the given string. May safely be typecast + * to {@code com.twelvemonkeys.util.Time} + * + * @see com.twelvemonkeys.util.Time + * @see com.twelvemonkeys.util.TimeFormat + * + * @throws ConversionException + */ + public Object toObject(String pString, Class pType, String pFormat) + throws ConversionException { + if (StringUtil.isEmpty(pString)) + return null; + + TimeFormat format; + + try { + if (pFormat == null) { + // Use system default format + format = TimeFormat.getInstance(); + } + else { + // Get format from cache + format = getTimeFormat(pFormat); + } + + return format.parse(pString); + } + catch (RuntimeException rte) { + throw new ConversionException(rte); + } + + } + + /** + * Converts the object to a string, using the given format + * + * @param pObject the object to convert. + * @param pFormat the format used for parsing. PropertyConverter + * implementations may choose to ignore this parameter. Also, + * implementations that require a parser format, should provide a default + * format, and allow {@code null} as the format argument. + * + * @return the string representation of the object, on the correct format. + * + * @throws ConversionException if the object is not a subclass of + * {@code com.twelvemonkeys.util.Time} + * + * @see com.twelvemonkeys.util.Time + * @see com.twelvemonkeys.util.TimeFormat + */ + public String toString(Object pObject, String pFormat) + throws ConversionException { + if (pObject == null) + return null; + + if (!(pObject instanceof com.twelvemonkeys.util.Time)) + throw new TypeMismathException(pObject.getClass()); + + try { + // Convert to string, default way + if (StringUtil.isEmpty(pFormat)) + return pObject.toString(); + + // Convert to string, using format + TimeFormat format = getTimeFormat(pFormat); + return format.format((Time) pObject); + } + catch (RuntimeException rte) { + throw new ConversionException(rte); + } + } + + private TimeFormat getTimeFormat(String pFormat) { + return (TimeFormat) getFormat(TimeFormat.class, pFormat); + } +} 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 17534e67..401b48c8 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 @@ -1,107 +1,107 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.regex; - -import com.twelvemonkeys.util.AbstractTokenIterator; - -import java.util.NoSuchElementException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -/** - * {@code StringTokenizer} replacement, that uses regular expressions to split - * strings into tokens. - *

- * @see java.util.regex.Pattern for pattern syntax. - * - * @author Harald Kuhr - * @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 matcher; - private boolean next = false; - - /** - * Creates a {@code RegExTokenIterator}. - * Default pettern is {@code "\S+"}. - * - * @param pString the string to be parsed. - * - * @throws IllegalArgumentException if {@code pString} is {@code null} - */ - public RegExTokenIterator(String pString) { - this(pString, "\\S+"); - } - - /** - * Creates a {@code RegExTokenIterator}. - * - * @see Pattern for pattern syntax. - * - * @param pString the string to be parsed. - * @param pPattern the pattern - * - * @throws PatternSyntaxException if {@code pPattern} is not a valid pattern - * @throws IllegalArgumentException if any of the arguments are {@code null} - */ - public RegExTokenIterator(String pString, String pPattern) { - if (pString == null) { - throw new IllegalArgumentException("string == null"); - } - - if (pPattern == null) { - throw new IllegalArgumentException("pattern == null"); - } - - matcher = Pattern.compile(pPattern).matcher(pString); - } - - /** - * Resets this iterator. - * - */ - public void reset() { - matcher.reset(); - } - - public boolean hasNext() { - return next || (next = matcher.find()); - } - - public String next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - next = false; - return matcher.group(); - } +/* + * 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 of the copyright holder 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 HOLDER 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.regex; + +import com.twelvemonkeys.util.AbstractTokenIterator; + +import java.util.NoSuchElementException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * {@code StringTokenizer} replacement, that uses regular expressions to split + * strings into tokens. + * + *@see java.util.regex.Pattern for pattern syntax. + * + * @author Harald Kuhr + * @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 matcher; + private boolean next = false; + + /** + * Creates a {@code RegExTokenIterator}. + * Default pettern is {@code "\S+"}. + * + * @param pString the string to be parsed. + * + * @throws IllegalArgumentException if {@code pString} is {@code null} + */ + public RegExTokenIterator(String pString) { + this(pString, "\\S+"); + } + + /** + * Creates a {@code RegExTokenIterator}. + * + * @see Pattern for pattern syntax. + * + * @param pString the string to be parsed. + * @param pPattern the pattern + * + * @throws PatternSyntaxException if {@code pPattern} is not a valid pattern + * @throws IllegalArgumentException if any of the arguments are {@code null} + */ + public RegExTokenIterator(String pString, String pPattern) { + if (pString == null) { + throw new IllegalArgumentException("string == null"); + } + + if (pPattern == null) { + throw new IllegalArgumentException("pattern == null"); + } + + matcher = Pattern.compile(pPattern).matcher(pString); + } + + /** + * Resets this iterator. + * + */ + public void reset() { + matcher.reset(); + } + + public boolean hasNext() { + return next || (next = matcher.find()); + } + + public String next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + 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 aa7f2e0c..13128f32 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 @@ -1,785 +1,776 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.regex; - -import java.io.PrintStream; - -/** - * This class parses arbitrary strings against a wildcard string mask provided. - * The wildcard characters are '*' and '?'. - *

- * The string masks provided are treated as case sensitive.
- * Null-valued string masks as well as null valued strings to be parsed, will lead to rejection. - *

- *

- *

- * This class is custom designed for wildcard string parsing and is several times faster than the implementation based on the Jakarta Regexp package. - *

- *


- *

- * This task is performed based on regular expression techniques. - * The possibilities of string generation with the well-known wildcard characters stated above, - * represent a subset of the possibilities of string generation with regular expressions.
- * The '*' corresponds to ([Union of all characters in the alphabet])*
- * The '?' corresponds to ([Union of all characters in the alphabet])
- *       These expressions are not suited for textual representation at all, I must say. Is there any math tags included in HTML? - *

- *

- *

- * The complete meta-language for regular expressions are much larger. - * This fact makes it fairly straightforward to build data structures for parsing because the amount of rules of building these structures are quite limited, as stated below. - *

- *

- *

- * To bring this over to mathematical terms: - * The parser ia a nondeterministic finite automaton (latin) representing the grammar which is stated by the string mask. - * The language accepted by this automaton is the set of all strings accepted by this automaton.
- * The formal automaton quintuple consists of: - *

    - *
  1. A finite set of states, depending on the wildcard string mask. - * 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. - *
  2. 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 {?}. - *
  3. A finite set of initial states, here only consisting of the state corresponding to the first character in the mask. - *
  4. A finite set of final states, here only consisting of the state corresponding to the last character in the mask. - *
  5. A transition relation that is a finite set of transitions satisfying some formal rules.
    - * This implementation on the other hand, only uses ad-hoc rules which start with an initial setup of the states as a sequence according to the string mask.
    - * Additionally, the following rules completes the building of the automaton: - *
      - *
    1. If the next state represents the same character as the next character in the string to test - go to this next state. - *
    2. If the next state represents '*' - go to this next state. - *
    3. If the next state represents '?' - go to this next state. - *
    4. If a '*' is followed by one or more '?', the last of these '?' state counts as a '*' state. Some extra checks regarding the number of characters read must be imposed if this is the case... - *
    5. 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. - *
    6. If there are no subsequent state (final state) and the state represents '*' - acceptance. - *
    7. If there are no subsequent state (final state) and the end of the string to test is reached - acceptance. - *
    - *
    - * - * Disclaimer: This class does not build a finite automaton according to formal mathematical rules. - * The proper way of implementation should be finding the complete set of transition relations, decomposing these into rules accepted by a deterministic finite automaton and finally build this automaton to be used for string parsing. - * Instead, this class is ad-hoc implemented based on the informal transition rules stated above. - * Therefore the correctness cannot be guaranteed before extensive testing has been imposed on this class... anyway, I think I have succeeded. - * Parsing faults must be reported to the author. - * - *
- *

- *


- *

- * Examples of usage:
- * This example will return "Accepted!". - *

- * WildcardStringParser parser = new WildcardStringParser("*_28????.jp*");
- * if (parser.parseString("gupu_280915.jpg")) {
- *     System.out.println("Accepted!");
- * } else {
- *     System.out.println("Not accepted!");
- * }
- * 
- *

- *


- *

- * Theories and concepts are based on the book Elements of the Theory of Computation, by Harry l. Lewis and Christos H. Papadimitriou, (c) 1981 by Prentice Hall. - *

- *

- * - * @author Eirik Torske - * @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', '\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 */ - public static final char FREE_RANGE_CHARACTER = '*'; - - /** Field FREE_PASS_CHARACTER */ - public static final char FREE_PASS_CHARACTER = '?'; - - // Members - boolean initialized; - String stringMask; - WildcardStringParserState initialState; - int totalNumberOfStringsParsed; - boolean debugging; - PrintStream out; - - // Properties - // Constructors - - /** - * Creates a wildcard string parser. - *

- * - * @param pStringMask the wildcard string mask. - */ - public WildcardStringParser(final String pStringMask) { - this(pStringMask, false); - } - - /** - * Creates a wildcard string parser. - *

- * - * @param pStringMask the wildcard string mask. - * @param pDebugging {@code true} will cause debug messages to be emitted to {@code System.out}. - */ - public WildcardStringParser(final String pStringMask, final boolean pDebugging) { - this(pStringMask, pDebugging, System.out); - } - - /** - * Creates a wildcard string parser. - *

- * - * @param pStringMask the wildcard string mask. - * @param pDebugging {@code true} will cause debug messages to be emitted. - * @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.stringMask = pStringMask; - this.debugging = pDebugging; - this.out = pDebuggingPrintStream; - initialized = buildAutomaton(); - } - - // Methods - private boolean checkIfStateInWildcardRange(WildcardStringParserState pState) { - - WildcardStringParserState runnerState = pState; - - while (runnerState.previousState != null) { - runnerState = runnerState.previousState; - if (isFreeRangeCharacter(runnerState.character)) { - return true; - } - if (!isFreePassCharacter(runnerState.character)) { - return false; - } // If free-pass char '?' - move on - } - return false; - } - - private boolean checkIfLastFreeRangeState(WildcardStringParserState pState) { - - if (isFreeRangeCharacter(pState.character)) { - return true; - } - if (isFreePassCharacter(pState.character)) { - if (checkIfStateInWildcardRange(pState)) { - return true; - } - } - return false; - } - - /** @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 < stringMask.length(); i++) { - if (!isFreeRangeCharacter(stringMask.charAt(i))) { - return false; - } - } - return true; - } - - private boolean buildAutomaton() { - - char activeChar; - WildcardStringParserState runnerState = null; - WildcardStringParserState newState = null; - WildcardStringParserState lastFreeRangeState = null; - - // Create the initial state of the automaton - 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; - initialState = runnerState; - initialState.automatonStateNumber = 0; - } - else { - System.err.println("string mask provided are null or empty - aborting!"); - return false; - } - - // Create the rest of the automaton - 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)))) { - System.err.println("one or more characters in string mask are not legal characters - aborting!"); - return false; - } - - // Set last free-range state before creating/checking the next state - runnerState.lastFreeRangeState = lastFreeRangeState; - - // Create next state, check if free-range state, set the state number and preceeding state - newState = new WildcardStringParserState(activeChar); - newState.automatonStateNumber = i; - newState.previousState = runnerState; - - // Special check if the state represents an '*' or '?' with only preceeding states representing '?' and '*' - if (checkIfLastFreeRangeState(newState)) { - lastFreeRangeState = newState; - } - - // Set the succeding state before moving to the next state - runnerState.nextState = newState; - - // Move to the next state - runnerState = newState; - - // Special setting of the last free-range state for the last element - if (runnerState.automatonStateNumber == stringMask.length() - 1) { - runnerState.lastFreeRangeState = lastFreeRangeState; - } - } - - // Initiate some statistics - totalNumberOfStringsParsed = 0; - return true; - } - - /** Tests if a certain character is a valid character in the alphabet that is applying for this automaton. */ - public static boolean isInAlphabet(final char pCharToCheck) { - - for (int i = 0; i < ALPHABET.length; i++) { - if (pCharToCheck == ALPHABET[i]) { - return true; - } - } - return false; - } - - /** Tests if a certain character is the designated "free-range" character ('*'). */ - public static boolean isFreeRangeCharacter(final char pCharToCheck) { - return pCharToCheck == FREE_RANGE_CHARACTER; - } - - /** Tests if a certain character is the designated "free-pass" character ('?'). */ - public static boolean isFreePassCharacter(final char pCharToCheck) { - return pCharToCheck == FREE_PASS_CHARACTER; - } - - /** Tests if a certain character is a wildcard character ('*' or '?'). */ - public static boolean isWildcardCharacter(final char pCharToCheck) { - return ((isFreeRangeCharacter(pCharToCheck)) || (isFreePassCharacter(pCharToCheck))); - } - - /** - * Gets the string mask that was used when building the parser atomaton. - *

- * - * @return the string mask used for building the parser automaton. - */ - public String getStringMask() { - return stringMask; - } - - /** - * Parses a string according to the rules stated above. - *

- * - * @param pStringToParse the string to parse. - * @return {@code true} if and only if the string are accepted by the automaton. - */ - public boolean parseString(final String pStringToParse) { - - if (debugging) { - out.println("parsing \"" + pStringToParse + "\"..."); - } - - // Update statistics - totalNumberOfStringsParsed++; - - // Check string to be parsed for nullness - if (pStringToParse == null) { - if (debugging) { - out.println("string to be parsed is null - rejection!"); - } - return false; - } - - // Create parsable string - ParsableString parsableString = new ParsableString(pStringToParse); - - // Check string to be parsed - if (!parsableString.checkString()) { - 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 (!initialized) { - System.err.println("automaton is not initialized - rejection!"); - return false; - } - - // Check if automaton is trivial (accepts all strings) - if (isTrivialAutomaton()) { - if (debugging) { - out.println("automaton represents a trivial string mask (accepts all strings) - acceptance!"); - } - return true; - } - - // Check if string to be parsed is empty - if (parsableString.isEmpty()) { - if (debugging) { - out.println("string to be parsed is empty and not trivial automaton - rejection!"); - } - return false; - } - - // Flag and more to indicate that state skipping due to sequence of '?' succeeding a '*' has been performed - boolean hasPerformedFreeRangeMovement = false; - int numberOfFreePassCharactersRead_SinceLastFreePassState = 0; - int numberOfParsedCharactersRead_SinceLastFreePassState = 0; - WildcardStringParserState runnerState = null; - - // Accepted by the first state? - if ((parsableString.charArray[0] == initialState.character) || isWildcardCharacter(initialState.character)) { - runnerState = initialState; - parsableString.index = 0; - } - else { - if (debugging) { - out.println("cannot enter first automaton state - rejection!"); - } - return false; - } - - // Initialize the free-pass character state visited count - if (isFreePassCharacter(runnerState.character)) { - numberOfFreePassCharactersRead_SinceLastFreePassState++; - } - - // Perform parsing according to the rules above - for (int i = 0; i < parsableString.length(); i++) { - if (debugging) { - out.println(); - } - if (debugging) { - out.println("parsing - index number " + i + ", active char: '" - + parsableString.getActiveChar() + "' char string index: " + parsableString.index - + " number of chars since last free-range state: " + numberOfParsedCharactersRead_SinceLastFreePassState); - } - if (debugging) { - out.println("parsing - state: " + runnerState.automatonStateNumber + " '" - + runnerState.character + "' - no of free-pass chars read: " + numberOfFreePassCharactersRead_SinceLastFreePassState); - } - if (debugging) { - out.println("parsing - hasPerformedFreeRangeMovement: " + hasPerformedFreeRangeMovement); - } - 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.character)) { - - // Special free-range skipping check - if (hasPerformedFreeRangeMovement) { - if (parsableString.reachedEndOfString()) { - if (numberOfFreePassCharactersRead_SinceLastFreePassState > numberOfParsedCharactersRead_SinceLastFreePassState) { - 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 (debugging) { - out.println( - "no subsequent state (final state) and the state represents '*' - end of parsing string and enough characters read - acceptance!"); - } - return true; - } - } - else { - if (numberOfFreePassCharactersRead_SinceLastFreePassState > numberOfParsedCharactersRead_SinceLastFreePassState) { - 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.index++; - numberOfParsedCharactersRead_SinceLastFreePassState++; - } - else { - if (debugging) { - out.println( - "no subsequent state (final state) and the state represents '*' - not the end of parsing string, but enough characters read - acceptance!"); - } - return true; - } - } - } - else { - if (debugging) { - out.println("no subsequent state (final state) and the state represents '*' - no skipping performed - acceptance!"); - } - return true; - } - } - - // If there are no subsequent state (final state) and no skipping has been performed and the end of the string to test is reached - acceptance! - else if (parsableString.reachedEndOfString()) { - - // Special free-range skipping check - if ((hasPerformedFreeRangeMovement) - && (numberOfFreePassCharactersRead_SinceLastFreePassState > numberOfParsedCharactersRead_SinceLastFreePassState)) { - 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 (debugging) { - out.println("no subsequent state (final state) and the end of the string to test is reached - acceptance!"); - } - return true; - } - else { - if (debugging) { - out.println("parsing - escaping process..."); - } - } - } - else { - 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.character)) { - numberOfFreePassCharactersRead_SinceLastFreePassState = 0; - numberOfParsedCharactersRead_SinceLastFreePassState = 0; - WildcardStringParserState freeRangeRunnerState = runnerState.nextState; - - while ((freeRangeRunnerState != null) && (isFreePassCharacter(freeRangeRunnerState.character))) { - runnerState = freeRangeRunnerState; - hasPerformedFreeRangeMovement = true; - numberOfFreePassCharactersRead_SinceLastFreePassState++; - freeRangeRunnerState = freeRangeRunnerState.nextState; - } - - // Special Case: if the mask is at the end - if (runnerState.nextState == null) { - if (debugging) { - out.println(); - } - if (debugging) { - out.println("parsing - index number " + i + ", active char: '" - + parsableString.getActiveChar() + "' char string index: " + parsableString.index - + " number of chars since last free-range state: " + numberOfParsedCharactersRead_SinceLastFreePassState); - } - if (debugging) { - out.println("parsing - state: " + runnerState.automatonStateNumber + " '" - + runnerState.character + "' - no of free-pass chars read: " + numberOfFreePassCharactersRead_SinceLastFreePassState); - } - if (debugging) { - out.println("parsing - hasPerformedFreeRangeMovement: " - + hasPerformedFreeRangeMovement); - } - if ((hasPerformedFreeRangeMovement) - && (numberOfFreePassCharactersRead_SinceLastFreePassState >= numberOfParsedCharactersRead_SinceLastFreePassState)) { - return true; - } - else { - return false; - } - } - } - - // If the next state represents '*' - go to this next state - 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.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.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.lastFreeRangeState != null) { - runnerState = runnerState.lastFreeRangeState; - parsableString.index++; - numberOfParsedCharactersRead_SinceLastFreePassState++; - } - else { - 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 (debugging) { - out.println("finished reading parsing string and not at any final state - rejection!"); - } - return false; - } - - /* - * Overriding mandatory methods from EntityObject's. - */ - - /** - * Method toString - * - * @return - */ - public String toString() { - - StringBuilder buffer = new StringBuilder(); - - if (!initialized) { - buffer.append(getClass().getName()); - buffer.append(": Not initialized properly!"); - buffer.append("\n"); - buffer.append("\n"); - } - else { - WildcardStringParserState runnerState = initialState; - - buffer.append(getClass().getName()); - buffer.append(": String mask "); - buffer.append(stringMask); - buffer.append("\n"); - buffer.append("\n"); - buffer.append(" Automaton: "); - while (runnerState != null) { - buffer.append(runnerState.automatonStateNumber); - buffer.append(": "); - buffer.append(runnerState.character); - buffer.append(" ("); - if (runnerState.lastFreeRangeState != null) { - buffer.append(runnerState.lastFreeRangeState.automatonStateNumber); - } - else { - buffer.append("-"); - } - buffer.append(")"); - if (runnerState.nextState != null) { - buffer.append(" --> "); - } - runnerState = runnerState.nextState; - } - buffer.append("\n"); - buffer.append(" Format: : ()"); - buffer.append("\n"); - buffer.append(" Number of strings parsed: " + totalNumberOfStringsParsed); - buffer.append("\n"); - } - return buffer.toString(); - } - - /** - * Method equals - * - * @param pObject - * @return - */ - public boolean equals(Object pObject) { - - if (pObject instanceof WildcardStringParser) { - WildcardStringParser externalParser = (WildcardStringParser) pObject; - - return ((externalParser.initialized == this.initialized) && (externalParser.stringMask == this.stringMask)); - } - return super.equals(pObject); - } - - // Just taking the lazy, easy and dangerous way out - - /** - * Method hashCode - * - * @return - */ - public int hashCode() { - return super.hashCode(); - } - - protected Object clone() throws CloneNotSupportedException { - - if (initialized) { - return new WildcardStringParser(stringMask); - } - return null; - } - - // Just taking the lazy, easy and dangerous way out - protected void finalize() throws Throwable { - } - - /** A simple holder class for an automaton state. */ - class WildcardStringParserState { - - // Constants - // Members - int automatonStateNumber; - char character; - WildcardStringParserState previousState; - WildcardStringParserState nextState; - WildcardStringParserState lastFreeRangeState; - - // Constructors - - /** - * Constructor WildcardStringParserState - * - * @param pChar - */ - public WildcardStringParserState(final char pChar) { - this.character = pChar; - } - - // Methods - // Debug - } - - /** A simple holder class for a string to be parsed. */ - class ParsableString { - - // Constants - // Members - char[] charArray; - int index; - - // Constructors - ParsableString(final String pStringToParse) { - - if (pStringToParse != null) { - charArray = pStringToParse.toCharArray(); - } - index = -1; - } - - // Methods - boolean reachedEndOfString() { - - //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 charArray.length; - } - - char getActiveChar() { - - if ((index > -1) && (index < charArray.length)) { - return charArray[index]; - } - System.err.println(getClass().getName() + ": trying to access character outside character array!"); - return ' '; - } - - char getSubsequentChar() { - - if ((index > -1) && (index + 1 < charArray.length)) { - return charArray[index + 1]; - } - System.err.println(getClass().getName() + ": trying to access character outside character array!"); - return ' '; - } - - boolean checkString() { - - if (!isEmpty()) { - - // Check if the string only contains chars that are elements in the alphabet - for (int i = 0; i < charArray.length; i++) { - if (!WildcardStringParser.isInAlphabet(charArray[i])) { - return false; - } - } - } - return true; - } - - boolean isEmpty() { - return ((charArray == null) || (charArray.length == 0)); - } - - /** - * Method toString - * - * @return - */ - public String toString() { - return new String(charArray); - } - } -} - - -/*--- Formatted in Sun Java Convention Style on ma, des 1, '03 ---*/ - - -/*------ Formatted by Jindent 3.23 Basic 1.0 --- http://www.jindent.de ------*/ +/* + * 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 of the copyright holder 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 HOLDER 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.regex; + +import java.io.PrintStream; + +/** + * This class parses arbitrary strings against a wildcard string mask provided. + * The wildcard characters are '*' and '?'. + *

+ * The string masks provided are treated as case sensitive.
+ * Null-valued string masks as well as null valued strings to be parsed, will lead to rejection. + *

+ *

+ * This class is custom designed for wildcard string parsing and is several times faster than the implementation based on the Jakarta Regexp package. + *

+ *
+ *

+ * This task is performed based on regular expression techniques. + * The possibilities of string generation with the well-known wildcard characters stated above, + * represent a subset of the possibilities of string generation with regular expressions.
+ * The '*' corresponds to ([Union of all characters in the alphabet])*
+ * The '?' corresponds to ([Union of all characters in the alphabet])
+ *       These expressions are not suited for textual representation at all, I must say. Is there any math tags included in HTML? + *

+ *

+ * The complete meta-language for regular expressions are much larger. + * This fact makes it fairly straightforward to build data structures for parsing because the amount of rules of building these structures are quite limited, as stated below. + *

+ *

+ * To bring this over to mathematical terms: + * The parser ia a nondeterministic finite automaton (latin) representing the grammar which is stated by the string mask. + * The language accepted by this automaton is the set of all strings accepted by this automaton.
+ * The formal automaton quintuple consists of: + *

+ *
    + *
  1. A finite set of states, depending on the wildcard string mask. + * 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. + *
  2. 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 {?}. + *
  3. A finite set of initial states, here only consisting of the state corresponding to the first character in the mask. + *
  4. A finite set of final states, here only consisting of the state corresponding to the last character in the mask. + *
  5. A transition relation that is a finite set of transitions satisfying some formal rules.
    + * This implementation on the other hand, only uses ad-hoc rules which start with an initial setup of the states as a sequence according to the string mask.
    + * Additionally, the following rules completes the building of the automaton: + *
      + *
    1. If the next state represents the same character as the next character in the string to test - go to this next state. + *
    2. If the next state represents '*' - go to this next state. + *
    3. If the next state represents '?' - go to this next state. + *
    4. If a '*' is followed by one or more '?', the last of these '?' state counts as a '*' state. Some extra checks regarding the number of characters read must be imposed if this is the case... + *
    5. 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. + *
    6. If there are no subsequent state (final state) and the state represents '*' - acceptance. + *
    7. If there are no subsequent state (final state) and the end of the string to test is reached - acceptance. + *
    + *
    + * + * Disclaimer: This class does not build a finite automaton according to formal mathematical rules. + * The proper way of implementation should be finding the complete set of transition relations, decomposing these into rules accepted by a deterministic finite automaton and finally build this automaton to be used for string parsing. + * Instead, this class is ad-hoc implemented based on the informal transition rules stated above. + * Therefore the correctness cannot be guaranteed before extensive testing has been imposed on this class... anyway, I think I have succeeded. + * Parsing faults must be reported to the author. + * + *
+ *
+ *

+ * Examples of usage:
+ * This example will return "Accepted!". + *

+ *
+ * WildcardStringParser parser = new WildcardStringParser("*_28????.jp*");
+ * if (parser.parseString("gupu_280915.jpg")) {
+ *     System.out.println("Accepted!");
+ * } else {
+ *     System.out.println("Not accepted!");
+ * }
+ * 
+ *
+ *

+ * Theories and concepts are based on the book Elements of the Theory of Computation, by Harry l. Lewis and Christos H. Papadimitriou, (c) 1981 by Prentice Hall. + *

+ * + * @author Eirik Torske + * @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', '\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 */ + public static final char FREE_RANGE_CHARACTER = '*'; + + /** Field FREE_PASS_CHARACTER */ + public static final char FREE_PASS_CHARACTER = '?'; + + // Members + boolean initialized; + String stringMask; + WildcardStringParserState initialState; + int totalNumberOfStringsParsed; + boolean debugging; + PrintStream out; + + // Properties + // Constructors + + /** + * Creates a wildcard string parser. + * + * @param pStringMask the wildcard string mask. + */ + public WildcardStringParser(final String pStringMask) { + this(pStringMask, false); + } + + /** + * Creates a wildcard string parser. + * + * @param pStringMask the wildcard string mask. + * @param pDebugging {@code true} will cause debug messages to be emitted to {@code System.out}. + */ + public WildcardStringParser(final String pStringMask, final boolean pDebugging) { + this(pStringMask, pDebugging, System.out); + } + + /** + * Creates a wildcard string parser. + * + * @param pStringMask the wildcard string mask. + * @param pDebugging {@code true} will cause debug messages to be emitted. + * @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.stringMask = pStringMask; + this.debugging = pDebugging; + this.out = pDebuggingPrintStream; + initialized = buildAutomaton(); + } + + // Methods + private boolean checkIfStateInWildcardRange(WildcardStringParserState pState) { + + WildcardStringParserState runnerState = pState; + + while (runnerState.previousState != null) { + runnerState = runnerState.previousState; + if (isFreeRangeCharacter(runnerState.character)) { + return true; + } + if (!isFreePassCharacter(runnerState.character)) { + return false; + } // If free-pass char '?' - move on + } + return false; + } + + private boolean checkIfLastFreeRangeState(WildcardStringParserState pState) { + + if (isFreeRangeCharacter(pState.character)) { + return true; + } + if (isFreePassCharacter(pState.character)) { + if (checkIfStateInWildcardRange(pState)) { + return true; + } + } + return false; + } + + /** @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 < stringMask.length(); i++) { + if (!isFreeRangeCharacter(stringMask.charAt(i))) { + return false; + } + } + return true; + } + + private boolean buildAutomaton() { + + char activeChar; + WildcardStringParserState runnerState = null; + WildcardStringParserState newState = null; + WildcardStringParserState lastFreeRangeState = null; + + // Create the initial state of the automaton + 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; + initialState = runnerState; + initialState.automatonStateNumber = 0; + } + else { + System.err.println("string mask provided are null or empty - aborting!"); + return false; + } + + // Create the rest of the automaton + 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)))) { + System.err.println("one or more characters in string mask are not legal characters - aborting!"); + return false; + } + + // Set last free-range state before creating/checking the next state + runnerState.lastFreeRangeState = lastFreeRangeState; + + // Create next state, check if free-range state, set the state number and preceeding state + newState = new WildcardStringParserState(activeChar); + newState.automatonStateNumber = i; + newState.previousState = runnerState; + + // Special check if the state represents an '*' or '?' with only preceeding states representing '?' and '*' + if (checkIfLastFreeRangeState(newState)) { + lastFreeRangeState = newState; + } + + // Set the succeding state before moving to the next state + runnerState.nextState = newState; + + // Move to the next state + runnerState = newState; + + // Special setting of the last free-range state for the last element + if (runnerState.automatonStateNumber == stringMask.length() - 1) { + runnerState.lastFreeRangeState = lastFreeRangeState; + } + } + + // Initiate some statistics + totalNumberOfStringsParsed = 0; + return true; + } + + /** Tests if a certain character is a valid character in the alphabet that is applying for this automaton. */ + public static boolean isInAlphabet(final char pCharToCheck) { + + for (int i = 0; i < ALPHABET.length; i++) { + if (pCharToCheck == ALPHABET[i]) { + return true; + } + } + return false; + } + + /** Tests if a certain character is the designated "free-range" character ('*'). */ + public static boolean isFreeRangeCharacter(final char pCharToCheck) { + return pCharToCheck == FREE_RANGE_CHARACTER; + } + + /** Tests if a certain character is the designated "free-pass" character ('?'). */ + public static boolean isFreePassCharacter(final char pCharToCheck) { + return pCharToCheck == FREE_PASS_CHARACTER; + } + + /** Tests if a certain character is a wildcard character ('*' or '?'). */ + public static boolean isWildcardCharacter(final char pCharToCheck) { + return ((isFreeRangeCharacter(pCharToCheck)) || (isFreePassCharacter(pCharToCheck))); + } + + /** + * Gets the string mask that was used when building the parser atomaton. + * + * @return the string mask used for building the parser automaton. + */ + public String getStringMask() { + return stringMask; + } + + /** + * Parses a string according to the rules stated above. + * + * @param pStringToParse the string to parse. + * @return {@code true} if and only if the string are accepted by the automaton. + */ + public boolean parseString(final String pStringToParse) { + + if (debugging) { + out.println("parsing \"" + pStringToParse + "\"..."); + } + + // Update statistics + totalNumberOfStringsParsed++; + + // Check string to be parsed for nullness + if (pStringToParse == null) { + if (debugging) { + out.println("string to be parsed is null - rejection!"); + } + return false; + } + + // Create parsable string + ParsableString parsableString = new ParsableString(pStringToParse); + + // Check string to be parsed + if (!parsableString.checkString()) { + 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 (!initialized) { + System.err.println("automaton is not initialized - rejection!"); + return false; + } + + // Check if automaton is trivial (accepts all strings) + if (isTrivialAutomaton()) { + if (debugging) { + out.println("automaton represents a trivial string mask (accepts all strings) - acceptance!"); + } + return true; + } + + // Check if string to be parsed is empty + if (parsableString.isEmpty()) { + if (debugging) { + out.println("string to be parsed is empty and not trivial automaton - rejection!"); + } + return false; + } + + // Flag and more to indicate that state skipping due to sequence of '?' succeeding a '*' has been performed + boolean hasPerformedFreeRangeMovement = false; + int numberOfFreePassCharactersRead_SinceLastFreePassState = 0; + int numberOfParsedCharactersRead_SinceLastFreePassState = 0; + WildcardStringParserState runnerState = null; + + // Accepted by the first state? + if ((parsableString.charArray[0] == initialState.character) || isWildcardCharacter(initialState.character)) { + runnerState = initialState; + parsableString.index = 0; + } + else { + if (debugging) { + out.println("cannot enter first automaton state - rejection!"); + } + return false; + } + + // Initialize the free-pass character state visited count + if (isFreePassCharacter(runnerState.character)) { + numberOfFreePassCharactersRead_SinceLastFreePassState++; + } + + // Perform parsing according to the rules above + for (int i = 0; i < parsableString.length(); i++) { + if (debugging) { + out.println(); + } + if (debugging) { + out.println("parsing - index number " + i + ", active char: '" + + parsableString.getActiveChar() + "' char string index: " + parsableString.index + + " number of chars since last free-range state: " + numberOfParsedCharactersRead_SinceLastFreePassState); + } + if (debugging) { + out.println("parsing - state: " + runnerState.automatonStateNumber + " '" + + runnerState.character + "' - no of free-pass chars read: " + numberOfFreePassCharactersRead_SinceLastFreePassState); + } + if (debugging) { + out.println("parsing - hasPerformedFreeRangeMovement: " + hasPerformedFreeRangeMovement); + } + 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.character)) { + + // Special free-range skipping check + if (hasPerformedFreeRangeMovement) { + if (parsableString.reachedEndOfString()) { + if (numberOfFreePassCharactersRead_SinceLastFreePassState > numberOfParsedCharactersRead_SinceLastFreePassState) { + 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 (debugging) { + out.println( + "no subsequent state (final state) and the state represents '*' - end of parsing string and enough characters read - acceptance!"); + } + return true; + } + } + else { + if (numberOfFreePassCharactersRead_SinceLastFreePassState > numberOfParsedCharactersRead_SinceLastFreePassState) { + 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.index++; + numberOfParsedCharactersRead_SinceLastFreePassState++; + } + else { + if (debugging) { + out.println( + "no subsequent state (final state) and the state represents '*' - not the end of parsing string, but enough characters read - acceptance!"); + } + return true; + } + } + } + else { + if (debugging) { + out.println("no subsequent state (final state) and the state represents '*' - no skipping performed - acceptance!"); + } + return true; + } + } + + // If there are no subsequent state (final state) and no skipping has been performed and the end of the string to test is reached - acceptance! + else if (parsableString.reachedEndOfString()) { + + // Special free-range skipping check + if ((hasPerformedFreeRangeMovement) + && (numberOfFreePassCharactersRead_SinceLastFreePassState > numberOfParsedCharactersRead_SinceLastFreePassState)) { + 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 (debugging) { + out.println("no subsequent state (final state) and the end of the string to test is reached - acceptance!"); + } + return true; + } + else { + if (debugging) { + out.println("parsing - escaping process..."); + } + } + } + else { + 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.character)) { + numberOfFreePassCharactersRead_SinceLastFreePassState = 0; + numberOfParsedCharactersRead_SinceLastFreePassState = 0; + WildcardStringParserState freeRangeRunnerState = runnerState.nextState; + + while ((freeRangeRunnerState != null) && (isFreePassCharacter(freeRangeRunnerState.character))) { + runnerState = freeRangeRunnerState; + hasPerformedFreeRangeMovement = true; + numberOfFreePassCharactersRead_SinceLastFreePassState++; + freeRangeRunnerState = freeRangeRunnerState.nextState; + } + + // Special Case: if the mask is at the end + if (runnerState.nextState == null) { + if (debugging) { + out.println(); + } + if (debugging) { + out.println("parsing - index number " + i + ", active char: '" + + parsableString.getActiveChar() + "' char string index: " + parsableString.index + + " number of chars since last free-range state: " + numberOfParsedCharactersRead_SinceLastFreePassState); + } + if (debugging) { + out.println("parsing - state: " + runnerState.automatonStateNumber + " '" + + runnerState.character + "' - no of free-pass chars read: " + numberOfFreePassCharactersRead_SinceLastFreePassState); + } + if (debugging) { + out.println("parsing - hasPerformedFreeRangeMovement: " + + hasPerformedFreeRangeMovement); + } + if ((hasPerformedFreeRangeMovement) + && (numberOfFreePassCharactersRead_SinceLastFreePassState >= numberOfParsedCharactersRead_SinceLastFreePassState)) { + return true; + } + else { + return false; + } + } + } + + // If the next state represents '*' - go to this next state + 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.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.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.lastFreeRangeState != null) { + runnerState = runnerState.lastFreeRangeState; + parsableString.index++; + numberOfParsedCharactersRead_SinceLastFreePassState++; + } + else { + 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 (debugging) { + out.println("finished reading parsing string and not at any final state - rejection!"); + } + return false; + } + + /* + * Overriding mandatory methods from EntityObject's. + */ + + /** + * Method toString + * + * @return + */ + public String toString() { + + StringBuilder buffer = new StringBuilder(); + + if (!initialized) { + buffer.append(getClass().getName()); + buffer.append(": Not initialized properly!"); + buffer.append("\n"); + buffer.append("\n"); + } + else { + WildcardStringParserState runnerState = initialState; + + buffer.append(getClass().getName()); + buffer.append(": String mask "); + buffer.append(stringMask); + buffer.append("\n"); + buffer.append("\n"); + buffer.append(" Automaton: "); + while (runnerState != null) { + buffer.append(runnerState.automatonStateNumber); + buffer.append(": "); + buffer.append(runnerState.character); + buffer.append(" ("); + if (runnerState.lastFreeRangeState != null) { + buffer.append(runnerState.lastFreeRangeState.automatonStateNumber); + } + else { + buffer.append("-"); + } + buffer.append(")"); + if (runnerState.nextState != null) { + buffer.append(" --> "); + } + runnerState = runnerState.nextState; + } + buffer.append("\n"); + buffer.append(" Format: : ()"); + buffer.append("\n"); + buffer.append(" Number of strings parsed: " + totalNumberOfStringsParsed); + buffer.append("\n"); + } + return buffer.toString(); + } + + /** + * Method equals + * + * @param pObject + * @return + */ + public boolean equals(Object pObject) { + + if (pObject instanceof WildcardStringParser) { + WildcardStringParser externalParser = (WildcardStringParser) pObject; + + return ((externalParser.initialized == this.initialized) && (externalParser.stringMask == this.stringMask)); + } + return super.equals(pObject); + } + + // Just taking the lazy, easy and dangerous way out + + /** + * Method hashCode + * + * @return + */ + public int hashCode() { + return super.hashCode(); + } + + protected Object clone() throws CloneNotSupportedException { + + if (initialized) { + return new WildcardStringParser(stringMask); + } + return null; + } + + // Just taking the lazy, easy and dangerous way out + protected void finalize() throws Throwable { + } + + /** A simple holder class for an automaton state. */ + class WildcardStringParserState { + + // Constants + // Members + int automatonStateNumber; + char character; + WildcardStringParserState previousState; + WildcardStringParserState nextState; + WildcardStringParserState lastFreeRangeState; + + // Constructors + + /** + * Constructor WildcardStringParserState + * + * @param pChar + */ + public WildcardStringParserState(final char pChar) { + this.character = pChar; + } + + // Methods + // Debug + } + + /** A simple holder class for a string to be parsed. */ + class ParsableString { + + // Constants + // Members + char[] charArray; + int index; + + // Constructors + ParsableString(final String pStringToParse) { + + if (pStringToParse != null) { + charArray = pStringToParse.toCharArray(); + } + index = -1; + } + + // Methods + boolean reachedEndOfString() { + + //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 charArray.length; + } + + char getActiveChar() { + + if ((index > -1) && (index < charArray.length)) { + return charArray[index]; + } + System.err.println(getClass().getName() + ": trying to access character outside character array!"); + return ' '; + } + + char getSubsequentChar() { + + if ((index > -1) && (index + 1 < charArray.length)) { + return charArray[index + 1]; + } + System.err.println(getClass().getName() + ": trying to access character outside character array!"); + return ' '; + } + + boolean checkString() { + + if (!isEmpty()) { + + // Check if the string only contains chars that are elements in the alphabet + for (int i = 0; i < charArray.length; i++) { + if (!WildcardStringParser.isInAlphabet(charArray[i])) { + return false; + } + } + } + return true; + } + + boolean isEmpty() { + return ((charArray == null) || (charArray.length == 0)); + } + + /** + * Method toString + * + * @return + */ + public String toString() { + return new String(charArray); + } + } +} + + +/*--- 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/service/RegisterableService.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/service/RegisterableService.java index e3c05e7d..102b8989 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/service/RegisterableService.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/service/RegisterableService.java @@ -1,63 +1,64 @@ -/* - * 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 of the copyright holder 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 HOLDER 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; - -/** - * An optional interface that may be implemented by service provider objects. - *

- * If this interface is implemented, the service provider objects will receive - * notification of registration and deregistration from the - * {@code ServiceRegistry}. - * - * @see ServiceRegistry - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/RegisterableService.java#1 $ - */ -public interface RegisterableService { - /** - * Called right after this service provider object is added to - * the given category of the given {@code ServiceRegistry}. - * - * @param pRegistry the {@code ServiceRegistry} {@code this} was added to - * @param pCategory the category {@code this} was added to - */ - void onRegistration(ServiceRegistry pRegistry, Class pCategory); - - /** - * Called right after this service provider object is removed - * from the given category of the given {@code ServiceRegistry}. - * - * @param pRegistry the {@code ServiceRegistry} {@code this} was added to - * @param pCategory the category {@code this} was added to - */ - void onDeregistration(ServiceRegistry pRegistry, Class pCategory); -} +/* + * 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 of the copyright holder 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 HOLDER 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; + +/** + * An optional interface that may be implemented by service provider objects. + *

+ * If this interface is implemented, the service provider objects will receive + * notification of registration and deregistration from the + * {@code ServiceRegistry}. + *

+ * + * @see ServiceRegistry + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/RegisterableService.java#1 $ + */ +public interface RegisterableService { + /** + * Called right after this service provider object is added to + * the given category of the given {@code ServiceRegistry}. + * + * @param pRegistry the {@code ServiceRegistry} {@code this} was added to + * @param pCategory the category {@code this} was added to + */ + void onRegistration(ServiceRegistry pRegistry, Class pCategory); + + /** + * Called right after this service provider object is removed + * from the given category of the given {@code ServiceRegistry}. + * + * @param pRegistry the {@code ServiceRegistry} {@code this} was added to + * @param pCategory the category {@code this} was added to + */ + void onDeregistration(ServiceRegistry pRegistry, Class pCategory); +} diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/service/ServiceConfigurationError.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/service/ServiceConfigurationError.java index 2e75c310..9d5f8c1c 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/service/ServiceConfigurationError.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/service/ServiceConfigurationError.java @@ -1,54 +1,54 @@ -/* - * 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 of the copyright holder 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 HOLDER 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; - -/** - * Error thrown by the {@code ServiceRegistry} in case of a configuration - * error. - *

- * @see ServiceRegistry - * - * @author Harald Kuhr - * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceConfigurationError.java#1 $ - */ -public class ServiceConfigurationError extends Error { - ServiceConfigurationError(Throwable pCause) { - super(pCause); - } - - ServiceConfigurationError(String pMessage) { - super(pMessage); - } - - ServiceConfigurationError(String pMessage, Throwable pCause) { - super(pMessage, pCause); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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; + +/** + * Error thrown by the {@code ServiceRegistry} in case of a configuration + * error. + * + * @see ServiceRegistry + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceConfigurationError.java#1 $ + */ +public class ServiceConfigurationError extends Error { + ServiceConfigurationError(Throwable pCause) { + super(pCause); + } + + ServiceConfigurationError(String pMessage) { + super(pMessage); + } + + ServiceConfigurationError(String pMessage, Throwable pCause) { + super(pMessage, pCause); + } +} 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 5aa059c1..18fdc3a0 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 @@ -1,548 +1,561 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.lang.Validate; -import com.twelvemonkeys.util.FilterIterator; - -import java.io.IOException; -import java.net.URL; -import java.util.*; - -/** - * A registry for service provider objects. - *

- * Service providers are looked up from the classpath, under the path - * {@code META-INF/services/}<full-class-name>. - *

- * For example:
- * {@code META-INF/services/com.company.package.spi.MyService}. - *

- * The file should contain a list of fully-qualified concrete class names, - * one per line. - *

- * The full-class-name represents an interface or (typically) an - * abstract class, and is the same class used as the category for this registry. - * Note that only one instance of a concrete subclass may be registered with a - * specific category at a time. - *

- * Implementation detail: This class is a clean room implementation of - * a service registry and does not use the proprietary {@code sun.misc.Service} - * class that is referred to in the JAR File specification. - * This class should work on any Java platform. - * - * - * @author Harald Kuhr - * @version $Id: com/twelvemonkeys/util/service/ServiceRegistry.java#2 $ - * @see RegisterableService - * @see JAR File Specification - */ -public class ServiceRegistry { - // TODO: Security issues? - // TODO: Application contexts? Probably use instance per thread group.. - - /** - * "META-INF/services/" - */ - public static final String SERVICES = "META-INF/services/"; - - // Class to CategoryRegistry mapping - private final Map, CategoryRegistry> categoryMap; - - /** - * Creates a {@code ServiceRegistry} instance with a set of categories - * taken from the {@code pCategories} argument. - *

- * The categories are constant during the lifetime of the registry, and may - * not be changed after initial creation. - * - * @param pCategories an {@code Iterator} containing - * {@code Class} objects that defines this registry's categories. - * @throws IllegalArgumentException if {@code pCategories} is {@code null}. - * @throws ClassCastException if {@code pCategories} contains anything - * but {@code Class} objects. - */ - public ServiceRegistry(final Iterator> pCategories) { - Validate.notNull(pCategories, "categories"); - - Map, CategoryRegistry> map = new LinkedHashMap, CategoryRegistry>(); - - while (pCategories.hasNext()) { - putCategory(map, pCategories.next()); - } - - // NOTE: Categories are constant for the lifetime of a registry - categoryMap = Collections.unmodifiableMap(map); - } - - private void putCategory(Map, CategoryRegistry> pMap, Class pCategory) { - CategoryRegistry registry = new CategoryRegistry(pCategory); - pMap.put(pCategory, registry); - } - - /** - * Registers all provider implementations for this {@code ServiceRegistry} - * found in the application classpath. - * - * @throws ServiceConfigurationError if an error occurred during registration - */ - public void registerApplicationClasspathSPIs() { - ClassLoader loader = Thread.currentThread().getContextClassLoader(); - Iterator> categories = categories(); - - while (categories.hasNext()) { - Class category = categories.next(); - - try { - // Find all META-INF/services/ + name on class path - String name = SERVICES + category.getName(); - Enumeration spiResources = loader.getResources(name); - - while (spiResources.hasMoreElements()) { - URL resource = spiResources.nextElement(); - registerSPIs(resource, category, loader); - } - } - catch (IOException e) { - throw new ServiceConfigurationError(e); - } - } - } - - /** - * Registers all SPIs listed in the given resource. - * - * @param pResource the resource to load SPIs from - * @param pCategory the category class - * @param pLoader the class loader to use - */ - void registerSPIs(final URL pResource, final Class pCategory, final ClassLoader pLoader) { - Properties classNames = new Properties(); - - try { - classNames.load(pResource.openStream()); - } - catch (IOException e) { - throw new ServiceConfigurationError(e); - } - - if (!classNames.isEmpty()) { - @SuppressWarnings({"unchecked"}) - CategoryRegistry registry = categoryMap.get(pCategory); - - Set providerClassNames = classNames.keySet(); - - for (Object providerClassName : providerClassNames) { - String className = (String) providerClassName; - try { - @SuppressWarnings({"unchecked"}) - Class providerClass = (Class) Class.forName(className, true, pLoader); - T provider = providerClass.newInstance(); - registry.register(provider); - } - catch (ClassNotFoundException e) { - throw new ServiceConfigurationError(e); - } - catch (IllegalAccessException e) { - throw new ServiceConfigurationError(e); - } - catch (InstantiationException e) { - throw new ServiceConfigurationError(e); - } - catch (IllegalArgumentException e) { - throw new ServiceConfigurationError(e); - } - } - } - } - - /** - * Returns an {@code Iterator} containing all providers in the given - * category. - *

- * The iterator supports removal. - *

- * - * NOTE: Removing a provider from the iterator, deregisters the current - * provider (as returned by the last invocation of {@code next()}) from - * {@code pCategory}, it does not remove the provider - * from other categories in the registry. - * - * - * @param pCategory the category class - * @return an {@code Iterator} containing all providers in the given - * category. - * @throws IllegalArgumentException if {@code pCategory} is not a valid - * category in this registry - */ - protected Iterator providers(Class pCategory) { - return getRegistry(pCategory).providers(); - } - - /** - * Returns an {@code Iterator} containing all categories in this registry. - *

- * The iterator does not support removal. - * - * @return an {@code Iterator} containing all categories in this registry. - */ - protected Iterator> categories() { - return categoryMap.keySet().iterator(); - } - - /** - * Returns an {@code Iterator} containing all categories in this registry - * the given {@code pProvider} may be registered with. - *

- * The iterator does not support removal. - * - * @param pProvider the provider instance - * @return an {@code Iterator} containing all categories in this registry - * the given {@code pProvider} may be registered with - */ - protected Iterator> compatibleCategories(final Object pProvider) { - return new FilterIterator>(categories(), - new FilterIterator.Filter>() { - public boolean accept(Class pElement) { - return pElement.isInstance(pProvider); - } - }); - } - - /** - * Returns an {@code Iterator} containing all categories in this registry - * the given {@code pProvider} is currently registered with. - *

- * The iterator supports removal. - *

- * - * 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. - * - * - * @param pProvider the provider instance - * @return an {@code Iterator} containing all categories in this registry - * the given {@code pProvider} may be registered with - */ - protected Iterator> containingCategories(final Object pProvider) { - // TODO: Is removal using the iterator really a good idea? - return new FilterIterator>(categories(), - new FilterIterator.Filter>() { - public boolean accept(Class pElement) { - return getRegistry(pElement).contains(pProvider); - } - }) { - Class current; - - public Class next() { - return (current = super.next()); - } - - public void remove() { - if (current == null) { - throw new IllegalStateException("No current element"); - } - - getRegistry(current).deregister(pProvider); - current = null; - } - }; - } - - /** - * Gets the category registry for the given category. - * - * @param pCategory the category class - * @return the {@code CategoryRegistry} for the given category - */ - private CategoryRegistry getRegistry(final Class pCategory) { - @SuppressWarnings({"unchecked"}) - CategoryRegistry registry = categoryMap.get(pCategory); - if (registry == null) { - throw new IllegalArgumentException("No such category: " + pCategory.getName()); - } - return registry; - } - - /** - * Registers the given provider for all categories it matches. - * - * @param pProvider the provider instance - * @return {@code true} if {@code pProvider} is now registered in - * one or more categories it was not registered in before. - * @see #compatibleCategories(Object) - */ - public boolean register(final Object pProvider) { - Iterator> categories = compatibleCategories(pProvider); - boolean registered = false; - while (categories.hasNext()) { - Class category = categories.next(); - if (registerImpl(pProvider, category) && !registered) { - registered = true; - } - } - return registered; - } - - private boolean registerImpl(final Object pProvider, final Class pCategory) { - return getRegistry(pCategory).register(pCategory.cast(pProvider)); - } - - /** - * Registers the given provider for the given category. - * - * @param pProvider the provider instance - * @param pCategory the category class - * @return {@code true} if {@code pProvider} is now registered in - * the given category - */ - public boolean register(final T pProvider, final Class pCategory) { - return registerImpl(pProvider, pCategory); - } - - /** - * 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 and is now de-registered. - * @see #containingCategories(Object) - */ - public boolean deregister(final Object pProvider) { - Iterator> categories = containingCategories(pProvider); - - boolean deregistered = false; - while (categories.hasNext()) { - Class category = categories.next(); - if (deregister(pProvider, category) && !deregistered) { - deregistered = true; - } - } - - return deregistered; - } - - /** - * Deregisters the given provider from the given category. - * - * @param pProvider the provider instance - * @param pCategory the category class - * @return {@code true} if {@code pProvider} was previously registered in - * the given category - */ - public boolean deregister(final Object pProvider, final Class pCategory) { - return getRegistry(pCategory).deregister(pProvider); - } - - /** - * Keeps track of each individual category. - */ - class CategoryRegistry { - private final Class category; - private final Map providers = new LinkedHashMap(); - - CategoryRegistry(Class pCategory) { - Validate.notNull(pCategory, "category"); - category = pCategory; - } - - private void checkCategory(final Object pProvider) { - 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 already have an instance of pProvider's class. - if (!contains(pProvider)) { - providers.put(pProvider.getClass(), pProvider); - processRegistration(pProvider); - return true; - } - - return false; - } - - void processRegistration(final T pProvider) { - if (pProvider instanceof RegisterableService) { - RegisterableService service = (RegisterableService) pProvider; - service.onRegistration(ServiceRegistry.this, category); - } - } - - public boolean deregister(final Object pProvider) { - checkCategory(pProvider); - - // NOTE: We remove any provider of the same class, this may or may - // not be the same instance as pProvider. - T oldProvider = providers.remove(pProvider.getClass()); - - if (oldProvider != null) { - processDeregistration(oldProvider); - return true; - } - - return false; - } - - void processDeregistration(final T pOldProvider) { - if (pOldProvider instanceof RegisterableService) { - RegisterableService service = (RegisterableService) pOldProvider; - service.onDeregistration(ServiceRegistry.this, category); - } - } - - public boolean contains(final Object pProvider) { - return providers.containsKey(pProvider != null ? pProvider.getClass() : null); - } - - public Iterator providers() { - // NOTE: The iterator must support removal because deregistering - // using the deregister method will result in - // ConcurrentModificationException in the iterator.. - // We wrap the iterator to track deregistration right. - final Iterator iterator = providers.values().iterator(); - return new Iterator() { - T current; - - public boolean hasNext() { - return iterator.hasNext(); - - } - - public T next() { - return (current = iterator.next()); - } - - public void remove() { - iterator.remove(); - processDeregistration(current); - } - }; - } - } - - @SuppressWarnings({"UnnecessaryFullyQualifiedName"}) - public static void main(String[] pArgs) { - abstract class Spi {} - class One extends Spi {} - class Two extends Spi {} - - ServiceRegistry testRegistry = new ServiceRegistry( - Arrays.>asList( - java.nio.charset.spi.CharsetProvider.class, - java.nio.channels.spi.SelectorProvider.class, - javax.imageio.spi.ImageReaderSpi.class, - javax.imageio.spi.ImageWriterSpi.class, - Spi.class - ).iterator() - ); - - testRegistry.registerApplicationClasspathSPIs(); - - One one = new One(); - Two two = new Two(); - testRegistry.register(one, Spi.class); - testRegistry.register(two, Spi.class); - testRegistry.deregister(one); - testRegistry.deregister(one, Spi.class); - testRegistry.deregister(two, Spi.class); - testRegistry.deregister(two); - - Iterator> categories = testRegistry.categories(); - System.out.println("Categories: "); - while (categories.hasNext()) { - Class category = categories.next(); - System.out.println(" " + category.getName() + ":"); - - Iterator providers = testRegistry.providers(category); - Object provider = null; - while (providers.hasNext()) { - provider = providers.next(); - System.out.println(" " + provider); - if (provider instanceof javax.imageio.spi.ImageReaderWriterSpi) { - System.out.println(" - " + ((javax.imageio.spi.ImageReaderWriterSpi) provider).getDescription(null)); - } - // javax.imageio.spi.ImageReaderWriterSpi provider = (javax.imageio.spi.ImageReaderWriterSpi) providers.next(); - // System.out.println(" " + provider); - // System.out.println(" " + provider.getVendorName()); - // System.out.println(" Formats:"); - // - // System.out.print(" "); - // String[] formatNames = provider.getFormatNames(); - // for (int i = 0; i < formatNames.length; i++) { - // if (i != 0) { - // System.out.print(", "); - // } - // System.out.print(formatNames[i]); - // } - // System.out.println(); - - // Don't remove last one, it's removed later to exercise more code :-) - if (providers.hasNext()) { - providers.remove(); - } - } - - // Remove the last item from all categories - if (provider != null) { - Iterator containers = testRegistry.containingCategories(provider); - int count = 0; - while (containers.hasNext()) { - if (category == containers.next()) { - containers.remove(); - count++; - } - } - - if (count != 1) { - System.err.println("Removed " + provider + " from " + count + " categories"); - } - } - - // Remove all using providers iterator - providers = testRegistry.providers(category); - if (!providers.hasNext()) { - System.out.println("All providers successfully deregistered"); - } - while (providers.hasNext()) { - System.err.println("Not removed: " + providers.next()); - } - } - } - - //*/ -} +/* + * 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 of the copyright holder 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 HOLDER 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.lang.Validate; +import com.twelvemonkeys.util.FilterIterator; + +import java.io.IOException; +import java.net.URL; +import java.util.*; + +/** + * A registry for service provider objects. + *

+ * Service providers are looked up from the classpath, under the path + * {@code META-INF/services/}<full-class-name>. + *

+ *

+ * For example: + *
+ * {@code META-INF/services/com.company.package.spi.MyService}. + *

+ *

+ * The file should contain a list of fully-qualified concrete class names, + * one per line. + *

+ *

+ * The full-class-name represents an interface or (typically) an + * abstract class, and is the same class used as the category for this registry. + * Note that only one instance of a concrete subclass may be registered with a + * specific category at a time. + *

+ *

+ * Implementation detail: This class is a clean room implementation of + * a service registry and does not use the proprietary {@code sun.misc.Service} + * class that is referred to in the JAR File specification. + * This class should work on any Java platform. + * + *

+ * + * @author Harald Kuhr + * @version $Id: com/twelvemonkeys/util/service/ServiceRegistry.java#2 $ + * @see RegisterableService + * @see JAR File Specification + */ +public class ServiceRegistry { + // TODO: Security issues? + // TODO: Application contexts? Probably use instance per thread group.. + + /** + * "META-INF/services/" + */ + public static final String SERVICES = "META-INF/services/"; + + // Class to CategoryRegistry mapping + private final Map, CategoryRegistry> categoryMap; + + /** + * Creates a {@code ServiceRegistry} instance with a set of categories + * taken from the {@code pCategories} argument. + *

+ * The categories are constant during the lifetime of the registry, and may + * not be changed after initial creation. + *

+ * + * @param pCategories an {@code Iterator} containing + * {@code Class} objects that defines this registry's categories. + * @throws IllegalArgumentException if {@code pCategories} is {@code null}. + * @throws ClassCastException if {@code pCategories} contains anything + * but {@code Class} objects. + */ + public ServiceRegistry(final Iterator> pCategories) { + Validate.notNull(pCategories, "categories"); + + Map, CategoryRegistry> map = new LinkedHashMap, CategoryRegistry>(); + + while (pCategories.hasNext()) { + putCategory(map, pCategories.next()); + } + + // NOTE: Categories are constant for the lifetime of a registry + categoryMap = Collections.unmodifiableMap(map); + } + + private void putCategory(Map, CategoryRegistry> pMap, Class pCategory) { + CategoryRegistry registry = new CategoryRegistry(pCategory); + pMap.put(pCategory, registry); + } + + /** + * Registers all provider implementations for this {@code ServiceRegistry} + * found in the application classpath. + * + * @throws ServiceConfigurationError if an error occurred during registration + */ + public void registerApplicationClasspathSPIs() { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + Iterator> categories = categories(); + + while (categories.hasNext()) { + Class category = categories.next(); + + try { + // Find all META-INF/services/ + name on class path + String name = SERVICES + category.getName(); + Enumeration spiResources = loader.getResources(name); + + while (spiResources.hasMoreElements()) { + URL resource = spiResources.nextElement(); + registerSPIs(resource, category, loader); + } + } + catch (IOException e) { + throw new ServiceConfigurationError(e); + } + } + } + + /** + * Registers all SPIs listed in the given resource. + * + * @param pResource the resource to load SPIs from + * @param pCategory the category class + * @param pLoader the class loader to use + */ + void registerSPIs(final URL pResource, final Class pCategory, final ClassLoader pLoader) { + Properties classNames = new Properties(); + + try { + classNames.load(pResource.openStream()); + } + catch (IOException e) { + throw new ServiceConfigurationError(e); + } + + if (!classNames.isEmpty()) { + @SuppressWarnings({"unchecked"}) + CategoryRegistry registry = categoryMap.get(pCategory); + + Set providerClassNames = classNames.keySet(); + + for (Object providerClassName : providerClassNames) { + String className = (String) providerClassName; + try { + @SuppressWarnings({"unchecked"}) + Class providerClass = (Class) Class.forName(className, true, pLoader); + T provider = providerClass.newInstance(); + registry.register(provider); + } + catch (ClassNotFoundException e) { + throw new ServiceConfigurationError(e); + } + catch (IllegalAccessException e) { + throw new ServiceConfigurationError(e); + } + catch (InstantiationException e) { + throw new ServiceConfigurationError(e); + } + catch (IllegalArgumentException e) { + throw new ServiceConfigurationError(e); + } + } + } + } + + /** + * Returns an {@code Iterator} containing all providers in the given + * category. + *

+ * The iterator supports removal. + *

+ *

+ * + * NOTE: Removing a provider from the iterator, deregisters the current + * provider (as returned by the last invocation of {@code next()}) from + * {@code pCategory}, it does not remove the provider + * from other categories in the registry. + * + *

+ * + * @param pCategory the category class + * @return an {@code Iterator} containing all providers in the given + * category. + * @throws IllegalArgumentException if {@code pCategory} is not a valid + * category in this registry + */ + protected Iterator providers(Class pCategory) { + return getRegistry(pCategory).providers(); + } + + /** + * Returns an {@code Iterator} containing all categories in this registry. + *

+ * The iterator does not support removal. + *

+ * + * @return an {@code Iterator} containing all categories in this registry. + */ + protected Iterator> categories() { + return categoryMap.keySet().iterator(); + } + + /** + * Returns an {@code Iterator} containing all categories in this registry + * the given {@code pProvider} may be registered with. + *

+ * The iterator does not support removal. + *

+ * + * @param pProvider the provider instance + * @return an {@code Iterator} containing all categories in this registry + * the given {@code pProvider} may be registered with + */ + protected Iterator> compatibleCategories(final Object pProvider) { + return new FilterIterator>(categories(), + new FilterIterator.Filter>() { + public boolean accept(Class pElement) { + return pElement.isInstance(pProvider); + } + }); + } + + /** + * Returns an {@code Iterator} containing all categories in this registry + * the given {@code pProvider} is currently registered with. + *

+ * The iterator supports removal. + *

+ *

+ * + * 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. + * + *

+ * + * @param pProvider the provider instance + * @return an {@code Iterator} containing all categories in this registry + * the given {@code pProvider} may be registered with + */ + protected Iterator> containingCategories(final Object pProvider) { + // TODO: Is removal using the iterator really a good idea? + return new FilterIterator>(categories(), + new FilterIterator.Filter>() { + public boolean accept(Class pElement) { + return getRegistry(pElement).contains(pProvider); + } + }) { + Class current; + + public Class next() { + return (current = super.next()); + } + + public void remove() { + if (current == null) { + throw new IllegalStateException("No current element"); + } + + getRegistry(current).deregister(pProvider); + current = null; + } + }; + } + + /** + * Gets the category registry for the given category. + * + * @param pCategory the category class + * @return the {@code CategoryRegistry} for the given category + */ + private CategoryRegistry getRegistry(final Class pCategory) { + @SuppressWarnings({"unchecked"}) + CategoryRegistry registry = categoryMap.get(pCategory); + if (registry == null) { + throw new IllegalArgumentException("No such category: " + pCategory.getName()); + } + return registry; + } + + /** + * Registers the given provider for all categories it matches. + * + * @param pProvider the provider instance + * @return {@code true} if {@code pProvider} is now registered in + * one or more categories it was not registered in before. + * @see #compatibleCategories(Object) + */ + public boolean register(final Object pProvider) { + Iterator> categories = compatibleCategories(pProvider); + boolean registered = false; + while (categories.hasNext()) { + Class category = categories.next(); + if (registerImpl(pProvider, category) && !registered) { + registered = true; + } + } + return registered; + } + + private boolean registerImpl(final Object pProvider, final Class pCategory) { + return getRegistry(pCategory).register(pCategory.cast(pProvider)); + } + + /** + * Registers the given provider for the given category. + * + * @param pProvider the provider instance + * @param pCategory the category class + * @return {@code true} if {@code pProvider} is now registered in + * the given category + */ + public boolean register(final T pProvider, final Class pCategory) { + return registerImpl(pProvider, pCategory); + } + + /** + * 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 and is now de-registered. + * @see #containingCategories(Object) + */ + public boolean deregister(final Object pProvider) { + Iterator> categories = containingCategories(pProvider); + + boolean deregistered = false; + while (categories.hasNext()) { + Class category = categories.next(); + if (deregister(pProvider, category) && !deregistered) { + deregistered = true; + } + } + + return deregistered; + } + + /** + * Deregisters the given provider from the given category. + * + * @param pProvider the provider instance + * @param pCategory the category class + * @return {@code true} if {@code pProvider} was previously registered in + * the given category + */ + public boolean deregister(final Object pProvider, final Class pCategory) { + return getRegistry(pCategory).deregister(pProvider); + } + + /** + * Keeps track of each individual category. + */ + class CategoryRegistry { + private final Class category; + private final Map providers = new LinkedHashMap(); + + CategoryRegistry(Class pCategory) { + Validate.notNull(pCategory, "category"); + category = pCategory; + } + + private void checkCategory(final Object pProvider) { + 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 already have an instance of pProvider's class. + if (!contains(pProvider)) { + providers.put(pProvider.getClass(), pProvider); + processRegistration(pProvider); + return true; + } + + return false; + } + + void processRegistration(final T pProvider) { + if (pProvider instanceof RegisterableService) { + RegisterableService service = (RegisterableService) pProvider; + service.onRegistration(ServiceRegistry.this, category); + } + } + + public boolean deregister(final Object pProvider) { + checkCategory(pProvider); + + // NOTE: We remove any provider of the same class, this may or may + // not be the same instance as pProvider. + T oldProvider = providers.remove(pProvider.getClass()); + + if (oldProvider != null) { + processDeregistration(oldProvider); + return true; + } + + return false; + } + + void processDeregistration(final T pOldProvider) { + if (pOldProvider instanceof RegisterableService) { + RegisterableService service = (RegisterableService) pOldProvider; + service.onDeregistration(ServiceRegistry.this, category); + } + } + + public boolean contains(final Object pProvider) { + return providers.containsKey(pProvider != null ? pProvider.getClass() : null); + } + + public Iterator providers() { + // NOTE: The iterator must support removal because deregistering + // using the deregister method will result in + // ConcurrentModificationException in the iterator.. + // We wrap the iterator to track deregistration right. + final Iterator iterator = providers.values().iterator(); + return new Iterator() { + T current; + + public boolean hasNext() { + return iterator.hasNext(); + + } + + public T next() { + return (current = iterator.next()); + } + + public void remove() { + iterator.remove(); + processDeregistration(current); + } + }; + } + } + + @SuppressWarnings({"UnnecessaryFullyQualifiedName"}) + public static void main(String[] pArgs) { + abstract class Spi {} + class One extends Spi {} + class Two extends Spi {} + + ServiceRegistry testRegistry = new ServiceRegistry( + Arrays.>asList( + java.nio.charset.spi.CharsetProvider.class, + java.nio.channels.spi.SelectorProvider.class, + javax.imageio.spi.ImageReaderSpi.class, + javax.imageio.spi.ImageWriterSpi.class, + Spi.class + ).iterator() + ); + + testRegistry.registerApplicationClasspathSPIs(); + + One one = new One(); + Two two = new Two(); + testRegistry.register(one, Spi.class); + testRegistry.register(two, Spi.class); + testRegistry.deregister(one); + testRegistry.deregister(one, Spi.class); + testRegistry.deregister(two, Spi.class); + testRegistry.deregister(two); + + Iterator> categories = testRegistry.categories(); + System.out.println("Categories: "); + while (categories.hasNext()) { + Class category = categories.next(); + System.out.println(" " + category.getName() + ":"); + + Iterator providers = testRegistry.providers(category); + Object provider = null; + while (providers.hasNext()) { + provider = providers.next(); + System.out.println(" " + provider); + if (provider instanceof javax.imageio.spi.ImageReaderWriterSpi) { + System.out.println(" - " + ((javax.imageio.spi.ImageReaderWriterSpi) provider).getDescription(null)); + } + // javax.imageio.spi.ImageReaderWriterSpi provider = (javax.imageio.spi.ImageReaderWriterSpi) providers.next(); + // System.out.println(" " + provider); + // System.out.println(" " + provider.getVendorName()); + // System.out.println(" Formats:"); + // + // System.out.print(" "); + // String[] formatNames = provider.getFormatNames(); + // for (int i = 0; i < formatNames.length; i++) { + // if (i != 0) { + // System.out.print(", "); + // } + // System.out.print(formatNames[i]); + // } + // System.out.println(); + + // Don't remove last one, it's removed later to exercise more code :-) + if (providers.hasNext()) { + providers.remove(); + } + } + + // Remove the last item from all categories + if (provider != null) { + Iterator containers = testRegistry.containingCategories(provider); + int count = 0; + while (containers.hasNext()) { + if (category == containers.next()) { + containers.remove(); + count++; + } + } + + if (count != 1) { + System.err.println("Removed " + provider + " from " + count + " categories"); + } + } + + // Remove all using providers iterator + providers = testRegistry.providers(category); + if (!providers.hasNext()) { + System.out.println("All providers successfully deregistered"); + } + while (providers.hasNext()) { + System.err.println("Not removed: " + providers.next()); + } + } + } + + //*/ +} diff --git a/common/common-lang/src/main/java/com/twelvemonkeys/util/service/package_info.java b/common/common-lang/src/main/java/com/twelvemonkeys/util/service/package_info.java index c35bc12b..78925b54 100755 --- a/common/common-lang/src/main/java/com/twelvemonkeys/util/service/package_info.java +++ b/common/common-lang/src/main/java/com/twelvemonkeys/util/service/package_info.java @@ -1,39 +1,40 @@ -/* - * 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 of the copyright holder 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 HOLDER 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. - */ - -/** - * Provides a service provider registry. - *

- * This package contains a service provider registry, as specified in the - * JAR File Specification. - * - * @see JAR File Specification - */ -package com.twelvemonkeys.util.service; +/* + * 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 of the copyright holder 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 HOLDER 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. + */ + +/** + * Provides a service provider registry. + *

+ * This package contains a service provider registry, as specified in the + * JAR File Specification. + *

+ * + * @see JAR File Specification + */ +package com.twelvemonkeys.util.service; diff --git a/common/common-lang/src/test/java/com/twelvemonkeys/util/BeanMapTest.java b/common/common-lang/src/test/java/com/twelvemonkeys/util/BeanMapTest.java index b5f49db1..68de2b41 100755 --- a/common/common-lang/src/test/java/com/twelvemonkeys/util/BeanMapTest.java +++ b/common/common-lang/src/test/java/com/twelvemonkeys/util/BeanMapTest.java @@ -36,13 +36,12 @@ import java.util.Map; /** * BeanMapTestCase - *

- * @todo Extend with BeanMap specific tests * * @author Harald Kuhr * @author last modified by $Author: haku $ * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/BeanMapTestCase.java#2 $ */ +// TODO: Extend with BeanMap specific tests public class BeanMapTest extends MapAbstractTest { public boolean isPutAddSupported() { 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 09ae66e6..2727867f 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 @@ -1,604 +1,604 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.svg; - -import com.twelvemonkeys.image.ImageUtil; -import com.twelvemonkeys.imageio.ImageReaderBase; -import com.twelvemonkeys.imageio.util.IIOUtil; -import org.apache.batik.anim.dom.SVGDOMImplementation; -import org.apache.batik.anim.dom.SVGOMDocument; -import org.apache.batik.bridge.*; -import org.apache.batik.dom.util.DOMUtilities; -import org.apache.batik.ext.awt.image.GraphicsUtil; -import org.apache.batik.gvt.CanvasGraphicsNode; -import org.apache.batik.gvt.GraphicsNode; -import org.apache.batik.gvt.renderer.ConcreteImageRendererFactory; -import org.apache.batik.gvt.renderer.ImageRenderer; -import org.apache.batik.gvt.renderer.ImageRendererFactory; -import org.apache.batik.transcoder.*; -import org.apache.batik.transcoder.image.ImageTranscoder; -import org.apache.batik.util.ParsedURL; -import org.w3c.dom.DOMImplementation; -import org.w3c.dom.Document; -import org.w3c.dom.svg.SVGSVGElement; - -import javax.imageio.IIOException; -import javax.imageio.ImageReadParam; -import javax.imageio.ImageTypeSpecifier; -import javax.imageio.spi.ImageReaderSpi; -import java.awt.*; -import java.awt.geom.AffineTransform; -import java.awt.geom.Dimension2D; -import java.awt.geom.Rectangle2D; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.Collections; -import java.util.Iterator; -import java.util.Map; - -/** - * Image reader for SVG document fragments. - *

- * - * @author Harald Kuhr - * @author Inpspired by code from the Batik Team - * @version $Id: $ - * @see batik-dev - */ -public class SVGImageReader extends ImageReaderBase { - private Rasterizer rasterizer; - - /** - * Creates an {@code SVGImageReader}. - * - * @param pProvider the provider - */ - public SVGImageReader(final ImageReaderSpi pProvider) { - super(pProvider); - } - - protected void resetMembers() { - rasterizer = new Rasterizer(); - } - - @Override - public void dispose() { - super.dispose(); - rasterizer = null; - } - - @Override - public void setInput(Object pInput, boolean seekForwardOnly, boolean ignoreMetadata) { - super.setInput(pInput, seekForwardOnly, ignoreMetadata); - - if (imageInput != null) { - TranscoderInput input = new TranscoderInput(IIOUtil.createStreamAdapter(imageInput)); - rasterizer.setInput(input); - } - } - - public BufferedImage read(int pIndex, ImageReadParam pParam) throws IOException { - checkBounds(pIndex); - - if (pParam instanceof SVGReadParam) { - SVGReadParam svgParam = (SVGReadParam) pParam; - - // Get the base URI - // This must be done before converting the params to hints - String baseURI = svgParam.getBaseURI(); - rasterizer.transcoderInput.setURI(baseURI); - - // Set ImageReadParams as hints - // Note: The cast to Map invokes a different method that preserves - // unset defaults, DO NOT REMOVE! - rasterizer.setTranscodingHints((Map) paramsToHints(svgParam)); - } - - Dimension size = null; - if (pParam != null) { - size = pParam.getSourceRenderSize(); - } - if (size == null) { - size = new Dimension(getWidth(pIndex), getHeight(pIndex)); - } - - BufferedImage destination = getDestination(pParam, getImageTypes(pIndex), size.width, size.height); - - // Read in the image, using the Batik Transcoder - try { - processImageStarted(pIndex); - - BufferedImage image = rasterizer.getImage(); - - Graphics2D g = destination.createGraphics(); - try { - g.setComposite(AlphaComposite.Src); - g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); - g.drawImage(image, 0, 0, null); // TODO: Dest offset? - } - finally { - g.dispose(); - } - - processImageComplete(); - - return destination; - } - catch (TranscoderException e) { - Throwable cause = unwrapException(e); - throw new IIOException(cause.getMessage(), cause); - } - } - - private static Throwable unwrapException(TranscoderException ex) { - // The TranscoderException is generally useless... - return ex.getException() != null ? ex.getException() : ex; - } - - private TranscodingHints paramsToHints(SVGReadParam pParam) throws IOException { - TranscodingHints hints = new TranscodingHints(); - // Note: We must allow generic ImageReadParams, so converting to - // TanscodingHints should be done outside the SVGReadParam class. - - // Set dimensions - Dimension size = pParam.getSourceRenderSize(); - Dimension origSize = new Dimension(getWidth(0), getHeight(0)); - if (size == null) { - // SVG is not a pixel based format, but we'll scale it, according to - // the subsampling for compatibility - size = getSourceRenderSizeFromSubsamping(pParam, origSize); - } - - if (size != null) { - hints.put(ImageTranscoder.KEY_WIDTH, (float) size.getWidth()); - hints.put(ImageTranscoder.KEY_HEIGHT, (float) size.getHeight()); - } - - // Set area of interest - Rectangle region = pParam.getSourceRegion(); - if (region != null) { - hints.put(ImageTranscoder.KEY_AOI, region); - - // Avoid that the batik transcoder scales the AOI up to original image size - if (size == null) { - hints.put(ImageTranscoder.KEY_WIDTH, (float) region.getWidth()); - hints.put(ImageTranscoder.KEY_HEIGHT, (float) region.getHeight()); - } - else { - // Need to resize here... - double xScale = size.getWidth() / origSize.getWidth(); - double yScale = size.getHeight() / origSize.getHeight(); - - hints.put(ImageTranscoder.KEY_WIDTH, (float) (region.getWidth() * xScale)); - hints.put(ImageTranscoder.KEY_HEIGHT, (float) (region.getHeight() * yScale)); - } - } - else if (size != null) { - // Allow non-uniform scaling - hints.put(ImageTranscoder.KEY_AOI, new Rectangle(origSize)); - } - - // Background color - Paint bg = pParam.getBackgroundColor(); - if (bg != null) { - hints.put(ImageTranscoder.KEY_BACKGROUND_COLOR, bg); - } - - return hints; - } - - private Dimension getSourceRenderSizeFromSubsamping(ImageReadParam pParam, Dimension pOrigSize) { - if (pParam.getSourceXSubsampling() > 1 || pParam.getSourceYSubsampling() > 1) { - return new Dimension((int) (pOrigSize.width / (float) pParam.getSourceXSubsampling()), - (int) (pOrigSize.height / (float) pParam.getSourceYSubsampling())); - } - return null; - } - - public SVGReadParam getDefaultReadParam() { - return new SVGReadParam(); - } - - public int getWidth(int pIndex) throws IOException { - checkBounds(pIndex); - try { - return rasterizer.getDefaultWidth(); - } - catch (TranscoderException e) { - throw new IIOException(e.getMessage(), e); - } - } - - public int getHeight(int pIndex) throws IOException { - checkBounds(pIndex); - try { - return rasterizer.getDefaultHeight(); - } - catch (TranscoderException e) { - throw new IIOException(e.getMessage(), e); - } - } - - public Iterator getImageTypes(int imageIndex) throws IOException { - return Collections.singleton(ImageTypeSpecifier.createFromRenderedImage(rasterizer.createImage(1, 1))).iterator(); - } - - /** - * An image transcoder that stores the resulting image. - *

- * NOTE: This class includes a lot of copy and paste code from the Batik classes - * and needs major refactoring! - */ - private class Rasterizer extends SVGAbstractTranscoder /*ImageTranscoder*/ { - - private BufferedImage image; - private TranscoderInput transcoderInput; - private float defaultWidth; - private float defaultHeight; - private boolean initialized = false; - private SVGOMDocument document; - private String uri; - private GraphicsNode gvtRoot; - private TranscoderException exception; - private BridgeContext context; - - private BufferedImage createImage(final int width, final int height) { - return ImageUtil.createTransparent(width, height); // BufferedImage.TYPE_INT_ARGB - } - - // This is cheating... We don't fully transcode after all - protected void transcode(Document document, final String uri, final TranscoderOutput output) throws TranscoderException { - // Sets up root, curTxf & curAoi - // ---- - if (document != null) { - if (!(document.getImplementation() instanceof SVGDOMImplementation)) { - DOMImplementation impl = (DOMImplementation) hints.get(KEY_DOM_IMPLEMENTATION); - document = DOMUtilities.deepCloneDocument(document, impl); - } - - if (uri != null) { - try { - URL url = new URL(uri); - ((SVGOMDocument) document).setURLObject(url); - } - catch (MalformedURLException ignore) { - } - } - } - - ctx = createBridgeContext(); - SVGOMDocument svgDoc = (SVGOMDocument) document; - - // build the GVT tree - builder = new GVTBuilder(); - // flag that indicates if the document is dynamic - boolean isDynamic = - (hints.containsKey(KEY_EXECUTE_ONLOAD) && - (Boolean) hints.get(KEY_EXECUTE_ONLOAD) && - BaseScriptingEnvironment.isDynamicDocument(ctx, svgDoc)); - - if (isDynamic) { - ctx.setDynamicState(BridgeContext.DYNAMIC); - } - - // Modified code below: - GraphicsNode root = null; - try { - root = builder.build(ctx, svgDoc); - } - catch (BridgeException ex) { - // Note: This might fail, but we STILL have the dimensions we need - // However, we need to reparse later... - exception = new TranscoderException(ex); - } - - // ---- - - // get the 'width' and 'height' attributes of the SVG document - Dimension2D docSize = ctx.getDocumentSize(); - if (docSize != null) { - defaultWidth = (float) docSize.getWidth(); - defaultHeight = (float) docSize.getHeight(); - } - else { - defaultWidth = 200; - defaultHeight = 200; - } - - // Hack to work around exception above - if (root != null) { - gvtRoot = root; - } - this.document = svgDoc; - this.uri = uri; - - // Hack to avoid the transcode method wacking my context... - context = ctx; - ctx = null; - } - - private BufferedImage readImage() throws TranscoderException { - init(); - - if (abortRequested()) { - processReadAborted(); - return null; - } - - processImageProgress(10f); - - // Hacky workaround below... - if (gvtRoot == null) { - // Try to reparse, if we had no URI last time... - if (uri != transcoderInput.getURI()) { - try { - context.dispose(); - document.setURLObject(new URL(transcoderInput.getURI())); - transcode(document, transcoderInput.getURI(), null); - } - catch (MalformedURLException ignore) { - // Ignored - } - } - - if (gvtRoot == null) { - throw exception; - } - } - ctx = context; - // /Hacky - - if (abortRequested()) { - processReadAborted(); - return null; - } - processImageProgress(20f); - - // ---- - SVGSVGElement root = document.getRootElement(); - // ---- - - - // ---- - setImageSize(defaultWidth, defaultHeight); - - if (abortRequested()) { - processReadAborted(); - return null; - } - processImageProgress(40f); - - // compute the preserveAspectRatio matrix - AffineTransform Px; - String ref = new ParsedURL(uri).getRef(); - - try { - Px = ViewBox.getViewTransform(ref, root, width, height, null); - - } - catch (BridgeException ex) { - throw new TranscoderException(ex); - } - - if (Px.isIdentity() && (width != defaultWidth || height != defaultHeight)) { - // The document has no viewBox, we need to resize it by hand. - // we want to keep the document size ratio - float xscale, yscale; - xscale = width / defaultWidth; - yscale = height / defaultHeight; - float scale = Math.min(xscale, yscale); - Px = AffineTransform.getScaleInstance(scale, scale); - } - // take the AOI into account if any - if (hints.containsKey(KEY_AOI)) { - Rectangle2D aoi = (Rectangle2D) hints.get(KEY_AOI); - // transform the AOI into the image's coordinate system - aoi = Px.createTransformedShape(aoi).getBounds2D(); - AffineTransform Mx = new AffineTransform(); - double sx = width / aoi.getWidth(); - double sy = height / aoi.getHeight(); - Mx.scale(sx, sy); - double tx = -aoi.getX(); - double ty = -aoi.getY(); - Mx.translate(tx, ty); - // take the AOI transformation matrix into account - // we apply first the preserveAspectRatio matrix - Px.preConcatenate(Mx); - curAOI = aoi; - } - else { - curAOI = new Rectangle2D.Float(0, 0, width, height); - } - - if (abortRequested()) { - processReadAborted(); - return null; - } - processImageProgress(50f); - - CanvasGraphicsNode cgn = getCanvasGraphicsNode(gvtRoot); - if (cgn != null) { - cgn.setViewingTransform(Px); - curTxf = new AffineTransform(); - } - else { - curTxf = Px; - } - - try { - // dispatch an 'onload' event if needed - if (ctx.isDynamic()) { - BaseScriptingEnvironment se; - se = new BaseScriptingEnvironment(ctx); - se.loadScripts(); - se.dispatchSVGLoadEvent(); - } - } - catch (BridgeException ex) { - throw new TranscoderException(ex); - } - - this.root = gvtRoot; - // ---- - - // NOTE: The code below is copied and pasted from the Batik - // ImageTranscoder class' transcode() method: - - // prepare the image to be painted - int w = (int) (width + 0.5); - int h = (int) (height + 0.5); - - // paint the SVG document using the bridge package - // create the appropriate renderer - ImageRendererFactory rendFactory = new ConcreteImageRendererFactory(); - // ImageRenderer renderer = rendFactory.createDynamicImageRenderer(); - ImageRenderer renderer = rendFactory.createStaticImageRenderer(); - renderer.updateOffScreen(w, h); - renderer.setTransform(curTxf); - renderer.setTree(this.root); - this.root = null; // We're done with it... - - if (abortRequested()) { - processReadAborted(); - return null; - } - processImageProgress(75f); - - try { - // now we are sure that the aoi is the image size - Shape raoi = new Rectangle2D.Float(0, 0, width, height); - // Warning: the renderer's AOI must be in user space - renderer.repaint(curTxf.createInverse().createTransformedShape(raoi)); - // NOTE: repaint above cause nullpointer exception with fonts..??? - - BufferedImage rend = renderer.getOffScreen(); - renderer = null; // We're done with it... - - BufferedImage dest = createImage(w, h); - - Graphics2D g2d = GraphicsUtil.createGraphics(dest); - try { - if (hints.containsKey(ImageTranscoder.KEY_BACKGROUND_COLOR)) { - Paint bgcolor = (Paint) hints.get(ImageTranscoder.KEY_BACKGROUND_COLOR); - g2d.setComposite(AlphaComposite.SrcOver); - g2d.setPaint(bgcolor); - g2d.fillRect(0, 0, w, h); - } - - if (rend != null) { // might be null if the svg document is empty - g2d.drawRenderedImage(rend, new AffineTransform()); - } - } - finally { - if (g2d != null) { - g2d.dispose(); - } - } - - if (abortRequested()) { - processReadAborted(); - return null; - } - processImageProgress(99f); - - return dest; - } - catch (Exception ex) { - TranscoderException exception = new TranscoderException(ex.getMessage()); - exception.initCause(ex); - throw exception; - } - finally { - if (context != null) { - context.dispose(); - } - } - } - - private synchronized void init() throws TranscoderException { - if (!initialized) { - if (transcoderInput == null) { - throw new IllegalStateException("input == null"); - } - - initialized = true; - - super.transcode(transcoderInput, null); - } - } - - private BufferedImage getImage() throws TranscoderException { - if (image == null) { - image = readImage(); - } - - return image; - } - - int getDefaultWidth() throws TranscoderException { - init(); - return (int) Math.ceil(defaultWidth); - } - - int getDefaultHeight() throws TranscoderException { - init(); - return (int) Math.ceil(defaultHeight); - } - - void setInput(final TranscoderInput pInput) { - transcoderInput = pInput; - } - - @Override - protected UserAgent createUserAgent() { - return new SVGImageReaderUserAgent(); - } - - private class SVGImageReaderUserAgent extends SVGAbstractTranscoderUserAgent { - @Override - public void displayError(Exception e) { - displayError(e.getMessage()); - } - - @Override - public void displayError(String message) { - displayMessage(message); - } - - @Override - public void displayMessage(String message) { - processWarningOccurred(message.replaceAll("[\\r\\n]+", " ")); - } - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.svg; + +import com.twelvemonkeys.image.ImageUtil; +import com.twelvemonkeys.imageio.ImageReaderBase; +import com.twelvemonkeys.imageio.util.IIOUtil; +import org.apache.batik.anim.dom.SVGDOMImplementation; +import org.apache.batik.anim.dom.SVGOMDocument; +import org.apache.batik.bridge.*; +import org.apache.batik.dom.util.DOMUtilities; +import org.apache.batik.ext.awt.image.GraphicsUtil; +import org.apache.batik.gvt.CanvasGraphicsNode; +import org.apache.batik.gvt.GraphicsNode; +import org.apache.batik.gvt.renderer.ConcreteImageRendererFactory; +import org.apache.batik.gvt.renderer.ImageRenderer; +import org.apache.batik.gvt.renderer.ImageRendererFactory; +import org.apache.batik.transcoder.*; +import org.apache.batik.transcoder.image.ImageTranscoder; +import org.apache.batik.util.ParsedURL; +import org.w3c.dom.DOMImplementation; +import org.w3c.dom.Document; +import org.w3c.dom.svg.SVGSVGElement; + +import javax.imageio.IIOException; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.spi.ImageReaderSpi; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; + +/** + * Image reader for SVG document fragments. + * + * @author Harald Kuhr + * @author Inpspired by code from the Batik Team + * @version $Id: $ + * @see batik-dev + */ +public class SVGImageReader extends ImageReaderBase { + private Rasterizer rasterizer; + + /** + * Creates an {@code SVGImageReader}. + * + * @param pProvider the provider + */ + public SVGImageReader(final ImageReaderSpi pProvider) { + super(pProvider); + } + + protected void resetMembers() { + rasterizer = new Rasterizer(); + } + + @Override + public void dispose() { + super.dispose(); + rasterizer = null; + } + + @Override + public void setInput(Object pInput, boolean seekForwardOnly, boolean ignoreMetadata) { + super.setInput(pInput, seekForwardOnly, ignoreMetadata); + + if (imageInput != null) { + TranscoderInput input = new TranscoderInput(IIOUtil.createStreamAdapter(imageInput)); + rasterizer.setInput(input); + } + } + + public BufferedImage read(int pIndex, ImageReadParam pParam) throws IOException { + checkBounds(pIndex); + + if (pParam instanceof SVGReadParam) { + SVGReadParam svgParam = (SVGReadParam) pParam; + + // Get the base URI + // This must be done before converting the params to hints + String baseURI = svgParam.getBaseURI(); + rasterizer.transcoderInput.setURI(baseURI); + + // Set ImageReadParams as hints + // Note: The cast to Map invokes a different method that preserves + // unset defaults, DO NOT REMOVE! + rasterizer.setTranscodingHints((Map) paramsToHints(svgParam)); + } + + Dimension size = null; + if (pParam != null) { + size = pParam.getSourceRenderSize(); + } + if (size == null) { + size = new Dimension(getWidth(pIndex), getHeight(pIndex)); + } + + BufferedImage destination = getDestination(pParam, getImageTypes(pIndex), size.width, size.height); + + // Read in the image, using the Batik Transcoder + try { + processImageStarted(pIndex); + + BufferedImage image = rasterizer.getImage(); + + Graphics2D g = destination.createGraphics(); + try { + g.setComposite(AlphaComposite.Src); + g.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); + g.drawImage(image, 0, 0, null); // TODO: Dest offset? + } + finally { + g.dispose(); + } + + processImageComplete(); + + return destination; + } + catch (TranscoderException e) { + Throwable cause = unwrapException(e); + throw new IIOException(cause.getMessage(), cause); + } + } + + private static Throwable unwrapException(TranscoderException ex) { + // The TranscoderException is generally useless... + return ex.getException() != null ? ex.getException() : ex; + } + + private TranscodingHints paramsToHints(SVGReadParam pParam) throws IOException { + TranscodingHints hints = new TranscodingHints(); + // Note: We must allow generic ImageReadParams, so converting to + // TanscodingHints should be done outside the SVGReadParam class. + + // Set dimensions + Dimension size = pParam.getSourceRenderSize(); + Dimension origSize = new Dimension(getWidth(0), getHeight(0)); + if (size == null) { + // SVG is not a pixel based format, but we'll scale it, according to + // the subsampling for compatibility + size = getSourceRenderSizeFromSubsamping(pParam, origSize); + } + + if (size != null) { + hints.put(ImageTranscoder.KEY_WIDTH, (float) size.getWidth()); + hints.put(ImageTranscoder.KEY_HEIGHT, (float) size.getHeight()); + } + + // Set area of interest + Rectangle region = pParam.getSourceRegion(); + if (region != null) { + hints.put(ImageTranscoder.KEY_AOI, region); + + // Avoid that the batik transcoder scales the AOI up to original image size + if (size == null) { + hints.put(ImageTranscoder.KEY_WIDTH, (float) region.getWidth()); + hints.put(ImageTranscoder.KEY_HEIGHT, (float) region.getHeight()); + } + else { + // Need to resize here... + double xScale = size.getWidth() / origSize.getWidth(); + double yScale = size.getHeight() / origSize.getHeight(); + + hints.put(ImageTranscoder.KEY_WIDTH, (float) (region.getWidth() * xScale)); + hints.put(ImageTranscoder.KEY_HEIGHT, (float) (region.getHeight() * yScale)); + } + } + else if (size != null) { + // Allow non-uniform scaling + hints.put(ImageTranscoder.KEY_AOI, new Rectangle(origSize)); + } + + // Background color + Paint bg = pParam.getBackgroundColor(); + if (bg != null) { + hints.put(ImageTranscoder.KEY_BACKGROUND_COLOR, bg); + } + + return hints; + } + + private Dimension getSourceRenderSizeFromSubsamping(ImageReadParam pParam, Dimension pOrigSize) { + if (pParam.getSourceXSubsampling() > 1 || pParam.getSourceYSubsampling() > 1) { + return new Dimension((int) (pOrigSize.width / (float) pParam.getSourceXSubsampling()), + (int) (pOrigSize.height / (float) pParam.getSourceYSubsampling())); + } + return null; + } + + public SVGReadParam getDefaultReadParam() { + return new SVGReadParam(); + } + + public int getWidth(int pIndex) throws IOException { + checkBounds(pIndex); + try { + return rasterizer.getDefaultWidth(); + } + catch (TranscoderException e) { + throw new IIOException(e.getMessage(), e); + } + } + + public int getHeight(int pIndex) throws IOException { + checkBounds(pIndex); + try { + return rasterizer.getDefaultHeight(); + } + catch (TranscoderException e) { + throw new IIOException(e.getMessage(), e); + } + } + + public Iterator getImageTypes(int imageIndex) throws IOException { + return Collections.singleton(ImageTypeSpecifier.createFromRenderedImage(rasterizer.createImage(1, 1))).iterator(); + } + + /** + * An image transcoder that stores the resulting image. + *

+ * NOTE: This class includes a lot of copy and paste code from the Batik classes + * and needs major refactoring! + *

+ */ + private class Rasterizer extends SVGAbstractTranscoder /*ImageTranscoder*/ { + + private BufferedImage image; + private TranscoderInput transcoderInput; + private float defaultWidth; + private float defaultHeight; + private boolean initialized = false; + private SVGOMDocument document; + private String uri; + private GraphicsNode gvtRoot; + private TranscoderException exception; + private BridgeContext context; + + private BufferedImage createImage(final int width, final int height) { + return ImageUtil.createTransparent(width, height); // BufferedImage.TYPE_INT_ARGB + } + + // This is cheating... We don't fully transcode after all + protected void transcode(Document document, final String uri, final TranscoderOutput output) throws TranscoderException { + // Sets up root, curTxf & curAoi + // ---- + if (document != null) { + if (!(document.getImplementation() instanceof SVGDOMImplementation)) { + DOMImplementation impl = (DOMImplementation) hints.get(KEY_DOM_IMPLEMENTATION); + document = DOMUtilities.deepCloneDocument(document, impl); + } + + if (uri != null) { + try { + URL url = new URL(uri); + ((SVGOMDocument) document).setURLObject(url); + } + catch (MalformedURLException ignore) { + } + } + } + + ctx = createBridgeContext(); + SVGOMDocument svgDoc = (SVGOMDocument) document; + + // build the GVT tree + builder = new GVTBuilder(); + // flag that indicates if the document is dynamic + boolean isDynamic = + (hints.containsKey(KEY_EXECUTE_ONLOAD) && + (Boolean) hints.get(KEY_EXECUTE_ONLOAD) && + BaseScriptingEnvironment.isDynamicDocument(ctx, svgDoc)); + + if (isDynamic) { + ctx.setDynamicState(BridgeContext.DYNAMIC); + } + + // Modified code below: + GraphicsNode root = null; + try { + root = builder.build(ctx, svgDoc); + } + catch (BridgeException ex) { + // Note: This might fail, but we STILL have the dimensions we need + // However, we need to reparse later... + exception = new TranscoderException(ex); + } + + // ---- + + // get the 'width' and 'height' attributes of the SVG document + Dimension2D docSize = ctx.getDocumentSize(); + if (docSize != null) { + defaultWidth = (float) docSize.getWidth(); + defaultHeight = (float) docSize.getHeight(); + } + else { + defaultWidth = 200; + defaultHeight = 200; + } + + // Hack to work around exception above + if (root != null) { + gvtRoot = root; + } + this.document = svgDoc; + this.uri = uri; + + // Hack to avoid the transcode method wacking my context... + context = ctx; + ctx = null; + } + + private BufferedImage readImage() throws TranscoderException { + init(); + + if (abortRequested()) { + processReadAborted(); + return null; + } + + processImageProgress(10f); + + // Hacky workaround below... + if (gvtRoot == null) { + // Try to reparse, if we had no URI last time... + if (uri != transcoderInput.getURI()) { + try { + context.dispose(); + document.setURLObject(new URL(transcoderInput.getURI())); + transcode(document, transcoderInput.getURI(), null); + } + catch (MalformedURLException ignore) { + // Ignored + } + } + + if (gvtRoot == null) { + throw exception; + } + } + ctx = context; + // /Hacky + + if (abortRequested()) { + processReadAborted(); + return null; + } + processImageProgress(20f); + + // ---- + SVGSVGElement root = document.getRootElement(); + // ---- + + + // ---- + setImageSize(defaultWidth, defaultHeight); + + if (abortRequested()) { + processReadAborted(); + return null; + } + processImageProgress(40f); + + // compute the preserveAspectRatio matrix + AffineTransform Px; + String ref = new ParsedURL(uri).getRef(); + + try { + Px = ViewBox.getViewTransform(ref, root, width, height, null); + + } + catch (BridgeException ex) { + throw new TranscoderException(ex); + } + + if (Px.isIdentity() && (width != defaultWidth || height != defaultHeight)) { + // The document has no viewBox, we need to resize it by hand. + // we want to keep the document size ratio + float xscale, yscale; + xscale = width / defaultWidth; + yscale = height / defaultHeight; + float scale = Math.min(xscale, yscale); + Px = AffineTransform.getScaleInstance(scale, scale); + } + // take the AOI into account if any + if (hints.containsKey(KEY_AOI)) { + Rectangle2D aoi = (Rectangle2D) hints.get(KEY_AOI); + // transform the AOI into the image's coordinate system + aoi = Px.createTransformedShape(aoi).getBounds2D(); + AffineTransform Mx = new AffineTransform(); + double sx = width / aoi.getWidth(); + double sy = height / aoi.getHeight(); + Mx.scale(sx, sy); + double tx = -aoi.getX(); + double ty = -aoi.getY(); + Mx.translate(tx, ty); + // take the AOI transformation matrix into account + // we apply first the preserveAspectRatio matrix + Px.preConcatenate(Mx); + curAOI = aoi; + } + else { + curAOI = new Rectangle2D.Float(0, 0, width, height); + } + + if (abortRequested()) { + processReadAborted(); + return null; + } + processImageProgress(50f); + + CanvasGraphicsNode cgn = getCanvasGraphicsNode(gvtRoot); + if (cgn != null) { + cgn.setViewingTransform(Px); + curTxf = new AffineTransform(); + } + else { + curTxf = Px; + } + + try { + // dispatch an 'onload' event if needed + if (ctx.isDynamic()) { + BaseScriptingEnvironment se; + se = new BaseScriptingEnvironment(ctx); + se.loadScripts(); + se.dispatchSVGLoadEvent(); + } + } + catch (BridgeException ex) { + throw new TranscoderException(ex); + } + + this.root = gvtRoot; + // ---- + + // NOTE: The code below is copied and pasted from the Batik + // ImageTranscoder class' transcode() method: + + // prepare the image to be painted + int w = (int) (width + 0.5); + int h = (int) (height + 0.5); + + // paint the SVG document using the bridge package + // create the appropriate renderer + ImageRendererFactory rendFactory = new ConcreteImageRendererFactory(); + // ImageRenderer renderer = rendFactory.createDynamicImageRenderer(); + ImageRenderer renderer = rendFactory.createStaticImageRenderer(); + renderer.updateOffScreen(w, h); + renderer.setTransform(curTxf); + renderer.setTree(this.root); + this.root = null; // We're done with it... + + if (abortRequested()) { + processReadAborted(); + return null; + } + processImageProgress(75f); + + try { + // now we are sure that the aoi is the image size + Shape raoi = new Rectangle2D.Float(0, 0, width, height); + // Warning: the renderer's AOI must be in user space + renderer.repaint(curTxf.createInverse().createTransformedShape(raoi)); + // NOTE: repaint above cause nullpointer exception with fonts..??? + + BufferedImage rend = renderer.getOffScreen(); + renderer = null; // We're done with it... + + BufferedImage dest = createImage(w, h); + + Graphics2D g2d = GraphicsUtil.createGraphics(dest); + try { + if (hints.containsKey(ImageTranscoder.KEY_BACKGROUND_COLOR)) { + Paint bgcolor = (Paint) hints.get(ImageTranscoder.KEY_BACKGROUND_COLOR); + g2d.setComposite(AlphaComposite.SrcOver); + g2d.setPaint(bgcolor); + g2d.fillRect(0, 0, w, h); + } + + if (rend != null) { // might be null if the svg document is empty + g2d.drawRenderedImage(rend, new AffineTransform()); + } + } + finally { + if (g2d != null) { + g2d.dispose(); + } + } + + if (abortRequested()) { + processReadAborted(); + return null; + } + processImageProgress(99f); + + return dest; + } + catch (Exception ex) { + TranscoderException exception = new TranscoderException(ex.getMessage()); + exception.initCause(ex); + throw exception; + } + finally { + if (context != null) { + context.dispose(); + } + } + } + + private synchronized void init() throws TranscoderException { + if (!initialized) { + if (transcoderInput == null) { + throw new IllegalStateException("input == null"); + } + + initialized = true; + + super.transcode(transcoderInput, null); + } + } + + private BufferedImage getImage() throws TranscoderException { + if (image == null) { + image = readImage(); + } + + return image; + } + + int getDefaultWidth() throws TranscoderException { + init(); + return (int) Math.ceil(defaultWidth); + } + + int getDefaultHeight() throws TranscoderException { + init(); + return (int) Math.ceil(defaultHeight); + } + + void setInput(final TranscoderInput pInput) { + transcoderInput = pInput; + } + + @Override + protected UserAgent createUserAgent() { + return new SVGImageReaderUserAgent(); + } + + private class SVGImageReaderUserAgent extends SVGAbstractTranscoderUserAgent { + @Override + public void displayError(Exception e) { + displayError(e.getMessage()); + } + + @Override + public void displayError(String message) { + displayMessage(message); + } + + @Override + public void displayMessage(String message) { + processWarningOccurred(message.replaceAll("[\\r\\n]+", " ")); + } + } + } +} diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderSpi.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderSpi.java index edaf1706..15141d80 100755 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderSpi.java +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/svg/SVGImageReaderSpi.java @@ -1,188 +1,187 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.svg; - -import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; -import com.twelvemonkeys.lang.SystemUtil; - -import javax.imageio.ImageReader; -import javax.imageio.spi.ServiceRegistry; -import javax.imageio.stream.ImageInputStream; -import java.io.EOFException; -import java.io.IOException; -import java.util.Locale; - -import static com.twelvemonkeys.imageio.util.IIOUtil.deregisterProvider; - -/** - * SVGImageReaderSpi - *

- * - * @author Harald Kuhr - * @version $Id: SVGImageReaderSpi.java,v 1.1 2003/12/02 16:45:00 haku Exp $ - */ -public final class SVGImageReaderSpi extends ImageReaderSpiBase { - - final static boolean SVG_READER_AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.svg.SVGImageReader", SVGImageReaderSpi.class); - - /** - * Creates an {@code SVGImageReaderSpi}. - */ - @SuppressWarnings("WeakerAccess") - public SVGImageReaderSpi() { - super(new SVGProviderInfo()); - } - - public boolean canDecodeInput(final Object pSource) throws IOException { - return pSource instanceof ImageInputStream && canDecode((ImageInputStream) pSource); - } - - @SuppressWarnings("StatementWithEmptyBody") - private static boolean canDecode(final ImageInputStream pInput) throws IOException { - // NOTE: This test is quite quick as it does not involve any parsing, - // however it may not recognize all kinds of SVG documents. - try { - pInput.mark(); - - // TODO: This is not ok for UTF-16 and other wide encodings - // TODO: Use an XML (encoding) aware Reader instance instead - // Need to figure out pretty fast if this is XML or not - int b; - while (Character.isWhitespace((char) (b = pInput.read()))) { - // Skip over leading WS - } - - // If it's not a tag, this can't be valid XML - if (b != '<') { - return false; - } - - // Algorithm for detecting SVG: - // - Skip until begin tag '<' and read 4 bytes - // - if next is "?" skip until "?>" and start over - // - else if next is "!--" skip until "-->" and start over - // - else if next is "!DOCTYPE " skip any whitespace - // - compare next 3 bytes against "svg", return result - // - else - // - compare next 3 bytes against "svg", return result - - byte[] buffer = new byte[4]; - while (true) { - pInput.readFully(buffer); - - if (buffer[0] == '?') { - // This is the XML declaration or a processing instruction - while (!((pInput.readByte() & 0xFF) == '?' && pInput.read() == '>')) { - // Skip until end of XML declaration or processing instruction or EOF - } - } - else if (buffer[0] == '!') { - if (buffer[1] == '-' && buffer[2] == '-') { - // This is a comment - while (!((pInput.readByte() & 0xFF) == '-' && pInput.read() == '-' && pInput.read() == '>')) { - // Skip until end of comment or EOF - } - } - else if (buffer[1] == 'D' && buffer[2] == 'O' && buffer[3] == 'C' - && pInput.read() == 'T' && pInput.read() == 'Y' - && pInput.read() == 'P' && pInput.read() == 'E') { - // This is the DOCTYPE declaration - while (Character.isWhitespace((char) (b = pInput.read()))) { - // Skip over WS - } - - if (b == 's' && pInput.read() == 'v' && pInput.read() == 'g') { - // It's SVG, identified by DOCTYPE - return true; - } - - // DOCTYPE found, but not SVG - return false; - } - - // Something else, we'll skip - } - else { - // This is a normal tag - if (buffer[0] == 's' && buffer[1] == 'v' && buffer[2] == 'g' - && (Character.isWhitespace((char) buffer[3]) || buffer[3] == ':')) { - // It's SVG, identified by root tag - // TODO: Support svg with prefix + recognize namespace (http://www.w3.org/2000/svg)! - return true; - } - - // If the tag is not "svg", this isn't SVG - return false; - } - - while ((pInput.readByte() & 0xFF) != '<') { - // Skip over, until next begin tag or EOF - } - } - } - catch (EOFException ignore) { - // Possible for small files... - return false; - } - finally { - //noinspection ThrowFromFinallyBlock - pInput.reset(); - } - } - - public ImageReader createReaderInstance(final Object extension) throws IOException { - return new SVGImageReader(this); - } - - public String getDescription(final Locale locale) { - return "Scalable Vector Graphics (SVG) format image reader"; - } - - @SuppressWarnings({"deprecation"}) - @Override - public void onRegistration(final ServiceRegistry registry, final Class category) { - // TODO: Perhaps just try to create an instance, and de-register if we fail? - if (!SVG_READER_AVAILABLE) { - System.err.println("Could not instantiate SVGImageReader (missing support classes)."); - - try { - // NOTE: This will break, but it gives us some useful debug info - new SVGImageReader(this); - } - catch (Throwable t) { - t.printStackTrace(); - } - - deregisterProvider(registry, this, category); - } - } -} - +/* + * 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 of the copyright holder 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 HOLDER 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.svg; + +import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; +import com.twelvemonkeys.lang.SystemUtil; + +import javax.imageio.ImageReader; +import javax.imageio.spi.ServiceRegistry; +import javax.imageio.stream.ImageInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.util.Locale; + +import static com.twelvemonkeys.imageio.util.IIOUtil.deregisterProvider; + +/** + * SVGImageReaderSpi + * + * @author Harald Kuhr + * @version $Id: SVGImageReaderSpi.java,v 1.1 2003/12/02 16:45:00 haku Exp $ + */ +public final class SVGImageReaderSpi extends ImageReaderSpiBase { + + final static boolean SVG_READER_AVAILABLE = SystemUtil.isClassAvailable("com.twelvemonkeys.imageio.plugins.svg.SVGImageReader", SVGImageReaderSpi.class); + + /** + * Creates an {@code SVGImageReaderSpi}. + */ + @SuppressWarnings("WeakerAccess") + public SVGImageReaderSpi() { + super(new SVGProviderInfo()); + } + + public boolean canDecodeInput(final Object pSource) throws IOException { + return pSource instanceof ImageInputStream && canDecode((ImageInputStream) pSource); + } + + @SuppressWarnings("StatementWithEmptyBody") + private static boolean canDecode(final ImageInputStream pInput) throws IOException { + // NOTE: This test is quite quick as it does not involve any parsing, + // however it may not recognize all kinds of SVG documents. + try { + pInput.mark(); + + // TODO: This is not ok for UTF-16 and other wide encodings + // TODO: Use an XML (encoding) aware Reader instance instead + // Need to figure out pretty fast if this is XML or not + int b; + while (Character.isWhitespace((char) (b = pInput.read()))) { + // Skip over leading WS + } + + // If it's not a tag, this can't be valid XML + if (b != '<') { + return false; + } + + // Algorithm for detecting SVG: + // - Skip until begin tag '<' and read 4 bytes + // - if next is "?" skip until "?>" and start over + // - else if next is "!--" skip until "-->" and start over + // - else if next is "!DOCTYPE " skip any whitespace + // - compare next 3 bytes against "svg", return result + // - else + // - compare next 3 bytes against "svg", return result + + byte[] buffer = new byte[4]; + while (true) { + pInput.readFully(buffer); + + if (buffer[0] == '?') { + // This is the XML declaration or a processing instruction + while (!((pInput.readByte() & 0xFF) == '?' && pInput.read() == '>')) { + // Skip until end of XML declaration or processing instruction or EOF + } + } + else if (buffer[0] == '!') { + if (buffer[1] == '-' && buffer[2] == '-') { + // This is a comment + while (!((pInput.readByte() & 0xFF) == '-' && pInput.read() == '-' && pInput.read() == '>')) { + // Skip until end of comment or EOF + } + } + else if (buffer[1] == 'D' && buffer[2] == 'O' && buffer[3] == 'C' + && pInput.read() == 'T' && pInput.read() == 'Y' + && pInput.read() == 'P' && pInput.read() == 'E') { + // This is the DOCTYPE declaration + while (Character.isWhitespace((char) (b = pInput.read()))) { + // Skip over WS + } + + if (b == 's' && pInput.read() == 'v' && pInput.read() == 'g') { + // It's SVG, identified by DOCTYPE + return true; + } + + // DOCTYPE found, but not SVG + return false; + } + + // Something else, we'll skip + } + else { + // This is a normal tag + if (buffer[0] == 's' && buffer[1] == 'v' && buffer[2] == 'g' + && (Character.isWhitespace((char) buffer[3]) || buffer[3] == ':')) { + // It's SVG, identified by root tag + // TODO: Support svg with prefix + recognize namespace (http://www.w3.org/2000/svg)! + return true; + } + + // If the tag is not "svg", this isn't SVG + return false; + } + + while ((pInput.readByte() & 0xFF) != '<') { + // Skip over, until next begin tag or EOF + } + } + } + catch (EOFException ignore) { + // Possible for small files... + return false; + } + finally { + //noinspection ThrowFromFinallyBlock + pInput.reset(); + } + } + + public ImageReader createReaderInstance(final Object extension) throws IOException { + return new SVGImageReader(this); + } + + public String getDescription(final Locale locale) { + return "Scalable Vector Graphics (SVG) format image reader"; + } + + @SuppressWarnings({"deprecation"}) + @Override + public void onRegistration(final ServiceRegistry registry, final Class category) { + // TODO: Perhaps just try to create an instance, and de-register if we fail? + if (!SVG_READER_AVAILABLE) { + System.err.println("Could not instantiate SVGImageReader (missing support classes)."); + + try { + // NOTE: This will break, but it gives us some useful debug info + new SVGImageReader(this); + } + catch (Throwable t) { + t.printStackTrace(); + } + + deregisterProvider(registry, this, category); + } + } +} + diff --git a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java index ea4b3a25..af957044 100755 --- a/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java +++ b/imageio/imageio-batik/src/main/java/com/twelvemonkeys/imageio/plugins/wmf/WMFImageReaderSpi.java @@ -1,102 +1,101 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.wmf; - -import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; -import com.twelvemonkeys.imageio.util.IIOUtil; - -import javax.imageio.ImageReader; -import javax.imageio.spi.ServiceRegistry; -import javax.imageio.stream.ImageInputStream; -import java.io.IOException; -import java.util.Locale; - -import static com.twelvemonkeys.imageio.plugins.wmf.WMFProviderInfo.WMF_READER_AVAILABLE; - -/** - * WMFImageReaderSpi - *

- * - * @author Harald Kuhr - * @version $Id: WMFImageReaderSpi.java,v 1.1 2003/12/02 16:45:00 wmhakur Exp $ - */ -public final class WMFImageReaderSpi extends ImageReaderSpiBase { - - /** - * Creates a {@code WMFImageReaderSpi}. - */ - public WMFImageReaderSpi() { - super(new WMFProviderInfo()); - } - - public boolean canDecodeInput(final Object source) throws IOException { - return source instanceof ImageInputStream && WMF_READER_AVAILABLE && canDecode((ImageInputStream) source); - } - - public static boolean canDecode(final ImageInputStream pInput) throws IOException { - if (pInput == null) { - throw new IllegalArgumentException("input == null"); - } - - try { - pInput.mark(); - - for (byte header : WMF.HEADER) { - int read = (byte) pInput.read(); - if (header != read) { - return false; - } - } - return true; - - } - finally { - pInput.reset(); - } - } - - public ImageReader createReaderInstance(final Object extension) throws IOException { - return new WMFImageReader(this); - } - - public String getDescription(final Locale locale) { - return "Windows Meta File (WMF) image reader"; - } - - @SuppressWarnings({"deprecation"}) - @Override - public void onRegistration(final ServiceRegistry registry, final Class category) { - if (!WMF_READER_AVAILABLE) { - IIOUtil.deregisterProvider(registry, this, category); - } - } -} - +/* + * 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 of the copyright holder 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 HOLDER 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.wmf; + +import com.twelvemonkeys.imageio.spi.ImageReaderSpiBase; +import com.twelvemonkeys.imageio.util.IIOUtil; + +import javax.imageio.ImageReader; +import javax.imageio.spi.ServiceRegistry; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.util.Locale; + +import static com.twelvemonkeys.imageio.plugins.wmf.WMFProviderInfo.WMF_READER_AVAILABLE; + +/** + * WMFImageReaderSpi + * + * @author Harald Kuhr + * @version $Id: WMFImageReaderSpi.java,v 1.1 2003/12/02 16:45:00 wmhakur Exp $ + */ +public final class WMFImageReaderSpi extends ImageReaderSpiBase { + + /** + * Creates a {@code WMFImageReaderSpi}. + */ + public WMFImageReaderSpi() { + super(new WMFProviderInfo()); + } + + public boolean canDecodeInput(final Object source) throws IOException { + return source instanceof ImageInputStream && WMF_READER_AVAILABLE && canDecode((ImageInputStream) source); + } + + public static boolean canDecode(final ImageInputStream pInput) throws IOException { + if (pInput == null) { + throw new IllegalArgumentException("input == null"); + } + + try { + pInput.mark(); + + for (byte header : WMF.HEADER) { + int read = (byte) pInput.read(); + if (header != read) { + return false; + } + } + return true; + + } + finally { + pInput.reset(); + } + } + + public ImageReader createReaderInstance(final Object extension) throws IOException { + return new WMFImageReader(this); + } + + public String getDescription(final Locale locale) { + return "Windows Meta File (WMF) image reader"; + } + + @SuppressWarnings({"deprecation"}) + @Override + public void onRegistration(final ServiceRegistry registry, final Class category) { + if (!WMF_READER_AVAILABLE) { + IIOUtil.deregisterProvider(registry, this, category); + } + } +} + diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/AbstractRLEDecoder.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/AbstractRLEDecoder.java index 9a43cc8c..69676e68 100644 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/AbstractRLEDecoder.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/AbstractRLEDecoder.java @@ -40,7 +40,6 @@ import java.util.Arrays; /** * Abstract base class for RLE decoding as specified by in the Windows BMP (aka DIB) file format. - *

* * @author Harald Kuhr * @version $Id: AbstractRLEDecoder.java#1 $ diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBHeader.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBHeader.java index 387a618b..d58586bd 100755 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBHeader.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBHeader.java @@ -231,10 +231,11 @@ abstract class DIBHeader { /** * OS/2 BitmapCoreHeader Version 2. - *

+ *

* NOTE: According to the docs this header is variable size. * However, it seems that the size is either 16, 40 or 64, which is covered * (40 is the size of the normal {@link BitmapInfoHeader}, and has the same layout). + *

* * @see OS/2 Bitmap File Format Summary */ @@ -290,9 +291,10 @@ abstract class DIBHeader { * Represents the DIB (Device Independent Bitmap) Windows 3.0 Bitmap Information header structure. * This is the common format for persistent DIB structures, even if Windows * may use the later versions at run-time. - *

+ *

* Note: Some variations that includes the mask fields into the header size exists, * but is no longer part of the documented spec. + *

* * @author Harald Kuhr * @version $Id: DIBHeader.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBImageReader.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBImageReader.java index 3773dad8..3e692dfe 100644 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBImageReader.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/DIBImageReader.java @@ -48,14 +48,13 @@ import java.awt.image.*; import java.io.File; import java.io.IOException; import java.nio.ByteOrder; -import java.util.*; import java.util.List; +import java.util.*; /** * 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$ diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/Directory.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/Directory.java index e31000ba..988b65f7 100755 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/Directory.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/Directory.java @@ -37,7 +37,6 @@ import java.util.List; /** * Directory - *

* * @author Harald Kuhr * @version $Id: Directory.java,v 1.0 25.feb.2006 00:29:44 haku Exp$ diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/RLE4Decoder.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/RLE4Decoder.java index 4e70ff94..60e08842 100644 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/RLE4Decoder.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/RLE4Decoder.java @@ -36,7 +36,6 @@ import java.util.Arrays; /** * Implements 4 bit RLE decoding as specified by in the Windows BMP (aka DIB) file format. - *

* * @author Harald Kuhr * @version $Id: RLE4Decoder.java#1 $ diff --git a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/RLE8Decoder.java b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/RLE8Decoder.java index 2e216a42..fee3de1e 100644 --- a/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/RLE8Decoder.java +++ b/imageio/imageio-bmp/src/main/java/com/twelvemonkeys/imageio/plugins/bmp/RLE8Decoder.java @@ -36,7 +36,6 @@ import java.util.Arrays; /** * Implements 8 bit RLE decoding as specified by in the Windows BMP (aka DIB) file format. - *

* * @author Harald Kuhr * @version $Id: RLE8Decoder.java#1 $ diff --git a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java index cafdd327..7ebb3721 100644 --- a/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java +++ b/imageio/imageio-core/src/main/java/com/twelvemonkeys/imageio/ImageReaderBase.java @@ -72,12 +72,13 @@ public abstract class ImageReaderBase extends ImageReader { /** * Constructs an {@code ImageReader} and sets its * {@code originatingProvider} field to the supplied value. - *

- *

Subclasses that make use of extensions should provide a + *

+ * Subclasses that make use of extensions should provide a * constructor with signature {@code (ImageReaderSpi, * Object)} in order to retrieve the extension object. If * the extension object is unsuitable, an * {@code IllegalArgumentException} should be thrown. + *

* * @param provider the {@code ImageReaderSpi} that is invoking this constructor, or {@code null}. */ @@ -205,9 +206,10 @@ public abstract class ImageReaderBase extends ImageReader { /** * 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 types}. + *

* * @param param an {@code ImageReadParam} to be used to get * the destination image or image type, or {@code null}. @@ -328,9 +330,10 @@ public abstract class ImageReaderBase extends ImageReader { * Utility method for getting the area of interest (AOI) of an image. * The AOI is defined by the {@link javax.imageio.IIOParam#setSourceRegion(java.awt.Rectangle)} * method. - *

+ *

* Note: If it is possible for the reader to read the AOI directly, such a * method should be used instead, for efficiency. + *

* * @param pImage the image to get AOI from * @param pParam the param optionally specifying the AOI @@ -348,12 +351,14 @@ public abstract class ImageReaderBase extends ImageReader { * The subsampling is defined by the * {@link javax.imageio.IIOParam#setSourceSubsampling(int, int, int, int)} * method. - *

+ *

* NOTE: This method does not take the subsampling offsets into * consideration. - *

+ *

+ *

* Note: If it is possible for the reader to subsample directly, such a * method should be used instead, for efficiency. + *

* * @param pImage the image to subsample * @param pParam the param optionally specifying subsampling 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 e8bb379b..f9e29d1b 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 @@ -60,12 +60,13 @@ public abstract class ImageWriterBase extends ImageWriter { * Constructs an {@code ImageWriter} and sets its * {@code originatingProvider} instance variable to the * supplied value. - *

- *

Subclasses that make use of extensions should provide a + *

+ * Subclasses that make use of extensions should provide a * constructor with signature {@code (ImageWriterSpi, * Object)} in order to retrieve the extension object. If * the extension object is unsuitable, an * {@code IllegalArgumentException} should be thrown. + *

* * @param provider the {@code ImageWriterSpi} that is constructing this object, or {@code null}. */ @@ -145,9 +146,10 @@ public abstract class ImageWriterBase extends ImageWriter { * Utility method for getting the area of interest (AOI) of an image. * The AOI is defined by the {@link javax.imageio.IIOParam#setSourceRegion(java.awt.Rectangle)} * method. - *

+ *

* Note: If it is possible for the writer to write the AOI directly, such a * method should be used instead, for efficiency. + *

* * @param pImage the image to get AOI from * @param pParam the param optionally specifying the AOI @@ -165,12 +167,14 @@ public abstract class ImageWriterBase extends ImageWriter { * The subsampling is defined by the * {@link javax.imageio.IIOParam#setSourceSubsampling(int, int, int, int)} * method. - *

+ *

* NOTE: This method does not take the subsampling offsets into * consideration. - *

+ *

+ *

* Note: If it is possible for the writer to subsample directly, such a * method should be used instead, for efficiency. + *

* * @param pImage the image to subsample * @param pParam the param optionally specifying subsampling 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 index 773ec5a6..49665ba0 100644 --- 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 @@ -50,10 +50,11 @@ 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 profiles. @@ -61,8 +62,10 @@ import java.util.Properties; * can be downloaded from * ICC, * Adobe or other places. - *

+ * *

+ *

* Example property file: + *

*
  * # icc_profiles.properties
  * ADOBE_RGB_1998=/path/to/Adobe RGB 1998.icc
@@ -117,9 +120,10 @@ public final class 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 @@ -245,11 +249,12 @@ public final class ColorSpaces { /** * 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 @@ -277,11 +282,12 @@ public final class ColorSpaces { /** * Tests whether an ICC color profile is valid. * Invalid profiles are 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 profile} if valid. @@ -298,9 +304,10 @@ public final class ColorSpaces { /** * 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. 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 fed0cac8..f9dfea2b 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 @@ -38,9 +38,10 @@ import java.io.InputStream; /** * IIOInputStreamAdapter - *

+ *

* Note: You should always wrap this stream in a {@code BufferedInputStream}. * If not, performance may degrade significantly. + *

* * @author Harald Kuhr * @author last modified by $Author: haraldk$ 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 9d9cf290..fa4dbe20 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 @@ -86,9 +86,10 @@ 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}. 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 448881a1..2115fd99 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 @@ -1,96 +1,95 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 javax.imageio.ImageReader; -import javax.imageio.ImageWriter; -import javax.imageio.event.IIOReadProgressListener; -import javax.imageio.event.IIOWriteProgressListener; - -/** - * ProgressListenerBase - *

- * - * @author Harald Kuhr - * @version $Id: ProgressListenerBase.java,v 1.0 26.aug.2005 14:29:42 haku Exp$ - */ -public abstract class ProgressListenerBase implements IIOReadProgressListener, IIOWriteProgressListener { - protected ProgressListenerBase() { - } - - public void imageComplete(ImageReader source) { - } - - public void imageProgress(ImageReader source, float percentageDone) { - } - - public void imageStarted(ImageReader source, int imageIndex) { - } - - public void readAborted(ImageReader source) { - } - - public void sequenceComplete(ImageReader source) { - } - - public void sequenceStarted(ImageReader source, int minIndex) { - } - - public void thumbnailComplete(ImageReader source) { - } - - public void thumbnailProgress(ImageReader source, float percentageDone) { - } - - public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) { - } - - public void imageComplete(ImageWriter source) { - } - - public void imageProgress(ImageWriter source, float percentageDone) { - } - - public void imageStarted(ImageWriter source, int imageIndex) { - } - - public void thumbnailComplete(ImageWriter source) { - } - - public void thumbnailProgress(ImageWriter source, float percentageDone) { - } - - public void thumbnailStarted(ImageWriter source, int imageIndex, int thumbnailIndex) { - } - - public void writeAborted(ImageWriter source) { - } -} +/* + * 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 of the copyright holder 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 HOLDER 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 javax.imageio.ImageReader; +import javax.imageio.ImageWriter; +import javax.imageio.event.IIOReadProgressListener; +import javax.imageio.event.IIOWriteProgressListener; + +/** + * ProgressListenerBase + * + * @author Harald Kuhr + * @version $Id: ProgressListenerBase.java,v 1.0 26.aug.2005 14:29:42 haku Exp$ + */ +public abstract class ProgressListenerBase implements IIOReadProgressListener, IIOWriteProgressListener { + protected ProgressListenerBase() { + } + + public void imageComplete(ImageReader source) { + } + + public void imageProgress(ImageReader source, float percentageDone) { + } + + public void imageStarted(ImageReader source, int imageIndex) { + } + + public void readAborted(ImageReader source) { + } + + public void sequenceComplete(ImageReader source) { + } + + public void sequenceStarted(ImageReader source, int minIndex) { + } + + public void thumbnailComplete(ImageReader source) { + } + + public void thumbnailProgress(ImageReader source, float percentageDone) { + } + + public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) { + } + + public void imageComplete(ImageWriter source) { + } + + public void imageProgress(ImageWriter source, float percentageDone) { + } + + public void imageStarted(ImageWriter source, int imageIndex) { + } + + public void thumbnailComplete(ImageWriter source) { + } + + public void thumbnailProgress(ImageWriter source, float percentageDone) { + } + + public void thumbnailStarted(ImageWriter source, int imageIndex, int thumbnailIndex) { + } + + 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 be67c875..56f968f7 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 @@ -1,101 +1,100 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.FileUtil; -import com.twelvemonkeys.lang.StringUtil; - -import javax.imageio.ImageIO; -import javax.swing.filechooser.FileFilter; -import java.io.File; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * ReaderFileSuffixFilter - *

- * - * @author Harald Kuhr - * @author last modified by $Author: haku$ - * @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 description; - private final Map knownSuffixes = new HashMap(32); - - public ReaderFileSuffixFilter() { - this("Images (all supported input formats)"); - } - - public ReaderFileSuffixFilter(String pDescription) { - description = pDescription; - } - - public boolean accept(File pFile) { - // Directories are always supported - if (pFile.isDirectory()) { - return true; - } - - // See if we have an ImageReader for this suffix - String suffix = FileUtil.getExtension(pFile); - - return !StringUtil.isEmpty(suffix) && hasReaderForSuffix(suffix); - } - - private boolean hasReaderForSuffix(String pSuffix) { - if (knownSuffixes.get(pSuffix) == Boolean.TRUE) { - return true; - } - - try { - // Cahce lookup - Iterator iterator = ImageIO.getImageReadersBySuffix(pSuffix); - - if (iterator.hasNext()) { - knownSuffixes.put(pSuffix, Boolean.TRUE); - return true; - } - else { - knownSuffixes.put(pSuffix, Boolean.FALSE); - return false; - } - } - catch (IllegalArgumentException iae) { - return false; - } - } - - public String getDescription() { - return description; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.FileUtil; +import com.twelvemonkeys.lang.StringUtil; + +import javax.imageio.ImageIO; +import javax.swing.filechooser.FileFilter; +import java.io.File; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * ReaderFileSuffixFilter + * + * @author Harald Kuhr + * @author last modified by $Author: haku$ + * @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 description; + private final Map knownSuffixes = new HashMap(32); + + public ReaderFileSuffixFilter() { + this("Images (all supported input formats)"); + } + + public ReaderFileSuffixFilter(String pDescription) { + description = pDescription; + } + + public boolean accept(File pFile) { + // Directories are always supported + if (pFile.isDirectory()) { + return true; + } + + // See if we have an ImageReader for this suffix + String suffix = FileUtil.getExtension(pFile); + + return !StringUtil.isEmpty(suffix) && hasReaderForSuffix(suffix); + } + + private boolean hasReaderForSuffix(String pSuffix) { + if (knownSuffixes.get(pSuffix) == Boolean.TRUE) { + return true; + } + + try { + // Cahce lookup + Iterator iterator = ImageIO.getImageReadersBySuffix(pSuffix); + + if (iterator.hasNext()) { + knownSuffixes.put(pSuffix, Boolean.TRUE); + return true; + } + else { + knownSuffixes.put(pSuffix, Boolean.FALSE); + return false; + } + } + catch (IllegalArgumentException iae) { + return false; + } + } + + public String getDescription() { + 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 a5d69140..54cb73b9 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 @@ -1,101 +1,100 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.FileUtil; -import com.twelvemonkeys.lang.StringUtil; - -import javax.imageio.ImageIO; -import javax.swing.filechooser.FileFilter; -import java.io.File; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; - -/** - * WriterFileSuffixFilter - *

- * - * @author Harald Kuhr - * @author last modified by $Author: haku$ - * @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 description; - private Map knownSuffixes = new HashMap(32); - - public WriterFileSuffixFilter() { - this("Images (all supported output formats)"); - } - - public WriterFileSuffixFilter(String pDescription) { - description = pDescription; - } - - public boolean accept(File pFile) { - // Directories are always supported - if (pFile.isDirectory()) { - return true; - } - - // Test if we have an ImageWriter for this suffix - String suffix = FileUtil.getExtension(pFile); - - return !StringUtil.isEmpty(suffix) && hasWriterForSuffix(suffix); - } - - private boolean hasWriterForSuffix(String pSuffix) { - if (knownSuffixes.get(pSuffix) == Boolean.TRUE) { - return true; - } - - try { - // Cahce lookup - Iterator iterator = ImageIO.getImageWritersBySuffix(pSuffix); - - if (iterator.hasNext()) { - knownSuffixes.put(pSuffix, Boolean.TRUE); - return true; - } - else { - knownSuffixes.put(pSuffix, Boolean.FALSE); - return false; - } - } - catch (IllegalArgumentException iae) { - return false; - } - } - - public String getDescription() { - return description; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.FileUtil; +import com.twelvemonkeys.lang.StringUtil; + +import javax.imageio.ImageIO; +import javax.swing.filechooser.FileFilter; +import java.io.File; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +/** + * WriterFileSuffixFilter + * + * @author Harald Kuhr + * @author last modified by $Author: haku$ + * @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 description; + private Map knownSuffixes = new HashMap(32); + + public WriterFileSuffixFilter() { + this("Images (all supported output formats)"); + } + + public WriterFileSuffixFilter(String pDescription) { + description = pDescription; + } + + public boolean accept(File pFile) { + // Directories are always supported + if (pFile.isDirectory()) { + return true; + } + + // Test if we have an ImageWriter for this suffix + String suffix = FileUtil.getExtension(pFile); + + return !StringUtil.isEmpty(suffix) && hasWriterForSuffix(suffix); + } + + private boolean hasWriterForSuffix(String pSuffix) { + if (knownSuffixes.get(pSuffix) == Boolean.TRUE) { + return true; + } + + try { + // Cahce lookup + Iterator iterator = ImageIO.getImageWritersBySuffix(pSuffix); + + if (iterator.hasNext()) { + knownSuffixes.put(pSuffix, Boolean.TRUE); + return true; + } + else { + knownSuffixes.put(pSuffix, Boolean.FALSE); + return false; + } + } + catch (IllegalArgumentException iae) { + return false; + } + } + + public String getDescription() { + return description; + } +} diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/RGBE.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/RGBE.java index b64607e1..5dd3f6cd 100644 --- a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/RGBE.java +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/RGBE.java @@ -12,14 +12,15 @@ import java.util.regex.Pattern; * (RGBE_DATA_??? values control this.) Only the mimimal header reading and * writing is implemented. Each routine does error checking and will return * a status value as defined below. This code is intended as a skeleton so - * feel free to modify it to suit your needs.

- *

- * Ported to Java and restructured by Kenneth Russell.
- * posted to http://www.graphics.cornell.edu/~bjw/
- * written by Bruce Walter (bjw@graphics.cornell.edu) 5/26/95
- * based on code written by Greg Ward
- *

- * Source: https://java.net/projects/jogl-demos/sources/svn/content/trunk/src/demos/hdr/RGBE.java + * feel free to modify it to suit your needs. + *

+ * Ported to Java and restructured by Kenneth Russell. + * posted to http://www.graphics.cornell.edu/~bjw/ + * written by Bruce Walter (bjw@graphics.cornell.edu) 5/26/95 + * based on code written by Greg Ward + *

+ * + * @see Source */ final class RGBE { // Flags indicating which fields in a Header are valid diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/DefaultToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/DefaultToneMapper.java index 964bbf17..0e3e0c6a 100644 --- a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/DefaultToneMapper.java +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/DefaultToneMapper.java @@ -32,12 +32,15 @@ package com.twelvemonkeys.imageio.plugins.hdr.tonemap; /** * DefaultToneMapper. - *

+ *

* Normalizes values to range [0...1] using: - * - *

Vout = Vin / (Vin + C)

- * + *

+ *

+ * Vout = Vin / (Vin + C) + *

+ *

* Where C is constant. + *

* * @author Harald Kuhr * @author last modified by $Author: harald.kuhr$ diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/GammaToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/GammaToneMapper.java index b1a05ead..e538714e 100644 --- a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/GammaToneMapper.java +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/GammaToneMapper.java @@ -32,13 +32,16 @@ package com.twelvemonkeys.imageio.plugins.hdr.tonemap; /** * GammaToneMapper. - *

+ *

* Normalizes values to range [0...1] using: - * - *

Vout = A Vin\u03b3

- * + *

+ *

+ * Vout = A Vin\u03b3 + *

+ *

* Where A is constant and \u03b3 is the gamma. - * Values > 1 are clamped. + * Values > 1 are clamped. + *

* * @author Harald Kuhr * @author last modified by $Author: harald.kuhr$ diff --git a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/NullToneMapper.java b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/NullToneMapper.java index a164167a..24b75be2 100644 --- a/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/NullToneMapper.java +++ b/imageio/imageio-hdr/src/main/java/com/twelvemonkeys/imageio/plugins/hdr/tonemap/NullToneMapper.java @@ -32,10 +32,11 @@ package com.twelvemonkeys.imageio.plugins.hdr.tonemap; /** * NullToneMapper. - *

+ *

* This {@code ToneMapper} does *not* normalize or clamp values * to range [0...1], but leaves the values as-is. * Useful for applications that implements custom tone mapping. + *

* * @author Harald Kuhr * @author last modified by $Author: harald.kuhr$ 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 f5a62d87..03186c27 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 @@ -37,7 +37,6 @@ import java.io.IOException; /** * BMHDChunk - *

* * @author Harald Kuhr * @version $Id: BMHDChunk.java,v 1.0 28.feb.2006 00:04:32 haku Exp$ 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 66884eef..fc583a4d 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 @@ -36,7 +36,6 @@ import java.io.IOException; /** * BODYChunk - *

* * @author Harald Kuhr * @version $Id: BODYChunk.java,v 1.0 28.feb.2006 01:25:49 haku Exp$ 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 d61ff5a8..54d09e94 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 @@ -37,7 +37,6 @@ import java.io.IOException; /** * CAMGChunk - *

* * @author Harald Kuhr * @version $Id: CAMGChunk.java,v 1.0 28.feb.2006 02:10:07 haku Exp$ 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 baed7ea3..ba17fdb5 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 @@ -41,7 +41,6 @@ import java.util.Arrays; /** * CMAPChunk - *

* * @author Harald Kuhr * @version $Id: CMAPChunk.java,v 1.0 28.feb.2006 00:38:05 haku Exp$ 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 c5b28d7f..06dc1482 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 @@ -39,7 +39,6 @@ import java.io.IOException; /** * GRABChunk - *

* * @author Harald Kuhr * @version $Id: GRABChunk.java,v 1.0 28.feb.2006 01:55:05 haku Exp$ 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 caaf4b8b..9a46e153 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 @@ -36,7 +36,6 @@ import java.io.IOException; /** * UnknownChunk - *

* * @author Harald Kuhr * @version $Id: UnknownChunk.java,v 1.0 28.feb.2006 00:53:47 haku Exp$ 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 2b3e31b5..1f032c30 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 @@ -32,7 +32,6 @@ package com.twelvemonkeys.imageio.plugins.iff; /** * IFF format constants. - *

* * @author Harald Kuhr * @version $Id: IFF.java,v 1.0 07.mar.2006 15:31:48 haku Exp$ 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 43b22051..5f27a1de 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 @@ -36,7 +36,6 @@ import java.io.IOException; /** * IFFChunk - *

* * @author Harald Kuhr * @version $Id: IFFChunk.java,v 1.0 28.feb.2006 00:00:45 haku Exp$ 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 1485dbc0..b378f48d 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 @@ -56,13 +56,14 @@ import java.util.List; * format (Packed BitMap). * The IFF format (Interchange File Format) is the standard file format * supported by allmost all image software for the Amiga computer. - *

+ *

* This reader supports the original palette-based 1-8 bit formats, including * EHB (Extra Half-Bright), HAM (Hold and Modify), and the more recent "deep" * formats, 8 bit gray, 24 bit RGB and 32 bit ARGB. * Uncompressed and ByteRun1 compressed (run length encoding) files are * supported. - *

+ *

+ *

* Palette based images are read as {@code BufferedImage} of * {@link BufferedImage#TYPE_BYTE_INDEXED TYPE_BYTE_INDEXED} or * {@link BufferedImage#TYPE_BYTE_BINARY BufferedImage#} @@ -73,7 +74,8 @@ import java.util.List; * {@link BufferedImage#TYPE_3BYTE_BGR TYPE_3BYTE_BGR}. * 32 bit true-color images are read as * {@link BufferedImage#TYPE_4BYTE_ABGR TYPE_4BYTE_ABGR}. - *

+ *

+ *

* Issues: HAM and HAM8 (Hold and Modify) formats are converted to RGB (24 bit), * as it seems to be very hard to create an {@code IndexColorModel} subclass * that would correctly describe these formats. @@ -82,10 +84,11 @@ import java.util.List; * HAM8 (8 bits) needs 18 bits storage/pixel, if unpacked to RGB (6 bits/gun). * See Wikipedia: HAM * for more information. - *
+ *
* EHB palette is expanded to an {@link IndexColorModel} with 64 entries. * See Wikipedia: EHB * for more information. + *

* * @author Harald Kuhr * @author last modified by $Author: haku $ diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java index ff776ac1..ea581be9 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFImageReaderSpi.java @@ -39,7 +39,6 @@ import java.util.Locale; /** * IFFImageReaderSpi - *

* * @author Harald Kuhr * @version $Id: IFFImageWriterSpi.java,v 1.0 28.feb.2006 19:21:05 haku Exp$ 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 b48979e8..beb0b38b 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 @@ -50,7 +50,6 @@ import java.io.OutputStream; * Writer for Commodore Amiga (Electronic Arts) IFF ILBM (InterLeaved BitMap) format. * The IFF format (Interchange File Format) is the standard file format * supported by almost all image software for the Amiga computer. - *

* * @author Harald Kuhr * @version $Id: IFFImageWriter.java,v 1.0 02.mar.2006 13:32:30 haku Exp$ 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 fe63302c..02d712fc 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 @@ -39,7 +39,6 @@ import java.util.Locale; /** * IFFImageWriterSpi - *

* * @author Harald Kuhr * @version $Id: IFFImageWriterSpi.java,v 1.0 02.mar.2006 19:21:05 haku Exp$ diff --git a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFUtil.java b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFUtil.java index 2ce66ec1..bad6b4d8 100755 --- a/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFUtil.java +++ b/imageio/imageio-iff/src/main/java/com/twelvemonkeys/imageio/plugins/iff/IFFUtil.java @@ -38,9 +38,10 @@ package com.twelvemonkeys.imageio.plugins.iff; /** * IFFUtil - *

+ *

* Bit rotate methods based on Sue-Ken Yap, "A Fast 90-Degree Bitmap Rotator," * in GRAPHICS GEMS II, James Arvo ed., Academic Press, 1991, ISBN 0-12-064480-0. + *

* * @author Unascribed (C version) * @author Harald Kuhr (Java port) 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 index 81bfc974..cecc9d01 100644 --- 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 @@ -39,8 +39,9 @@ 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$ 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 index 729bec55..9fd5c2b2 100644 --- 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 @@ -70,7 +70,7 @@ import java.util.*; /** * A JPEG {@code ImageReader} implementation based on the JRE {@code JPEGImageReader}, * that adds support and properly handles cases where the JRE version throws exceptions. - *

+ *
* Main features: *

    *
  • Support for YCbCr JPEGs without JFIF segment (converted to RGB, using the embedded ICC profile if applicable)
  • diff --git a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Rational.java b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Rational.java index ef63945b..3788d260 100644 --- a/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Rational.java +++ b/imageio/imageio-metadata/src/main/java/com/twelvemonkeys/imageio/metadata/tiff/Rational.java @@ -40,10 +40,11 @@ package com.twelvemonkeys.imageio.metadata.tiff; * Represents a rational number with a {@code long} numerator and {@code long} denominator. * Rational numbers are stored in reduced form with the sign stored with the numerator. * Rationals are immutable. - *

    + *

    * Adapted from sample code featured in * "Intro to Programming in Java: An Interdisciplinary Approach" (Addison Wesley) * by Robert Sedgewick and Kevin Wayne. Permission granted to redistribute under BSD license. + *

    * * @author Harald Kuhr * @author Robert Sedgewick and Kevin Wayne (original version) 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 00d13ed9..94e34342 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 @@ -48,7 +48,7 @@ import java.nio.charset.Charset; public final class XMPScanner { /** * {@code <?xpacket begin=} - *

    + *

    *

      *
    • * 8-bit (UTF-8): @@ -63,6 +63,7 @@ public final class XMPScanner { *
    • 32-bit encoding (UCS-4): * As 16 bit UCS2, with three 0x00 instead of one.
    • *
    + *

    */ private static final byte[] XMP_PACKET_BEGIN = { 0x3C, 0x3F, 0x78, 0x70, 0x61, 0x63, 0x6B, 0x65, 0x74, 0x20, @@ -81,11 +82,13 @@ public final class XMPScanner { * Scans the given input for an XML metadata packet. * The scanning process involves reading every byte in the file, while searching for an XMP packet. * This process is very inefficient, compared to reading a known file format. - *

    + *

    * NOTE: The XMP Specification says this method of reading an XMP packet - * should be considered a last resort.
    + * should be considered a last resort. + *
    * This is because files may contain multiple XMP packets, some which may be related to embedded resources, * some which may be obsolete (or even incomplete). + *

    * * @param pInput the input to scan. The input may be an {@link javax.imageio.stream.ImageInputStream} or * any object that can be passed to {@link ImageIO#createImageInputStream(Object)}. diff --git a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/BitRotator.java b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/BitRotator.java index 06fc17f1..5b30928f 100755 --- a/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/BitRotator.java +++ b/imageio/imageio-pcx/src/main/java/com/twelvemonkeys/imageio/plugins/pcx/BitRotator.java @@ -32,9 +32,10 @@ package com.twelvemonkeys.imageio.plugins.pcx; /** * IFFUtil - *

    + *

    * Bit rotate methods based on Sue-Ken Yap, "A Fast 90-Degree Bitmap Rotator," * in GRAPHICS GEMS II, James Arvo ed., Academic Press, 1991, ISBN 0-12-064480-0. + *

    * * @author Unascribed (C version) * @author Harald Kuhr (Java port) diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICT.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICT.java index c1ecafad..620dcc82 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICT.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICT.java @@ -32,7 +32,6 @@ package com.twelvemonkeys.imageio.plugins.pict; /** * PICT format constants. - *

    * * @author Harald Kuhr * @version $Id: PICT.java,v 1.0 06.apr.2006 12:53:17 haku Exp$ 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 277ae45b..564f119a 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 @@ -83,7 +83,6 @@ import java.util.List; /** * Reader for Apple Mac Paint Picture (PICT) format. - *

    * * @author Harald Kuhr * @author Kary Främling (original PICT/QuickDraw parsing) @@ -91,7 +90,7 @@ import java.util.List; * @version $Id: PICTReader.java,v 1.0 05.apr.2006 15:20:48 haku Exp$ */ /* - * @todo New paint strategy: Need to have a PEN and a PEN MODE, in addition to BG and PATTERN and PATTERN MODE. + * TODO: New paint strategy: Need to have a PEN and a PEN MODE, in addition to BG and PATTERN and PATTERN MODE. * - These must be set before each frame/paint/invert/erase/fill operation. * This is because there isn't a one-to-one mapping, between Java and PICT drawing. * - Subclass Graphics? @@ -100,8 +99,8 @@ import java.util.List; * - Or methods like frameRect(pen, penmode, penwidth, rect), frameOval(pen, penmode, penwidth, rect), etc? * - Or methods like frameShape(pen, penmode, penwidth, shape), paintShape(pen, penmode, shape) etc?? * QuickDrawContext that wraps an AWT Grpahics, and with methods macthing opcodes, seems like the best fit ATM - * @todo Some MAJOR clean up - * @todo As we now have Graphics2D with more options, support more of the format? + * TODO: Some MAJOR clean up + * TODO: As we now have Graphics2D with more options, support more of the format? */ public final class PICTImageReader extends ImageReaderBase { @@ -328,8 +327,9 @@ public final class PICTImageReader extends ImageReaderBase { * Reads the PICT stream. * The contents of the stream will be drawn onto the supplied graphics * object. - *

    + *

    * If "DEBUG" is true, the elements read are listed on stdout. + *

    * * @param pGraphics the graphics object to draw onto. * 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 6576e9d2..216e0a15 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 @@ -40,7 +40,6 @@ import java.util.Locale; /** * PICTImageReaderSpi - *

    * * @author Harald Kuhr * @version $Id: PICTImageReaderSpi.java,v 1.0 28.feb.2006 19:21:05 haku Exp$ 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 7691490f..dc84b573 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 @@ -77,9 +77,10 @@ import java.io.*; /** * Writer for Apple Mac Paint Picture (PICT) format. - *

    + *

    * Images are stored using the "opDirectBitsRect" opcode, which directly * stores RGB values (using PackBits run-length encoding). + *

    * * @author Kary Främling * @author Harald Kuhr @@ -100,12 +101,13 @@ public final class PICTImageWriter extends ImageWriterBase { * Constructs an {@code ImageWriter} and sets its * {@code originatingProvider} instance variable to the * supplied value. - *

    - *

    Subclasses that make use of extensions should provide a - * constructor with signature {@code (ImageWriterSpi, - *Object)} in order to retrieve the extension object. If + *

    + * Subclasses that make use of extensions should provide a + * constructor with signature {@code (ImageWriterSpi, Object)} + * in order to retrieve the extension object. If * the extension object is unsuitable, an * {@code IllegalArgumentException} should be thrown. + *

    * * @param pProvider the {@code ImageWriterSpi} that * is constructing this object, or {@code null}. diff --git a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriterSpi.java b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriterSpi.java index 5ec68dc6..f710121c 100755 --- a/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriterSpi.java +++ b/imageio/imageio-pict/src/main/java/com/twelvemonkeys/imageio/plugins/pict/PICTImageWriterSpi.java @@ -39,7 +39,6 @@ import java.util.Locale; /** * PICTImageWriterSpi - *

    * * @author Harald Kuhr * @version $Id: PICTImageWriterSpi.java,v 1.0 02.mar.2006 19:21:05 haku Exp$ 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 f80842dd..fd362ccb 100644 --- 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 @@ -40,7 +40,6 @@ import static java.lang.Math.sqrt; /** * Emulates an Apple QuickDraw rendering context, backed by a Java {@link Graphics2D}. - *

    * * @author Harald Kuhr * @version $Id: QuickDrawContext.java,v 1.0 Oct 3, 2007 1:24:35 AM haraldk Exp$ @@ -921,8 +920,9 @@ class QuickDrawContext { /** * CopyBits. - *

    + *

    * Note that the destination is always {@code this}. + *

    * * @param pSrcBitmap the source bitmap to copy pixels from * @param pSrcRect the source rectangle 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 2e9343cf..2c8df8fb 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 @@ -50,8 +50,8 @@ import java.awt.image.*; import java.io.DataInputStream; import java.io.File; import java.io.IOException; -import java.util.*; import java.util.List; +import java.util.*; /** * ImageReader for Adobe Photoshop Document (PSD) format. @@ -59,8 +59,8 @@ import java.util.List; * @author Harald Kuhr * @author last modified by $Author: haraldk$ * @version $Id: PSDImageReader.java,v 1.0 Apr 29, 2008 4:45:52 PM haraldk Exp$ - * @see Adobe Photoshop File Formats Specification - * @see Adobe Photoshop File Format Summary + * @see Adobe Photoshop File Formats Specification + * @see Adobe Photoshop File Format Summary */ // TODO: Implement ImageIO meta data interface // TODO: Figure out of we should assume Adobe RGB (1998) color model, if no embedded profile? 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 cf73d6f7..cf6a9fee 100755 --- 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 @@ -67,9 +67,10 @@ public final class PSDMetadataFormat extends IIOMetadataFormatImpl { /** * Private constructor. - *

    + *

    * The {@link javax.imageio.metadata.IIOMetadata} class will instantiate this class * by reflection, invoking the static {@code getInstance()} method. + *

    * * @see javax.imageio.metadata.IIOMetadata#getMetadataFormat * @see #getInstance() 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 5eb968f7..c7586831 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 @@ -75,8 +75,9 @@ public final class Catalog implements Iterable { /** * Reads the {@code Catalog} entry from the given input stream. - *

    + *

    * The data is assumed to be in little endian byte order. + *

    * * @param pDataInput the input stream * @return a new {@code Catalog} 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 7fdb8036..066ff541 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 @@ -62,7 +62,7 @@ import java.util.SortedSet; * @author last modified by $Author: haku$ * @version $Id: ThumbsDBImageReader.java,v 1.0 22.jan.2007 18:49:38 haku Exp$ * @see com.twelvemonkeys.io.ole2.CompoundDocument - * @see Wikipedia: Thumbs.db */ public final class ThumbsDBImageReader extends ImageReaderBase { private static final int THUMBNAIL_OFFSET = 12; @@ -107,10 +107,11 @@ public final class ThumbsDBImageReader extends ImageReaderBase { /** * Instructs the reader wether it should read and cache alle thumbnails * in sequence, during the first read operation. - *

    + *

    * This is useful mainly if you need to read all the thumbnails, and you * need them in random order, as it requires less repositioning in the * underlying stream. + *

    * * @param pLoadEagerly {@code true} if the reader should read all thumbs on first read */ 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 e73a80eb..0cabeef8 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 @@ -46,7 +46,6 @@ import static com.twelvemonkeys.imageio.util.IIOUtil.lookupProviderByName; /** * ThumbsDBImageReaderSpi - *

    * * @author Harald Kuhr * @version $Id: ThumbsDBImageReaderSpi.java,v 1.0 28.feb.2006 19:21:05 haku Exp$ diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/BigTIFFImageReaderSpi.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/BigTIFFImageReaderSpi.java index f07477d2..05247661 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/BigTIFFImageReaderSpi.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/BigTIFFImageReaderSpi.java @@ -38,9 +38,10 @@ import java.util.Locale; /** * BigTIFFImageReaderSpi. - *

    + *

    * This is a separate service provider for the BigTIFF format, to support * special cases where one does not want BigTIFF support. + *

    * * @author Harald Kuhr * @author last modified by $Author: haraldk$ diff --git a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java index e065b5ae..5519d020 100644 --- a/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java +++ b/imageio/imageio-tiff/src/main/java/com/twelvemonkeys/imageio/plugins/tiff/LZWEncoder.java @@ -39,12 +39,13 @@ import java.util.Arrays; /** * LZWEncoder - *

    + *

    * Inspired by LZWTreeEncoder by Wen Yu and the * algorithm described by Bob Montgomery * which * "[...] uses a tree method to search if a new string is already in the table, * which is much simpler, faster, and easier to understand than hashing." + *

    * * @author Harald Kuhr * @author last modified by $Author: haraldk$ 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 index 59466d61..5f053b05 100644 --- 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 @@ -88,8 +88,9 @@ import static java.util.Arrays.asList; /** * 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
    • diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java index 5c243ad6..1e68ab83 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericFilter.java @@ -1,384 +1,394 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.lang.BeanUtil; - -import javax.servlet.*; -import java.io.IOException; -import java.io.Serializable; -import java.lang.reflect.InvocationTargetException; -import java.util.Enumeration; - -/** - * Defines a generic, protocol-independent filter. - *

      - * {@code GenericFilter} is inspired by {@link GenericServlet}, and - * implements the {@code Filter} and {@code FilterConfig} interfaces. - *

      - * {@code GenericFilter} makes writing filters easier. It provides simple - * versions of the lifecycle methods {@code init} and {@code destroy} - * and of the methods in the {@code FilterConfig} interface. - * {@code GenericFilter} also implements the {@code log} methods, - * declared in the {@code ServletContext} interface. - *

      - * To write a generic filter, you need only override the abstract - * {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * - * @version $Id: GenericFilter.java#1 $ - * - * @see Filter - * @see FilterConfig - */ -public abstract class GenericFilter implements Filter, FilterConfig, Serializable { - // TODO: Rewrite to use ServletConfigurator instead of BeanUtil - - /** - * The filter config. - */ - private transient FilterConfig filterConfig = null; - - /** - * Makes sure the filter runs once per request - *

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

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

      - * @see #isRunOnce - * @see #ATTRIB_RUN_ONCE_EXT - * @see #oncePerRequest - */ - private static final Object ATTRIB_RUN_ONCE_VALUE = new Object(); - - /** - * Indicates if this filter should run once per request ({@code true}), - * or for each forward/include resource ({@code false}). - *

      - * Set this variable to true, to make sure the filter runs once per request. - * - * NOTE: As of Servlet 2.4, this field - * should always be left to it's default value ({@code false}). - *
      - * To run the filter once per request, the {@code filter-mapping} element - * of the web-descriptor should include a {@code dispatcher} element: - *

      <dispatcher>REQUEST</dispatcher>
      - * - */ - protected boolean oncePerRequest = false; - - /** - * Does nothing. - */ - public GenericFilter() {} - - /** - * Called by the web container to indicate to a filter that it is being - * placed into service. - *

      - * This implementation stores the {@code FilterConfig} object it - * receives from the servlet container for later use. - * Generally, there's no reason to override this method, override the - * no-argument {@code init} instead. However, if you are - * overriding this form of the method, - * always call {@code super.init(config)}. - *

      - * This implementation will also set all configured key/value pairs, that - * have a matching setter method annotated with {@link InitParam}. - * - * @param pConfig the filter config - * @throws ServletException if an error occurs during init - * - * @see Filter#init(javax.servlet.FilterConfig) - * @see #init() init - * @see BeanUtil#configure(Object, java.util.Map, boolean) - */ - public void init(final FilterConfig pConfig) throws ServletException { - if (pConfig == null) { - throw new ServletConfigException("filter config == null"); - } - - // Store filter config - filterConfig = pConfig; - - // Configure this - try { - BeanUtil.configure(this, ServletUtil.asMap(pConfig), true); - } - catch (InvocationTargetException e) { - throw new ServletConfigException("Could not configure " + getFilterName(), e.getCause()); - } - - // Create run-once attribute name - attribRunOnce = pConfig.getFilterName() + ATTRIB_RUN_ONCE_EXT; - log("init (oncePerRequest=" + oncePerRequest + ", attribRunOnce=" + attribRunOnce + ")"); - init(); - } - - /** - * A convenience method which can be overridden so that there's no need to - * call {@code super.init(config)}. - * - * @see #init(FilterConfig) - * - * @throws ServletException if an error occurs during init - */ - public void init() throws ServletException {} - - /** - * The {@code doFilter} method of the Filter is called by the container - * each time a request/response pair is passed through the chain due to a - * client request for a resource at the end of the chain. - *

      - * Subclasses should not override this method, but rather the - * abstract {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method. - * - * @param pRequest the servlet request - * @param pResponse the servlet response - * @param pFilterChain the filter chain - * - * @throws IOException - * @throws ServletException - * - * @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(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; - } - - // Do real filter - doFilterImpl(pRequest, pResponse, pFilterChain); - } - - /** - * If request is filtered, returns true, otherwise marks request as filtered - * and returns false. - * A return value of false, indicates that the filter has not yet run. - * A return value of true, indicates that the filter has run for this - * request, and processing should not continue. - *

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

      - * @see #ATTRIB_RUN_ONCE_EXT - * @see #ATTRIB_RUN_ONCE_VALUE - * - * @param pRequest the servlet request - * @return {@code true} if the request is already filtered, otherwise - * {@code false}. - */ - 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(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 #oncePerRequest} member variable. - * - * @param pRequest the servlet request - * @param pResponse the servlet response - * @param pChain the filter chain - * - * @throws IOException if an I/O error occurs - * @throws ServletException if an exception occurs during the filter process - * - * @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; - - /** - * Called by the web container to indicate to a filter that it is being - * taken out of service. - * - * @see Filter#destroy - */ - public void destroy() { - log("destroy"); - filterConfig = null; - } - - /** - * Returns the filter-name of this filter as defined in the deployment - * descriptor. - * - * @return the filter-name - * @see FilterConfig#getFilterName - */ - public String getFilterName() { - return filterConfig.getFilterName(); - } - - /** - * Returns a reference to the {@link ServletContext} in which the caller is - * executing. - * - * @return the {@code ServletContext} object, used by the caller to - * interact with its servlet container - * @see FilterConfig#getServletContext - * @see ServletContext - */ - public ServletContext getServletContext() { - return filterConfig.getServletContext(); - } - - /** - * Returns a {@code String} containing the value of the named - * initialization parameter, or null if the parameter does not exist. - * - * @param pKey a {@code String} specifying the name of the - * initialization parameter - * @return a {@code String} containing the value of the initialization - * parameter - */ - public String getInitParameter(final String pKey) { - return filterConfig.getInitParameter(pKey); - } - - /** - * Returns the names of the servlet's initialization parameters as an - * {@code Enumeration} of {@code String} objects, or an empty - * {@code Enumeration} if the servlet has no initialization parameters. - * - * @return an {@code Enumeration} of {@code String} objects - * containing the mNames of the servlet's initialization parameters - */ - public Enumeration getInitParameterNames() { - return filterConfig.getInitParameterNames(); - } - - /** - * Writes the specified message to a servlet log file, prepended by the - * filter's name. - * - * @param pMessage the log message - * @see ServletContext#log(String) - */ - protected void log(final String pMessage) { - getServletContext().log(getFilterName() + ": " + pMessage); - } - - /** - * Writes an explanatory message and a stack trace for a given - * {@code Throwable} to the servlet log file, prepended by the - * filter's name. - * - * @param pMessage the log message - * @param pThrowable the exception - * @see ServletContext#log(String,Throwable) - */ - protected void log(final String pMessage, final Throwable pThrowable) { - getServletContext().log(getFilterName() + ": " + pMessage, pThrowable); - } - - /** - * Initializes the filter. - * - * @param pFilterConfig the filter config - * @see #init init - * - * @deprecated For compatibility only, use {@link #init init} instead. - */ - @SuppressWarnings("UnusedDeclaration") - public void setFilterConfig(final FilterConfig pFilterConfig) { - try { - init(pFilterConfig); - } - catch (ServletException e) { - log("Error in init(), see stack trace for details.", e); - } - } - - /** - * Gets the {@code FilterConfig} for this filter. - * - * @return the {@code FilterConfig} for this filter - * @see FilterConfig - */ - public FilterConfig getFilterConfig() { - return filterConfig; - } - - /** - * Specifies if this filter should run once per request ({@code true}), - * or for each forward/include resource ({@code false}). - * Called automatically from the {@code init}-method, with settings - * from web.xml. - * - * @param pOncePerRequest {@code true} if the filter should run only - * once per request - * @see #oncePerRequest - */ - @InitParam(name = "once-per-request") - public void setOncePerRequest(final boolean pOncePerRequest) { - oncePerRequest = pOncePerRequest; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.lang.BeanUtil; + +import javax.servlet.*; +import java.io.IOException; +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.util.Enumeration; + +/** + * Defines a generic, protocol-independent filter. + *

      + * {@code GenericFilter} is inspired by {@link GenericServlet}, and + * implements the {@code Filter} and {@code FilterConfig} interfaces. + *

      + *

      + * {@code GenericFilter} makes writing filters easier. It provides simple + * versions of the lifecycle methods {@code init} and {@code destroy} + * and of the methods in the {@code FilterConfig} interface. + * {@code GenericFilter} also implements the {@code log} methods, + * declared in the {@code ServletContext} interface. + *

      + *

      + * {@code GenericFilter} has an auto-init system, that automatically invokes + * the method matching the signature {@code void setX(<Type>)}, + * for every init-parameter {@code x}. Both camelCase and lisp-style parameter + * naming is supported, lisp-style names will be converted to camelCase. + * Parameter values are automatically converted from string representation to + * most basic types, if necessary. + *

      + *

      + * To write a generic filter, you need only override the abstract + * {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method. + *

      + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: GenericFilter.java#1 $ + * + * @see Filter + * @see FilterConfig + */ +public abstract class GenericFilter implements Filter, FilterConfig, Serializable { + // TODO: Rewrite to use ServletConfigurator instead of BeanUtil + + /** + * The filter config. + */ + private transient FilterConfig filterConfig = null; + + /** + * Makes sure the filter runs once per request + * + * @see #isRunOnce + * @see #ATTRIB_RUN_ONCE_VALUE + * @see #oncePerRequest + */ + private final static String ATTRIB_RUN_ONCE_EXT = ".REQUEST_HANDLED"; + + /** + * Makes sure the filter runs once per request. + * Must be configured through init method, as the filter name is not + * available before we have a {@code FilterConfig} object. + * + * @see #isRunOnce + * @see #ATTRIB_RUN_ONCE_VALUE + * @see #oncePerRequest + */ + private String attribRunOnce = null; + + /** + * Makes sure the filter runs once per request + * + * @see #isRunOnce + * @see #ATTRIB_RUN_ONCE_EXT + * @see #oncePerRequest + */ + private static final Object ATTRIB_RUN_ONCE_VALUE = new Object(); + + /** + * Indicates if this filter should run once per request ({@code true}), + * or for each forward/include resource ({@code false}). + *

      + * Set this variable to true, to make sure the filter runs once per request. + *

      + *

      + * NOTE: As of Servlet 2.4, this field + * should always be left to it's default value ({@code false}). + *
      + * To run the filter once per request, the {@code filter-mapping} element + * of the web-descriptor should include a {@code dispatcher} element: + *
      + *

      + *
      <dispatcher>REQUEST</dispatcher>
      + */ + protected boolean oncePerRequest = false; + + /** + * Does nothing. + */ + public GenericFilter() {} + + /** + * Called by the web container to indicate to a filter that it is being + * placed into service. + *

      + * This implementation stores the {@code FilterConfig} object it + * receives from the servlet container for later use. + * Generally, there's no reason to override this method, override the + * no-argument {@code init} instead. However, if you are + * overriding this form of the method, + * always call {@code super.init(config)}. + *

      + *

      + * This implementation will also set all configured key/value pairs, that + * have a matching setter method annotated with {@link InitParam}. + *

      + * + * @param pConfig the filter config + * @throws ServletException if an error occurs during init + * + * @see Filter#init(javax.servlet.FilterConfig) + * @see #init() init + * @see BeanUtil#configure(Object, java.util.Map, boolean) + */ + public void init(final FilterConfig pConfig) throws ServletException { + if (pConfig == null) { + throw new ServletConfigException("filter config == null"); + } + + // Store filter config + filterConfig = pConfig; + + // Configure this + try { + BeanUtil.configure(this, ServletUtil.asMap(pConfig), true); + } + catch (InvocationTargetException e) { + throw new ServletConfigException("Could not configure " + getFilterName(), e.getCause()); + } + + // Create run-once attribute name + attribRunOnce = pConfig.getFilterName() + ATTRIB_RUN_ONCE_EXT; + log("init (oncePerRequest=" + oncePerRequest + ", attribRunOnce=" + attribRunOnce + ")"); + init(); + } + + /** + * A convenience method which can be overridden so that there's no need to + * call {@code super.init(config)}. + * + * @see #init(FilterConfig) + * + * @throws ServletException if an error occurs during init + */ + public void init() throws ServletException {} + + /** + * The {@code doFilter} method of the Filter is called by the container + * each time a request/response pair is passed through the chain due to a + * client request for a resource at the end of the chain. + *

      + * Subclasses should not override this method, but rather the + * abstract {@link #doFilterImpl(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)} doFilterImpl} method. + *

      + * + * @param pRequest the servlet request + * @param pResponse the servlet response + * @param pFilterChain the filter chain + * + * @throws IOException + * @throws ServletException + * + * @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(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; + } + + // Do real filter + doFilterImpl(pRequest, pResponse, pFilterChain); + } + + /** + * If request is filtered, returns true, otherwise marks request as filtered + * and returns false. + * A return value of false, indicates that the filter has not yet run. + * A return value of true, indicates that the filter has run for this + * request, and processing should not continue. + *

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

      + * + * @see #ATTRIB_RUN_ONCE_EXT + * @see #ATTRIB_RUN_ONCE_VALUE + * + * @param pRequest the servlet request + * @return {@code true} if the request is already filtered, otherwise + * {@code false}. + */ + 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(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 #oncePerRequest} member variable. + * + * @param pRequest the servlet request + * @param pResponse the servlet response + * @param pChain the filter chain + * + * @throws IOException if an I/O error occurs + * @throws ServletException if an exception occurs during the filter process + * + * @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; + + /** + * Called by the web container to indicate to a filter that it is being + * taken out of service. + * + * @see Filter#destroy + */ + public void destroy() { + log("destroy"); + filterConfig = null; + } + + /** + * Returns the filter-name of this filter as defined in the deployment + * descriptor. + * + * @return the filter-name + * @see FilterConfig#getFilterName + */ + public String getFilterName() { + return filterConfig.getFilterName(); + } + + /** + * Returns a reference to the {@link ServletContext} in which the caller is + * executing. + * + * @return the {@code ServletContext} object, used by the caller to + * interact with its servlet container + * @see FilterConfig#getServletContext + * @see ServletContext + */ + public ServletContext getServletContext() { + return filterConfig.getServletContext(); + } + + /** + * Returns a {@code String} containing the value of the named + * initialization parameter, or null if the parameter does not exist. + * + * @param pKey a {@code String} specifying the name of the + * initialization parameter + * @return a {@code String} containing the value of the initialization + * parameter + */ + public String getInitParameter(final String pKey) { + return filterConfig.getInitParameter(pKey); + } + + /** + * Returns the names of the servlet's initialization parameters as an + * {@code Enumeration} of {@code String} objects, or an empty + * {@code Enumeration} if the servlet has no initialization parameters. + * + * @return an {@code Enumeration} of {@code String} objects + * containing the mNames of the servlet's initialization parameters + */ + public Enumeration getInitParameterNames() { + return filterConfig.getInitParameterNames(); + } + + /** + * Writes the specified message to a servlet log file, prepended by the + * filter's name. + * + * @param pMessage the log message + * @see ServletContext#log(String) + */ + protected void log(final String pMessage) { + getServletContext().log(getFilterName() + ": " + pMessage); + } + + /** + * Writes an explanatory message and a stack trace for a given + * {@code Throwable} to the servlet log file, prepended by the + * filter's name. + * + * @param pMessage the log message + * @param pThrowable the exception + * @see ServletContext#log(String,Throwable) + */ + protected void log(final String pMessage, final Throwable pThrowable) { + getServletContext().log(getFilterName() + ": " + pMessage, pThrowable); + } + + /** + * Initializes the filter. + * + * @param pFilterConfig the filter config + * @see #init init + * + * @deprecated For compatibility only, use {@link #init init} instead. + */ + @SuppressWarnings("UnusedDeclaration") + public void setFilterConfig(final FilterConfig pFilterConfig) { + try { + init(pFilterConfig); + } + catch (ServletException e) { + log("Error in init(), see stack trace for details.", e); + } + } + + /** + * Gets the {@code FilterConfig} for this filter. + * + * @return the {@code FilterConfig} for this filter + * @see FilterConfig + */ + public FilterConfig getFilterConfig() { + return filterConfig; + } + + /** + * Specifies if this filter should run once per request ({@code true}), + * or for each forward/include resource ({@code false}). + * Called automatically from the {@code init}-method, with settings + * from web.xml. + * + * @param pOncePerRequest {@code true} if the filter should run only + * once per request + * @see #oncePerRequest + */ + @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 343440fc..2e7521b9 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/GenericServlet.java @@ -1,90 +1,93 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.lang.BeanUtil; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import java.lang.reflect.InvocationTargetException; - -/** - * Defines a generic, protocol-independent servlet. - *

      - * {@code GenericServlet} has an auto-init system, that automatically invokes - * the method matching the signature {@code void setX(<Type>)}, - * for every init-parameter {@code x}. Both camelCase and lisp-style parameter - * naming is supported, lisp-style names will be converted to camelCase. - * Parameter values are automatically converted from string representation to - * most basic types, if necessary. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * - * @version $Id: GenericServlet.java#1 $ - */ -public abstract class GenericServlet extends javax.servlet.GenericServlet { - // TODO: Rewrite to use ServletConfigurator instead of BeanUtil - - /** - * Called by the web container to indicate to a servlet that it is being - * placed into service. - *

      - * This implementation stores the {@code ServletConfig} object it - * receives from the servlet container for later use. When overriding this - * form of the method, call {@code super.init(config)}. - *

      - * This implementation will also set all configured key/value pairs, that - * have a matching setter method annotated with {@link InitParam}. - * - * @param pConfig the servlet config - * @throws ServletException - * - * @see javax.servlet.GenericServlet#init - * @see #init() init - * @see BeanUtil#configure(Object, java.util.Map, boolean) - */ - @Override - public void init(final ServletConfig pConfig) throws ServletException { - if (pConfig == null) { - throw new ServletConfigException("servlet config == null"); - } - - try { - BeanUtil.configure(this, ServletUtil.asMap(pConfig), true); - } - catch (InvocationTargetException e) { - throw new ServletConfigException("Could not configure " + getServletName(), e.getCause()); - } - - super.init(pConfig); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.lang.BeanUtil; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import java.lang.reflect.InvocationTargetException; + +/** + * Defines a generic, protocol-independent servlet. + *

      + * {@code GenericServlet} has an auto-init system, that automatically invokes + * the method matching the signature {@code void setX(<Type>)}, + * for every init-parameter {@code x}. Both camelCase and lisp-style parameter + * naming is supported, lisp-style names will be converted to camelCase. + * Parameter values are automatically converted from string representation to + * most basic types, if necessary. + *

      + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: GenericServlet.java#1 $ + */ +public abstract class GenericServlet extends javax.servlet.GenericServlet { + // TODO: Rewrite to use ServletConfigurator instead of BeanUtil + + /** + * Called by the web container to indicate to a servlet that it is being + * placed into service. + *

      + * This implementation stores the {@code ServletConfig} object it + * receives from the servlet container for later use. When overriding this + * form of the method, call {@code super.init(config)}. + *

      + *

      + * This implementation will also set all configured key/value pairs, that + * have a matching setter method annotated with {@link InitParam}. + *

      + * + * @param pConfig the servlet config + * @throws ServletException if the servlet could not be initialized. + * + * @see javax.servlet.GenericServlet#init + * @see #init() init + * @see BeanUtil#configure(Object, java.util.Map, boolean) + */ + @Override + public void init(final ServletConfig pConfig) throws ServletException { + if (pConfig == null) { + throw new ServletConfigException("servlet config == null"); + } + + try { + BeanUtil.configure(this, ServletUtil.asMap(pConfig), true); + } + catch (InvocationTargetException e) { + throw new ServletConfigException("Could not configure " + getServletName(), e.getCause()); + } + + super.init(pConfig); + } +} diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java b/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java index 6302ec92..88b57519 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/HttpServlet.java @@ -1,90 +1,93 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.lang.BeanUtil; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import java.lang.reflect.InvocationTargetException; - -/** - * Defines a generic, HTTP specific servlet. - *

      - * {@code HttpServlet} has an auto-init system, that automatically invokes - * the method matching the signature {@code void setX(<Type>)}, - * for every init-parameter {@code x}. Both camelCase and lisp-style parameter - * naming is supported, lisp-style names will be converted to camelCase. - * Parameter values are automatically converted from string representation to - * most basic types, if necessary. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * - * @version $Id: HttpServlet.java#1 $ - */ -public abstract class HttpServlet extends javax.servlet.http.HttpServlet { - // TODO: Rewrite to use ServletConfigurator instead of BeanUtil - - /** - * Called by the web container to indicate to a servlet that it is being - * placed into service. - *

      - * This implementation stores the {@code ServletConfig} object it - * receives from the servlet container for later use. When overriding this - * form of the method, call {@code super.init(config)}. - *

      - * This implementation will also set all configured key/value pairs, that - * have a matching setter method annotated with {@link InitParam}. - * - * @param pConfig the servlet config - * @throws ServletException if an error occurred during init - * - * @see javax.servlet.GenericServlet#init - * @see #init() init - * @see BeanUtil#configure(Object, java.util.Map, boolean) - */ - @Override - public void init(ServletConfig pConfig) throws ServletException { - if (pConfig == null) { - throw new ServletConfigException("servlet config == null"); - } - - try { - BeanUtil.configure(this, ServletUtil.asMap(pConfig), true); - } - catch (InvocationTargetException e) { - throw new ServletConfigException("Could not configure " + getServletName(), e.getCause()); - } - - super.init(pConfig); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.lang.BeanUtil; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import java.lang.reflect.InvocationTargetException; + +/** + * Defines a generic, HTTP specific servlet. + *

      + * {@code HttpServlet} has an auto-init system, that automatically invokes + * the method matching the signature {@code void setX(<Type>)}, + * for every init-parameter {@code x}. Both camelCase and lisp-style parameter + * naming is supported, lisp-style names will be converted to camelCase. + * Parameter values are automatically converted from string representation to + * most basic types, if necessary. + *

      + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: HttpServlet.java#1 $ + */ +public abstract class HttpServlet extends javax.servlet.http.HttpServlet { + // TODO: Rewrite to use ServletConfigurator instead of BeanUtil + + /** + * Called by the web container to indicate to a servlet that it is being + * placed into service. + *

      + * This implementation stores the {@code ServletConfig} object it + * receives from the servlet container for later use. When overriding this + * form of the method, call {@code super.init(config)}. + *

      + *

      + * This implementation will also set all configured key/value pairs, that + * have a matching setter method annotated with {@link InitParam}. + *

      + * + * @param pConfig the servlet config + * @throws ServletException if an error occurred during init + * + * @see javax.servlet.GenericServlet#init + * @see #init() init + * @see BeanUtil#configure(Object, java.util.Map, boolean) + */ + @Override + public void init(ServletConfig pConfig) throws ServletException { + if (pConfig == null) { + throw new ServletConfigException("servlet config == null"); + } + + try { + BeanUtil.configure(this, ServletUtil.asMap(pConfig), true); + } + catch (InvocationTargetException e) { + throw new ServletConfigException("Could not configure " + getServletName(), e.getCause()); + } + + super.init(pConfig); + } +} diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java index 62dfd2d6..c287da01 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/OutputStreamAdapter.java @@ -1,122 +1,125 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.lang.Validate; - -import javax.servlet.ServletOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * A {@code ServletOutputStream} implementation backed by a - * {@link java.io.OutputStream}. For filters that need to buffer the - * response and do post filtering, it may be used like this:
      - * ByteArrayOutputStream buffer = new ByteArraOutputStream();
      - * ServletOutputStream adapter = new OutputStreamAdapter(buffer);
      - * 
      - *

      - * As a {@code ServletOutputStream} is itself an {@code OutputStream}, this - * class may also be used as a superclass for wrappers of other - * {@code ServletOutputStream}s, like this:

      - * class FilterServletOutputStream extends OutputStreamAdapter {
      - *    public FilterServletOutputStream(ServletOutputStream out) {
      - *       super(out);
      - *    }
      - *
      - *    public void write(int abyte) {
      - *       // do filtering...
      - *       super.write(...);
      - *    }
      - * }
      - *
      - * ...
      - *
      - * ServletOutputStream original = response.getOutputStream();
      - * ServletOutputStream wrapper = new FilterServletOutputStream(original);
      - * 
      - * @author Harald Kuhr - * @author $Author: haku $ - * @version $Id: OutputStreamAdapter.java#1 $ - * - */ -public class OutputStreamAdapter extends ServletOutputStream { - - /** The wrapped {@code OutputStream}. */ - protected final OutputStream out; - - /** - * Creates an {@code OutputStreamAdapter}. - * - * @param pOut the wrapped {@code OutputStream} - * - * @throws IllegalArgumentException if {@code pOut} is {@code null}. - */ - public OutputStreamAdapter(final OutputStream pOut) { - Validate.notNull(pOut, "out"); - out = pOut; - } - - /** - * Returns the wrapped {@code OutputStream}. - * - * @return the wrapped {@code OutputStream}. - */ - public OutputStream getOutputStream() { - return out; - } - - @Override - public String toString() { - return "ServletOutputStream adapted from " + out.toString(); - } - - /** - * Writes a byte to the underlying stream. - * - * @param pByte the byte to write. - * - * @throws IOException if an error occurs during writing - */ - public void write(final int pByte) throws IOException { - out.write(pByte); - } - - // Overide for efficiency - public void write(final byte pBytes[]) throws IOException { - out.write(pBytes); - } - - // Overide for efficiency - public void write(final byte pBytes[], final int pOff, final int pLen) throws IOException { - out.write(pBytes, pOff, pLen); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.lang.Validate; + +import javax.servlet.ServletOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * A {@code ServletOutputStream} implementation backed by a + * {@link java.io.OutputStream}. For filters that need to buffer the + * response and do post filtering, it may be used like this:
      + * ByteArrayOutputStream buffer = new ByteArraOutputStream();
      + * ServletOutputStream adapter = new OutputStreamAdapter(buffer);
      + * 
      + *

      + * As a {@code ServletOutputStream} is itself an {@code OutputStream}, this + * class may also be used as a superclass for wrappers of other + * {@code ServletOutputStream}s, like this: + *

      + *
      + * class FilterServletOutputStream extends OutputStreamAdapter {
      + *    public FilterServletOutputStream(ServletOutputStream out) {
      + *       super(out);
      + *    }
      + *
      + *    public void write(int abyte) {
      + *       // do filtering...
      + *       super.write(...);
      + *    }
      + * }
      + *
      + * ...
      + *
      + * ServletOutputStream original = response.getOutputStream();
      + * ServletOutputStream wrapper = new FilterServletOutputStream(original);
      + * 
      + * + * @author Harald Kuhr + * @author $Author: haku $ + * @version $Id: OutputStreamAdapter.java#1 $ + * + */ +public class OutputStreamAdapter extends ServletOutputStream { + + /** The wrapped {@code OutputStream}. */ + protected final OutputStream out; + + /** + * Creates an {@code OutputStreamAdapter}. + * + * @param pOut the wrapped {@code OutputStream} + * + * @throws IllegalArgumentException if {@code pOut} is {@code null}. + */ + public OutputStreamAdapter(final OutputStream pOut) { + Validate.notNull(pOut, "out"); + out = pOut; + } + + /** + * Returns the wrapped {@code OutputStream}. + * + * @return the wrapped {@code OutputStream}. + */ + public OutputStream getOutputStream() { + return out; + } + + @Override + public String toString() { + return "ServletOutputStream adapted from " + out.toString(); + } + + /** + * Writes a byte to the underlying stream. + * + * @param pByte the byte to write. + * + * @throws IOException if an error occurs during writing + */ + public void write(final int pByte) throws IOException { + out.write(pByte); + } + + // Overide for efficiency + public void write(final byte pBytes[]) throws IOException { + out.write(pBytes); + } + + // Overide for efficiency + 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 b6d2e429..12d7c28b 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ProxyServlet.java @@ -1,437 +1,440 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.lang.StringUtil; - -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.ConnectException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Enumeration; - -/** - * A simple proxy servlet implementation. Supports HTTP and HTTPS. - *

      - * Note: The servlet is not a true HTTP proxy as described in - * RFC 2616, - * instead it passes on all incoming HTTP requests to the configured remote - * server. - * Useful for bypassing firewalls or to avoid exposing internal network - * infrastructure to external clients. - *

      - * At the moment, no caching of content is implemented. - *

      - * If the {@code remoteServer} init parameter is not set, the servlet will - * respond by sending a {@code 500 Internal Server Error} response to the client. - * If the configured remote server is down, or unreachable, the servlet will - * respond by sending a {@code 502 Bad Gateway} response to the client. - * Otherwise, the response from the remote server will be tunneled unmodified - * to the client. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * - * @version $Id: ProxyServlet.java#1 $ - */ -public class ProxyServlet extends GenericServlet { - - /** Remote server host name or IP address */ - protected String remoteServer = null; - /** Remote server port */ - protected int remotePort = 80; - /** Remote server "mount" path */ - protected String remotePath = ""; - - private static final String HTTP_REQUEST_HEADER_HOST = "host"; - private static final String HTTP_RESPONSE_HEADER_SERVER = "server"; - private static final String MESSAGE_REMOTE_SERVER_NOT_CONFIGURED = "Remote server not configured."; - - /** - * Called by {@code init} to set the remote server. Must be a valid host - * name or IP address. No default. - * - * @param pRemoteServer - */ - public void setRemoteServer(String pRemoteServer) { - remoteServer = pRemoteServer; - } - - /** - * Called by {@code init} to set the remote port. Must be a number. - * Default is {@code 80}. - * - * @param pRemotePort - */ - public void setRemotePort(String pRemotePort) { - try { - remotePort = Integer.parseInt(pRemotePort); - } - catch (NumberFormatException e) { - log("RemotePort must be a number!", e); - } - } - - /** - * Called by {@code init} to set the remote path. May be an empty string - * for the root path, or any other valid path on the remote server. - * Default is {@code ""}. - * - * @param pRemotePath - */ - public void setRemotePath(String pRemotePath) { - if (StringUtil.isEmpty(pRemotePath)) { - pRemotePath = ""; - } - else if (pRemotePath.charAt(0) != '/') { - pRemotePath = "/" + pRemotePath; - } - - remotePath = pRemotePath; - } - - /** - * Override {@code service} to use HTTP specifics. - * - * @param pRequest - * @param pResponse - * - * @throws ServletException - * @throws IOException - * - * @see #service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) - */ - public final void service(ServletRequest pRequest, ServletResponse pResponse) throws ServletException, IOException { - service((HttpServletRequest) pRequest, (HttpServletResponse) pResponse); - } - - /** - * Services a single request. - * Supports HTTP and HTTPS. - * - * @param pRequest - * @param pResponse - * - * @throws ServletException - * @throws IOException - * - * @see ProxyServlet Class descrition - */ - protected void service(HttpServletRequest pRequest, HttpServletResponse pResponse) throws ServletException, IOException { - // Sanity check configuration - if (remoteServer == null) { - log(MESSAGE_REMOTE_SERVER_NOT_CONFIGURED); - pResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, - MESSAGE_REMOTE_SERVER_NOT_CONFIGURED); - return; - } - - HttpURLConnection remoteConnection = null; - try { - // Recreate request URI for remote request - String requestURI = createRemoteRequestURI(pRequest); - 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... - // NOTE: The HttpURLConnection is supposed to handle multiple - // requests to the same server internally - String method = pRequest.getMethod(); - remoteConnection = (HttpURLConnection) remoteURL.openConnection(); - remoteConnection.setRequestMethod(method); - - // Copy header fields - copyHeadersFromClient(pRequest, remoteConnection); - - // Do proxy specifc stuff? - // TODO: Read up the specs from RFC 2616 (HTTP) on proxy behaviour - // TODO: RFC 2616 says "[a] proxy server MUST NOT establish an HTTP/1.1 - // persistent connection with an HTTP/1.0 client" - - // Copy message body from client to remote server - copyBodyFromClient(pRequest, remoteConnection); - - // Set response status code from remote server to client - int responseCode = remoteConnection.getResponseCode(); - pResponse.setStatus(responseCode); - //System.out.println("Response is: " + responseCode + " " + remoteConnection.getResponseMessage()); - - // Copy header fields back - copyHeadersToClient(remoteConnection, pResponse); - - // More proxy specific stuff? - - // Copy message body from remote server to client - copyBodyToClient(remoteConnection, pResponse); - } - catch (ConnectException e) { - // In case we could not connecto to the remote server - log("Could not connect to remote server.", e); - pResponse.sendError(HttpServletResponse.SC_BAD_GATEWAY, e.getMessage()); - } - finally { - // Disconnect from server - // TODO: Should we actually do this? - if (remoteConnection != null) { - remoteConnection.disconnect(); - } - } - } - - /** - * Copies the message body from the remote server to the client (outgoing - * {@code HttpServletResponse}). - * - * @param pRemoteConnection - * @param pResponse - * - * @throws IOException - */ - private void copyBodyToClient(HttpURLConnection pRemoteConnection, HttpServletResponse pResponse) throws IOException { - InputStream fromRemote = null; - OutputStream toClient = null; - - try { - // Get either input or error stream - try { - fromRemote = pRemoteConnection.getInputStream(); - } - catch (IOException e) { - // If exception, use errorStream instead - fromRemote = pRemoteConnection.getErrorStream(); - } - - // I guess the stream might be null if there is no response other - // than headers (Continue, No Content, etc). - if (fromRemote != null) { - toClient = pResponse.getOutputStream(); - FileUtil.copy(fromRemote, toClient); - } - } - finally { - if (fromRemote != null) { - try { - fromRemote.close(); - } - catch (IOException e) { - log("Stream from remote could not be closed.", e); - } - } - if (toClient != null) { - try { - toClient.close(); - } - catch (IOException e) { - log("Stream to client could not be closed.", e); - } - } - } - } - - /** - * Copies the message body from the client (incomming - * {@code HttpServletRequest}) to the remote server if the request method - * is {@code POST} or PUT. - * Otherwise this method does nothing. - * - * @param pRequest - * @param pRemoteConnection - * - * @throws java.io.IOException - */ - private void copyBodyFromClient(HttpServletRequest pRequest, HttpURLConnection pRemoteConnection) throws IOException { - // If this is a POST or PUT, copy message body from client remote server - if (!("POST".equals(pRequest.getMethod()) || "PUT".equals(pRequest.getMethod()))) { - return; - } - - // NOTE: Setting doOutput to true, will make it a POST request (why?)... - pRemoteConnection.setDoOutput(true); - - // Get streams and do the copying - InputStream fromClient = null; - OutputStream toRemote = null; - try { - fromClient = pRequest.getInputStream(); - toRemote = pRemoteConnection.getOutputStream(); - FileUtil.copy(fromClient, toRemote); - } - finally { - if (fromClient != null) { - try { - fromClient.close(); - } - catch (IOException e) { - log("Stream from client could not be closed.", e); - } - } - if (toRemote != null) { - try { - toRemote.close(); - } - catch (IOException e) { - log("Stream to remote could not be closed.", e); - } - } - } - } - - /** - * Creates the remote request URI based on the incoming request. - * The URI will include any query strings etc. - * - * @param pRequest - * - * @return a {@code String} representing the remote request URI - */ - private String createRemoteRequestURI(HttpServletRequest pRequest) { - StringBuilder requestURI = new StringBuilder(remotePath); - requestURI.append(pRequest.getPathInfo()); - - if (!StringUtil.isEmpty(pRequest.getQueryString())) { - requestURI.append("?"); - requestURI.append(pRequest.getQueryString()); - } - - return requestURI.toString(); - } - - /** - * Copies headers from the remote connection back to the client - * (the outgoing HttpServletResponse). All headers except the "Server" - * header are copied. - * - * @param pRemoteConnection - * @param pResponse - */ - private void copyHeadersToClient(HttpURLConnection pRemoteConnection, HttpServletResponse pResponse) { - // NOTE: There is no getHeaderFieldCount method or similar... - // Also, the getHeaderFields() method was introduced in J2SE 1.4, and - // we want to be 1.2 compatible. - // So, just try to loop until there are no more headers. - int i = 0; - while (true) { - String key = pRemoteConnection.getHeaderFieldKey(i); - // NOTE: getHeaderField(String) returns only the last value - String value = pRemoteConnection.getHeaderField(i); - - // If the key is not null, life is simple, and Sun is shining - // However, the default implementations includes the HTTP response - // code ("HTTP/1.1 200 Ok" or similar) as a header field with - // key "null" (why..?)... - // In addition, we want to skip the original "Server" header - if (key != null && !HTTP_RESPONSE_HEADER_SERVER.equalsIgnoreCase(key)) { - //System.out.println("client <<<-- remote: " + key + ": " + value); - pResponse.setHeader(key, value); - } - else if (value == null) { - // If BOTH key and value is null, there are no more header fields - break; - } - - i++; - } - - /* 1.4+ version below.... - Map headers = pRemoteConnection.getHeaderFields(); - for (Iterator iterator = headers.entrySet().iterator(); iterator.hasNext();) { - Map.Entry header = (Map.Entry) iterator.next(); - - List values = (List) header.getValue(); - - for (Iterator valueIter = values.iterator(); valueIter.hasNext();) { - String value = (String) valueIter.next(); - String key = (String) header.getKey(); - - // Skip the server header - if (HTTP_RESPONSE_HEADER_SERVER.equalsIgnoreCase(key)) { - key = null; - } - - // The default implementations includes the HTTP response code - // ("HTTP/1.1 200 Ok" or similar) as a header field with - // key "null" (why..?)... - if (key != null) { - //System.out.println("client <<<-- remote: " + key + ": " + value); - pResponse.setHeader(key, value); - } - } - } - */ - } - - /** - * Copies headers from the client (the incoming {@code HttpServletRequest}) - * to the outgoing connection. - * All headers except the "Host" header are copied. - * - * @param pRequest - * @param pRemoteConnection - */ - private void copyHeadersFromClient(HttpServletRequest pRequest, HttpURLConnection pRemoteConnection) { - Enumeration headerNames = pRequest.getHeaderNames(); - while (headerNames.hasMoreElements()) { - String headerName = (String) headerNames.nextElement(); - Enumeration headerValues = pRequest.getHeaders(headerName); - - // Skip the "host" header, as we want something else - if (HTTP_REQUEST_HEADER_HOST.equalsIgnoreCase(headerName)) { - // Skip this header - headerName = null; - } - - // Set the the header to the remoteConnection - if (headerName != null) { - // Convert from multiple line to single line, comma separated, as - // there seems to be a shortcoming in the URLConneciton API... - StringBuilder headerValue = new StringBuilder(); - while (headerValues.hasMoreElements()) { - String value = (String) headerValues.nextElement(); - headerValue.append(value); - if (headerValues.hasMoreElements()) { - headerValue.append(", "); - } - } - - //System.out.println("client -->>> remote: " + headerName + ": " + headerValue); - pRemoteConnection.setRequestProperty(headerName, headerValue.toString()); - } - } - } -} +/* + * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.lang.StringUtil; + +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ConnectException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Enumeration; + +/** + * A simple proxy servlet implementation. Supports HTTP and HTTPS. + *

      + * Note: The servlet is not a true HTTP proxy as described in + * RFC 2616, + * instead it passes on all incoming HTTP requests to the configured remote + * server. + * Useful for bypassing firewalls or to avoid exposing internal network + * infrastructure to external clients. + *

      + *

      + * At the moment, no caching of content is implemented. + *

      + *

      + * If the {@code remoteServer} init parameter is not set, the servlet will + * respond by sending a {@code 500 Internal Server Error} response to the client. + * If the configured remote server is down, or unreachable, the servlet will + * respond by sending a {@code 502 Bad Gateway} response to the client. + * Otherwise, the response from the remote server will be tunneled unmodified + * to the client. + *

      + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * + * @version $Id: ProxyServlet.java#1 $ + */ +public class ProxyServlet extends GenericServlet { + + /** Remote server host name or IP address */ + protected String remoteServer = null; + /** Remote server port */ + protected int remotePort = 80; + /** Remote server "mount" path */ + protected String remotePath = ""; + + private static final String HTTP_REQUEST_HEADER_HOST = "host"; + private static final String HTTP_RESPONSE_HEADER_SERVER = "server"; + private static final String MESSAGE_REMOTE_SERVER_NOT_CONFIGURED = "Remote server not configured."; + + /** + * Called by {@code init} to set the remote server. Must be a valid host + * name or IP address. No default. + * + * @param pRemoteServer + */ + public void setRemoteServer(String pRemoteServer) { + remoteServer = pRemoteServer; + } + + /** + * Called by {@code init} to set the remote port. Must be a number. + * Default is {@code 80}. + * + * @param pRemotePort + */ + public void setRemotePort(String pRemotePort) { + try { + remotePort = Integer.parseInt(pRemotePort); + } + catch (NumberFormatException e) { + log("RemotePort must be a number!", e); + } + } + + /** + * Called by {@code init} to set the remote path. May be an empty string + * for the root path, or any other valid path on the remote server. + * Default is {@code ""}. + * + * @param pRemotePath + */ + public void setRemotePath(String pRemotePath) { + if (StringUtil.isEmpty(pRemotePath)) { + pRemotePath = ""; + } + else if (pRemotePath.charAt(0) != '/') { + pRemotePath = "/" + pRemotePath; + } + + remotePath = pRemotePath; + } + + /** + * Override {@code service} to use HTTP specifics. + * + * @param pRequest + * @param pResponse + * + * @throws ServletException + * @throws IOException + * + * @see #service(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) + */ + public final void service(ServletRequest pRequest, ServletResponse pResponse) throws ServletException, IOException { + service((HttpServletRequest) pRequest, (HttpServletResponse) pResponse); + } + + /** + * Services a single request. + * Supports HTTP and HTTPS. + * + * @param pRequest + * @param pResponse + * + * @throws ServletException + * @throws IOException + * + * @see ProxyServlet Class descrition + */ + protected void service(HttpServletRequest pRequest, HttpServletResponse pResponse) throws ServletException, IOException { + // Sanity check configuration + if (remoteServer == null) { + log(MESSAGE_REMOTE_SERVER_NOT_CONFIGURED); + pResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + MESSAGE_REMOTE_SERVER_NOT_CONFIGURED); + return; + } + + HttpURLConnection remoteConnection = null; + try { + // Recreate request URI for remote request + String requestURI = createRemoteRequestURI(pRequest); + 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... + // NOTE: The HttpURLConnection is supposed to handle multiple + // requests to the same server internally + String method = pRequest.getMethod(); + remoteConnection = (HttpURLConnection) remoteURL.openConnection(); + remoteConnection.setRequestMethod(method); + + // Copy header fields + copyHeadersFromClient(pRequest, remoteConnection); + + // Do proxy specifc stuff? + // TODO: Read up the specs from RFC 2616 (HTTP) on proxy behaviour + // TODO: RFC 2616 says "[a] proxy server MUST NOT establish an HTTP/1.1 + // persistent connection with an HTTP/1.0 client" + + // Copy message body from client to remote server + copyBodyFromClient(pRequest, remoteConnection); + + // Set response status code from remote server to client + int responseCode = remoteConnection.getResponseCode(); + pResponse.setStatus(responseCode); + //System.out.println("Response is: " + responseCode + " " + remoteConnection.getResponseMessage()); + + // Copy header fields back + copyHeadersToClient(remoteConnection, pResponse); + + // More proxy specific stuff? + + // Copy message body from remote server to client + copyBodyToClient(remoteConnection, pResponse); + } + catch (ConnectException e) { + // In case we could not connecto to the remote server + log("Could not connect to remote server.", e); + pResponse.sendError(HttpServletResponse.SC_BAD_GATEWAY, e.getMessage()); + } + finally { + // Disconnect from server + // TODO: Should we actually do this? + if (remoteConnection != null) { + remoteConnection.disconnect(); + } + } + } + + /** + * Copies the message body from the remote server to the client (outgoing + * {@code HttpServletResponse}). + * + * @param pRemoteConnection + * @param pResponse + * + * @throws IOException + */ + private void copyBodyToClient(HttpURLConnection pRemoteConnection, HttpServletResponse pResponse) throws IOException { + InputStream fromRemote = null; + OutputStream toClient = null; + + try { + // Get either input or error stream + try { + fromRemote = pRemoteConnection.getInputStream(); + } + catch (IOException e) { + // If exception, use errorStream instead + fromRemote = pRemoteConnection.getErrorStream(); + } + + // I guess the stream might be null if there is no response other + // than headers (Continue, No Content, etc). + if (fromRemote != null) { + toClient = pResponse.getOutputStream(); + FileUtil.copy(fromRemote, toClient); + } + } + finally { + if (fromRemote != null) { + try { + fromRemote.close(); + } + catch (IOException e) { + log("Stream from remote could not be closed.", e); + } + } + if (toClient != null) { + try { + toClient.close(); + } + catch (IOException e) { + log("Stream to client could not be closed.", e); + } + } + } + } + + /** + * Copies the message body from the client (incomming + * {@code HttpServletRequest}) to the remote server if the request method + * is {@code POST} or PUT. + * Otherwise this method does nothing. + * + * @param pRequest + * @param pRemoteConnection + * + * @throws java.io.IOException + */ + private void copyBodyFromClient(HttpServletRequest pRequest, HttpURLConnection pRemoteConnection) throws IOException { + // If this is a POST or PUT, copy message body from client remote server + if (!("POST".equals(pRequest.getMethod()) || "PUT".equals(pRequest.getMethod()))) { + return; + } + + // NOTE: Setting doOutput to true, will make it a POST request (why?)... + pRemoteConnection.setDoOutput(true); + + // Get streams and do the copying + InputStream fromClient = null; + OutputStream toRemote = null; + try { + fromClient = pRequest.getInputStream(); + toRemote = pRemoteConnection.getOutputStream(); + FileUtil.copy(fromClient, toRemote); + } + finally { + if (fromClient != null) { + try { + fromClient.close(); + } + catch (IOException e) { + log("Stream from client could not be closed.", e); + } + } + if (toRemote != null) { + try { + toRemote.close(); + } + catch (IOException e) { + log("Stream to remote could not be closed.", e); + } + } + } + } + + /** + * Creates the remote request URI based on the incoming request. + * The URI will include any query strings etc. + * + * @param pRequest + * + * @return a {@code String} representing the remote request URI + */ + private String createRemoteRequestURI(HttpServletRequest pRequest) { + StringBuilder requestURI = new StringBuilder(remotePath); + requestURI.append(pRequest.getPathInfo()); + + if (!StringUtil.isEmpty(pRequest.getQueryString())) { + requestURI.append("?"); + requestURI.append(pRequest.getQueryString()); + } + + return requestURI.toString(); + } + + /** + * Copies headers from the remote connection back to the client + * (the outgoing HttpServletResponse). All headers except the "Server" + * header are copied. + * + * @param pRemoteConnection + * @param pResponse + */ + private void copyHeadersToClient(HttpURLConnection pRemoteConnection, HttpServletResponse pResponse) { + // NOTE: There is no getHeaderFieldCount method or similar... + // Also, the getHeaderFields() method was introduced in J2SE 1.4, and + // we want to be 1.2 compatible. + // So, just try to loop until there are no more headers. + int i = 0; + while (true) { + String key = pRemoteConnection.getHeaderFieldKey(i); + // NOTE: getHeaderField(String) returns only the last value + String value = pRemoteConnection.getHeaderField(i); + + // If the key is not null, life is simple, and Sun is shining + // However, the default implementations includes the HTTP response + // code ("HTTP/1.1 200 Ok" or similar) as a header field with + // key "null" (why..?)... + // In addition, we want to skip the original "Server" header + if (key != null && !HTTP_RESPONSE_HEADER_SERVER.equalsIgnoreCase(key)) { + //System.out.println("client <<<-- remote: " + key + ": " + value); + pResponse.setHeader(key, value); + } + else if (value == null) { + // If BOTH key and value is null, there are no more header fields + break; + } + + i++; + } + + /* 1.4+ version below.... + Map headers = pRemoteConnection.getHeaderFields(); + for (Iterator iterator = headers.entrySet().iterator(); iterator.hasNext();) { + Map.Entry header = (Map.Entry) iterator.next(); + + List values = (List) header.getValue(); + + for (Iterator valueIter = values.iterator(); valueIter.hasNext();) { + String value = (String) valueIter.next(); + String key = (String) header.getKey(); + + // Skip the server header + if (HTTP_RESPONSE_HEADER_SERVER.equalsIgnoreCase(key)) { + key = null; + } + + // The default implementations includes the HTTP response code + // ("HTTP/1.1 200 Ok" or similar) as a header field with + // key "null" (why..?)... + if (key != null) { + //System.out.println("client <<<-- remote: " + key + ": " + value); + pResponse.setHeader(key, value); + } + } + } + */ + } + + /** + * Copies headers from the client (the incoming {@code HttpServletRequest}) + * to the outgoing connection. + * All headers except the "Host" header are copied. + * + * @param pRequest + * @param pRemoteConnection + */ + private void copyHeadersFromClient(HttpServletRequest pRequest, HttpURLConnection pRemoteConnection) { + Enumeration headerNames = pRequest.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = (String) headerNames.nextElement(); + Enumeration headerValues = pRequest.getHeaders(headerName); + + // Skip the "host" header, as we want something else + if (HTTP_REQUEST_HEADER_HOST.equalsIgnoreCase(headerName)) { + // Skip this header + headerName = null; + } + + // Set the the header to the remoteConnection + if (headerName != null) { + // Convert from multiple line to single line, comma separated, as + // there seems to be a shortcoming in the URLConneciton API... + StringBuilder headerValue = new StringBuilder(); + while (headerValues.hasMoreElements()) { + String value = (String) headerValues.nextElement(); + headerValue.append(value); + if (headerValues.hasMoreElements()) { + headerValue.append(", "); + } + } + + //System.out.println("client -->>> remote: " + headerName + ": " + headerValue); + pRemoteConnection.setRequestProperty(headerName, headerValue.toString()); + } + } + } +} diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java index b9adec7c..25be94dd 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletConfigMapAdapter.java @@ -1,284 +1,284 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.lang.StringUtil; -import com.twelvemonkeys.lang.Validate; - -import javax.servlet.FilterConfig; -import javax.servlet.ServletConfig; -import javax.servlet.ServletContext; -import java.io.Serializable; -import java.util.*; - -/** - * {@code ServletConfig} or {@code FilterConfig} adapter, that implements - * the {@code Map} interface for interoperability with collection-based API's. - *

      - * This {@code Map} is not synchronized. - *

      - * - * @author Harald Kuhr - * @version $Id: ServletConfigMapAdapter.java#2 $ - */ -class ServletConfigMapAdapter extends AbstractMap implements Map, Serializable, Cloneable { - - enum ConfigType { - ServletConfig, FilterConfig, ServletContext - } - - private final ConfigType type; - - private final ServletConfig servletConfig; - private final FilterConfig filterConfig; - private final ServletContext servletContext; - - // Cache the entry set - private transient Set> entrySet; - - public ServletConfigMapAdapter(final ServletConfig pConfig) { - this(pConfig, ConfigType.ServletConfig); - } - - public ServletConfigMapAdapter(final FilterConfig pConfig) { - this(pConfig, ConfigType.FilterConfig); - } - - public ServletConfigMapAdapter(final ServletContext pContext) { - this(pContext, ConfigType.ServletContext); - } - - private ServletConfigMapAdapter(final Object pConfig, final ConfigType pType) { - // Could happen if client code invokes with null reference - Validate.notNull(pConfig, "config"); - - type = pType; - - switch (type) { - case ServletConfig: - servletConfig = (ServletConfig) pConfig; - filterConfig = null; - servletContext = null; - break; - case FilterConfig: - servletConfig = null; - filterConfig = (FilterConfig) pConfig; - servletContext = null; - break; - case ServletContext: - servletConfig = null; - filterConfig = null; - servletContext = (ServletContext) pConfig; - break; - default: - throw new IllegalArgumentException("Wrong type: " + pType); - } - } - - /** - * Gets the servlet or filter name from the config. - * - * @return the servlet or filter name - */ - public final String getName() { - switch (type) { - case ServletConfig: - return servletConfig.getServletName(); - case FilterConfig: - return filterConfig.getFilterName(); - case ServletContext: - return servletContext.getServletContextName(); - default: - throw new IllegalStateException(); - } - } - - /** - * Gets the servlet context from the config. - * - * @return the servlet context - */ - public final ServletContext getServletContext() { - switch (type) { - case ServletConfig: - return servletConfig.getServletContext(); - case FilterConfig: - return filterConfig.getServletContext(); - case ServletContext: - return servletContext; - default: - throw new IllegalStateException(); - } - } - - public final Enumeration getInitParameterNames() { - switch (type) { - case ServletConfig: - return servletConfig.getInitParameterNames(); - case FilterConfig: - return filterConfig.getInitParameterNames(); - case ServletContext: - return servletContext.getInitParameterNames(); - default: - throw new IllegalStateException(); - } - } - - public final String getInitParameter(final String pName) { - switch (type) { - case ServletConfig: - return servletConfig.getInitParameter(pName); - case FilterConfig: - return filterConfig.getInitParameter(pName); - case ServletContext: - return servletContext.getInitParameter(pName); - default: - throw new IllegalStateException(); - } - } - - public Set> entrySet() { - if (entrySet == null) { - entrySet = createEntrySet(); - } - return entrySet; - } - - private Set> createEntrySet() { - return new AbstractSet>() { - // Cache size, if requested, -1 means not calculated - private int size = -1; - - public Iterator> iterator() { - return new Iterator>() { - // Iterator is backed by initParameterNames enumeration - final Enumeration names = getInitParameterNames(); - - public boolean hasNext() { - return names.hasMoreElements(); - } - - public Entry next() { - final String key = (String) names.nextElement(); - return new Entry() { - public String getKey() { - return key; - } - - public String getValue() { - return get(key); - } - - public String setValue(String pValue) { - throw new UnsupportedOperationException(); - } - - // NOTE: Override equals - public boolean equals(Object pOther) { - if (!(pOther instanceof Map.Entry)) { - return false; - } - - Map.Entry e = (Map.Entry) pOther; - Object value = get(key); - Object rKey = e.getKey(); - Object rValue = e.getValue(); - return (key == null ? rKey == null : key.equals(rKey)) - && (value == null ? rValue == null : value.equals(rValue)); - } - - // NOTE: Override hashCode to keep the map's - // hashCode constant and compatible - public int hashCode() { - Object value = get(key); - return ((key == null) ? 0 : key.hashCode()) ^ - ((value == null) ? 0 : value.hashCode()); - } - - public String toString() { - return key + "=" + get(key); - } - }; - } - - public void remove() { - throw new UnsupportedOperationException(); - } - }; - } - - public int size() { - if (size < 0) { - size = calculateSize(); - } - - return size; - } - - private int calculateSize() { - final Enumeration names = getInitParameterNames(); - - int size = 0; - while (names.hasMoreElements()) { - size++; - names.nextElement(); - } - - return size; - } - }; - } - - public String get(Object pKey) { - return getInitParameter(StringUtil.valueOf(pKey)); - } - - /// Unsupported Map methods - @Override - public String put(String pKey, String pValue) { - throw new UnsupportedOperationException(); - } - - @Override - public String remove(Object pKey) { - throw new UnsupportedOperationException(); - } - - @Override - public void putAll(Map pMap) { - throw new UnsupportedOperationException(); - } - - @Override - public void clear() { - throw new UnsupportedOperationException(); - } +/* + * 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 of the copyright holder 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 HOLDER 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.lang.StringUtil; +import com.twelvemonkeys.lang.Validate; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import java.io.Serializable; +import java.util.*; + +/** + * {@code ServletConfig} or {@code FilterConfig} adapter, that implements + * the {@code Map} interface for interoperability with collection-based API's. + *

      + * This {@code Map} is not synchronized. + *

      + * + * @author Harald Kuhr + * @version $Id: ServletConfigMapAdapter.java#2 $ + */ +class ServletConfigMapAdapter extends AbstractMap implements Map, Serializable, Cloneable { + + enum ConfigType { + ServletConfig, FilterConfig, ServletContext + } + + private final ConfigType type; + + private final ServletConfig servletConfig; + private final FilterConfig filterConfig; + private final ServletContext servletContext; + + // Cache the entry set + private transient Set> entrySet; + + public ServletConfigMapAdapter(final ServletConfig pConfig) { + this(pConfig, ConfigType.ServletConfig); + } + + public ServletConfigMapAdapter(final FilterConfig pConfig) { + this(pConfig, ConfigType.FilterConfig); + } + + public ServletConfigMapAdapter(final ServletContext pContext) { + this(pContext, ConfigType.ServletContext); + } + + private ServletConfigMapAdapter(final Object pConfig, final ConfigType pType) { + // Could happen if client code invokes with null reference + Validate.notNull(pConfig, "config"); + + type = pType; + + switch (type) { + case ServletConfig: + servletConfig = (ServletConfig) pConfig; + filterConfig = null; + servletContext = null; + break; + case FilterConfig: + servletConfig = null; + filterConfig = (FilterConfig) pConfig; + servletContext = null; + break; + case ServletContext: + servletConfig = null; + filterConfig = null; + servletContext = (ServletContext) pConfig; + break; + default: + throw new IllegalArgumentException("Wrong type: " + pType); + } + } + + /** + * Gets the servlet or filter name from the config. + * + * @return the servlet or filter name + */ + public final String getName() { + switch (type) { + case ServletConfig: + return servletConfig.getServletName(); + case FilterConfig: + return filterConfig.getFilterName(); + case ServletContext: + return servletContext.getServletContextName(); + default: + throw new IllegalStateException(); + } + } + + /** + * Gets the servlet context from the config. + * + * @return the servlet context + */ + public final ServletContext getServletContext() { + switch (type) { + case ServletConfig: + return servletConfig.getServletContext(); + case FilterConfig: + return filterConfig.getServletContext(); + case ServletContext: + return servletContext; + default: + throw new IllegalStateException(); + } + } + + public final Enumeration getInitParameterNames() { + switch (type) { + case ServletConfig: + return servletConfig.getInitParameterNames(); + case FilterConfig: + return filterConfig.getInitParameterNames(); + case ServletContext: + return servletContext.getInitParameterNames(); + default: + throw new IllegalStateException(); + } + } + + public final String getInitParameter(final String pName) { + switch (type) { + case ServletConfig: + return servletConfig.getInitParameter(pName); + case FilterConfig: + return filterConfig.getInitParameter(pName); + case ServletContext: + return servletContext.getInitParameter(pName); + default: + throw new IllegalStateException(); + } + } + + public Set> entrySet() { + if (entrySet == null) { + entrySet = createEntrySet(); + } + return entrySet; + } + + private Set> createEntrySet() { + return new AbstractSet>() { + // Cache size, if requested, -1 means not calculated + private int size = -1; + + public Iterator> iterator() { + return new Iterator>() { + // Iterator is backed by initParameterNames enumeration + final Enumeration names = getInitParameterNames(); + + public boolean hasNext() { + return names.hasMoreElements(); + } + + public Entry next() { + final String key = (String) names.nextElement(); + return new Entry() { + public String getKey() { + return key; + } + + public String getValue() { + return get(key); + } + + public String setValue(String pValue) { + throw new UnsupportedOperationException(); + } + + // NOTE: Override equals + public boolean equals(Object pOther) { + if (!(pOther instanceof Map.Entry)) { + return false; + } + + Map.Entry e = (Map.Entry) pOther; + Object value = get(key); + Object rKey = e.getKey(); + Object rValue = e.getValue(); + return (key == null ? rKey == null : key.equals(rKey)) + && (value == null ? rValue == null : value.equals(rValue)); + } + + // NOTE: Override hashCode to keep the map's + // hashCode constant and compatible + public int hashCode() { + Object value = get(key); + return ((key == null) ? 0 : key.hashCode()) ^ + ((value == null) ? 0 : value.hashCode()); + } + + public String toString() { + return key + "=" + get(key); + } + }; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + public int size() { + if (size < 0) { + size = calculateSize(); + } + + return size; + } + + private int calculateSize() { + final Enumeration names = getInitParameterNames(); + + int size = 0; + while (names.hasMoreElements()) { + size++; + names.nextElement(); + } + + return size; + } + }; + } + + public String get(Object pKey) { + return getInitParameter(StringUtil.valueOf(pKey)); + } + + /// Unsupported Map methods + @Override + public String put(String pKey, String pValue) { + throw new UnsupportedOperationException(); + } + + @Override + public String remove(Object pKey) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map pMap) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } } \ 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 bc0bdcc2..d2450b95 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletResponseStreamDelegate.java @@ -1,116 +1,118 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 javax.servlet.ServletOutputStream; -import javax.servlet.ServletResponse; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; - -import static com.twelvemonkeys.lang.Validate.notNull; - -/** - * A delegate for handling stream support in wrapped servlet responses. - *

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

      - * This implementation simply returns the output stream from the wrapped - * response. - * - * @return the {@code OutputStream} to use for the response - * @throws IOException if an I/O exception occurs - */ - protected OutputStream createOutputStream() throws IOException { - return response.getOutputStream(); - } - - public void flushBuffer() throws IOException { - if (out instanceof ServletOutputStream) { - ((ServletOutputStream) out).flush(); - } - else if (out != null) { - ((PrintWriter) out).flush(); - } - } - - public void resetBuffer() { - out = 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 of the copyright holder 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 HOLDER 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 javax.servlet.ServletOutputStream; +import javax.servlet.ServletResponse; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; + +import static com.twelvemonkeys.lang.Validate.notNull; + +/** + * A delegate for handling stream support in wrapped servlet responses. + *

      + * Client code should delegate {@code getOutputStream}, {@code getWriter}, + * {@code flushBuffer} and {@code resetBuffer} methods from the servlet response. + *

      + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: ServletResponseStreamDelegate.java#2 $ + */ +public class ServletResponseStreamDelegate { + private Object out = null; + protected final ServletResponse response; + + public ServletResponseStreamDelegate(final ServletResponse pResponse) { + response = notNull(pResponse, "response"); + } + + // NOTE: Intentionally NOT thread safe, as one request/response should be handled by one thread ONLY. + public final ServletOutputStream getOutputStream() throws IOException { + if (out == null) { + OutputStream out = createOutputStream(); + this.out = out instanceof ServletOutputStream ? out : new OutputStreamAdapter(out); + } + else if (out instanceof PrintWriter) { + throw new IllegalStateException("getWriter() already called."); + } + + return (ServletOutputStream) out; + } + + // NOTE: Intentionally NOT thread safe, as one request/response should be handled by one thread ONLY. + public final PrintWriter getWriter() throws IOException { + if (out == null) { + // NOTE: getCharacterEncoding may/should not return null + OutputStream out = createOutputStream(); + String charEncoding = response.getCharacterEncoding(); + this.out = new PrintWriter(charEncoding != null ? new OutputStreamWriter(out, charEncoding) : new OutputStreamWriter(out)); + } + else if (out instanceof ServletOutputStream) { + throw new IllegalStateException("getOutputStream() already called."); + } + + return (PrintWriter) out; + } + + /** + * Returns the {@code OutputStream}. + * Subclasses should override this method to provide a decorated output stream. + * This method is guaranteed to be invoked only once for a request/response + * (unless {@code resetBuffer} is invoked). + *

      + * This implementation simply returns the output stream from the wrapped + * response. + *

      + * + * @return the {@code OutputStream} to use for the response + * @throws IOException if an I/O exception occurs + */ + protected OutputStream createOutputStream() throws IOException { + return response.getOutputStream(); + } + + public void flushBuffer() throws IOException { + if (out instanceof ServletOutputStream) { + ((ServletOutputStream) out).flush(); + } + else if (out != null) { + ((PrintWriter) out).flush(); + } + } + + public void resetBuffer() { + 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 892d3d8e..5d72cff2 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ServletUtil.java @@ -1,775 +1,776 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.lang.StringUtil; -import com.twelvemonkeys.util.convert.ConversionException; -import com.twelvemonkeys.util.convert.Converter; - -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; -import java.io.File; -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.List; -import java.util.Map; - - -/** - * Various servlet related helper methods. - * - * @author Harald Kuhr - * @author Eirik Torske - * @author last modified by $Author: haku $ - * @version $Id: ServletUtil.java#3 $ - */ -public final class ServletUtil { - - /** - * {@code "javax.servlet.include.request_uri"} - */ - private final static String ATTRIB_INC_REQUEST_URI = "javax.servlet.include.request_uri"; - - /** - * {@code "javax.servlet.include.context_path"} - */ - private final static String ATTRIB_INC_CONTEXT_PATH = "javax.servlet.include.context_path"; - - /** - * {@code "javax.servlet.include.servlet_path"} - */ - private final static String ATTRIB_INC_SERVLET_PATH = "javax.servlet.include.servlet_path"; - - /** - * {@code "javax.servlet.include.path_info"} - */ - private final static String ATTRIB_INC_PATH_INFO = "javax.servlet.include.path_info"; - - /** - * {@code "javax.servlet.include.query_string"} - */ - private final static String ATTRIB_INC_QUERY_STRING = "javax.servlet.include.query_string"; - - /** - * {@code "javax.servlet.forward.request_uri"} - */ - private final static String ATTRIB_FWD_REQUEST_URI = "javax.servlet.forward.request_uri"; - - /** - * {@code "javax.servlet.forward.context_path"} - */ - private final static String ATTRIB_FWD_CONTEXT_PATH = "javax.servlet.forward.context_path"; - - /** - * {@code "javax.servlet.forward.servlet_path"} - */ - private final static String ATTRIB_FWD_SERVLET_PATH = "javax.servlet.forward.servlet_path"; - - /** - * {@code "javax.servlet.forward.path_info"} - */ - private final static String ATTRIB_FWD_PATH_INFO = "javax.servlet.forward.path_info"; - - /** - * {@code "javax.servlet.forward.query_string"} - */ - private final static String ATTRIB_FWD_QUERY_STRING = "javax.servlet.forward.query_string"; - - /** - * Don't create, static methods only - */ - private ServletUtil() { - } - - /** - * Gets the value of the given parameter from the request, or if the - * parameter is not set, the default value. - * - * @param pReq the servlet request - * @param pName the parameter name - * @param pDefault the default value - * @return the value of the parameter, or the default value, if the - * parameter is not set. - */ - public static String getParameter(final ServletRequest pReq, final String pName, final String pDefault) { - String str = pReq.getParameter(pName); - - return str != null ? str : pDefault; - } - - /** - * Gets the value of the given parameter from the request converted to - * an Object. 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 pType the type of object (class) to return - * @param pFormat the format to use (might be {@code null} in many cases) - * @param pDefault the default value - * @return the value of the parameter converted to a boolean, or the - * default value, if the parameter is not set. - * @throws IllegalArgumentException if {@code pDefault} is - * 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... We probably don't want default if conversion fails... - * @see Converter#toObject - */ - 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()); - } - - String str = pReq.getParameter(pName); - - if (str == null) { - return pDefault; - } - - try { - return pType.cast(Converter.getInstance().toObject(str, pType, pFormat)); - } - catch (ConversionException ce) { - return pDefault; - } - } - - /** - * Gets the value of the given parameter from the request converted to - * 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 {@code boolean}, or the - * default value, if the parameter is not set. - */ - 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; - } - catch (NumberFormatException nfe) { - return pDefault; - } - } - - /** - * Gets the value of the given parameter from the request converted to - * 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 {@code int}, or the default - * value, if the parameter is not set. - */ - 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; - } - catch (NumberFormatException nfe) { - return pDefault; - } - } - - /** - * Gets the value of the given parameter from the request converted to - * 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 {@code long}, or the default - * value, if the parameter is not set. - */ - 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; - } - catch (NumberFormatException nfe) { - return pDefault; - } - } - - /** - * Gets the value of the given parameter from the request converted to - * 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 {@code float}, or the default - * value, if the parameter is not set. - */ - 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; - } - catch (NumberFormatException nfe) { - return pDefault; - } - } - - /** - * Gets the value of the given parameter from the request converted to - * 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 {@code double}, or the default - * value, if the parameter is not set. - */ - 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; - } - catch (NumberFormatException nfe) { - return pDefault; - } - } - - /** - * Gets the value of the given parameter from the request converted to - * 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 {@code Date}, or the - * default value, if the parameter is not set. - * @see com.twelvemonkeys.lang.StringUtil#toDate(String) - */ - 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; - } - catch (IllegalArgumentException iae) { - return pDefault; - } - } - - /** - * Gets the value of the given parameter from the request converted to - * a 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 pFormat the date format to use - * @param pDefault the default value - * @return the value of the parameter converted to a Date, or the - * default value, if the parameter is not set. - * @see com.twelvemonkeys.lang.StringUtil#toDate(String,String) - */ - /* - public static long getDateParameter(ServletRequest pReq, String pName, String pFormat, long pDefault) { - String str = pReq.getParameter(pName); - - try { - return ((str != null) ? StringUtil.toDate(str, pFormat).getTime() : pDefault); - } - catch (IllegalArgumentException iae) { - return pDefault; - } - } - */ - - /** - * Builds a full-blown HTTP/HTTPS URL from a - * {@code javax.servlet.http.HttpServletRequest} object. - *

      - * - * @param pRequest The HTTP servlet request object. - * @return the reproduced URL - * @deprecated Use {@link javax.servlet.http.HttpServletRequest#getRequestURL()} - * instead. - */ - static StringBuffer buildHTTPURL(final HttpServletRequest pRequest) { - StringBuffer resultURL = new StringBuffer(); - - // Scheme, as in http, https, ftp etc - String scheme = pRequest.getScheme(); - resultURL.append(scheme); - resultURL.append("://"); - resultURL.append(pRequest.getServerName()); - - // Append port only if not default port - int port = pRequest.getServerPort(); - if (port > 0 && - !(("http".equals(scheme) && port == 80) || - ("https".equals(scheme) && port == 443))) { - resultURL.append(":"); - resultURL.append(port); - } - - // Append URI - resultURL.append(pRequest.getRequestURI()); - - // If present, append extra path info - String pathInfo = pRequest.getPathInfo(); - if (pathInfo != null) { - resultURL.append(pathInfo); - } - - return resultURL; - } - - /** - * Gets the URI of the resource currently included. - * The value is read from the request attribute - * {@code "javax.servlet.include.request_uri"} - * - * @param pRequest the servlet request - * @return the URI of the included resource, or {@code null} if no include - * @see HttpServletRequest#getRequestURI - * @since Servlet 2.2 - */ - public static String getIncludeRequestURI(final ServletRequest pRequest) { - return (String) pRequest.getAttribute(ATTRIB_INC_REQUEST_URI); - } - - /** - * Gets the context path of the resource currently included. - * The value is read from the request attribute - * {@code "javax.servlet.include.context_path"} - * - * @param pRequest the servlet request - * @return the context path of the included resource, or {@code null} if no include - * @see HttpServletRequest#getContextPath - * @since Servlet 2.2 - */ - public static String getIncludeContextPath(final ServletRequest pRequest) { - return (String) pRequest.getAttribute(ATTRIB_INC_CONTEXT_PATH); - } - - /** - * Gets the servlet path of the resource currently included. - * The value is read from the request attribute - * {@code "javax.servlet.include.servlet_path"} - * - * @param pRequest the servlet request - * @return the servlet path of the included resource, or {@code null} if no include - * @see HttpServletRequest#getServletPath - * @since Servlet 2.2 - */ - public static String getIncludeServletPath(final ServletRequest pRequest) { - return (String) pRequest.getAttribute(ATTRIB_INC_SERVLET_PATH); - } - - /** - * Gets the path info of the resource currently included. - * The value is read from the request attribute - * {@code "javax.servlet.include.path_info"} - * - * @param pRequest the servlet request - * @return the path info of the included resource, or {@code null} if no include - * @see HttpServletRequest#getPathInfo - * @since Servlet 2.2 - */ - public static String getIncludePathInfo(final ServletRequest pRequest) { - return (String) pRequest.getAttribute(ATTRIB_INC_PATH_INFO); - } - - /** - * Gets the query string of the resource currently included. - * The value is read from the request attribute - * {@code "javax.servlet.include.query_string"} - * - * @param pRequest the servlet request - * @return the query string of the included resource, or {@code null} if no include - * @see HttpServletRequest#getQueryString - * @since Servlet 2.2 - */ - public static String getIncludeQueryString(final ServletRequest pRequest) { - return (String) pRequest.getAttribute(ATTRIB_INC_QUERY_STRING); - } - - /** - * Gets the URI of the resource this request was forwarded from. - * The value is read from the request attribute - * {@code "javax.servlet.forward.request_uri"} - * - * @param pRequest the servlet request - * @return the URI of the resource, or {@code null} if not forwarded - * @see HttpServletRequest#getRequestURI - * @since Servlet 2.4 - */ - public static String getForwardRequestURI(final ServletRequest pRequest) { - return (String) pRequest.getAttribute(ATTRIB_FWD_REQUEST_URI); - } - - /** - * Gets the context path of the resource this request was forwarded from. - * The value is read from the request attribute - * {@code "javax.servlet.forward.context_path"} - * - * @param pRequest the servlet request - * @return the context path of the resource, or {@code null} if not forwarded - * @see HttpServletRequest#getContextPath - * @since Servlet 2.4 - */ - public static String getForwardContextPath(final ServletRequest pRequest) { - return (String) pRequest.getAttribute(ATTRIB_FWD_CONTEXT_PATH); - } - - /** - * Gets the servlet path of the resource this request was forwarded from. - * The value is read from the request attribute - * {@code "javax.servlet.forward.servlet_path"} - * - * @param pRequest the servlet request - * @return the servlet path of the resource, or {@code null} if not forwarded - * @see HttpServletRequest#getServletPath - * @since Servlet 2.4 - */ - public static String getForwardServletPath(final ServletRequest pRequest) { - return (String) pRequest.getAttribute(ATTRIB_FWD_SERVLET_PATH); - } - - /** - * Gets the path info of the resource this request was forwarded from. - * The value is read from the request attribute - * {@code "javax.servlet.forward.path_info"} - * - * @param pRequest the servlet request - * @return the path info of the resource, or {@code null} if not forwarded - * @see HttpServletRequest#getPathInfo - * @since Servlet 2.4 - */ - public static String getForwardPathInfo(final ServletRequest pRequest) { - return (String) pRequest.getAttribute(ATTRIB_FWD_PATH_INFO); - } - - /** - * Gets the query string of the resource this request was forwarded from. - * The value is read from the request attribute - * {@code "javax.servlet.forward.query_string"} - * - * @param pRequest the servlet request - * @return the query string of the resource, or {@code null} if not forwarded - * @see HttpServletRequest#getQueryString - * @since Servlet 2.4 - */ - public static String getForwardQueryString(final ServletRequest pRequest) { - return (String) pRequest.getAttribute(ATTRIB_FWD_QUERY_STRING); - } - - /** - * Gets the name of the servlet or the script that generated the servlet. - * - * @param pRequest The HTTP servlet request object. - * @return the script name. - * @todo Read the spec, seems to be a mismatch with the Servlet API... - * @see javax.servlet.http.HttpServletRequest#getServletPath() - */ - static String getScriptName(final HttpServletRequest pRequest) { - String requestURI = pRequest.getRequestURI(); - return StringUtil.getLastElement(requestURI, "/"); - } - - /** - * Gets the request URI relative to the current context path. - *

      - * As an example:

      -     * requestURI = "/webapp/index.jsp"
      -     * contextPath = "/webapp"
      -     * 
      - * The method will return {@code "/index.jsp"}. - * - * @param pRequest the current HTTP request - * @return the request URI relative to the current context path. - */ - 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(); - } - - /** - * Returns a {@code URL} containing the real path for a given virtual - * path, on URL form. - * Note that this method will return {@code null} for all the same reasons - * as {@code ServletContext.getRealPath(java.lang.String)} does. - * - * @param pContext the servlet context - * @param pPath the virtual path - * @return a {@code URL} object containing the path, or {@code null}. - * @throws MalformedURLException if the path refers to a malformed URL - * @see ServletContext#getRealPath(java.lang.String) - * @see ServletContext#getResource(java.lang.String) - */ - 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; - } - - /** - * Gets the temp directory for the given {@code ServletContext} (web app). - * - * @param pContext the servlet context - * @return the temp directory - */ - public static File getTempDir(final ServletContext pContext) { - return (File) pContext.getAttribute("javax.servlet.context.tempdir"); - } - - /** - * 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(final HttpServletRequest pRequest) { - HttpSession session = pRequest.getSession(); - - return (session != null) ? session.getId() : null; - } - - /** - * Creates an unmodifiable {@code Map} view of the given - * {@code ServletConfig}s init-parameters. - * Note: The returned {@code Map} is optimized for {@code get} - * operations and iterating over it's {@code keySet}. - * For other operations it may not perform well. - * - * @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(final ServletConfig pConfig) { - return new ServletConfigMapAdapter(pConfig); - } - - /** - * Creates an unmodifiable {@code Map} view of the given - * {@code FilterConfig}s init-parameters. - * Note: The returned {@code Map} is optimized for {@code get} - * operations and iterating over it's {@code keySet}. - * For other operations it may not perform well. - * - * @param pConfig the servlet filter configuration - * @return a {@code Map} view of the config - * @throws IllegalArgumentException if {@code pConfig} is {@code null} - */ - public static Map asMap(final FilterConfig pConfig) { - return new ServletConfigMapAdapter(pConfig); - } - - /** - * Creates an unmodifiable {@code Map} view of the given - * {@code ServletContext}s init-parameters. - * Note: The returned {@code Map} is optimized for {@code get} - * operations and iterating over it's {@code keySet}. - * For other operations it may not perform well. - * - * @param pContext the servlet context - * @return a {@code Map} view of the init parameters - * @throws IllegalArgumentException if {@code pContext} is {@code null} - */ - public static Map initParamsAsMap(final ServletContext pContext) { - return new ServletConfigMapAdapter(pContext); - } - - /** - * Creates an modifiable {@code Map} view of the given - * {@code ServletContext}s attributes. - * - * @param pContext the servlet context - * @return a {@code Map} view of the attributes - * @throws IllegalArgumentException if {@code pContext} is {@code null} - */ - public static Map attributesAsMap(final ServletContext pContext) { - return new ServletAttributesMapAdapter(pContext); - } - - /** - * Creates an modifiable {@code Map} view of the given - * {@code ServletRequest}s attributes. - * - * @param pRequest the servlet request - * @return a {@code Map} view of the attributes - * @throws IllegalArgumentException if {@code pContext} is {@code null} - */ - public static Map attributesAsMap(final ServletRequest pRequest) { - return new ServletAttributesMapAdapter(pRequest); - } - - /** - * Creates an unmodifiable {@code Map} view of the given - * {@code HttpServletRequest}s request parameters. - * - * @param pRequest the request - * @return a {@code Map} view of the request parameters - * @throws IllegalArgumentException if {@code pRequest} is {@code null} - */ - public static Map> parametersAsMap(final ServletRequest pRequest) { - return new ServletParametersMapAdapter(pRequest); - } - - /** - * Creates an unmodifiable {@code Map} view of the given - * {@code HttpServletRequest}s request headers. - * - * @param pRequest the request - * @return a {@code Map} view of the request headers - * @throws IllegalArgumentException if {@code pRequest} is {@code null} - */ - public static Map> headersAsMap(final HttpServletRequest pRequest) { - return new ServletHeadersMapAdapter(pRequest); - } - - /** - * Creates a wrapper that implements either {@code ServletResponse} or - * {@code HttpServletResponse}, depending on the type of - * {@code pImplementation.getResponse()}. - * - * @param pImplementation the servlet response to create a wrapper for - * @return a {@code ServletResponse} or - * {@code HttpServletResponse}, depending on the type of - * {@code pImplementation.getResponse()} - */ - public static ServletResponse createWrapper(final ServletResponseWrapper pImplementation) { - // TODO: Get all interfaces from implementation - if (pImplementation.getResponse() instanceof HttpServletResponse) { - return (HttpServletResponse) Proxy.newProxyInstance(pImplementation.getClass().getClassLoader(), - new Class[]{HttpServletResponse.class, ServletResponse.class}, - new HttpServletResponseHandler(pImplementation)); - } - return pImplementation; - } - - /** - * Creates a wrapper that implements either {@code ServletRequest} or - * {@code HttpServletRequest}, depending on the type of - * {@code pImplementation.getRequest()}. - * - * @param pImplementation the servlet request to create a wrapper for - * @return a {@code ServletResponse} or - * {@code HttpServletResponse}, depending on the type of - * {@code pImplementation.getResponse()} - */ - public static ServletRequest createWrapper(final ServletRequestWrapper pImplementation) { - // TODO: Get all interfaces from implementation - if (pImplementation.getRequest() instanceof HttpServletRequest) { - return (HttpServletRequest) Proxy.newProxyInstance(pImplementation.getClass().getClassLoader(), - new Class[]{HttpServletRequest.class, ServletRequest.class}, - new HttpServletRequestHandler(pImplementation)); - } - return pImplementation; - } - - private static class HttpServletResponseHandler implements InvocationHandler { - private final ServletResponseWrapper response; - - HttpServletResponseHandler(final ServletResponseWrapper pResponse) { - response = pResponse; - } - - public Object invoke(final Object pProxy, final Method pMethod, final Object[] pArgs) throws Throwable { - try { - // TODO: Allow partial implementing? - if (pMethod.getDeclaringClass().isInstance(response)) { - return pMethod.invoke(response, pArgs); - } - - // Method is not implemented in wrapper - return pMethod.invoke(response.getResponse(), pArgs); - } - catch (InvocationTargetException e) { - // Unwrap, to avoid UndeclaredThrowableException... - throw e.getTargetException(); - } - } - } - - private static class HttpServletRequestHandler implements InvocationHandler { - private final ServletRequestWrapper request; - - HttpServletRequestHandler(final ServletRequestWrapper pRequest) { - request = pRequest; - } - - public Object invoke(final Object pProxy, final Method pMethod, final Object[] pArgs) throws Throwable { - try { - // TODO: Allow partial implementing? - if (pMethod.getDeclaringClass().isInstance(request)) { - return pMethod.invoke(request, pArgs); - } - - // Method is not implemented in wrapper - return pMethod.invoke(request.getRequest(), pArgs); - } - catch (InvocationTargetException e) { - // Unwrap, to avoid UndeclaredThrowableException... - throw e.getTargetException(); - } - } - } -} - +/* + * 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 of the copyright holder 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 HOLDER 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.lang.StringUtil; +import com.twelvemonkeys.util.convert.ConversionException; +import com.twelvemonkeys.util.convert.Converter; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.io.File; +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.List; +import java.util.Map; + + +/** + * Various servlet related helper methods. + * + * @author Harald Kuhr + * @author Eirik Torske + * @author last modified by $Author: haku $ + * @version $Id: ServletUtil.java#3 $ + */ +public final class ServletUtil { + + /** + * {@code "javax.servlet.include.request_uri"} + */ + private final static String ATTRIB_INC_REQUEST_URI = "javax.servlet.include.request_uri"; + + /** + * {@code "javax.servlet.include.context_path"} + */ + private final static String ATTRIB_INC_CONTEXT_PATH = "javax.servlet.include.context_path"; + + /** + * {@code "javax.servlet.include.servlet_path"} + */ + private final static String ATTRIB_INC_SERVLET_PATH = "javax.servlet.include.servlet_path"; + + /** + * {@code "javax.servlet.include.path_info"} + */ + private final static String ATTRIB_INC_PATH_INFO = "javax.servlet.include.path_info"; + + /** + * {@code "javax.servlet.include.query_string"} + */ + private final static String ATTRIB_INC_QUERY_STRING = "javax.servlet.include.query_string"; + + /** + * {@code "javax.servlet.forward.request_uri"} + */ + private final static String ATTRIB_FWD_REQUEST_URI = "javax.servlet.forward.request_uri"; + + /** + * {@code "javax.servlet.forward.context_path"} + */ + private final static String ATTRIB_FWD_CONTEXT_PATH = "javax.servlet.forward.context_path"; + + /** + * {@code "javax.servlet.forward.servlet_path"} + */ + private final static String ATTRIB_FWD_SERVLET_PATH = "javax.servlet.forward.servlet_path"; + + /** + * {@code "javax.servlet.forward.path_info"} + */ + private final static String ATTRIB_FWD_PATH_INFO = "javax.servlet.forward.path_info"; + + /** + * {@code "javax.servlet.forward.query_string"} + */ + private final static String ATTRIB_FWD_QUERY_STRING = "javax.servlet.forward.query_string"; + + /** + * Don't create, static methods only + */ + private ServletUtil() { + } + + /** + * Gets the value of the given parameter from the request, or if the + * parameter is not set, the default value. + * + * @param pReq the servlet request + * @param pName the parameter name + * @param pDefault the default value + * @return the value of the parameter, or the default value, if the + * parameter is not set. + */ + public static String getParameter(final ServletRequest pReq, final String pName, final String pDefault) { + String str = pReq.getParameter(pName); + + return str != null ? str : pDefault; + } + + /** + * Gets the value of the given parameter from the request converted to + * an Object. 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 pType the type of object (class) to return + * @param pFormat the format to use (might be {@code null} in many cases) + * @param pDefault the default value + * @return the value of the parameter converted to a boolean, or the + * default value, if the parameter is not set. + * @throws IllegalArgumentException if {@code pDefault} is + * non-{@code null} and not an instance of {@code pType} + * @throws NullPointerException if {@code pReq}, {@code pName} or + * {@code pType} is {@code null}. + * @see Converter#toObject + */ + // TODO: Well, it's done. Need some thinking... We probably don't want default if conversion fails... + 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()); + } + + String str = pReq.getParameter(pName); + + if (str == null) { + return pDefault; + } + + try { + return pType.cast(Converter.getInstance().toObject(str, pType, pFormat)); + } + catch (ConversionException ce) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * 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 {@code boolean}, or the + * default value, if the parameter is not set. + */ + 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; + } + catch (NumberFormatException nfe) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * 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 {@code int}, or the default + * value, if the parameter is not set. + */ + 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; + } + catch (NumberFormatException nfe) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * 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 {@code long}, or the default + * value, if the parameter is not set. + */ + 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; + } + catch (NumberFormatException nfe) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * 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 {@code float}, or the default + * value, if the parameter is not set. + */ + 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; + } + catch (NumberFormatException nfe) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * 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 {@code double}, or the default + * value, if the parameter is not set. + */ + 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; + } + catch (NumberFormatException nfe) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * 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 {@code Date}, or the + * default value, if the parameter is not set. + * @see com.twelvemonkeys.lang.StringUtil#toDate(String) + */ + 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; + } + catch (IllegalArgumentException iae) { + return pDefault; + } + } + + /** + * Gets the value of the given parameter from the request converted to + * a 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 pFormat the date format to use + * @param pDefault the default value + * @return the value of the parameter converted to a Date, or the + * default value, if the parameter is not set. + * @see com.twelvemonkeys.lang.StringUtil#toDate(String,String) + */ + /* + public static long getDateParameter(ServletRequest pReq, String pName, String pFormat, long pDefault) { + String str = pReq.getParameter(pName); + + try { + return ((str != null) ? StringUtil.toDate(str, pFormat).getTime() : pDefault); + } + catch (IllegalArgumentException iae) { + return pDefault; + } + } + */ + + /** + * Builds a full-blown HTTP/HTTPS URL from a + * {@code javax.servlet.http.HttpServletRequest} object. + * + * @param pRequest The HTTP servlet request object. + * @return the reproduced URL + * @deprecated Use {@link javax.servlet.http.HttpServletRequest#getRequestURL()} + * instead. + */ + static StringBuffer buildHTTPURL(final HttpServletRequest pRequest) { + StringBuffer resultURL = new StringBuffer(); + + // Scheme, as in http, https, ftp etc + String scheme = pRequest.getScheme(); + resultURL.append(scheme); + resultURL.append("://"); + resultURL.append(pRequest.getServerName()); + + // Append port only if not default port + int port = pRequest.getServerPort(); + if (port > 0 && + !(("http".equals(scheme) && port == 80) || + ("https".equals(scheme) && port == 443))) { + resultURL.append(":"); + resultURL.append(port); + } + + // Append URI + resultURL.append(pRequest.getRequestURI()); + + // If present, append extra path info + String pathInfo = pRequest.getPathInfo(); + if (pathInfo != null) { + resultURL.append(pathInfo); + } + + return resultURL; + } + + /** + * Gets the URI of the resource currently included. + * The value is read from the request attribute + * {@code "javax.servlet.include.request_uri"} + * + * @param pRequest the servlet request + * @return the URI of the included resource, or {@code null} if no include + * @see HttpServletRequest#getRequestURI + * @since Servlet 2.2 + */ + public static String getIncludeRequestURI(final ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_INC_REQUEST_URI); + } + + /** + * Gets the context path of the resource currently included. + * The value is read from the request attribute + * {@code "javax.servlet.include.context_path"} + * + * @param pRequest the servlet request + * @return the context path of the included resource, or {@code null} if no include + * @see HttpServletRequest#getContextPath + * @since Servlet 2.2 + */ + public static String getIncludeContextPath(final ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_INC_CONTEXT_PATH); + } + + /** + * Gets the servlet path of the resource currently included. + * The value is read from the request attribute + * {@code "javax.servlet.include.servlet_path"} + * + * @param pRequest the servlet request + * @return the servlet path of the included resource, or {@code null} if no include + * @see HttpServletRequest#getServletPath + * @since Servlet 2.2 + */ + public static String getIncludeServletPath(final ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_INC_SERVLET_PATH); + } + + /** + * Gets the path info of the resource currently included. + * The value is read from the request attribute + * {@code "javax.servlet.include.path_info"} + * + * @param pRequest the servlet request + * @return the path info of the included resource, or {@code null} if no include + * @see HttpServletRequest#getPathInfo + * @since Servlet 2.2 + */ + public static String getIncludePathInfo(final ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_INC_PATH_INFO); + } + + /** + * Gets the query string of the resource currently included. + * The value is read from the request attribute + * {@code "javax.servlet.include.query_string"} + * + * @param pRequest the servlet request + * @return the query string of the included resource, or {@code null} if no include + * @see HttpServletRequest#getQueryString + * @since Servlet 2.2 + */ + public static String getIncludeQueryString(final ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_INC_QUERY_STRING); + } + + /** + * Gets the URI of the resource this request was forwarded from. + * The value is read from the request attribute + * {@code "javax.servlet.forward.request_uri"} + * + * @param pRequest the servlet request + * @return the URI of the resource, or {@code null} if not forwarded + * @see HttpServletRequest#getRequestURI + * @since Servlet 2.4 + */ + public static String getForwardRequestURI(final ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_FWD_REQUEST_URI); + } + + /** + * Gets the context path of the resource this request was forwarded from. + * The value is read from the request attribute + * {@code "javax.servlet.forward.context_path"} + * + * @param pRequest the servlet request + * @return the context path of the resource, or {@code null} if not forwarded + * @see HttpServletRequest#getContextPath + * @since Servlet 2.4 + */ + public static String getForwardContextPath(final ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_FWD_CONTEXT_PATH); + } + + /** + * Gets the servlet path of the resource this request was forwarded from. + * The value is read from the request attribute + * {@code "javax.servlet.forward.servlet_path"} + * + * @param pRequest the servlet request + * @return the servlet path of the resource, or {@code null} if not forwarded + * @see HttpServletRequest#getServletPath + * @since Servlet 2.4 + */ + public static String getForwardServletPath(final ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_FWD_SERVLET_PATH); + } + + /** + * Gets the path info of the resource this request was forwarded from. + * The value is read from the request attribute + * {@code "javax.servlet.forward.path_info"} + * + * @param pRequest the servlet request + * @return the path info of the resource, or {@code null} if not forwarded + * @see HttpServletRequest#getPathInfo + * @since Servlet 2.4 + */ + public static String getForwardPathInfo(final ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_FWD_PATH_INFO); + } + + /** + * Gets the query string of the resource this request was forwarded from. + * The value is read from the request attribute + * {@code "javax.servlet.forward.query_string"} + * + * @param pRequest the servlet request + * @return the query string of the resource, or {@code null} if not forwarded + * @see HttpServletRequest#getQueryString + * @since Servlet 2.4 + */ + public static String getForwardQueryString(final ServletRequest pRequest) { + return (String) pRequest.getAttribute(ATTRIB_FWD_QUERY_STRING); + } + + /** + * Gets the name of the servlet or the script that generated the servlet. + * + * @param pRequest The HTTP servlet request object. + * @return the script name. + * @see javax.servlet.http.HttpServletRequest#getServletPath() + */ + // TODO: Read the spec, seems to be a mismatch with the Servlet API... + static String getScriptName(final HttpServletRequest pRequest) { + String requestURI = pRequest.getRequestURI(); + return StringUtil.getLastElement(requestURI, "/"); + } + + /** + * Gets the request URI relative to the current context path. + *

      + * As an example: + *

      + *
      +     * requestURI = "/webapp/index.jsp"
      +     * contextPath = "/webapp"
      +     * 
      + * The method will return {@code "/index.jsp"}. + * + * @param pRequest the current HTTP request + * @return the request URI relative to the current context path. + */ + 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(); + } + + /** + * Returns a {@code URL} containing the real path for a given virtual + * path, on URL form. + * Note that this method will return {@code null} for all the same reasons + * as {@code ServletContext.getRealPath(java.lang.String)} does. + * + * @param pContext the servlet context + * @param pPath the virtual path + * @return a {@code URL} object containing the path, or {@code null}. + * @throws MalformedURLException if the path refers to a malformed URL + * @see ServletContext#getRealPath(java.lang.String) + * @see ServletContext#getResource(java.lang.String) + */ + 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; + } + + /** + * Gets the temp directory for the given {@code ServletContext} (web app). + * + * @param pContext the servlet context + * @return the temp directory + */ + public static File getTempDir(final ServletContext pContext) { + return (File) pContext.getAttribute("javax.servlet.context.tempdir"); + } + + /** + * 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(final HttpServletRequest pRequest) { + HttpSession session = pRequest.getSession(); + + return (session != null) ? session.getId() : null; + } + + /** + * Creates an unmodifiable {@code Map} view of the given + * {@code ServletConfig}s init-parameters. + * Note: The returned {@code Map} is optimized for {@code get} + * operations and iterating over it's {@code keySet}. + * For other operations it may not perform well. + * + * @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(final ServletConfig pConfig) { + return new ServletConfigMapAdapter(pConfig); + } + + /** + * Creates an unmodifiable {@code Map} view of the given + * {@code FilterConfig}s init-parameters. + * Note: The returned {@code Map} is optimized for {@code get} + * operations and iterating over it's {@code keySet}. + * For other operations it may not perform well. + * + * @param pConfig the servlet filter configuration + * @return a {@code Map} view of the config + * @throws IllegalArgumentException if {@code pConfig} is {@code null} + */ + public static Map asMap(final FilterConfig pConfig) { + return new ServletConfigMapAdapter(pConfig); + } + + /** + * Creates an unmodifiable {@code Map} view of the given + * {@code ServletContext}s init-parameters. + * Note: The returned {@code Map} is optimized for {@code get} + * operations and iterating over it's {@code keySet}. + * For other operations it may not perform well. + * + * @param pContext the servlet context + * @return a {@code Map} view of the init parameters + * @throws IllegalArgumentException if {@code pContext} is {@code null} + */ + public static Map initParamsAsMap(final ServletContext pContext) { + return new ServletConfigMapAdapter(pContext); + } + + /** + * Creates an modifiable {@code Map} view of the given + * {@code ServletContext}s attributes. + * + * @param pContext the servlet context + * @return a {@code Map} view of the attributes + * @throws IllegalArgumentException if {@code pContext} is {@code null} + */ + public static Map attributesAsMap(final ServletContext pContext) { + return new ServletAttributesMapAdapter(pContext); + } + + /** + * Creates an modifiable {@code Map} view of the given + * {@code ServletRequest}s attributes. + * + * @param pRequest the servlet request + * @return a {@code Map} view of the attributes + * @throws IllegalArgumentException if {@code pContext} is {@code null} + */ + public static Map attributesAsMap(final ServletRequest pRequest) { + return new ServletAttributesMapAdapter(pRequest); + } + + /** + * Creates an unmodifiable {@code Map} view of the given + * {@code HttpServletRequest}s request parameters. + * + * @param pRequest the request + * @return a {@code Map} view of the request parameters + * @throws IllegalArgumentException if {@code pRequest} is {@code null} + */ + public static Map> parametersAsMap(final ServletRequest pRequest) { + return new ServletParametersMapAdapter(pRequest); + } + + /** + * Creates an unmodifiable {@code Map} view of the given + * {@code HttpServletRequest}s request headers. + * + * @param pRequest the request + * @return a {@code Map} view of the request headers + * @throws IllegalArgumentException if {@code pRequest} is {@code null} + */ + public static Map> headersAsMap(final HttpServletRequest pRequest) { + return new ServletHeadersMapAdapter(pRequest); + } + + /** + * Creates a wrapper that implements either {@code ServletResponse} or + * {@code HttpServletResponse}, depending on the type of + * {@code pImplementation.getResponse()}. + * + * @param pImplementation the servlet response to create a wrapper for + * @return a {@code ServletResponse} or + * {@code HttpServletResponse}, depending on the type of + * {@code pImplementation.getResponse()} + */ + public static ServletResponse createWrapper(final ServletResponseWrapper pImplementation) { + // TODO: Get all interfaces from implementation + if (pImplementation.getResponse() instanceof HttpServletResponse) { + return (HttpServletResponse) Proxy.newProxyInstance(pImplementation.getClass().getClassLoader(), + new Class[]{HttpServletResponse.class, ServletResponse.class}, + new HttpServletResponseHandler(pImplementation)); + } + return pImplementation; + } + + /** + * Creates a wrapper that implements either {@code ServletRequest} or + * {@code HttpServletRequest}, depending on the type of + * {@code pImplementation.getRequest()}. + * + * @param pImplementation the servlet request to create a wrapper for + * @return a {@code ServletResponse} or + * {@code HttpServletResponse}, depending on the type of + * {@code pImplementation.getResponse()} + */ + public static ServletRequest createWrapper(final ServletRequestWrapper pImplementation) { + // TODO: Get all interfaces from implementation + if (pImplementation.getRequest() instanceof HttpServletRequest) { + return (HttpServletRequest) Proxy.newProxyInstance(pImplementation.getClass().getClassLoader(), + new Class[]{HttpServletRequest.class, ServletRequest.class}, + new HttpServletRequestHandler(pImplementation)); + } + return pImplementation; + } + + private static class HttpServletResponseHandler implements InvocationHandler { + private final ServletResponseWrapper response; + + HttpServletResponseHandler(final ServletResponseWrapper pResponse) { + response = pResponse; + } + + public Object invoke(final Object pProxy, final Method pMethod, final Object[] pArgs) throws Throwable { + try { + // TODO: Allow partial implementing? + if (pMethod.getDeclaringClass().isInstance(response)) { + return pMethod.invoke(response, pArgs); + } + + // Method is not implemented in wrapper + return pMethod.invoke(response.getResponse(), pArgs); + } + catch (InvocationTargetException e) { + // Unwrap, to avoid UndeclaredThrowableException... + throw e.getTargetException(); + } + } + } + + private static class HttpServletRequestHandler implements InvocationHandler { + private final ServletRequestWrapper request; + + HttpServletRequestHandler(final ServletRequestWrapper pRequest) { + request = pRequest; + } + + public Object invoke(final Object pProxy, final Method pMethod, final Object[] pArgs) throws Throwable { + try { + // TODO: Allow partial implementing? + if (pMethod.getDeclaringClass().isInstance(request)) { + return pMethod.invoke(request, pArgs); + } + + // Method is not implemented in wrapper + return pMethod.invoke(request.getRequest(), pArgs); + } + catch (InvocationTargetException e) { + // Unwrap, to avoid UndeclaredThrowableException... + throw e.getTargetException(); + } + } + } +} + diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java b/servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java index aa2bae79..c531cc3f 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/ThrottleFilter.java @@ -1,308 +1,309 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.lang.StringUtil; - -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.HttpServletResponse; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * ThrottleFilter, a filter for easing server during heavy load. - *

      - * 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: ThrottleFilter.java#1 $ - * @see #setMaxConcurrentThreadCount - * @see #setResponseMessages - */ -public class ThrottleFilter extends GenericFilter { - - /** - * Minimum free thread count, defaults to {@code 10} - */ - protected int maxConcurrentThreadCount = 10; - - /** - * The number of running request threads - */ - private int runningThreads = 0; - private final Object runningThreadsLock = new Object(); - - /** - * Default response message sent to user agents, if the request is rejected - */ - protected final static String DEFUALT_RESPONSE_MESSAGE = - "Service temporarily unavailable, please try again later."; - - /** - * Default response content type - */ - protected static final String DEFAULT_TYPE = "text/html"; - - /** - * The reposne message sent to user agenta, if the request is rejected - */ - private Map responseMessageNames = new HashMap(10); - - /** - * The reposne message sent to user agents, if the request is rejected - */ - private String[] responseMessageTypes = null; - - /** - * Cache for response messages - */ - private Map responseCache = new HashMap(10); - - - /** - * Sets the minimum free thread count. - * - * @param pMaxConcurrentThreadCount - */ - public void setMaxConcurrentThreadCount(String pMaxConcurrentThreadCount) { - if (!StringUtil.isEmpty(pMaxConcurrentThreadCount)) { - try { - maxConcurrentThreadCount = Integer.parseInt(pMaxConcurrentThreadCount); - } - catch (NumberFormatException nfe) { - // Use default - } - } - } - - /** - * Sets the response message sent to the user agent, if the request is - * rejected. - *
      - * The format is {@code <mime-type>=<filename>, - * <mime-type>=<filename>}. - *
      - * Example: {@code <text/vnd.wap.wmlgt;=</errors/503.wml>, - * <text/html>=</errors/503.html>} - * - * @param pResponseMessages - */ - public void setResponseMessages(String pResponseMessages) { - // Split string in type=filename pairs - String[] mappings = StringUtil.toStringArray(pResponseMessages, ", \r\n\t"); - List types = new ArrayList(); - - for (String pair : mappings) { - // Split pairs on '=' - 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]); - responseMessageNames.put(mapping[0], mapping[1]); - } - - // Create arrays - responseMessageTypes = types.toArray(new String[types.size()]); - } - - /** - * @param pRequest - * @param pResponse - * @param pChain - * @throws IOException - * @throws ServletException - */ - protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { - try { - if (beginRequest()) { - // Continue request - pChain.doFilter(pRequest, pResponse); - } - else { - // Send error and end request - // Get HTTP specific versions - HttpServletRequest request = (HttpServletRequest) pRequest; - HttpServletResponse response = (HttpServletResponse) pResponse; - - // Get content type - String contentType = getContentType(request); - - // Note: This is not the way the spec says you should do it. - // However, we handle error response this way for preformace reasons. - // The "correct" way would be to use sendError() and register a servlet - // that does the content negotiation as errorpage in the web descriptor. - response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); - response.setContentType(contentType); - response.getWriter().println(getMessage(contentType)); - - // Log warning, as this shouldn't happen too often - log("Request denied, no more available threads for requestURI=" + request.getRequestURI()); - } - } - finally { - doneRequest(); - } - } - - /** - * Marks the beginning of a request - * - * @return {@code true} if the request should be handled. - */ - private boolean beginRequest() { - synchronized (runningThreadsLock) { - runningThreads++; - } - - return (runningThreads <= maxConcurrentThreadCount); - } - - /** - * Marks the end of the request - */ - private void doneRequest() { - synchronized (runningThreadsLock) { - runningThreads--; - } - } - - /** - * Gets the content type for the response, suitable for the requesting user agent. - * - * @param pRequest - * @return the content type - */ - private String getContentType(HttpServletRequest pRequest) { - if (responseMessageTypes != null) { - String accept = pRequest.getHeader("Accept"); - - 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)) { - return type; - } - } - } - - // If none found, return default - return DEFAULT_TYPE; - } - - /** - * Gets the response message for the given content type. - * - * @param pContentType - * @return the message - */ - private String getMessage(String pContentType) { - String fileName = responseMessageNames.get(pContentType); - - // Get cached value - CacheEntry entry = responseCache.get(fileName); - - if ((entry == null) || entry.isExpired()) { - - // Create and add or replace cached value - entry = new CacheEntry(readMessage(fileName)); - responseCache.put(fileName, entry); - } - - // Return value - return (entry.getValue() != null) - ? (String) entry.getValue() - : DEFUALT_RESPONSE_MESSAGE; - } - - /** - * Reads the response message from a file in the current web app. - * - * @param pFileName - * @return the message - */ - private String readMessage(String pFileName) { - try { - // Read resource from web app - InputStream is = getServletContext().getResourceAsStream(pFileName); - - if (is != null) { - return new String(FileUtil.read(is)); - } - else { - log("File not found: " + pFileName); - } - } - catch (IOException ioe) { - log("Error reading file: " + pFileName + " (" + ioe.getMessage() + ")"); - } - return null; - } - - /** - * Keeps track of Cached objects - */ - private static class CacheEntry { - private Object value; - private long timestamp = -1; - - CacheEntry(Object pValue) { - value = pValue; - timestamp = System.currentTimeMillis(); - } - - Object getValue() { - return value; - } - - boolean isExpired() { - return (System.currentTimeMillis() - timestamp) > 60000; // Cache 1 minute - } - } +/* + * 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 of the copyright holder 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 HOLDER 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 com.twelvemonkeys.lang.StringUtil; + +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.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * ThrottleFilter, a filter for easing server during heavy load. + *

      + * 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: ThrottleFilter.java#1 $ + * @see #setMaxConcurrentThreadCount + * @see #setResponseMessages + */ +public class ThrottleFilter extends GenericFilter { + + /** + * Minimum free thread count, defaults to {@code 10} + */ + protected int maxConcurrentThreadCount = 10; + + /** + * The number of running request threads + */ + private int runningThreads = 0; + private final Object runningThreadsLock = new Object(); + + /** + * Default response message sent to user agents, if the request is rejected + */ + protected final static String DEFUALT_RESPONSE_MESSAGE = + "Service temporarily unavailable, please try again later."; + + /** + * Default response content type + */ + protected static final String DEFAULT_TYPE = "text/html"; + + /** + * The reposne message sent to user agenta, if the request is rejected + */ + private Map responseMessageNames = new HashMap(10); + + /** + * The reposne message sent to user agents, if the request is rejected + */ + private String[] responseMessageTypes = null; + + /** + * Cache for response messages + */ + private Map responseCache = new HashMap(10); + + + /** + * Sets the minimum free thread count. + * + * @param pMaxConcurrentThreadCount + */ + public void setMaxConcurrentThreadCount(String pMaxConcurrentThreadCount) { + if (!StringUtil.isEmpty(pMaxConcurrentThreadCount)) { + try { + maxConcurrentThreadCount = Integer.parseInt(pMaxConcurrentThreadCount); + } + catch (NumberFormatException nfe) { + // Use default + } + } + } + + /** + * Sets the response message sent to the user agent, if the request is + * rejected. + *
      + * The format is {@code <mime-type>=<filename>, + * <mime-type>=<filename>}. + *
      + * Example: {@code <text/vnd.wap.wmlgt;=</errors/503.wml>, + * <text/html>=</errors/503.html>} + * + * @param pResponseMessages + */ + public void setResponseMessages(String pResponseMessages) { + // Split string in type=filename pairs + String[] mappings = StringUtil.toStringArray(pResponseMessages, ", \r\n\t"); + List types = new ArrayList(); + + for (String pair : mappings) { + // Split pairs on '=' + 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]); + responseMessageNames.put(mapping[0], mapping[1]); + } + + // Create arrays + responseMessageTypes = types.toArray(new String[types.size()]); + } + + /** + * @param pRequest + * @param pResponse + * @param pChain + * @throws IOException + * @throws ServletException + */ + protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { + try { + if (beginRequest()) { + // Continue request + pChain.doFilter(pRequest, pResponse); + } + else { + // Send error and end request + // Get HTTP specific versions + HttpServletRequest request = (HttpServletRequest) pRequest; + HttpServletResponse response = (HttpServletResponse) pResponse; + + // Get content type + String contentType = getContentType(request); + + // Note: This is not the way the spec says you should do it. + // However, we handle error response this way for preformace reasons. + // The "correct" way would be to use sendError() and register a servlet + // that does the content negotiation as errorpage in the web descriptor. + response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE); + response.setContentType(contentType); + response.getWriter().println(getMessage(contentType)); + + // Log warning, as this shouldn't happen too often + log("Request denied, no more available threads for requestURI=" + request.getRequestURI()); + } + } + finally { + doneRequest(); + } + } + + /** + * Marks the beginning of a request + * + * @return {@code true} if the request should be handled. + */ + private boolean beginRequest() { + synchronized (runningThreadsLock) { + runningThreads++; + } + + return (runningThreads <= maxConcurrentThreadCount); + } + + /** + * Marks the end of the request + */ + private void doneRequest() { + synchronized (runningThreadsLock) { + runningThreads--; + } + } + + /** + * Gets the content type for the response, suitable for the requesting user agent. + * + * @param pRequest + * @return the content type + */ + private String getContentType(HttpServletRequest pRequest) { + if (responseMessageTypes != null) { + String accept = pRequest.getHeader("Accept"); + + 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)) { + return type; + } + } + } + + // If none found, return default + return DEFAULT_TYPE; + } + + /** + * Gets the response message for the given content type. + * + * @param pContentType + * @return the message + */ + private String getMessage(String pContentType) { + String fileName = responseMessageNames.get(pContentType); + + // Get cached value + CacheEntry entry = responseCache.get(fileName); + + if ((entry == null) || entry.isExpired()) { + + // Create and add or replace cached value + entry = new CacheEntry(readMessage(fileName)); + responseCache.put(fileName, entry); + } + + // Return value + return (entry.getValue() != null) + ? (String) entry.getValue() + : DEFUALT_RESPONSE_MESSAGE; + } + + /** + * Reads the response message from a file in the current web app. + * + * @param pFileName + * @return the message + */ + private String readMessage(String pFileName) { + try { + // Read resource from web app + InputStream is = getServletContext().getResourceAsStream(pFileName); + + if (is != null) { + return new String(FileUtil.read(is)); + } + else { + log("File not found: " + pFileName); + } + } + catch (IOException ioe) { + log("Error reading file: " + pFileName + " (" + ioe.getMessage() + ")"); + } + return null; + } + + /** + * Keeps track of Cached objects + */ + private static class CacheEntry { + private Object value; + private long timestamp = -1; + + CacheEntry(Object pValue) { + value = pValue; + timestamp = System.currentTimeMillis(); + } + + Object getValue() { + return value; + } + + boolean isExpired() { + return (System.currentTimeMillis() - timestamp) > 60000; // Cache 1 minute + } + } } \ 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 574559f9..07ab319c 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/TrimWhiteSpaceFilter.java @@ -1,235 +1,239 @@ -/* - * 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 of the copyright holder 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 HOLDER 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 javax.servlet.*; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; - -/** - * Removes extra unneccessary white space from a servlet response. - * White space is defined as per {@link Character#isWhitespace(char)}. - *

      - * This filter has no understanding of the content in the reponse, and will - * remove repeated white space anywhere in the stream. It is intended for - * removing white space from HTML or XML streams, but this limitation makes it - * less suited for filtering HTML/XHTML with embedded CSS or JavaScript, - * in case white space should be significant here. It is strongly reccommended - * you keep CSS and JavaScript in separate files (this will have the added - * benefit of further reducing the ammount of data communicated between - * server and client). - *

      - * At the moment this filter has no concept of encoding. - * This means, that if some multi-byte escape sequence contains one or more - * bytes that individually is treated as a white space, these bytes - * may be skipped. - * As UTF-8 - * guarantees that no bytes are repeated in this way, this filter can safely - * filter UTF-8. - * Simple 8 bit character encodings, like the - * ISO/IEC 8859 standard, or - * - * are always safe. - *

      - * Configuration
      - * To use {@code TrimWhiteSpaceFilter} in your web-application, you simply need - * to add it to your web descriptor ({@code web.xml}). - * If using a servlet container that supports the Servlet 2.4 spec, the new - * {@code dispatcher} element should be used, and set to - * {@code REQUEST/FORWARD}, to make sure the filter is invoked only once for - * requests. - * If using an older web descriptor, set the {@code init-param} - * {@code "once-per-request"} to {@code "true"} (this will have the same effect, - * but might perform slightly worse than the 2.4 version). - * Please see the examples below. - *

      - * Servlet 2.4 version, filter section:
      - *

      - * <!-- TrimWS Filter Configuration -->
      - * <filter>
      - *      <filter-name>trimws</filter-name>
      - *      <filter-class>com.twelvemonkeys.servlet.TrimWhiteSpaceFilter</filter-class>
      - *      <!-- auto-flush=true is the default, may be omitted -->
      - *      <init-param>
      - *          <param-name>auto-flush</param-name>
      - *          <param-value>true</param-value>
      - *      </init-param>
      - * </filter>
      - * 
      - * Filter-mapping section:
      - *
      - * <!-- TimWS Filter Mapping -->
      - * <filter-mapping>
      - *      <filter-name>trimws</filter-name>
      - *      <url-pattern>*.html</url-pattern>
      - *      <dispatcher>REQUEST</dispatcher>
      - *      <dispatcher>FORWARD</dispatcher>
      - * </filter-mapping>
      - * <filter-mapping>
      - *      <filter-name>trimws</filter-name>
      - *      <url-pattern>*.jsp</url-pattern>
      - *      <dispatcher>REQUEST</dispatcher>
      - *      <dispatcher>FORWARD</dispatcher>
      - * </filter-mapping>
      - * 
      - * - * @author
      Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: TrimWhiteSpaceFilter.java#2 $ - */ -public class TrimWhiteSpaceFilter extends GenericFilter { - - private boolean autoFlush = true; - - @InitParam - public void setAutoFlush(final boolean pAutoFlush) { - autoFlush = pAutoFlush; - } - - public void init() throws ServletException { - super.init(); - 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 (autoFlush) { - wrapped.flushBuffer(); - } - } - - static final class TrimWSFilterOutputStream extends FilterOutputStream { - boolean lastWasWS = true; // Avoids leading WS by init to true - - public TrimWSFilterOutputStream(OutputStream pOut) { - super(pOut); - } - - // Override this, in case the wrapped outputstream overrides... - public final void write(byte pBytes[]) throws IOException { - write(pBytes, 0, pBytes.length); - } - - // Override this, in case the wrapped outputstream overrides... - public final void write(byte pBytes[], int pOff, int pLen) throws IOException { - if (pBytes == null) { - throw new NullPointerException("bytes == null"); - } - else if (pOff < 0 || pLen < 0 || (pOff + pLen > pBytes.length)) { - throw new IndexOutOfBoundsException("Bytes: " + pBytes.length + " Offset: " + pOff + " Length: " + pLen); - } - - for (int i = 0; i < pLen ; i++) { - write(pBytes[pOff + i]); - } - } - - public void write(int pByte) throws IOException { - // TODO: Is this good enough for multi-byte encodings like UTF-16? - // Consider writing through a Writer that does that for us, and - // also buffer whitespace, so we write a linefeed every time there's - // one in the original... - - // According to http://en.wikipedia.org/wiki/UTF-8: - // "[...] US-ASCII octet values do not appear otherwise in a UTF-8 - // encoded character stream. This provides compatibility with file - // systems or other software (e.g., the printf() function in - // C libraries) that parse based on US-ASCII values but are - // transparent to other values." - - if (!Character.isWhitespace((char) pByte)) { - // If char is not WS, just store - super.write(pByte); - lastWasWS = false; - } - else { - // TODO: Consider writing only 0x0a (LF) and 0x20 (space) - // Else, if char is WS, store first, skip the rest - if (!lastWasWS) { - if (pByte == 0x0d) { // Convert all CR/LF's to 0x0a - super.write(0x0a); - } - else { - super.write(pByte); - } - } - lastWasWS = true; - } - } - } - - private static class TrimWSStreamDelegate extends ServletResponseStreamDelegate { - public TrimWSStreamDelegate(ServletResponse pResponse) { - super(pResponse); - } - - protected OutputStream createOutputStream() throws IOException { - return new TrimWSFilterOutputStream(response.getOutputStream()); - } - } - - static class TrimWSServletResponseWrapper extends ServletResponseWrapper { - private final ServletResponseStreamDelegate streamDelegate = new TrimWSStreamDelegate(getResponse()); - - public TrimWSServletResponseWrapper(ServletResponse pResponse) { - super(pResponse); - } - - public ServletOutputStream getOutputStream() throws IOException { - return streamDelegate.getOutputStream(); - } - - public PrintWriter getWriter() throws IOException { - return streamDelegate.getWriter(); - } - - public void setContentLength(int pLength) { - // Will be changed by filter, so don't set. - } - - @Override - public void flushBuffer() throws IOException { - streamDelegate.flushBuffer(); - } - - @Override - public void resetBuffer() { - streamDelegate.resetBuffer(); - } - - // TODO: Consider picking up content-type/encoding, as we can only - // filter US-ASCII, UTF-8 and other compatible encodings? - } +/* + * 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 of the copyright holder 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 HOLDER 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 javax.servlet.*; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; + +/** + * Removes extra unneccessary white space from a servlet response. + * White space is defined as per {@link Character#isWhitespace(char)}. + *

      + * This filter has no understanding of the content in the reponse, and will + * remove repeated white space anywhere in the stream. It is intended for + * removing white space from HTML or XML streams, but this limitation makes it + * less suited for filtering HTML/XHTML with embedded CSS or JavaScript, + * in case white space should be significant here. It is strongly reccommended + * you keep CSS and JavaScript in separate files (this will have the added + * benefit of further reducing the ammount of data communicated between + * server and client). + *

      + *

      + * At the moment this filter has no concept of encoding. + * This means, that if some multi-byte escape sequence contains one or more + * bytes that individually is treated as a white space, these bytes + * may be skipped. + * As UTF-8 + * guarantees that no bytes are repeated in this way, this filter can safely + * filter UTF-8. + * Simple 8 bit character encodings, like the + * ISO/IEC 8859 standard, or + * Windows-1252" + * are always safe. + *

      + *

      + * Configuration + *
      + * To use {@code TrimWhiteSpaceFilter} in your web-application, you simply need + * to add it to your web descriptor ({@code web.xml}). + * If using a servlet container that supports the Servlet 2.4 spec, the new + * {@code dispatcher} element should be used, and set to + * {@code REQUEST/FORWARD}, to make sure the filter is invoked only once for + * requests. + * If using an older web descriptor, set the {@code init-param} + * {@code "once-per-request"} to {@code "true"} (this will have the same effect, + * but might perform slightly worse than the 2.4 version). + * Please see the examples below. + *

      + *

      + * Servlet 2.4 version, filter section: + *

      + *
      + * <!-- TrimWS Filter Configuration -->
      + * <filter>
      + *      <filter-name>trimws</filter-name>
      + *      <filter-class>com.twelvemonkeys.servlet.TrimWhiteSpaceFilter</filter-class>
      + *      <!-- auto-flush=true is the default, may be omitted -->
      + *      <init-param>
      + *          <param-name>auto-flush</param-name>
      + *          <param-value>true</param-value>
      + *      </init-param>
      + * </filter>
      + * 
      + * Filter-mapping section:
      + *
      + * <!-- TimWS Filter Mapping -->
      + * <filter-mapping>
      + *      <filter-name>trimws</filter-name>
      + *      <url-pattern>*.html</url-pattern>
      + *      <dispatcher>REQUEST</dispatcher>
      + *      <dispatcher>FORWARD</dispatcher>
      + * </filter-mapping>
      + * <filter-mapping>
      + *      <filter-name>trimws</filter-name>
      + *      <url-pattern>*.jsp</url-pattern>
      + *      <dispatcher>REQUEST</dispatcher>
      + *      <dispatcher>FORWARD</dispatcher>
      + * </filter-mapping>
      + * 
      + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: TrimWhiteSpaceFilter.java#2 $ + */ +public class TrimWhiteSpaceFilter extends GenericFilter { + + private boolean autoFlush = true; + + @InitParam + public void setAutoFlush(final boolean pAutoFlush) { + autoFlush = pAutoFlush; + } + + public void init() throws ServletException { + super.init(); + 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 (autoFlush) { + wrapped.flushBuffer(); + } + } + + static final class TrimWSFilterOutputStream extends FilterOutputStream { + boolean lastWasWS = true; // Avoids leading WS by init to true + + public TrimWSFilterOutputStream(OutputStream pOut) { + super(pOut); + } + + // Override this, in case the wrapped outputstream overrides... + public final void write(byte pBytes[]) throws IOException { + write(pBytes, 0, pBytes.length); + } + + // Override this, in case the wrapped outputstream overrides... + public final void write(byte pBytes[], int pOff, int pLen) throws IOException { + if (pBytes == null) { + throw new NullPointerException("bytes == null"); + } + else if (pOff < 0 || pLen < 0 || (pOff + pLen > pBytes.length)) { + throw new IndexOutOfBoundsException("Bytes: " + pBytes.length + " Offset: " + pOff + " Length: " + pLen); + } + + for (int i = 0; i < pLen ; i++) { + write(pBytes[pOff + i]); + } + } + + public void write(int pByte) throws IOException { + // TODO: Is this good enough for multi-byte encodings like UTF-16? + // Consider writing through a Writer that does that for us, and + // also buffer whitespace, so we write a linefeed every time there's + // one in the original... + + // According to http://en.wikipedia.org/wiki/UTF-8: + // "[...] US-ASCII octet values do not appear otherwise in a UTF-8 + // encoded character stream. This provides compatibility with file + // systems or other software (e.g., the printf() function in + // C libraries) that parse based on US-ASCII values but are + // transparent to other values." + + if (!Character.isWhitespace((char) pByte)) { + // If char is not WS, just store + super.write(pByte); + lastWasWS = false; + } + else { + // TODO: Consider writing only 0x0a (LF) and 0x20 (space) + // Else, if char is WS, store first, skip the rest + if (!lastWasWS) { + if (pByte == 0x0d) { // Convert all CR/LF's to 0x0a + super.write(0x0a); + } + else { + super.write(pByte); + } + } + lastWasWS = true; + } + } + } + + private static class TrimWSStreamDelegate extends ServletResponseStreamDelegate { + public TrimWSStreamDelegate(ServletResponse pResponse) { + super(pResponse); + } + + protected OutputStream createOutputStream() throws IOException { + return new TrimWSFilterOutputStream(response.getOutputStream()); + } + } + + static class TrimWSServletResponseWrapper extends ServletResponseWrapper { + private final ServletResponseStreamDelegate streamDelegate = new TrimWSStreamDelegate(getResponse()); + + public TrimWSServletResponseWrapper(ServletResponse pResponse) { + super(pResponse); + } + + public ServletOutputStream getOutputStream() throws IOException { + return streamDelegate.getOutputStream(); + } + + public PrintWriter getWriter() throws IOException { + return streamDelegate.getWriter(); + } + + public void setContentLength(int pLength) { + // Will be changed by filter, so don't set. + } + + @Override + public void flushBuffer() throws IOException { + streamDelegate.flushBuffer(); + } + + @Override + public void resetBuffer() { + streamDelegate.resetBuffer(); + } + + // TODO: Consider picking up content-type/encoding, as we can only + // filter US-ASCII, UTF-8 and other compatible encodings? + } } \ No newline at end of file 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 5cebf2f9..507e81bd 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheFilter.java @@ -1,202 +1,203 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.cache; - -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.servlet.GenericFilter; -import com.twelvemonkeys.servlet.ServletConfigException; -import com.twelvemonkeys.servlet.ServletUtil; - -import javax.servlet.*; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.File; -import java.io.IOException; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A Filter that provides response caching, for HTTP {@code GET} requests. - *

      - * Originally based on ideas and code found in the ONJava article - * Two - * Servlet Filters Every Web Application Should Have - * by Jayson Falkner. - * - * @author Jayson Falkner - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: CacheFilter.java#4 $ - * - */ -public class CacheFilter extends GenericFilter { - - HTTPCache cache; - - /** - * Initializes the filter - * - * @throws javax.servlet.ServletException - */ - public void init() throws ServletException { - FilterConfig config = getFilterConfig(); - - // Default don't delete cache files on exit (persistent cache) - boolean deleteCacheOnExit = "TRUE".equalsIgnoreCase(config.getInitParameter("deleteCacheOnExit")); - - // Default expiry time 10 minutes - int expiryTime = 10 * 60 * 1000; - - 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) { - throw new ServletConfigException("Could not parse expiryTime: " + e.toString(), e); - } - } - - // Default max mem cache size 10 MB - int memCacheSize = 10; - - String memCacheSizeStr = config.getInitParameter("memCacheSize"); - if (!StringUtil.isEmpty(memCacheSizeStr)) { - try { - memCacheSize = Integer.parseInt(memCacheSizeStr); - } - catch (NumberFormatException e) { - throw new ServletConfigException("Could not parse memCacheSize: " + e.toString(), e); - } - } - - int maxCachedEntites = 10000; - - try { - cache = new HTTPCache( - getTempFolder(), - expiryTime, - memCacheSize * 1024 * 1024, - maxCachedEntites, - deleteCacheOnExit, - new ServletContextLoggerAdapter(getFilterName(), getServletContext()) - ) { - @Override - protected File getRealFile(CacheRequest pRequest) { - String contextRelativeURI = ServletUtil.getContextRelativeURI(((ServletCacheRequest) pRequest).getRequest()); - - String path = getServletContext().getRealPath(contextRelativeURI); - - if (path != null) { - return new File(path); - } - - return null; - } - }; - log("Created cache: " + cache); - } - catch (IllegalArgumentException e) { - throw new ServletConfigException("Could not create cache: " + e.toString(), e); - } - } - - private File getTempFolder() { - File tempRoot = (File) getServletContext().getAttribute("javax.servlet.context.tempdir"); - if (tempRoot == null) { - throw new IllegalStateException("Missing context attribute \"javax.servlet.context.tempdir\""); - } - return new File(tempRoot, getFilterName()); - } - - public void destroy() { - log("Destroying cache: " + cache); - cache = null; - super.destroy(); - } - - protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { - // We can only cache HTTP GET/HEAD requests - if (!(pRequest instanceof HttpServletRequest - && pResponse instanceof HttpServletResponse - && isCachable((HttpServletRequest) pRequest))) { - pChain.doFilter(pRequest, pResponse); // Continue chain - } - else { - ServletCacheRequest cacheRequest = new ServletCacheRequest((HttpServletRequest) pRequest); - ServletCacheResponse cacheResponse = new ServletCacheResponse((HttpServletResponse) pResponse); - ServletResponseResolver resolver = new ServletResponseResolver(cacheRequest, cacheResponse, pChain); - - // Render fast - try { - cache.doCached(cacheRequest, cacheResponse, resolver); - } - catch (CacheException e) { - if (e.getCause() instanceof ServletException) { - throw (ServletException) e.getCause(); - } - else { - throw new ServletException(e); - } - } - finally { - pResponse.flushBuffer(); - } - } - } - - private boolean isCachable(HttpServletRequest pRequest) { - // TODO: Get Cache-Control: no-cache/max-age=0 and Pragma: no-cache from REQUEST too? - return "GET".equals(pRequest.getMethod()) || "HEAD".equals(pRequest.getMethod()); - } - - // 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 context; - - public ServletContextLoggerAdapter(String pName, ServletContext pContext) { - super(pName, null); - context = pContext; - } - - @Override - public void log(Level pLevel, String pMessage) { - context.log(pMessage); - } - - @Override - public void log(Level pLevel, String pMessage, Throwable pThrowable) { - context.log(pMessage, pThrowable); - } - } +/* + * 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 of the copyright holder 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 HOLDER 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.cache; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.servlet.GenericFilter; +import com.twelvemonkeys.servlet.ServletConfigException; +import com.twelvemonkeys.servlet.ServletUtil; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A Filter that provides response caching, for HTTP {@code GET} requests. + *

      + * Originally based on ideas and code found in the ONJava article + * Two + * Servlet Filters Every Web Application Should Have + * by Jayson Falkner. + *

      + * + * @author Jayson Falkner + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: CacheFilter.java#4 $ + * + */ +public class CacheFilter extends GenericFilter { + + HTTPCache cache; + + /** + * Initializes the filter + * + * @throws javax.servlet.ServletException + */ + public void init() throws ServletException { + FilterConfig config = getFilterConfig(); + + // Default don't delete cache files on exit (persistent cache) + boolean deleteCacheOnExit = "TRUE".equalsIgnoreCase(config.getInitParameter("deleteCacheOnExit")); + + // Default expiry time 10 minutes + int expiryTime = 10 * 60 * 1000; + + 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) { + throw new ServletConfigException("Could not parse expiryTime: " + e.toString(), e); + } + } + + // Default max mem cache size 10 MB + int memCacheSize = 10; + + String memCacheSizeStr = config.getInitParameter("memCacheSize"); + if (!StringUtil.isEmpty(memCacheSizeStr)) { + try { + memCacheSize = Integer.parseInt(memCacheSizeStr); + } + catch (NumberFormatException e) { + throw new ServletConfigException("Could not parse memCacheSize: " + e.toString(), e); + } + } + + int maxCachedEntites = 10000; + + try { + cache = new HTTPCache( + getTempFolder(), + expiryTime, + memCacheSize * 1024 * 1024, + maxCachedEntites, + deleteCacheOnExit, + new ServletContextLoggerAdapter(getFilterName(), getServletContext()) + ) { + @Override + protected File getRealFile(CacheRequest pRequest) { + String contextRelativeURI = ServletUtil.getContextRelativeURI(((ServletCacheRequest) pRequest).getRequest()); + + String path = getServletContext().getRealPath(contextRelativeURI); + + if (path != null) { + return new File(path); + } + + return null; + } + }; + log("Created cache: " + cache); + } + catch (IllegalArgumentException e) { + throw new ServletConfigException("Could not create cache: " + e.toString(), e); + } + } + + private File getTempFolder() { + File tempRoot = (File) getServletContext().getAttribute("javax.servlet.context.tempdir"); + if (tempRoot == null) { + throw new IllegalStateException("Missing context attribute \"javax.servlet.context.tempdir\""); + } + return new File(tempRoot, getFilterName()); + } + + public void destroy() { + log("Destroying cache: " + cache); + cache = null; + super.destroy(); + } + + protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { + // We can only cache HTTP GET/HEAD requests + if (!(pRequest instanceof HttpServletRequest + && pResponse instanceof HttpServletResponse + && isCachable((HttpServletRequest) pRequest))) { + pChain.doFilter(pRequest, pResponse); // Continue chain + } + else { + ServletCacheRequest cacheRequest = new ServletCacheRequest((HttpServletRequest) pRequest); + ServletCacheResponse cacheResponse = new ServletCacheResponse((HttpServletResponse) pResponse); + ServletResponseResolver resolver = new ServletResponseResolver(cacheRequest, cacheResponse, pChain); + + // Render fast + try { + cache.doCached(cacheRequest, cacheResponse, resolver); + } + catch (CacheException e) { + if (e.getCause() instanceof ServletException) { + throw (ServletException) e.getCause(); + } + else { + throw new ServletException(e); + } + } + finally { + pResponse.flushBuffer(); + } + } + } + + private boolean isCachable(HttpServletRequest pRequest) { + // TODO: Get Cache-Control: no-cache/max-age=0 and Pragma: no-cache from REQUEST too? + return "GET".equals(pRequest.getMethod()) || "HEAD".equals(pRequest.getMethod()); + } + + // 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 context; + + public ServletContextLoggerAdapter(String pName, ServletContext pContext) { + super(pName, null); + context = pContext; + } + + @Override + public void log(Level pLevel, String pMessage) { + context.log(pMessage); + } + + @Override + public void log(Level pLevel, String pMessage, Throwable pThrowable) { + context.log(pMessage, pThrowable); + } + } } \ No newline at end of file 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 ae4bbdeb..e8f495e2 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/CacheResponseWrapper.java @@ -1,263 +1,264 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.cache; - -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.net.HTTPUtil; -import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponseWrapper; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; - -/** - * CacheResponseWrapper class description. - *

      - * Based on ideas and code found in the ONJava article - * Two - * Servlet Filters Every Web Application Should Have - * by Jayson Falkner. - * - * @author Jayson Falkner - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: CacheResponseWrapper.java#3 $ - */ -class CacheResponseWrapper extends HttpServletResponseWrapper { - private ServletResponseStreamDelegate streamDelegate; - - private CacheResponse response; - private CachedEntity cached; - private WritableCachedResponse cachedResponse; - - private Boolean cacheable; - private int status; - - public CacheResponseWrapper(final ServletCacheResponse pResponse, final CachedEntity pCached) { - super(pResponse.getResponse()); - response = pResponse; - cached = pCached; - init(); - } - - /* - 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() { - cacheable = null; - status = SC_OK; - cachedResponse = cached.createCachedResponse(); - streamDelegate = new ServletResponseStreamDelegate(this) { - protected OutputStream createOutputStream() throws IOException { - // Test if this request is really cacheable, otherwise, - // just write through to underlying response, and don't cache - if (isCacheable()) { - return cachedResponse.getOutputStream(); - } - else { - cachedResponse.setStatus(status); - cachedResponse.writeHeadersTo(CacheResponseWrapper.this.response); - return super.getOutputStream(); - } - } - }; - } - - CachedResponse getCachedResponse() { - return cachedResponse.getCachedResponse(); - } - - public boolean isCacheable() { - // NOTE: Intentionally not synchronized - if (cacheable == null) { - cacheable = isCacheableImpl(); - } - - return cacheable; - } - - private boolean isCacheableImpl() { - if (status != SC_OK) { - return false; - } - - // Vary: * - String[] values = cachedResponse.getHeaderValues(HTTPCache.HEADER_VARY); - if (values != null) { - for (String value : values) { - if ("*".equals(value)) { - return false; - } - } - } - - // Cache-Control: no-cache, no-store, must-revalidate - values = cachedResponse.getHeaderValues(HTTPCache.HEADER_CACHE_CONTROL); - if (values != null) { - for (String value : values) { - if (StringUtil.contains(value, "no-cache") - || StringUtil.contains(value, "no-store") - || StringUtil.contains(value, "must-revalidate")) { - return false; - } - } - } - - // Pragma: no-cache - values = cachedResponse.getHeaderValues(HTTPCache.HEADER_PRAGMA); - if (values != null) { - for (String value : values) { - if (StringUtil.contains(value, "no-cache")) { - return false; - } - } - } - - return true; - } - - public void flushBuffer() throws IOException { - streamDelegate.flushBuffer(); - } - - public void resetBuffer() { - // Servlet 2.3 - streamDelegate.resetBuffer(); - } - - public void reset() { - if (Boolean.FALSE.equals(cacheable)) { - super.reset(); - } - // No else, might be cacheable after all.. - init(); - } - - public ServletOutputStream getOutputStream() throws IOException { - return streamDelegate.getOutputStream(); - } - - public PrintWriter getWriter() throws IOException { - return streamDelegate.getWriter(); - } - - public boolean containsHeader(String name) { - return cachedResponse.getHeaderValues(name) != null; - } - - public void sendError(int pStatusCode, String msg) throws IOException { - // NOT cacheable - status = pStatusCode; - super.sendError(pStatusCode, msg); - } - - public void sendError(int pStatusCode) throws IOException { - // NOT cacheable - status = pStatusCode; - super.sendError(pStatusCode); - } - - public void setStatus(int pStatusCode, String sm) { - // NOTE: This method is deprecated - setStatus(pStatusCode); - } - - public void setStatus(int pStatusCode) { - // NOT cacheable unless pStatusCode == 200 (or a FEW others?) - if (pStatusCode != SC_OK) { - status = pStatusCode; - super.setStatus(pStatusCode); - } - } - - public void sendRedirect(String pLocation) throws IOException { - // 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(cacheable)) { - super.setDateHeader(pName, pValue); - } - cachedResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue)); - } - - public void addDateHeader(String pName, long pValue) { - // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(cacheable)) { - super.addDateHeader(pName, pValue); - } - cachedResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue)); - } - - public void setHeader(String pName, String pValue) { - // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(cacheable)) { - super.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(cacheable)) { - super.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(cacheable)) { - super.setIntHeader(pName, 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(cacheable)) { - super.addIntHeader(pName, pValue); - } - cachedResponse.addHeader(pName, String.valueOf(pValue)); - } - - public final void setContentType(String type) { - setHeader(HTTPCache.HEADER_CONTENT_TYPE, type); - } +/* + * 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 of the copyright holder 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 HOLDER 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.cache; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.net.HTTPUtil; +import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; + +/** + * CacheResponseWrapper class description. + *

      + * Based on ideas and code found in the ONJava article + * Two + * Servlet Filters Every Web Application Should Have + * by Jayson Falkner. + *

      + * + * @author Jayson Falkner + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: CacheResponseWrapper.java#3 $ + */ +class CacheResponseWrapper extends HttpServletResponseWrapper { + private ServletResponseStreamDelegate streamDelegate; + + private CacheResponse response; + private CachedEntity cached; + private WritableCachedResponse cachedResponse; + + private Boolean cacheable; + private int status; + + public CacheResponseWrapper(final ServletCacheResponse pResponse, final CachedEntity pCached) { + super(pResponse.getResponse()); + response = pResponse; + cached = pCached; + init(); + } + + /* + 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() { + cacheable = null; + status = SC_OK; + cachedResponse = cached.createCachedResponse(); + streamDelegate = new ServletResponseStreamDelegate(this) { + protected OutputStream createOutputStream() throws IOException { + // Test if this request is really cacheable, otherwise, + // just write through to underlying response, and don't cache + if (isCacheable()) { + return cachedResponse.getOutputStream(); + } + else { + cachedResponse.setStatus(status); + cachedResponse.writeHeadersTo(CacheResponseWrapper.this.response); + return super.getOutputStream(); + } + } + }; + } + + CachedResponse getCachedResponse() { + return cachedResponse.getCachedResponse(); + } + + public boolean isCacheable() { + // NOTE: Intentionally not synchronized + if (cacheable == null) { + cacheable = isCacheableImpl(); + } + + return cacheable; + } + + private boolean isCacheableImpl() { + if (status != SC_OK) { + return false; + } + + // Vary: * + String[] values = cachedResponse.getHeaderValues(HTTPCache.HEADER_VARY); + if (values != null) { + for (String value : values) { + if ("*".equals(value)) { + return false; + } + } + } + + // Cache-Control: no-cache, no-store, must-revalidate + values = cachedResponse.getHeaderValues(HTTPCache.HEADER_CACHE_CONTROL); + if (values != null) { + for (String value : values) { + if (StringUtil.contains(value, "no-cache") + || StringUtil.contains(value, "no-store") + || StringUtil.contains(value, "must-revalidate")) { + return false; + } + } + } + + // Pragma: no-cache + values = cachedResponse.getHeaderValues(HTTPCache.HEADER_PRAGMA); + if (values != null) { + for (String value : values) { + if (StringUtil.contains(value, "no-cache")) { + return false; + } + } + } + + return true; + } + + public void flushBuffer() throws IOException { + streamDelegate.flushBuffer(); + } + + public void resetBuffer() { + // Servlet 2.3 + streamDelegate.resetBuffer(); + } + + public void reset() { + if (Boolean.FALSE.equals(cacheable)) { + super.reset(); + } + // No else, might be cacheable after all.. + init(); + } + + public ServletOutputStream getOutputStream() throws IOException { + return streamDelegate.getOutputStream(); + } + + public PrintWriter getWriter() throws IOException { + return streamDelegate.getWriter(); + } + + public boolean containsHeader(String name) { + return cachedResponse.getHeaderValues(name) != null; + } + + public void sendError(int pStatusCode, String msg) throws IOException { + // NOT cacheable + status = pStatusCode; + super.sendError(pStatusCode, msg); + } + + public void sendError(int pStatusCode) throws IOException { + // NOT cacheable + status = pStatusCode; + super.sendError(pStatusCode); + } + + public void setStatus(int pStatusCode, String sm) { + // NOTE: This method is deprecated + setStatus(pStatusCode); + } + + public void setStatus(int pStatusCode) { + // NOT cacheable unless pStatusCode == 200 (or a FEW others?) + if (pStatusCode != SC_OK) { + status = pStatusCode; + super.setStatus(pStatusCode); + } + } + + public void sendRedirect(String pLocation) throws IOException { + // 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(cacheable)) { + super.setDateHeader(pName, pValue); + } + cachedResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue)); + } + + public void addDateHeader(String pName, long pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(cacheable)) { + super.addDateHeader(pName, pValue); + } + cachedResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue)); + } + + public void setHeader(String pName, String pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(cacheable)) { + super.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(cacheable)) { + super.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(cacheable)) { + super.setIntHeader(pName, 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(cacheable)) { + super.addIntHeader(pName, pValue); + } + cachedResponse.addHeader(pName, String.valueOf(pValue)); + } + + public final void setContentType(String type) { + setHeader(HTTPCache.HEADER_CONTENT_TYPE, type); + } } \ No newline at end of file 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 0c092fa3..69676ea3 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/HTTPCache.java @@ -1,1152 +1,1155 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.cache; - -import com.twelvemonkeys.io.FileUtil; -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.lang.Validate; -import com.twelvemonkeys.net.HTTPUtil; -import com.twelvemonkeys.net.MIMEUtil; -import com.twelvemonkeys.util.LRUHashMap; -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; -import java.util.logging.Logger; - -/** - * A "simple" HTTP cache. - *

      - * - * @author Harald Kuhr - * @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? - * (Hashcodes of Strings are constant). - * - Store full filenames in .vary, instead of just extension, and use - * short filenames? (and only one .vary per dir). - *

      - * - * @todo TEST: Battle-testing using some URL-hammer tool and maybe a profiler - * @todo ETag/Conditional (If-None-Match) support! - * @todo Rewrite to use java.util.concurrent Locks (if possible) for performance - * Maybe use ConcurrentHashMap instead fo synchronized HashMap? - * @todo Rewrite to use NIO for performance - * @todo Allow no tempdir for in-memory only cache - * @todo Specify max size of disk-cache - */ -public class HTTPCache { - /** - * The HTTP header {@code "Cache-Control"} - */ - protected static final String HEADER_CACHE_CONTROL = "Cache-Control"; - /** - * The HTTP header {@code "Content-Type"} - */ - protected static final String HEADER_CONTENT_TYPE = "Content-Type"; - /** - * The HTTP header {@code "Date"} - */ - protected static final String HEADER_DATE = "Date"; - /** - * The HTTP header {@code "ETag"} - */ - protected static final String HEADER_ETAG = "ETag"; - /** - * The HTTP header {@code "Expires"} - */ - protected static final String HEADER_EXPIRES = "Expires"; - /** - * The HTTP header {@code "If-Modified-Since"} - */ - protected static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; - /** - * The HTTP header {@code "If-None-Match"} - */ - protected static final String HEADER_IF_NONE_MATCH = "If-None-Match"; - /** - * The HTTP header {@code "Last-Modified"} - */ - protected static final String HEADER_LAST_MODIFIED = "Last-Modified"; - /** - * The HTTP header {@code "Pragma"} - */ - protected static final String HEADER_PRAGMA = "Pragma"; - /** - * The HTTP header {@code "Vary"} - */ - protected static final String HEADER_VARY = "Vary"; - /** - * The HTTP header {@code "Warning"} - */ - protected static final String HEADER_WARNING = "Warning"; - /** - * HTTP extension header {@code "X-Cached-At"} - */ - protected static final String HEADER_CACHED_TIME = "X-Cached-At"; - - /** - * The file extension for header files ({@code ".headers"}) - */ - protected static final String FILE_EXT_HEADERS = ".headers"; - /** - * The file extension for varation-info files ({@code ".vary"}) - */ - protected static final String FILE_EXT_VARY = ".vary"; - - /** - * The directory used for the disk-based cache - */ - private File tempDir; - - /** - * Indicates wether the disk-based cache should be deleted when the - * container shuts down/VM exits - */ - private boolean deleteCacheOnExit; - - /** - * In-memory content cache - */ - private final Map contentCache; - /** - * In-memory enity cache - */ - private final Map entityCache; - /** - * In-memory varyiation-info cache - */ - private final Map varyCache; - - private long defaultExpiryTime = -1; - - private final Logger logger; - - // Internal constructor for sublcasses only - protected HTTPCache( - final File pTempFolder, - final long pDefaultCacheExpiryTime, - final int pMaxMemCacheSize, - final int pMaxCachedEntites, - final boolean pDeleteCacheOnExit, - final Logger pLogger - ) { - 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"); - - 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 -// contentCache = new TimeoutMap(backing, null, pDefaultCacheExpiryTime); - contentCache = new SizedLRUMap(pMaxMemCacheSize); // size in bytes - } - else { - contentCache = new NullMap(); - } - - entityCache = new LRUHashMap(pMaxCachedEntites); - varyCache = new LRUHashMap(pMaxCachedEntites); - - deleteCacheOnExit = pDeleteCacheOnExit; - - tempDir = pTempFolder; - - logger = pLogger != null ? pLogger : Logger.getLogger(getClass().getName()); - } - - /** - * Creates an {@code HTTPCache}. - * - * @param pTempFolder the temp folder for this cache. - * @param pDefaultCacheExpiryTime Default expiry time for cached entities, - * {@code >= 0} - * @param pMaxMemCacheSize Maximum size of in-memory cache for content - * in bytes, {@code >= 0} ({@code 0} means no - * in-memory cache) - * @param pMaxCachedEntites Maximum number of entities in cache - * @param pDeleteCacheOnExit specifies wether the file cache should be - * deleted when the application or VM shuts down - * @throws IllegalArgumentException if {@code pName} or {@code pContext} is - * {@code null} or if any of {@code pDefaultCacheExpiryTime}, - * {@code pMaxMemCacheSize} or {@code pMaxCachedEntites} are - * negative, - * or if the directory as given in the context attribute - * {@code "javax.servlet.context.tempdir"} does not exist, and - * cannot be created. - */ - public HTTPCache(final File pTempFolder, - final long pDefaultCacheExpiryTime, - final int pMaxMemCacheSize, final int pMaxCachedEntites, - final boolean pDeleteCacheOnExit) { - this(pTempFolder, pDefaultCacheExpiryTime, pMaxMemCacheSize, pMaxCachedEntites, pDeleteCacheOnExit, null); - } - - - /** - * Creates an {@code HTTPCache}. - * - * @param pName Name of this cache (should be unique per application). - * Used for temp folder - * @param pContext Servlet context for the application. - * @param pDefaultCacheExpiryTime Default expiry time for cached entities, - * {@code >= 0} - * @param pMaxMemCacheSize Maximum size of in-memory cache for content - * in bytes, {@code >= 0} ({@code 0} means no - * in-memory cache) - * @param pMaxCachedEntites Maximum number of entities in cache - * @param pDeleteCacheOnExit specifies wether the file cache should be - * deleted when the application or VM shuts down - * @throws IllegalArgumentException if {@code pName} or {@code pContext} is - * {@code null} or if any of {@code pDefaultCacheExpiryTime}, - * {@code pMaxMemCacheSize} or {@code pMaxCachedEntites} are - * negative, - * or if the directory as given in the context attribute - * {@code "javax.servlet.context.tempdir"} does not exist, and - * cannot be created. - * @deprecated Use {@link #HTTPCache(File, long, int, int, boolean)} instead. - */ - public HTTPCache(final String pName, final ServletContext pContext, - final int pDefaultCacheExpiryTime, final int pMaxMemCacheSize, - final int pMaxCachedEntites, final boolean pDeleteCacheOnExit) { - this( - getTempFolder(pName, pContext), - pDefaultCacheExpiryTime, pMaxMemCacheSize, pMaxCachedEntites, pDeleteCacheOnExit, - new CacheFilter.ServletContextLoggerAdapter(pName, pContext) - ); - } - - private static File getTempFolder(String pName, ServletContext pContext) { - 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); - } - - public String toString() { - StringBuilder buf = new StringBuilder(getClass().getSimpleName()); - buf.append("["); - buf.append("Temp dir: "); - buf.append(tempDir.getAbsolutePath()); - if (deleteCacheOnExit) { - buf.append(" (non-persistent)"); - } - else { - buf.append(" (persistent)"); - } - buf.append(", EntityCache: {"); - buf.append(entityCache.size()); - buf.append(" entries in a "); - buf.append(entityCache.getClass().getName()); - buf.append("}, VaryCache: {"); - buf.append(varyCache.size()); - buf.append(" entries in a "); - buf.append(varyCache.getClass().getName()); - buf.append("}, ContentCache: {"); - buf.append(contentCache.size()); - buf.append(" entries in a "); - buf.append(contentCache.getClass().getName()); - buf.append("}]"); - - return buf.toString(); - } - - void log(final String pMessage) { - logger.log(Level.INFO, pMessage); - } - - void log(final String pMessage, Throwable pException) { - logger.log(Level.WARNING, pMessage, pException); - } - - /** - * Looks up the {@code CachedEntity} for the given request. - * - * @param pRequest the request - * @param pResponse the response - * @param pResolver the resolver - * @throws java.io.IOException if an I/O error occurs - * @throws CacheException if the cached entity can't be resolved for some reason - */ - public void doCached(final CacheRequest pRequest, final CacheResponse pResponse, final ResponseResolver pResolver) throws IOException, CacheException { - // TODO: Expire cached items on PUT/POST/DELETE/PURGE - // If not cachable request, resolve directly - if (!isCacheable(pRequest)) { - pResolver.resolve(pRequest, pResponse); - } - else { - // Generate cacheURI - String cacheURI = generateCacheURI(pRequest); -// System.out.println(" ## HTTPCache ## Request Id (cacheURI): " + cacheURI); - - // Get/create cached entity - CachedEntity cached; - synchronized (entityCache) { - cached = entityCache.get(cacheURI); - if (cached == null) { - cached = new CachedEntityImpl(cacheURI, this); - entityCache.put(cacheURI, cached); - } - } - - // else if (not cached || stale), resolve through wrapped (caching) response - // else render to response - - // TODO: This is a bottleneck for uncachable resources. Should not - // synchronize, if we know (HOW?) the resource is not cachable. - synchronized (cached) { - if (cached.isStale(pRequest) /* TODO: NOT CACHED?! */) { - // Go fetch... - WritableCachedResponse cachedResponse = cached.createCachedResponse(); - pResolver.resolve(pRequest, cachedResponse); - - if (isCachable(cachedResponse)) { -// System.out.println("Registering content: " + cachedResponse.getCachedResponse()); - registerContent(cacheURI, pRequest, cachedResponse.getCachedResponse()); - } - else { - // TODO: What about non-cachable responses? We need to either remove them from cache, or mark them as stale... - // Best is probably to mark as non-cacheable for later, and NOT store content (performance) -// System.out.println("Non-cacheable response: " + cachedResponse); - - // TODO: Write, but should really do this unbuffered.... And some resolver might be able to do just that? - // Might need a resolver.isWriteThroughForUncachableResources() method... - pResponse.setStatus(cachedResponse.getStatus()); - cachedResponse.writeHeadersTo(pResponse); - cachedResponse.writeContentsTo(pResponse.getOutputStream()); - return; - } - } - } - - cached.render(pRequest, pResponse); - } - } - - protected void invalidate(CacheRequest pRequest) { - // Generate cacheURI - String cacheURI = generateCacheURI(pRequest); - - // Get/create cached entity - CachedEntity cached; - synchronized (entityCache) { - cached = entityCache.get(cacheURI); - if (cached != null) { - // TODO; Remove all variants - entityCache.remove(cacheURI); - } - } - - } - - private boolean isCacheable(final CacheRequest pRequest) { - // TODO: Support public/private cache (a cache probably have to be one of the two, when created) - // TODO: Only private caches should cache requests with Authorization - - // TODO: OptimizeMe! - // It's probably best to cache the "cacheableness" of a request and a resource separately - List cacheControlValues = pRequest.getHeaders().get(HEADER_CACHE_CONTROL); - if (cacheControlValues != null) { - Map cacheControl = new HashMap(); - for (String cc : cacheControlValues) { - List directives = Arrays.asList(cc.split(",")); - for (String directive : directives) { - directive = directive.trim(); - if (directive.length() > 0) { - String[] directiveParts = directive.split("=", 2); - cacheControl.put(directiveParts[0], directiveParts.length > 1 ? directiveParts[1] : null); - } - } - } - - if (cacheControl.containsKey("no-cache") || cacheControl.containsKey("no-store")) { - return false; - } - - /* - "no-cache" ; Section 14.9.1 - | "no-store" ; Section 14.9.2 - | "max-age" "=" delta-seconds ; Section 14.9.3, 14.9.4 - | "max-stale" [ "=" delta-seconds ] ; Section 14.9.3 - | "min-fresh" "=" delta-seconds ; Section 14.9.3 - | "no-transform" ; Section 14.9.5 - | "only-if-cached" - */ - } - - return true; - } - - private boolean isCachable(final CacheResponse pResponse) { - if (pResponse.getStatus() != HttpServletResponse.SC_OK) { - return false; - } - - // Vary: * - List values = pResponse.getHeaders().get(HTTPCache.HEADER_VARY); - if (values != null) { - for (String value : values) { - if ("*".equals(value)) { - return false; - } - } - } - - // Cache-Control: no-cache, no-store, must-revalidate - values = pResponse.getHeaders().get(HTTPCache.HEADER_CACHE_CONTROL); - if (values != null) { - for (String value : values) { - if (StringUtil.contains(value, "no-cache") - || StringUtil.contains(value, "no-store") - || StringUtil.contains(value, "must-revalidate")) { - return false; - } - } - } - - // Pragma: no-cache - values = pResponse.getHeaders().get(HTTPCache.HEADER_PRAGMA); - if (values != null) { - for (String value : values) { - if (StringUtil.contains(value, "no-cache")) { - return false; - } - } - } - - return true; - } - - - /** - * Allows a server-side cache mechanism to peek at the real file. - * Default implementation return {@code null}. - * - * @param pRequest the request - * @return {@code null}, always - */ - protected File getRealFile(final CacheRequest pRequest) { - // TODO: Create callback for this? Only possible for server-side cache... Maybe we can get away without this? - // For now: Default implementation that returns null - return null; -/* - String contextRelativeURI = ServletUtil.getContextRelativeURI(pRequest); - // System.out.println(" ## HTTPCache ## Context relative URI: " + contextRelativeURI); - - String path = mContext.getRealPath(contextRelativeURI); - // System.out.println(" ## HTTPCache ## Real path: " + path); - - if (path != null) { - return new File(path); - } - - return null; -*/ - } - - private File getCachedFile(final String pCacheURI, final CacheRequest pRequest) { - File file = null; - - // Get base dir - File base = new File(tempDir, "./" + pCacheURI); - final String basePath = base.getAbsolutePath(); - File directory = base.getParentFile(); - - // Get list of files that are candidates - File[] candidates = directory.listFiles(new FileFilter() { - public boolean accept(File pFile) { - return pFile.getAbsolutePath().startsWith(basePath) - && !pFile.getName().endsWith(FILE_EXT_HEADERS) - && !pFile.getName().endsWith(FILE_EXT_VARY); - } - }); - - // Negotiation - if (candidates != null) { - String extension = getVaryExtension(pCacheURI, pRequest); - //System.out.println("-- Vary ext: " + extension); - if (extension != null) { - for (File candidate : candidates) { - //System.out.println("-- Candidate: " + candidates[i]); - - if (extension.equals("ANY") || extension.equals(FileUtil.getExtension(candidate))) { - //System.out.println("-- Candidate selected"); - file = candidate; - break; - } - } - } - } - else if (base.exists()) { - //System.out.println("-- File not a directory: " + directory); - log("File not a directory: " + directory); - } - - return file; - } - - private String getVaryExtension(final String pCacheURI, final CacheRequest pRequest) { - Properties variations = getVaryProperties(pCacheURI); - - String[] varyHeaders = StringUtil.toStringArray(variations.getProperty(HEADER_VARY, "")); -// System.out.println("-- Vary: \"" + variations.getProperty(HEADER_VARY) + "\""); - - String varyKey = createVaryKey(varyHeaders, pRequest); -// System.out.println("-- Vary key: \"" + varyKey + "\""); - - // If no vary, just go with any version... - return StringUtil.isEmpty(varyKey) ? "ANY" : variations.getProperty(varyKey, null); - } - - private String createVaryKey(final String[] pVaryHeaders, final CacheRequest pRequest) { - if (pVaryHeaders == null) { - return null; - } - - StringBuilder headerValues = new StringBuilder(); - for (String varyHeader : pVaryHeaders) { - List varies = pRequest.getHeaders().get(varyHeader); - String headerValue = varies != null && varies.size() > 0 ? varies.get(0) : null; - - headerValues.append(varyHeader); - headerValues.append("__V_"); - headerValues.append(createSafeHeader(headerValue)); - } - - return headerValues.toString(); - } - - private void storeVaryProperties(final String pCacheURI, final Properties pVariations) { - synchronized (pVariations) { - try { - File file = getVaryPropertiesFile(pCacheURI); - if (!file.exists() && deleteCacheOnExit) { - file.deleteOnExit(); - } - - FileOutputStream out = new FileOutputStream(file); - try { - pVariations.store(out, pCacheURI + " Vary info"); - } - finally { - out.close(); - } - } - catch (IOException ioe) { - log("Error: Could not store Vary info: " + ioe); - } - } - } - - private Properties getVaryProperties(final String pCacheURI) { - Properties variations; - - synchronized (varyCache) { - variations = varyCache.get(pCacheURI); - if (variations == null) { - variations = loadVaryProperties(pCacheURI); - varyCache.put(pCacheURI, variations); - } - } - - return variations; - } - - private Properties loadVaryProperties(final String pCacheURI) { - // Read Vary info, for content negotiation - Properties variations = new Properties(); - File vary = getVaryPropertiesFile(pCacheURI); - if (vary.exists()) { - try { - FileInputStream in = new FileInputStream(vary); - try { - variations.load(in); - } - finally { - in.close(); - } - } - catch (IOException ioe) { - log("Error: Could not load Vary info: " + ioe); - } - } - return variations; - } - - private File getVaryPropertiesFile(final String pCacheURI) { - return new File(tempDir, "./" + pCacheURI + FILE_EXT_VARY); - } - - private static String generateCacheURI(final CacheRequest pRequest) { - StringBuilder buffer = new StringBuilder(); - - // Note: As the '/'s are not replaced, the directory structure will be recreated - // TODO: Old mehtod relied on context relativization, that must now be handled byt the ServletCacheRequest -// String contextRelativeURI = ServletUtil.getContextRelativeURI(pRequest); - String contextRelativeURI = pRequest.getRequestURI().getPath(); - buffer.append(contextRelativeURI); - - // Create directory for all resources - if (contextRelativeURI.charAt(contextRelativeURI.length() - 1) != '/') { - buffer.append('/'); - } - - // Get parameters from request, and recreate query to avoid unneccessary - // regeneration/caching when parameters are out of order - // Also makes caching work for POST - appendSortedRequestParams(pRequest, buffer); - - return buffer.toString(); - } - - private static void appendSortedRequestParams(final CacheRequest pRequest, final StringBuilder pBuffer) { - Set names = pRequest.getParameters().keySet(); - if (names.isEmpty()) { - pBuffer.append("defaultVersion"); - return; - } - - // We now have parameters - pBuffer.append('_'); // append '_' for '?', to avoid clash with default - - // Create a sorted map - SortedMap> sortedQueryMap = new TreeMap>(); - for (String name : names) { - List values = pRequest.getParameters().get(name); - - sortedQueryMap.put(name, values); - } - - // Iterate over sorted map, and append to stringbuffer - for (Iterator>> iterator = sortedQueryMap.entrySet().iterator(); iterator.hasNext();) { - Map.Entry> entry = iterator.next(); - pBuffer.append(createSafe(entry.getKey())); - - List values = entry.getValue(); - if (values != null && values.size() > 0) { - pBuffer.append("_V"); // = - for (int i = 0; i < values.size(); i++) { - String value = values.get(i); - if (i != 0) { - pBuffer.append(','); - } - pBuffer.append(createSafe(value)); - } - } - - if (iterator.hasNext()) { - pBuffer.append("_P"); // & - } - } - } - - private static String createSafe(final String pKey) { - return pKey.replace('/', '-') - .replace('&', '-') // In case they are encoded - .replace('#', '-') - .replace(';', '-'); - } - - private static String createSafeHeader(final String pHeaderValue) { - if (pHeaderValue == null) { - return "NULL"; - } - - return pHeaderValue.replace(' ', '_') - .replace(':', '_') - .replace('=', '_'); - } - - /** - * Registers content for the given URI in the cache. - * - * @param pCacheURI the cache URI - * @param pRequest the request - * @param pCachedResponse the cached response - * @throws IOException if the content could not be cached - */ - void registerContent( - final String pCacheURI, - final CacheRequest pRequest, - final CachedResponse pCachedResponse - ) throws IOException { - // System.out.println(" ## HTTPCache ## Registering content for " + pCacheURI); - -// pRequest.removeAttribute(ATTRIB_IS_STALE); -// pRequest.setAttribute(ATTRIB_CACHED_RESPONSE, pCachedResponse); - - if ("HEAD".equals(pRequest.getMethod())) { - // System.out.println(" ## HTTPCache ## Was HEAD request, will NOT store content."); - return; - } - - // TODO: Several resources may have same extension... - String extension = MIMEUtil.getExtension(pCachedResponse.getHeaderValue(HEADER_CONTENT_TYPE)); - if (extension == null) { - extension = "[NULL]"; - } - - synchronized (contentCache) { - contentCache.put(pCacheURI + '.' + extension, pCachedResponse); - - // This will be the default version - if (!contentCache.containsKey(pCacheURI)) { - contentCache.put(pCacheURI, pCachedResponse); - } - } - - // Write the cached content to disk - File content = new File(tempDir, "./" + pCacheURI + '.' + extension); - if (deleteCacheOnExit && !content.exists()) { - content.deleteOnExit(); - } - - File parent = content.getParentFile(); - if (!(parent.exists() || parent.mkdirs())) { - log("Could not create directory " + parent.getAbsolutePath()); - - // TODO: Make sure vary-info is still created in memory - - return; - } - - OutputStream mContentStream = new BufferedOutputStream(new FileOutputStream(content)); - - try { - pCachedResponse.writeContentsTo(mContentStream); - } - finally { - try { - mContentStream.close(); - } - catch (IOException e) { - log("Error closing content stream: " + e.getMessage(), e); - } - } - - // Write the cached headers to disk (in pseudo-properties-format) - File headers = new File(content.getAbsolutePath() + FILE_EXT_HEADERS); - if (deleteCacheOnExit && !headers.exists()) { - headers.deleteOnExit(); - } - - FileWriter writer = new FileWriter(headers); - PrintWriter headerWriter = new PrintWriter(writer); - try { - String[] names = pCachedResponse.getHeaderNames(); - - for (String name : names) { - String[] values = pCachedResponse.getHeaderValues(name); - - headerWriter.print(name); - headerWriter.print(": "); - headerWriter.println(StringUtil.toCSVString(values, "\\")); - } - } - finally { - headerWriter.flush(); - try { - writer.close(); - } - catch (IOException e) { - log("Error closing header stream: " + e.getMessage(), e); - } - } - - // TODO: Make this more robust, if some weird entity is not - // consistent in it's vary-headers.. - // (sometimes Vary, sometimes not, or somtimes different Vary headers). - - // Write extra Vary info to disk - String[] varyHeaders = pCachedResponse.getHeaderValues(HEADER_VARY); - - // If no variations, then don't store vary info - if (varyHeaders != null && varyHeaders.length > 0) { - Properties variations = getVaryProperties(pCacheURI); - - String vary = StringUtil.toCSVString(varyHeaders); - variations.setProperty(HEADER_VARY, vary); - - // Create Vary-key and map to file extension... - String varyKey = createVaryKey(varyHeaders, pRequest); -// System.out.println("varyKey: " + varyKey); -// System.out.println("extension: " + extension); - variations.setProperty(varyKey, extension); - - storeVaryProperties(pCacheURI, variations); - } - } - - /** - * @param pCacheURI the cache URI - * @param pRequest the request - * @return a {@code CachedResponse} object - */ - CachedResponse getContent(final String pCacheURI, final CacheRequest pRequest) { -// System.err.println(" ## HTTPCache ## Looking up content for " + pCacheURI); -// Thread.dumpStack(); - - String extension = getVaryExtension(pCacheURI, pRequest); - - CachedResponse response; - synchronized (contentCache) { -// System.out.println(" ## HTTPCache ## Looking up content with ext: \"" + extension + "\" from memory cache (" + contentCache /*.size()*/ + " entries)..."); - if ("ANY".equals(extension)) { - response = contentCache.get(pCacheURI); - } - else { - response = contentCache.get(pCacheURI + '.' + extension); - } - - if (response == null) { -// System.out.println(" ## HTTPCache ## Content not found in memory cache."); -// -// System.out.println(" ## HTTPCache ## Looking up content from disk cache..."); - // Read from disk-cache - response = readFromDiskCache(pCacheURI, pRequest); - } - -// if (response == null) { -// System.out.println(" ## HTTPCache ## Content not found in disk cache."); -// } -// else { -// System.out.println(" ## HTTPCache ## Content for " + pCacheURI + " found: " + response); -// } - } - - return response; - } - - private CachedResponse readFromDiskCache(String pCacheURI, CacheRequest pRequest) { - CachedResponse response = null; - try { - File content = getCachedFile(pCacheURI, pRequest); - if (content != null && content.exists()) { - // Read contents - byte[] contents = FileUtil.read(content); - - // Read headers - File headers = new File(content.getAbsolutePath() + FILE_EXT_HEADERS); - int headerSize = (int) headers.length(); - - BufferedReader reader = new BufferedReader(new FileReader(headers)); - LinkedHashMap> headerMap = new LinkedHashMap>(); - String line; - while ((line = reader.readLine()) != null) { - int colIdx = line.indexOf(':'); - String name; - String value; - if (colIdx >= 0) { - name = line.substring(0, colIdx); - value = line.substring(colIdx + 2); // ": " - } - else { - name = line; - value = ""; - } - - headerMap.put(name, Arrays.asList(StringUtil.toStringArray(value, "\\"))); - } - - response = new CachedResponseImpl(HttpServletResponse.SC_OK, headerMap, headerSize, contents); - contentCache.put(pCacheURI + '.' + FileUtil.getExtension(content), response); - } - } - catch (IOException e) { - log("Error reading from cache: " + e.getMessage(), e); - } - return response; - } - - boolean isContentStale(final String pCacheURI, final CacheRequest pRequest) { - // NOTE: Content is either stale or not, for the duration of one request, unless re-fetched - // Means that we must retry after a registerContent(), if caching as request-attribute - Boolean stale; -// stale = (Boolean) pRequest.getAttribute(ATTRIB_IS_STALE); -// if (stale != null) { -// return stale; -// } - - stale = isContentStaleImpl(pCacheURI, pRequest); -// pRequest.setAttribute(ATTRIB_IS_STALE, stale); - - return stale; - } - - private boolean isContentStaleImpl(final String pCacheURI, final CacheRequest pRequest) { - CachedResponse response = getContent(pCacheURI, pRequest); - - if (response == null) { - // System.out.println(" ## HTTPCache ## Content is stale (no content)."); - return true; - } - - // TODO: Get max-age=... from REQUEST too! - - // TODO: What about time skew? Now should be (roughly) same as: - // long now = pRequest.getDateHeader("Date"); - // TODO: If the time differs (server "now" vs client "now"), should we - // take that into consideration when testing for stale content? - // Probably, yes. - // TODO: Define rules for how to handle time skews - - // Set timestamp check - // NOTE: HTTP Dates are always in GMT time zone - long now = (System.currentTimeMillis() / 1000L) * 1000L; - long expires = getDateHeader(response.getHeaderValue(HEADER_EXPIRES)); - //long lastModified = getDateHeader(response, HEADER_LAST_MODIFIED); - long lastModified = getDateHeader(response.getHeaderValue(HEADER_CACHED_TIME)); - - // If expires header is not set, compute it - if (expires == -1L) { - /* - // Note: Not all content has Last-Modified header. We should then - // use lastModified() of the cached file, to compute expires time. - if (lastModified == -1L) { - File cached = getCachedFile(pCacheURI, pRequest); - if (cached != null && cached.exists()) { - lastModified = cached.lastModified(); - //// System.out.println(" ## HTTPCache ## Last-Modified is " + HTTPUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); - } - } - */ - - // 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 + defaultExpiryTime; - //// System.out.println(" ## HTTPCache ## Expires is " + HTTPUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry"); - } - else { - expires = lastModified + (maxAge * 1000L); // max-age is seconds - //// System.out.println(" ## HTTPCache ## Expires is " + HTTPUtil.formatHTTPDate(expires) + ", using lastModified + maxAge"); - } - } - /* - else { - // System.out.println(" ## HTTPCache ## Expires header is " + response.getHeaderValue(HEADER_EXPIRES)); - } - */ - - // Expired? - if (expires < now) { - // System.out.println(" ## HTTPCache ## Content is stale (content expired: " - // + HTTPUtil.formatHTTPDate(expires) + " before " + HTTPUtil.formatHTTPDate(now) + ")."); - return true; - } - - /* - if (lastModified == -1L) { - // Note: Not all content has Last-Modified header. We should then - // use lastModified() of the cached file, to compute expires time. - File cached = getCachedFile(pCacheURI, pRequest); - if (cached != null && cached.exists()) { - lastModified = cached.lastModified(); - //// System.out.println(" ## HTTPCache ## Last-Modified is " + HTTPUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); - } - } - */ - - // Get the real file for this request, if any - File real = getRealFile(pRequest); - //noinspection RedundantIfStatement - if (real != null && real.exists() && real.lastModified() > lastModified) { - // System.out.println(" ## HTTPCache ## Content is stale (new content" - // + HTTPUtil.formatHTTPDate(lastModified) + " before " + HTTPUtil.formatHTTPDate(real.lastModified()) + ")."); - return true; - } - - return false; - } - - /** - * Parses a cached header with directive to an int. - * E.g: Cache-Control: max-age=60, returns 60 - * - * @param pCached the cached response - * @param pHeaderName the header name (e.g: {@code CacheControl}) - * @param pDirective the directive (e.g: {@code max-age} - * @return the int value, or {@code -1} if not found - */ - private int getIntHeader(final CachedResponse pCached, final String pHeaderName, final String pDirective) { - String[] headerValues = pCached.getHeaderValues(pHeaderName); - int value = -1; - - if (headerValues != null) { - for (String headerValue : headerValues) { - if (pDirective == null) { - if (!StringUtil.isEmpty(headerValue)) { - value = Integer.parseInt(headerValue); - } - break; - } - else { - int start = headerValue.indexOf(pDirective); - - // Directive found - if (start >= 0) { - - int end = headerValue.lastIndexOf(','); - if (end < start) { - end = headerValue.length(); - } - - headerValue = headerValue.substring(start, end); - - if (!StringUtil.isEmpty(headerValue)) { - value = Integer.parseInt(headerValue); - } - - break; - } - } - } - } - - return value; - } - - /** - * Utility to read a date header from a cached response. - * - * @param pHeaderValue the header value - * @return the parsed date as a long, or {@code -1L} if not found - * @see javax.servlet.http.HttpServletRequest#getDateHeader(String) - */ - static long getDateHeader(final String pHeaderValue) { - long date = -1L; - if (pHeaderValue != null) { - date = HTTPUtil.parseHTTPDate(pHeaderValue); - } - return date; - } - - // TODO: Extract and make public? - final static class SizedLRUMap extends LRUHashMap { - int currentSize; - int maxSize; - - public SizedLRUMap(int pMaxSize) { - //super(true); - super(); // Note: super.maxSize doesn't count... - maxSize = pMaxSize; - } - - - // In super (LRUMap?) this could just return 1... - protected int sizeOf(Object pValue) { - // HACK: As this is used as a backing for a TimeoutMap, the values - // will themselves be Entries... - while (pValue instanceof Map.Entry) { - pValue = ((Map.Entry) pValue).getValue(); - } - - CachedResponse cached = (CachedResponse) pValue; - return (cached != null ? cached.size() : 0); - } - - @Override - public V put(K pKey, V pValue) { - currentSize += sizeOf(pValue); - - V old = super.put(pKey, pValue); - if (old != null) { - currentSize -= sizeOf(old); - } - return old; - } - - @Override - public V remove(Object pKey) { - V old = super.remove(pKey); - if (old != null) { - currentSize -= sizeOf(old); - } - return old; - } - - @Override - protected boolean removeEldestEntry(Map.Entry pEldest) { - if (maxSize <= currentSize) { // NOTE: maxSize here is mem size - removeLRU(); - } - return false; - } - - @Override - public void removeLRU() { - while (maxSize <= currentSize) { // NOTE: maxSize here is mem size - super.removeLRU(); - } - } - } - +/* + * 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 of the copyright holder 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 HOLDER 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.cache; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.Validate; +import com.twelvemonkeys.net.HTTPUtil; +import com.twelvemonkeys.net.MIMEUtil; +import com.twelvemonkeys.util.LRUHashMap; +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; +import java.util.logging.Logger; + +/** + * A "simple" HTTP cache. + * + * - Use a mix of parameters and hashcode + lenght with fixed (max) lenght? + * (Hashcodes of Strings are constant). + * - Store full filenames in .vary, instead of just extension, and use + * short filenames? (and only one .vary per dir). + *

      + * + *

      + * + * @author Harald Kuhr + * @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.. +// TODO: TEST: Battle-testing using some URL-hammer tool and maybe a profiler +// TODO: ETag/Conditional (If-None-Match) support! +// TODO: Rewrite to use java.util.concurrent Locks (if possible) for performance +// Maybe use ConcurrentHashMap instead fo synchronized HashMap? +// TODO: Rewrite to use NIO for performance +// TODO: Allow no tempdir for in-memory only cache +// TODO: Specify max size of disk-cache +public class HTTPCache { + /** + * The HTTP header {@code "Cache-Control"} + */ + protected static final String HEADER_CACHE_CONTROL = "Cache-Control"; + /** + * The HTTP header {@code "Content-Type"} + */ + protected static final String HEADER_CONTENT_TYPE = "Content-Type"; + /** + * The HTTP header {@code "Date"} + */ + protected static final String HEADER_DATE = "Date"; + /** + * The HTTP header {@code "ETag"} + */ + protected static final String HEADER_ETAG = "ETag"; + /** + * The HTTP header {@code "Expires"} + */ + protected static final String HEADER_EXPIRES = "Expires"; + /** + * The HTTP header {@code "If-Modified-Since"} + */ + protected static final String HEADER_IF_MODIFIED_SINCE = "If-Modified-Since"; + /** + * The HTTP header {@code "If-None-Match"} + */ + protected static final String HEADER_IF_NONE_MATCH = "If-None-Match"; + /** + * The HTTP header {@code "Last-Modified"} + */ + protected static final String HEADER_LAST_MODIFIED = "Last-Modified"; + /** + * The HTTP header {@code "Pragma"} + */ + protected static final String HEADER_PRAGMA = "Pragma"; + /** + * The HTTP header {@code "Vary"} + */ + protected static final String HEADER_VARY = "Vary"; + /** + * The HTTP header {@code "Warning"} + */ + protected static final String HEADER_WARNING = "Warning"; + /** + * HTTP extension header {@code "X-Cached-At"} + */ + protected static final String HEADER_CACHED_TIME = "X-Cached-At"; + + /** + * The file extension for header files ({@code ".headers"}) + */ + protected static final String FILE_EXT_HEADERS = ".headers"; + /** + * The file extension for varation-info files ({@code ".vary"}) + */ + protected static final String FILE_EXT_VARY = ".vary"; + + /** + * The directory used for the disk-based cache + */ + private File tempDir; + + /** + * Indicates wether the disk-based cache should be deleted when the + * container shuts down/VM exits + */ + private boolean deleteCacheOnExit; + + /** + * In-memory content cache + */ + private final Map contentCache; + /** + * In-memory enity cache + */ + private final Map entityCache; + /** + * In-memory varyiation-info cache + */ + private final Map varyCache; + + private long defaultExpiryTime = -1; + + private final Logger logger; + + // Internal constructor for sublcasses only + protected HTTPCache( + final File pTempFolder, + final long pDefaultCacheExpiryTime, + final int pMaxMemCacheSize, + final int pMaxCachedEntites, + final boolean pDeleteCacheOnExit, + final Logger pLogger + ) { + 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"); + + 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 +// contentCache = new TimeoutMap(backing, null, pDefaultCacheExpiryTime); + contentCache = new SizedLRUMap(pMaxMemCacheSize); // size in bytes + } + else { + contentCache = new NullMap(); + } + + entityCache = new LRUHashMap(pMaxCachedEntites); + varyCache = new LRUHashMap(pMaxCachedEntites); + + deleteCacheOnExit = pDeleteCacheOnExit; + + tempDir = pTempFolder; + + logger = pLogger != null ? pLogger : Logger.getLogger(getClass().getName()); + } + + /** + * Creates an {@code HTTPCache}. + * + * @param pTempFolder the temp folder for this cache. + * @param pDefaultCacheExpiryTime Default expiry time for cached entities, + * {@code >= 0} + * @param pMaxMemCacheSize Maximum size of in-memory cache for content + * in bytes, {@code >= 0} ({@code 0} means no + * in-memory cache) + * @param pMaxCachedEntites Maximum number of entities in cache + * @param pDeleteCacheOnExit specifies wether the file cache should be + * deleted when the application or VM shuts down + * @throws IllegalArgumentException if {@code pName} or {@code pContext} is + * {@code null} or if any of {@code pDefaultCacheExpiryTime}, + * {@code pMaxMemCacheSize} or {@code pMaxCachedEntites} are + * negative, + * or if the directory as given in the context attribute + * {@code "javax.servlet.context.tempdir"} does not exist, and + * cannot be created. + */ + public HTTPCache(final File pTempFolder, + final long pDefaultCacheExpiryTime, + final int pMaxMemCacheSize, final int pMaxCachedEntites, + final boolean pDeleteCacheOnExit) { + this(pTempFolder, pDefaultCacheExpiryTime, pMaxMemCacheSize, pMaxCachedEntites, pDeleteCacheOnExit, null); + } + + + /** + * Creates an {@code HTTPCache}. + * + * @param pName Name of this cache (should be unique per application). + * Used for temp folder + * @param pContext Servlet context for the application. + * @param pDefaultCacheExpiryTime Default expiry time for cached entities, + * {@code >= 0} + * @param pMaxMemCacheSize Maximum size of in-memory cache for content + * in bytes, {@code >= 0} ({@code 0} means no + * in-memory cache) + * @param pMaxCachedEntites Maximum number of entities in cache + * @param pDeleteCacheOnExit specifies wether the file cache should be + * deleted when the application or VM shuts down + * @throws IllegalArgumentException if {@code pName} or {@code pContext} is + * {@code null} or if any of {@code pDefaultCacheExpiryTime}, + * {@code pMaxMemCacheSize} or {@code pMaxCachedEntites} are + * negative, + * or if the directory as given in the context attribute + * {@code "javax.servlet.context.tempdir"} does not exist, and + * cannot be created. + * @deprecated Use {@link #HTTPCache(File, long, int, int, boolean)} instead. + */ + public HTTPCache(final String pName, final ServletContext pContext, + final int pDefaultCacheExpiryTime, final int pMaxMemCacheSize, + final int pMaxCachedEntites, final boolean pDeleteCacheOnExit) { + this( + getTempFolder(pName, pContext), + pDefaultCacheExpiryTime, pMaxMemCacheSize, pMaxCachedEntites, pDeleteCacheOnExit, + new CacheFilter.ServletContextLoggerAdapter(pName, pContext) + ); + } + + private static File getTempFolder(String pName, ServletContext pContext) { + 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); + } + + public String toString() { + StringBuilder buf = new StringBuilder(getClass().getSimpleName()); + buf.append("["); + buf.append("Temp dir: "); + buf.append(tempDir.getAbsolutePath()); + if (deleteCacheOnExit) { + buf.append(" (non-persistent)"); + } + else { + buf.append(" (persistent)"); + } + buf.append(", EntityCache: {"); + buf.append(entityCache.size()); + buf.append(" entries in a "); + buf.append(entityCache.getClass().getName()); + buf.append("}, VaryCache: {"); + buf.append(varyCache.size()); + buf.append(" entries in a "); + buf.append(varyCache.getClass().getName()); + buf.append("}, ContentCache: {"); + buf.append(contentCache.size()); + buf.append(" entries in a "); + buf.append(contentCache.getClass().getName()); + buf.append("}]"); + + return buf.toString(); + } + + void log(final String pMessage) { + logger.log(Level.INFO, pMessage); + } + + void log(final String pMessage, Throwable pException) { + logger.log(Level.WARNING, pMessage, pException); + } + + /** + * Looks up the {@code CachedEntity} for the given request. + * + * @param pRequest the request + * @param pResponse the response + * @param pResolver the resolver + * @throws java.io.IOException if an I/O error occurs + * @throws CacheException if the cached entity can't be resolved for some reason + */ + public void doCached(final CacheRequest pRequest, final CacheResponse pResponse, final ResponseResolver pResolver) throws IOException, CacheException { + // TODO: Expire cached items on PUT/POST/DELETE/PURGE + // If not cachable request, resolve directly + if (!isCacheable(pRequest)) { + pResolver.resolve(pRequest, pResponse); + } + else { + // Generate cacheURI + String cacheURI = generateCacheURI(pRequest); +// System.out.println(" ## HTTPCache ## Request Id (cacheURI): " + cacheURI); + + // Get/create cached entity + CachedEntity cached; + synchronized (entityCache) { + cached = entityCache.get(cacheURI); + if (cached == null) { + cached = new CachedEntityImpl(cacheURI, this); + entityCache.put(cacheURI, cached); + } + } + + // else if (not cached || stale), resolve through wrapped (caching) response + // else render to response + + // TODO: This is a bottleneck for uncachable resources. Should not + // synchronize, if we know (HOW?) the resource is not cachable. + synchronized (cached) { + if (cached.isStale(pRequest) /* TODO: NOT CACHED?! */) { + // Go fetch... + WritableCachedResponse cachedResponse = cached.createCachedResponse(); + pResolver.resolve(pRequest, cachedResponse); + + if (isCachable(cachedResponse)) { +// System.out.println("Registering content: " + cachedResponse.getCachedResponse()); + registerContent(cacheURI, pRequest, cachedResponse.getCachedResponse()); + } + else { + // TODO: What about non-cachable responses? We need to either remove them from cache, or mark them as stale... + // Best is probably to mark as non-cacheable for later, and NOT store content (performance) +// System.out.println("Non-cacheable response: " + cachedResponse); + + // TODO: Write, but should really do this unbuffered.... And some resolver might be able to do just that? + // Might need a resolver.isWriteThroughForUncachableResources() method... + pResponse.setStatus(cachedResponse.getStatus()); + cachedResponse.writeHeadersTo(pResponse); + cachedResponse.writeContentsTo(pResponse.getOutputStream()); + return; + } + } + } + + cached.render(pRequest, pResponse); + } + } + + protected void invalidate(CacheRequest pRequest) { + // Generate cacheURI + String cacheURI = generateCacheURI(pRequest); + + // Get/create cached entity + CachedEntity cached; + synchronized (entityCache) { + cached = entityCache.get(cacheURI); + if (cached != null) { + // TODO; Remove all variants + entityCache.remove(cacheURI); + } + } + + } + + private boolean isCacheable(final CacheRequest pRequest) { + // TODO: Support public/private cache (a cache probably have to be one of the two, when created) + // TODO: Only private caches should cache requests with Authorization + + // TODO: OptimizeMe! + // It's probably best to cache the "cacheableness" of a request and a resource separately + List cacheControlValues = pRequest.getHeaders().get(HEADER_CACHE_CONTROL); + if (cacheControlValues != null) { + Map cacheControl = new HashMap(); + for (String cc : cacheControlValues) { + List directives = Arrays.asList(cc.split(",")); + for (String directive : directives) { + directive = directive.trim(); + if (directive.length() > 0) { + String[] directiveParts = directive.split("=", 2); + cacheControl.put(directiveParts[0], directiveParts.length > 1 ? directiveParts[1] : null); + } + } + } + + if (cacheControl.containsKey("no-cache") || cacheControl.containsKey("no-store")) { + return false; + } + + /* + "no-cache" ; Section 14.9.1 + | "no-store" ; Section 14.9.2 + | "max-age" "=" delta-seconds ; Section 14.9.3, 14.9.4 + | "max-stale" [ "=" delta-seconds ] ; Section 14.9.3 + | "min-fresh" "=" delta-seconds ; Section 14.9.3 + | "no-transform" ; Section 14.9.5 + | "only-if-cached" + */ + } + + return true; + } + + private boolean isCachable(final CacheResponse pResponse) { + if (pResponse.getStatus() != HttpServletResponse.SC_OK) { + return false; + } + + // Vary: * + List values = pResponse.getHeaders().get(HTTPCache.HEADER_VARY); + if (values != null) { + for (String value : values) { + if ("*".equals(value)) { + return false; + } + } + } + + // Cache-Control: no-cache, no-store, must-revalidate + values = pResponse.getHeaders().get(HTTPCache.HEADER_CACHE_CONTROL); + if (values != null) { + for (String value : values) { + if (StringUtil.contains(value, "no-cache") + || StringUtil.contains(value, "no-store") + || StringUtil.contains(value, "must-revalidate")) { + return false; + } + } + } + + // Pragma: no-cache + values = pResponse.getHeaders().get(HTTPCache.HEADER_PRAGMA); + if (values != null) { + for (String value : values) { + if (StringUtil.contains(value, "no-cache")) { + return false; + } + } + } + + return true; + } + + + /** + * Allows a server-side cache mechanism to peek at the real file. + * Default implementation return {@code null}. + * + * @param pRequest the request + * @return {@code null}, always + */ + protected File getRealFile(final CacheRequest pRequest) { + // TODO: Create callback for this? Only possible for server-side cache... Maybe we can get away without this? + // For now: Default implementation that returns null + return null; +/* + String contextRelativeURI = ServletUtil.getContextRelativeURI(pRequest); + // System.out.println(" ## HTTPCache ## Context relative URI: " + contextRelativeURI); + + String path = mContext.getRealPath(contextRelativeURI); + // System.out.println(" ## HTTPCache ## Real path: " + path); + + if (path != null) { + return new File(path); + } + + return null; +*/ + } + + private File getCachedFile(final String pCacheURI, final CacheRequest pRequest) { + File file = null; + + // Get base dir + File base = new File(tempDir, "./" + pCacheURI); + final String basePath = base.getAbsolutePath(); + File directory = base.getParentFile(); + + // Get list of files that are candidates + File[] candidates = directory.listFiles(new FileFilter() { + public boolean accept(File pFile) { + return pFile.getAbsolutePath().startsWith(basePath) + && !pFile.getName().endsWith(FILE_EXT_HEADERS) + && !pFile.getName().endsWith(FILE_EXT_VARY); + } + }); + + // Negotiation + if (candidates != null) { + String extension = getVaryExtension(pCacheURI, pRequest); + //System.out.println("-- Vary ext: " + extension); + if (extension != null) { + for (File candidate : candidates) { + //System.out.println("-- Candidate: " + candidates[i]); + + if (extension.equals("ANY") || extension.equals(FileUtil.getExtension(candidate))) { + //System.out.println("-- Candidate selected"); + file = candidate; + break; + } + } + } + } + else if (base.exists()) { + //System.out.println("-- File not a directory: " + directory); + log("File not a directory: " + directory); + } + + return file; + } + + private String getVaryExtension(final String pCacheURI, final CacheRequest pRequest) { + Properties variations = getVaryProperties(pCacheURI); + + String[] varyHeaders = StringUtil.toStringArray(variations.getProperty(HEADER_VARY, "")); +// System.out.println("-- Vary: \"" + variations.getProperty(HEADER_VARY) + "\""); + + String varyKey = createVaryKey(varyHeaders, pRequest); +// System.out.println("-- Vary key: \"" + varyKey + "\""); + + // If no vary, just go with any version... + return StringUtil.isEmpty(varyKey) ? "ANY" : variations.getProperty(varyKey, null); + } + + private String createVaryKey(final String[] pVaryHeaders, final CacheRequest pRequest) { + if (pVaryHeaders == null) { + return null; + } + + StringBuilder headerValues = new StringBuilder(); + for (String varyHeader : pVaryHeaders) { + List varies = pRequest.getHeaders().get(varyHeader); + String headerValue = varies != null && varies.size() > 0 ? varies.get(0) : null; + + headerValues.append(varyHeader); + headerValues.append("__V_"); + headerValues.append(createSafeHeader(headerValue)); + } + + return headerValues.toString(); + } + + private void storeVaryProperties(final String pCacheURI, final Properties pVariations) { + synchronized (pVariations) { + try { + File file = getVaryPropertiesFile(pCacheURI); + if (!file.exists() && deleteCacheOnExit) { + file.deleteOnExit(); + } + + FileOutputStream out = new FileOutputStream(file); + try { + pVariations.store(out, pCacheURI + " Vary info"); + } + finally { + out.close(); + } + } + catch (IOException ioe) { + log("Error: Could not store Vary info: " + ioe); + } + } + } + + private Properties getVaryProperties(final String pCacheURI) { + Properties variations; + + synchronized (varyCache) { + variations = varyCache.get(pCacheURI); + if (variations == null) { + variations = loadVaryProperties(pCacheURI); + varyCache.put(pCacheURI, variations); + } + } + + return variations; + } + + private Properties loadVaryProperties(final String pCacheURI) { + // Read Vary info, for content negotiation + Properties variations = new Properties(); + File vary = getVaryPropertiesFile(pCacheURI); + if (vary.exists()) { + try { + FileInputStream in = new FileInputStream(vary); + try { + variations.load(in); + } + finally { + in.close(); + } + } + catch (IOException ioe) { + log("Error: Could not load Vary info: " + ioe); + } + } + return variations; + } + + private File getVaryPropertiesFile(final String pCacheURI) { + return new File(tempDir, "./" + pCacheURI + FILE_EXT_VARY); + } + + private static String generateCacheURI(final CacheRequest pRequest) { + StringBuilder buffer = new StringBuilder(); + + // Note: As the '/'s are not replaced, the directory structure will be recreated + // TODO: Old mehtod relied on context relativization, that must now be handled byt the ServletCacheRequest +// String contextRelativeURI = ServletUtil.getContextRelativeURI(pRequest); + String contextRelativeURI = pRequest.getRequestURI().getPath(); + buffer.append(contextRelativeURI); + + // Create directory for all resources + if (contextRelativeURI.charAt(contextRelativeURI.length() - 1) != '/') { + buffer.append('/'); + } + + // Get parameters from request, and recreate query to avoid unneccessary + // regeneration/caching when parameters are out of order + // Also makes caching work for POST + appendSortedRequestParams(pRequest, buffer); + + return buffer.toString(); + } + + private static void appendSortedRequestParams(final CacheRequest pRequest, final StringBuilder pBuffer) { + Set names = pRequest.getParameters().keySet(); + if (names.isEmpty()) { + pBuffer.append("defaultVersion"); + return; + } + + // We now have parameters + pBuffer.append('_'); // append '_' for '?', to avoid clash with default + + // Create a sorted map + SortedMap> sortedQueryMap = new TreeMap>(); + for (String name : names) { + List values = pRequest.getParameters().get(name); + + sortedQueryMap.put(name, values); + } + + // Iterate over sorted map, and append to stringbuffer + for (Iterator>> iterator = sortedQueryMap.entrySet().iterator(); iterator.hasNext();) { + Map.Entry> entry = iterator.next(); + pBuffer.append(createSafe(entry.getKey())); + + List values = entry.getValue(); + if (values != null && values.size() > 0) { + pBuffer.append("_V"); // = + for (int i = 0; i < values.size(); i++) { + String value = values.get(i); + if (i != 0) { + pBuffer.append(','); + } + pBuffer.append(createSafe(value)); + } + } + + if (iterator.hasNext()) { + pBuffer.append("_P"); // & + } + } + } + + private static String createSafe(final String pKey) { + return pKey.replace('/', '-') + .replace('&', '-') // In case they are encoded + .replace('#', '-') + .replace(';', '-'); + } + + private static String createSafeHeader(final String pHeaderValue) { + if (pHeaderValue == null) { + return "NULL"; + } + + return pHeaderValue.replace(' ', '_') + .replace(':', '_') + .replace('=', '_'); + } + + /** + * Registers content for the given URI in the cache. + * + * @param pCacheURI the cache URI + * @param pRequest the request + * @param pCachedResponse the cached response + * @throws IOException if the content could not be cached + */ + void registerContent( + final String pCacheURI, + final CacheRequest pRequest, + final CachedResponse pCachedResponse + ) throws IOException { + // System.out.println(" ## HTTPCache ## Registering content for " + pCacheURI); + +// pRequest.removeAttribute(ATTRIB_IS_STALE); +// pRequest.setAttribute(ATTRIB_CACHED_RESPONSE, pCachedResponse); + + if ("HEAD".equals(pRequest.getMethod())) { + // System.out.println(" ## HTTPCache ## Was HEAD request, will NOT store content."); + return; + } + + // TODO: Several resources may have same extension... + String extension = MIMEUtil.getExtension(pCachedResponse.getHeaderValue(HEADER_CONTENT_TYPE)); + if (extension == null) { + extension = "[NULL]"; + } + + synchronized (contentCache) { + contentCache.put(pCacheURI + '.' + extension, pCachedResponse); + + // This will be the default version + if (!contentCache.containsKey(pCacheURI)) { + contentCache.put(pCacheURI, pCachedResponse); + } + } + + // Write the cached content to disk + File content = new File(tempDir, "./" + pCacheURI + '.' + extension); + if (deleteCacheOnExit && !content.exists()) { + content.deleteOnExit(); + } + + File parent = content.getParentFile(); + if (!(parent.exists() || parent.mkdirs())) { + log("Could not create directory " + parent.getAbsolutePath()); + + // TODO: Make sure vary-info is still created in memory + + return; + } + + OutputStream mContentStream = new BufferedOutputStream(new FileOutputStream(content)); + + try { + pCachedResponse.writeContentsTo(mContentStream); + } + finally { + try { + mContentStream.close(); + } + catch (IOException e) { + log("Error closing content stream: " + e.getMessage(), e); + } + } + + // Write the cached headers to disk (in pseudo-properties-format) + File headers = new File(content.getAbsolutePath() + FILE_EXT_HEADERS); + if (deleteCacheOnExit && !headers.exists()) { + headers.deleteOnExit(); + } + + FileWriter writer = new FileWriter(headers); + PrintWriter headerWriter = new PrintWriter(writer); + try { + String[] names = pCachedResponse.getHeaderNames(); + + for (String name : names) { + String[] values = pCachedResponse.getHeaderValues(name); + + headerWriter.print(name); + headerWriter.print(": "); + headerWriter.println(StringUtil.toCSVString(values, "\\")); + } + } + finally { + headerWriter.flush(); + try { + writer.close(); + } + catch (IOException e) { + log("Error closing header stream: " + e.getMessage(), e); + } + } + + // TODO: Make this more robust, if some weird entity is not + // consistent in it's vary-headers.. + // (sometimes Vary, sometimes not, or somtimes different Vary headers). + + // Write extra Vary info to disk + String[] varyHeaders = pCachedResponse.getHeaderValues(HEADER_VARY); + + // If no variations, then don't store vary info + if (varyHeaders != null && varyHeaders.length > 0) { + Properties variations = getVaryProperties(pCacheURI); + + String vary = StringUtil.toCSVString(varyHeaders); + variations.setProperty(HEADER_VARY, vary); + + // Create Vary-key and map to file extension... + String varyKey = createVaryKey(varyHeaders, pRequest); +// System.out.println("varyKey: " + varyKey); +// System.out.println("extension: " + extension); + variations.setProperty(varyKey, extension); + + storeVaryProperties(pCacheURI, variations); + } + } + + /** + * @param pCacheURI the cache URI + * @param pRequest the request + * @return a {@code CachedResponse} object + */ + CachedResponse getContent(final String pCacheURI, final CacheRequest pRequest) { +// System.err.println(" ## HTTPCache ## Looking up content for " + pCacheURI); +// Thread.dumpStack(); + + String extension = getVaryExtension(pCacheURI, pRequest); + + CachedResponse response; + synchronized (contentCache) { +// System.out.println(" ## HTTPCache ## Looking up content with ext: \"" + extension + "\" from memory cache (" + contentCache /*.size()*/ + " entries)..."); + if ("ANY".equals(extension)) { + response = contentCache.get(pCacheURI); + } + else { + response = contentCache.get(pCacheURI + '.' + extension); + } + + if (response == null) { +// System.out.println(" ## HTTPCache ## Content not found in memory cache."); +// +// System.out.println(" ## HTTPCache ## Looking up content from disk cache..."); + // Read from disk-cache + response = readFromDiskCache(pCacheURI, pRequest); + } + +// if (response == null) { +// System.out.println(" ## HTTPCache ## Content not found in disk cache."); +// } +// else { +// System.out.println(" ## HTTPCache ## Content for " + pCacheURI + " found: " + response); +// } + } + + return response; + } + + private CachedResponse readFromDiskCache(String pCacheURI, CacheRequest pRequest) { + CachedResponse response = null; + try { + File content = getCachedFile(pCacheURI, pRequest); + if (content != null && content.exists()) { + // Read contents + byte[] contents = FileUtil.read(content); + + // Read headers + File headers = new File(content.getAbsolutePath() + FILE_EXT_HEADERS); + int headerSize = (int) headers.length(); + + BufferedReader reader = new BufferedReader(new FileReader(headers)); + LinkedHashMap> headerMap = new LinkedHashMap>(); + String line; + while ((line = reader.readLine()) != null) { + int colIdx = line.indexOf(':'); + String name; + String value; + if (colIdx >= 0) { + name = line.substring(0, colIdx); + value = line.substring(colIdx + 2); // ": " + } + else { + name = line; + value = ""; + } + + headerMap.put(name, Arrays.asList(StringUtil.toStringArray(value, "\\"))); + } + + response = new CachedResponseImpl(HttpServletResponse.SC_OK, headerMap, headerSize, contents); + contentCache.put(pCacheURI + '.' + FileUtil.getExtension(content), response); + } + } + catch (IOException e) { + log("Error reading from cache: " + e.getMessage(), e); + } + return response; + } + + boolean isContentStale(final String pCacheURI, final CacheRequest pRequest) { + // NOTE: Content is either stale or not, for the duration of one request, unless re-fetched + // Means that we must retry after a registerContent(), if caching as request-attribute + Boolean stale; +// stale = (Boolean) pRequest.getAttribute(ATTRIB_IS_STALE); +// if (stale != null) { +// return stale; +// } + + stale = isContentStaleImpl(pCacheURI, pRequest); +// pRequest.setAttribute(ATTRIB_IS_STALE, stale); + + return stale; + } + + private boolean isContentStaleImpl(final String pCacheURI, final CacheRequest pRequest) { + CachedResponse response = getContent(pCacheURI, pRequest); + + if (response == null) { + // System.out.println(" ## HTTPCache ## Content is stale (no content)."); + return true; + } + + // TODO: Get max-age=... from REQUEST too! + + // TODO: What about time skew? Now should be (roughly) same as: + // long now = pRequest.getDateHeader("Date"); + // TODO: If the time differs (server "now" vs client "now"), should we + // take that into consideration when testing for stale content? + // Probably, yes. + // TODO: Define rules for how to handle time skews + + // Set timestamp check + // NOTE: HTTP Dates are always in GMT time zone + long now = (System.currentTimeMillis() / 1000L) * 1000L; + long expires = getDateHeader(response.getHeaderValue(HEADER_EXPIRES)); + //long lastModified = getDateHeader(response, HEADER_LAST_MODIFIED); + long lastModified = getDateHeader(response.getHeaderValue(HEADER_CACHED_TIME)); + + // If expires header is not set, compute it + if (expires == -1L) { + /* + // Note: Not all content has Last-Modified header. We should then + // use lastModified() of the cached file, to compute expires time. + if (lastModified == -1L) { + File cached = getCachedFile(pCacheURI, pRequest); + if (cached != null && cached.exists()) { + lastModified = cached.lastModified(); + //// System.out.println(" ## HTTPCache ## Last-Modified is " + HTTPUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); + } + } + */ + + // 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 + defaultExpiryTime; + //// System.out.println(" ## HTTPCache ## Expires is " + HTTPUtil.formatHTTPDate(expires) + ", using lastModified + defaultExpiry"); + } + else { + expires = lastModified + (maxAge * 1000L); // max-age is seconds + //// System.out.println(" ## HTTPCache ## Expires is " + HTTPUtil.formatHTTPDate(expires) + ", using lastModified + maxAge"); + } + } + /* + else { + // System.out.println(" ## HTTPCache ## Expires header is " + response.getHeaderValue(HEADER_EXPIRES)); + } + */ + + // Expired? + if (expires < now) { + // System.out.println(" ## HTTPCache ## Content is stale (content expired: " + // + HTTPUtil.formatHTTPDate(expires) + " before " + HTTPUtil.formatHTTPDate(now) + ")."); + return true; + } + + /* + if (lastModified == -1L) { + // Note: Not all content has Last-Modified header. We should then + // use lastModified() of the cached file, to compute expires time. + File cached = getCachedFile(pCacheURI, pRequest); + if (cached != null && cached.exists()) { + lastModified = cached.lastModified(); + //// System.out.println(" ## HTTPCache ## Last-Modified is " + HTTPUtil.formatHTTPDate(lastModified) + ", using cachedFile.lastModified()"); + } + } + */ + + // Get the real file for this request, if any + File real = getRealFile(pRequest); + //noinspection RedundantIfStatement + if (real != null && real.exists() && real.lastModified() > lastModified) { + // System.out.println(" ## HTTPCache ## Content is stale (new content" + // + HTTPUtil.formatHTTPDate(lastModified) + " before " + HTTPUtil.formatHTTPDate(real.lastModified()) + ")."); + return true; + } + + return false; + } + + /** + * Parses a cached header with directive to an int. + * E.g: Cache-Control: max-age=60, returns 60 + * + * @param pCached the cached response + * @param pHeaderName the header name (e.g: {@code CacheControl}) + * @param pDirective the directive (e.g: {@code max-age} + * @return the int value, or {@code -1} if not found + */ + private int getIntHeader(final CachedResponse pCached, final String pHeaderName, final String pDirective) { + String[] headerValues = pCached.getHeaderValues(pHeaderName); + int value = -1; + + if (headerValues != null) { + for (String headerValue : headerValues) { + if (pDirective == null) { + if (!StringUtil.isEmpty(headerValue)) { + value = Integer.parseInt(headerValue); + } + break; + } + else { + int start = headerValue.indexOf(pDirective); + + // Directive found + if (start >= 0) { + + int end = headerValue.lastIndexOf(','); + if (end < start) { + end = headerValue.length(); + } + + headerValue = headerValue.substring(start, end); + + if (!StringUtil.isEmpty(headerValue)) { + value = Integer.parseInt(headerValue); + } + + break; + } + } + } + } + + return value; + } + + /** + * Utility to read a date header from a cached response. + * + * @param pHeaderValue the header value + * @return the parsed date as a long, or {@code -1L} if not found + * @see javax.servlet.http.HttpServletRequest#getDateHeader(String) + */ + static long getDateHeader(final String pHeaderValue) { + long date = -1L; + if (pHeaderValue != null) { + date = HTTPUtil.parseHTTPDate(pHeaderValue); + } + return date; + } + + // TODO: Extract and make public? + final static class SizedLRUMap extends LRUHashMap { + int currentSize; + int maxSize; + + public SizedLRUMap(int pMaxSize) { + //super(true); + super(); // Note: super.maxSize doesn't count... + maxSize = pMaxSize; + } + + + // In super (LRUMap?) this could just return 1... + protected int sizeOf(Object pValue) { + // HACK: As this is used as a backing for a TimeoutMap, the values + // will themselves be Entries... + while (pValue instanceof Map.Entry) { + pValue = ((Map.Entry) pValue).getValue(); + } + + CachedResponse cached = (CachedResponse) pValue; + return (cached != null ? cached.size() : 0); + } + + @Override + public V put(K pKey, V pValue) { + currentSize += sizeOf(pValue); + + V old = super.put(pKey, pValue); + if (old != null) { + currentSize -= sizeOf(old); + } + return old; + } + + @Override + public V remove(Object pKey) { + V old = super.remove(pKey); + if (old != null) { + currentSize -= sizeOf(old); + } + return old; + } + + @Override + protected boolean removeEldestEntry(Map.Entry pEldest) { + if (maxSize <= currentSize) { // NOTE: maxSize here is mem size + removeLRU(); + } + return false; + } + + @Override + public void removeLRU() { + while (maxSize <= currentSize) { // NOTE: maxSize here is mem size + super.removeLRU(); + } + } + } + } \ No newline at end of file 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 96f16afe..09e1a4b7 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/cache/SerlvetCacheResponseWrapper.java @@ -1,275 +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 of the copyright holder 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 HOLDER 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.cache; - -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.net.HTTPUtil; -import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; -import java.io.IOException; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.List; -import java.util.Map; - -/** - * CacheResponseWrapper class description. - *

      - * Based on ideas and code found in the ONJava article - * Two - * Servlet Filters Every Web Application Should Have - * by Jayson Falkner. - * - * @author Jayson Falkner - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: SerlvetCacheResponseWrapper.java#2 $ - */ -class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { - private ServletResponseStreamDelegate streamDelegate; - - private CacheResponse cacheResponse; - - private Boolean cacheable; - private int status; - - public SerlvetCacheResponseWrapper(final HttpServletResponse pServletResponse, final CacheResponse pResponse) { - super(pServletResponse); - cacheResponse = pResponse; - init(); - } - - - /* - 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() { - cacheable = null; - status = SC_OK; - streamDelegate = new ServletResponseStreamDelegate(this) { - protected OutputStream createOutputStream() throws IOException { - // Test if this request is really cacheable, otherwise, - // just write through to underlying response, and don't cache - if (isCacheable()) { - return cacheResponse.getOutputStream(); - } - else { - // TODO: We need to tell the cache about this, somehow... - writeHeaders(cacheResponse, (HttpServletResponse) getResponse()); - return super.getOutputStream(); - } - } - }; - } - - private void writeHeaders(final CacheResponse pResponse, final HttpServletResponse pServletResponse) { - Map> headers = pResponse.getHeaders(); - for (Map.Entry> header : headers.entrySet()) { - for (int i = 0; i < header.getValue().size(); i++) { - String value = header.getValue().get(i); - if (i == 0) { - pServletResponse.setHeader(header.getKey(), value); - } - else { - pServletResponse.addHeader(header.getKey(), value); - } - } - } - } - - public boolean isCacheable() { - // NOTE: Intentionally not synchronized - if (cacheable == null) { - cacheable = isCacheableImpl(); - } - - return cacheable; - } - - private boolean isCacheableImpl() { - // TODO: This code is duped in the cache... - if (status != SC_OK) { - return false; - } - - // Vary: * - List values = cacheResponse.getHeaders().get(HTTPCache.HEADER_VARY); - if (values != null) { - for (String value : values) { - if ("*".equals(value)) { - return false; - } - } - } - - // Cache-Control: no-cache, no-store, must-revalidate - values = cacheResponse.getHeaders().get(HTTPCache.HEADER_CACHE_CONTROL); - if (values != null) { - for (String value : values) { - if (StringUtil.contains(value, "no-cache") - || StringUtil.contains(value, "no-store") - || StringUtil.contains(value, "must-revalidate")) { - return false; - } - } - } - - // Pragma: no-cache - values = cacheResponse.getHeaders().get(HTTPCache.HEADER_PRAGMA); - if (values != null) { - for (String value : values) { - if (StringUtil.contains(value, "no-cache")) { - return false; - } - } - } - - return true; - } - - public void flushBuffer() throws IOException { - streamDelegate.flushBuffer(); - } - - public void resetBuffer() { - // Servlet 2.3 - streamDelegate.resetBuffer(); - } - - public void reset() { - if (Boolean.FALSE.equals(cacheable)) { - super.reset(); - } - // No else, might be cacheable after all.. - init(); - } - - public ServletOutputStream getOutputStream() throws IOException { - return streamDelegate.getOutputStream(); - } - - public PrintWriter getWriter() throws IOException { - return streamDelegate.getWriter(); - } - - public boolean containsHeader(String name) { - return cacheResponse.getHeaders().get(name) != null; - } - - public void sendError(int pStatusCode, String msg) throws IOException { - // NOT cacheable - status = pStatusCode; - super.sendError(pStatusCode, msg); - } - - public void sendError(int pStatusCode) throws IOException { - // NOT cacheable - status = pStatusCode; - super.sendError(pStatusCode); - } - - public void setStatus(int pStatusCode, String sm) { - // NOTE: This method is deprecated - setStatus(pStatusCode); - } - - public void setStatus(int pStatusCode) { - // NOT cacheable unless pStatusCode == 200 (or a FEW others?) - if (pStatusCode != SC_OK) { - status = pStatusCode; - super.setStatus(pStatusCode); - } - } - - public void sendRedirect(String pLocation) throws IOException { - // 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(cacheable)) { - super.setDateHeader(pName, pValue); - } - cacheResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue)); - } - - public void addDateHeader(String pName, long pValue) { - // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(cacheable)) { - super.addDateHeader(pName, pValue); - } - cacheResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue)); - } - - public void setHeader(String pName, String pValue) { - // If in write-trough-mode, set headers directly - if (Boolean.FALSE.equals(cacheable)) { - super.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(cacheable)) { - super.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(cacheable)) { - super.setIntHeader(pName, 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(cacheable)) { - super.addIntHeader(pName, pValue); - } - cacheResponse.addHeader(pName, String.valueOf(pValue)); - } - - public final void setContentType(String type) { - setHeader(HTTPCache.HEADER_CONTENT_TYPE, type); - } +/* + * 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 of the copyright holder 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 HOLDER 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.cache; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.net.HTTPUtil; +import com.twelvemonkeys.servlet.ServletResponseStreamDelegate; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; + +/** + * CacheResponseWrapper class description. + *

      + * Based on ideas and code found in the ONJava article + * Two + * Servlet Filters Every Web Application Should Have + * by Jayson Falkner. + *

      + * + * @author Jayson Falkner + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: SerlvetCacheResponseWrapper.java#2 $ + */ +class SerlvetCacheResponseWrapper extends HttpServletResponseWrapper { + private ServletResponseStreamDelegate streamDelegate; + + private CacheResponse cacheResponse; + + private Boolean cacheable; + private int status; + + public SerlvetCacheResponseWrapper(final HttpServletResponse pServletResponse, final CacheResponse pResponse) { + super(pServletResponse); + cacheResponse = pResponse; + init(); + } + + + /* + 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() { + cacheable = null; + status = SC_OK; + streamDelegate = new ServletResponseStreamDelegate(this) { + protected OutputStream createOutputStream() throws IOException { + // Test if this request is really cacheable, otherwise, + // just write through to underlying response, and don't cache + if (isCacheable()) { + return cacheResponse.getOutputStream(); + } + else { + // TODO: We need to tell the cache about this, somehow... + writeHeaders(cacheResponse, (HttpServletResponse) getResponse()); + return super.getOutputStream(); + } + } + }; + } + + private void writeHeaders(final CacheResponse pResponse, final HttpServletResponse pServletResponse) { + Map> headers = pResponse.getHeaders(); + for (Map.Entry> header : headers.entrySet()) { + for (int i = 0; i < header.getValue().size(); i++) { + String value = header.getValue().get(i); + if (i == 0) { + pServletResponse.setHeader(header.getKey(), value); + } + else { + pServletResponse.addHeader(header.getKey(), value); + } + } + } + } + + public boolean isCacheable() { + // NOTE: Intentionally not synchronized + if (cacheable == null) { + cacheable = isCacheableImpl(); + } + + return cacheable; + } + + private boolean isCacheableImpl() { + // TODO: This code is duped in the cache... + if (status != SC_OK) { + return false; + } + + // Vary: * + List values = cacheResponse.getHeaders().get(HTTPCache.HEADER_VARY); + if (values != null) { + for (String value : values) { + if ("*".equals(value)) { + return false; + } + } + } + + // Cache-Control: no-cache, no-store, must-revalidate + values = cacheResponse.getHeaders().get(HTTPCache.HEADER_CACHE_CONTROL); + if (values != null) { + for (String value : values) { + if (StringUtil.contains(value, "no-cache") + || StringUtil.contains(value, "no-store") + || StringUtil.contains(value, "must-revalidate")) { + return false; + } + } + } + + // Pragma: no-cache + values = cacheResponse.getHeaders().get(HTTPCache.HEADER_PRAGMA); + if (values != null) { + for (String value : values) { + if (StringUtil.contains(value, "no-cache")) { + return false; + } + } + } + + return true; + } + + public void flushBuffer() throws IOException { + streamDelegate.flushBuffer(); + } + + public void resetBuffer() { + // Servlet 2.3 + streamDelegate.resetBuffer(); + } + + public void reset() { + if (Boolean.FALSE.equals(cacheable)) { + super.reset(); + } + // No else, might be cacheable after all.. + init(); + } + + public ServletOutputStream getOutputStream() throws IOException { + return streamDelegate.getOutputStream(); + } + + public PrintWriter getWriter() throws IOException { + return streamDelegate.getWriter(); + } + + public boolean containsHeader(String name) { + return cacheResponse.getHeaders().get(name) != null; + } + + public void sendError(int pStatusCode, String msg) throws IOException { + // NOT cacheable + status = pStatusCode; + super.sendError(pStatusCode, msg); + } + + public void sendError(int pStatusCode) throws IOException { + // NOT cacheable + status = pStatusCode; + super.sendError(pStatusCode); + } + + public void setStatus(int pStatusCode, String sm) { + // NOTE: This method is deprecated + setStatus(pStatusCode); + } + + public void setStatus(int pStatusCode) { + // NOT cacheable unless pStatusCode == 200 (or a FEW others?) + if (pStatusCode != SC_OK) { + status = pStatusCode; + super.setStatus(pStatusCode); + } + } + + public void sendRedirect(String pLocation) throws IOException { + // 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(cacheable)) { + super.setDateHeader(pName, pValue); + } + cacheResponse.setHeader(pName, HTTPUtil.formatHTTPDate(pValue)); + } + + public void addDateHeader(String pName, long pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(cacheable)) { + super.addDateHeader(pName, pValue); + } + cacheResponse.addHeader(pName, HTTPUtil.formatHTTPDate(pValue)); + } + + public void setHeader(String pName, String pValue) { + // If in write-trough-mode, set headers directly + if (Boolean.FALSE.equals(cacheable)) { + super.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(cacheable)) { + super.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(cacheable)) { + super.setIntHeader(pName, 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(cacheable)) { + super.addIntHeader(pName, pValue); + } + cacheResponse.addHeader(pName, String.valueOf(pValue)); + } + + public final void setContentType(String type) { + setHeader(HTTPCache.HEADER_CONTENT_TYPE, type); + } } \ 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 946823dd..b043de45 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileSizeExceededException.java @@ -1,44 +1,43 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.fileupload; - -/** - * FileSizeExceededException - *

      - * - * @author Harald Kuhr - * @version $Id: FileSizeExceededException.java#1 $ - */ -public class FileSizeExceededException extends FileUploadException { - public FileSizeExceededException(Throwable pCause) { - super(pCause.getMessage(), pCause); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.fileupload; + +/** + * FileSizeExceededException + * + * @author Harald Kuhr + * @version $Id: FileSizeExceededException.java#1 $ + */ +public class FileSizeExceededException extends FileUploadException { + public FileSizeExceededException(Throwable pCause) { + super(pCause.getMessage(), 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 b44d802a..9167a7ad 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/fileupload/FileUploadException.java @@ -1,54 +1,53 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.fileupload; - -import javax.servlet.ServletException; - -/** - * FileUploadException - *

      - * - * @author Harald Kuhr - * @version $Id: FileUploadException.java#1 $ - */ -public class FileUploadException extends ServletException { - public FileUploadException(String pMessage) { - super(pMessage); - } - - public FileUploadException(String pMessage, Throwable pCause) { - super(pMessage, pCause); - } - - public FileUploadException(Throwable pCause) { - super(pCause.getMessage(), pCause); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.fileupload; + +import javax.servlet.ServletException; + +/** + * FileUploadException + * + * @author Harald Kuhr + * @version $Id: FileUploadException.java#1 $ + */ +public class FileUploadException extends ServletException { + public FileUploadException(String pMessage) { + super(pMessage); + } + + public FileUploadException(String pMessage, Throwable pCause) { + super(pMessage, pCause); + } + + public FileUploadException(Throwable pCause) { + super(pCause.getMessage(), pCause); + } +} 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 0b15f5b0..96a3b8f1 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPFilter.java @@ -1,143 +1,149 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.gzip; - -import com.twelvemonkeys.servlet.GenericFilter; - -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.HttpServletResponse; -import java.io.IOException; - -/** - * A filter to reduce the output size of web resources. - *

      - * The HTTP protocol supports compression of the content to reduce network - * bandwidth. The important headers involved, are the {@code Accept-Encoding} - * request header, and the {@code Content-Encoding} response header. - * This feature can be used to further reduce the number of bytes transferred - * over the network, at the cost of some extra processing time at both endpoints. - * Most modern browsers supports compression in GZIP format, which is fairly - * efficient in cost/compression ratio. - *

      - * The filter tests for the presence of an {@code Accept-Encoding} header with a - * value of {@code "gzip"} (several different encoding header values are - * possible in one header). If not present, the filter simply passes the - * request/response pair through, leaving it untouched. If present, the - * {@code Content-Encoding} header is set, with the value {@code "gzip"}, - * and the response is wrapped. - * The response output stream is wrapped in a - * {@link java.util.zip.GZIPOutputStream} which performs the GZIP encoding. - * For efficiency, the filter does not buffer the response, but writes through - * the gzipped output stream. - *

      - * Configuration
      - * To use {@code GZIPFilter} in your web-application, you simply need to add it - * to your web descriptor ({@code web.xml}). If using a servlet container that - * supports the Servlet 2.4 spec, the new {@code dispatcher} element should be - * used, and set to {@code REQUEST/FORWARD}, to make sure the filter is invoked - * only once for requests. - * If using an older web descriptor, set the {@code init-param} - * {@code "once-per-request"} to {@code "true"} (this will have the same effect, - * but might perform slightly worse than the 2.4 version). - * Please see the examples below. - * Servlet 2.4 version, filter section:
      - *

      - * <!-- GZIP Filter Configuration -->
      - * <filter>
      - *      <filter-name>gzip</filter-name>
      - *      <filter-class>com.twelvemonkeys.servlet.GZIPFilter</filter-class>
      - * </filter>
      - * 
      - * Filter-mapping section:
      - *
      - * <!-- GZIP Filter Mapping -->
      - * <filter-mapping>
      - *      <filter-name>gzip</filter-name>
      - *      <url-pattern>*.html</url-pattern>
      - *      <dispatcher>REQUEST</dispatcher>
      - *      <dispatcher>FORWARD</dispatcher>
      - * </filter-mapping>
      - * <filter-mapping>
      - *      <filter-name>gzip</filter-name>
      - *      <url-pattern>*.jsp< /url-pattern>
      - *      <dispatcher>REQUEST</dispatcher>
      - *      <dispatcher>FORWARD</dispatcher>
      - * </filter-mapping>
      - * 
      - *

      - * Based on ideas and code found in the ONJava article - * Two - * Servlet Filters Every Web Application Should Have - * by Jayson Falkner. - *

      - * - * @author Jayson Falkner - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: GZIPFilter.java#1 $ - */ -public class GZIPFilter extends GenericFilter { - - { - oncePerRequest = true; - } - - protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { - // Can only filter HTTP responses - if (pRequest instanceof HttpServletRequest) { - HttpServletRequest request = (HttpServletRequest) pRequest; - HttpServletResponse response = (HttpServletResponse) pResponse; - - // If GZIP is supported, use compression - String accept = request.getHeader("Accept-Encoding"); - if (accept != null && accept.contains("gzip")) { - //System.out.println("GZIP supported, compressing."); - GZIPResponseWrapper wrapped = new GZIPResponseWrapper(response); - - try { - pChain.doFilter(pRequest, wrapped); - } - finally { - wrapped.flushResponse(); - } - - return; - } - } - - // Else, continue chain - pChain.doFilter(pRequest, pResponse); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.gzip; + +import com.twelvemonkeys.servlet.GenericFilter; + +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.HttpServletResponse; +import java.io.IOException; + +/** + * A filter to reduce the output size of web resources. + *

      + * The HTTP protocol supports compression of the content to reduce network + * bandwidth. The important headers involved, are the {@code Accept-Encoding} + * request header, and the {@code Content-Encoding} response header. + * This feature can be used to further reduce the number of bytes transferred + * over the network, at the cost of some extra processing time at both endpoints. + * Most modern browsers supports compression in GZIP format, which is fairly + * efficient in cost/compression ratio. + *

      + *

      + * The filter tests for the presence of an {@code Accept-Encoding} header with a + * value of {@code "gzip"} (several different encoding header values are + * possible in one header). If not present, the filter simply passes the + * request/response pair through, leaving it untouched. If present, the + * {@code Content-Encoding} header is set, with the value {@code "gzip"}, + * and the response is wrapped. + * The response output stream is wrapped in a + * {@link java.util.zip.GZIPOutputStream} which performs the GZIP encoding. + * For efficiency, the filter does not buffer the response, but writes through + * the gzipped output stream. + *

      + *

      + * Configuration + *
      + * To use {@code GZIPFilter} in your web-application, you simply need to add it + * to your web descriptor ({@code web.xml}). If using a servlet container that + * supports the Servlet 2.4 spec, the new {@code dispatcher} element should be + * used, and set to {@code REQUEST/FORWARD}, to make sure the filter is invoked + * only once for requests. + * If using an older web descriptor, set the {@code init-param} + * {@code "once-per-request"} to {@code "true"} (this will have the same effect, + * but might perform slightly worse than the 2.4 version). + * Please see the examples below. + * Servlet 2.4 version, filter section: + *

      + *
      + *
      + * <!-- GZIP Filter Configuration -->
      + * <filter>
      + *      <filter-name>gzip</filter-name>
      + *      <filter-class>com.twelvemonkeys.servlet.GZIPFilter</filter-class>
      + * </filter>
      + * 
      + * Filter-mapping section: + *
      + *
      + * <!-- GZIP Filter Mapping -->
      + * <filter-mapping>
      + *      <filter-name>gzip</filter-name>
      + *      <url-pattern>*.html</url-pattern>
      + *      <dispatcher>REQUEST</dispatcher>
      + *      <dispatcher>FORWARD</dispatcher>
      + * </filter-mapping>
      + * <filter-mapping>
      + *      <filter-name>gzip</filter-name>
      + *      <url-pattern>*.jsp< /url-pattern>
      + *      <dispatcher>REQUEST</dispatcher>
      + *      <dispatcher>FORWARD</dispatcher>
      + * </filter-mapping>
      + * 
      + *

      + * Based on ideas and code found in the ONJava article + * Two + * Servlet Filters Every Web Application Should Have + * by Jayson Falkner. + *

      + * + * @author Jayson Falkner + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: GZIPFilter.java#1 $ + */ +public class GZIPFilter extends GenericFilter { + + { + oncePerRequest = true; + } + + protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { + // Can only filter HTTP responses + if (pRequest instanceof HttpServletRequest) { + HttpServletRequest request = (HttpServletRequest) pRequest; + HttpServletResponse response = (HttpServletResponse) pResponse; + + // If GZIP is supported, use compression + String accept = request.getHeader("Accept-Encoding"); + if (accept != null && accept.contains("gzip")) { + //System.out.println("GZIP supported, compressing."); + GZIPResponseWrapper wrapped = new GZIPResponseWrapper(response); + + try { + pChain.doFilter(pRequest, wrapped); + } + finally { + wrapped.flushResponse(); + } + + return; + } + } + + // 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 329aff33..bce73bdd 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/gzip/GZIPResponseWrapper.java @@ -1,149 +1,150 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.gzip; - -import com.twelvemonkeys.servlet.OutputStreamAdapter; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; -import java.util.zip.GZIPOutputStream; - -/** - * GZIPResponseWrapper class description. - *

      - * Based on ideas and code found in the ONJava article - * Two Servlet Filters Every Web Application Should Have - * by Jayson Falkner. - * - * @author Jayson Falkner - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: GZIPResponseWrapper.java#1 $ - */ -public class GZIPResponseWrapper extends HttpServletResponseWrapper { - // TODO: Remove/update ETags if needed? Read the spec (RFC 2616) on Vary/ETag for caching - - 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 (contentLength >= 0) { - gzipOut = new GZIPOutputStream(getResponse().getOutputStream(), contentLength); - } - else { - gzipOut = new GZIPOutputStream(getResponse().getOutputStream()); - } - - // Wrap in ServletOutputStream and return - return new OutputStreamAdapter(gzipOut); - } - - // TODO: Move this to flushbuffer or something? Hmmm.. - public void flushResponse() throws IOException { - try { - // Finish GZIP encodig - if (gzipOut != null) { - gzipOut.finish(); - } - - flushBuffer(); - } - finally { - // Close stream - if (writer != null) { - writer.close(); - } - else { - if (out != null) { - out.close(); - } - } - } - } - - public void flushBuffer() throws IOException { - if (writer != null) { - writer.flush(); - } - else if (out != null) { - out.flush(); - } - } - - public ServletOutputStream getOutputStream() throws IOException { - if (writer != null) { - throw new IllegalStateException("getWriter() has already been called!"); - } - - if (out == null) { - out = createOutputStream(); - } - - return out; - } - - public PrintWriter getWriter() throws IOException { - if (writer != null) { - return (writer); - } - - if (out != null) { - throw new IllegalStateException("getOutputStream() has already been called!"); - } - - 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. - contentLength = pLength; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.gzip; + +import com.twelvemonkeys.servlet.OutputStreamAdapter; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.util.zip.GZIPOutputStream; + +/** + * GZIPResponseWrapper class description. + *

      + * Based on ideas and code found in the ONJava article + * Two Servlet Filters Every Web Application Should Have + * by Jayson Falkner. + *

      + * + * @author Jayson Falkner + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: GZIPResponseWrapper.java#1 $ + */ +public class GZIPResponseWrapper extends HttpServletResponseWrapper { + // TODO: Remove/update ETags if needed? Read the spec (RFC 2616) on Vary/ETag for caching + + 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 (contentLength >= 0) { + gzipOut = new GZIPOutputStream(getResponse().getOutputStream(), contentLength); + } + else { + gzipOut = new GZIPOutputStream(getResponse().getOutputStream()); + } + + // Wrap in ServletOutputStream and return + return new OutputStreamAdapter(gzipOut); + } + + // TODO: Move this to flushbuffer or something? Hmmm.. + public void flushResponse() throws IOException { + try { + // Finish GZIP encodig + if (gzipOut != null) { + gzipOut.finish(); + } + + flushBuffer(); + } + finally { + // Close stream + if (writer != null) { + writer.close(); + } + else { + if (out != null) { + out.close(); + } + } + } + } + + public void flushBuffer() throws IOException { + if (writer != null) { + writer.flush(); + } + else if (out != null) { + out.flush(); + } + } + + public ServletOutputStream getOutputStream() throws IOException { + if (writer != null) { + throw new IllegalStateException("getWriter() has already been called!"); + } + + if (out == null) { + out = createOutputStream(); + } + + return out; + } + + public PrintWriter getWriter() throws IOException { + if (writer != null) { + return (writer); + } + + if (out != null) { + throw new IllegalStateException("getOutputStream() has already been called!"); + } + + 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. + contentLength = pLength; + } +} 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 0f7d67ec..dd169448 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ColorServlet.java @@ -1,213 +1,214 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.servlet.GenericServlet; - -import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import java.io.IOException; -import java.util.zip.CRC32; - -/** - * Creates a minimal 1 x 1 pixel PNG image, in a color specified by the - * {@code "color"} parameter. The color is HTML-style #RRGGBB, with two - * digits hex number for red, green and blue (the hash, '#', is optional). - *

      - * The class does only byte manipulation, there is no server-side image - * processing involving AWT ({@code Toolkit} class) of any kind. - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: ColorServlet.java#2 $ - */ -public class ColorServlet extends GenericServlet { - private final static String RGB_PARAME = "color"; - - // A minimal, one color indexed PNG - private final static byte[] PNG_IMG = new byte[]{ - (byte) 0x89, (byte) 'P', (byte) 'N', (byte) 'G', // PNG signature (8 bytes) - 0x0d, 0x0a, 0x1a, 0x0a, - - 0x00, 0x00, 0x00, 0x0d, // IHDR length (13) - (byte) 'I', (byte) 'H', (byte) 'D', (byte) 'R', // Image header - 0x00, 0x00, 0x00, 0x01, // width - 0x00, 0x00, 0x00, 0x01, // height - 0x01, 0x03, 0x00, 0x00, 0x00, // bits, color type, compression, filter, interlace - 0x25, (byte) 0xdb, 0x56, (byte) 0xca, // IHDR CRC - - 0x00, 0x00, 0x00, 0x03, // PLTE length (3) - (byte) 'P', (byte) 'L', (byte) 'T', (byte) 'E', // Palette - 0x00, 0x00, (byte) 0xff, // red, green, blue (updated by this servlet) - (byte) 0x8a, (byte) 0x78, (byte) 0xd2, 0x57, // PLTE CRC - - 0x00, 0x00, 0x00, 0x0a, // IDAT length (10) - (byte) 'I', (byte) 'D', (byte) 'A', (byte) 'T', // Image data - 0x78, (byte) 0xda, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, - (byte) 0xe5, 0x27, (byte) 0xde, (byte) 0xfc, // IDAT CRC - - - 0x00, 0x00, 0x00, 0x00, // IEND length (0) - (byte) 'I', (byte) 'E', (byte) 'N', (byte) 'D', // Image end - (byte) 0xae, (byte) 0x42, (byte) 0x60, (byte) 0x82 // IEND CRC - }; - - private final static int PLTE_CHUNK_START = 37; // after chunk length - private final static int PLTE_CHUNK_LENGTH = 7; // chunk name & data - - private final static int RED_IDX = 4; - private final static int GREEN_IDX = RED_IDX + 1; - private final static int BLUE_IDX = GREEN_IDX + 1; - - private final CRC32 crc = new CRC32(); - - /** - * Creates a ColorDroplet. - */ - public ColorServlet() { - super(); - } - - /** - * Renders the 1 x 1 single color PNG to the response. - * - * @see ColorServlet class description - * - * @param pRequest the request - * @param pResponse the response - * - * @throws IOException - * @throws ServletException - */ - public void service(ServletRequest pRequest, ServletResponse pResponse) throws IOException, ServletException { - int red = 0; - int green = 0; - int blue = 0; - - // Get color parameter and parse color - String rgb = pRequest.getParameter(RGB_PARAME); - if (rgb != null && rgb.length() >= 6 && rgb.length() <= 7) { - int index = 0; - - // If the hash ('#') character is included, skip it. - if (rgb.length() == 7) { - index++; - } - - try { - // Two digit hex for each color - String r = rgb.substring(index, index += 2); - red = Integer.parseInt(r, 0x10); - - String g = rgb.substring(index, index += 2); - green = Integer.parseInt(g, 0x10); - - String b = rgb.substring(index, index += 2); - blue = Integer.parseInt(b, 0x10); - } - catch (NumberFormatException nfe) { - log("Wrong color format for ColorDroplet: " + rgb + ". Must be RRGGBB."); - } - } - - // Set MIME type for PNG - pResponse.setContentType("image/png"); - ServletOutputStream out = pResponse.getOutputStream(); - - try { - // Write header (and palette chunk length) - out.write(PNG_IMG, 0, PLTE_CHUNK_START); - - // Create palette chunk, excl lenght, and write - byte[] palette = makePalette(red, green, blue); - out.write(palette); - - // Write image data until end - int pos = PLTE_CHUNK_START + PLTE_CHUNK_LENGTH + 4; - out.write(PNG_IMG, pos, PNG_IMG.length - pos); - } - finally { - out.flush(); - } - } - - /** - * Updates the CRC for a byte array. Note that the byte array must be at - * least {@code pOff + pLen + 4} bytes long, as the CRC is stored in the - * 4 last bytes. - * - * @param pBytes the bytes to create CRC for - * @param pOff the offset into the byte array to create CRC for - * @param pLen the length of the byte array to create CRC for - */ - private void updateCRC(byte[] pBytes, int pOff, int pLen) { - int value; - - synchronized (crc) { - crc.reset(); - crc.update(pBytes, pOff, pLen); - value = (int) crc.getValue(); - } - - pBytes[pOff + pLen ] = (byte) ((value >> 24) & 0xff); - pBytes[pOff + pLen + 1] = (byte) ((value >> 16) & 0xff); - pBytes[pOff + pLen + 2] = (byte) ((value >> 8) & 0xff); - pBytes[pOff + pLen + 3] = (byte) ( value & 0xff); - } - - /** - * Creates a PNG palette (PLTE) chunk with one color. - * The palette chunk data is always 3 bytes in length (one byte per color - * component). - * The returned byte array is then {@code 4 + 3 + 4 = 11} bytes, - * including chunk header, data and CRC. - * - * @param pRed the red component - * @param pGreen the reen component - * @param pBlue the blue component - * - * @return the bytes for the PLTE chunk, including CRC (but not length) - */ - private byte[] makePalette(int pRed, int pGreen, int pBlue) { - byte[] palette = new byte[PLTE_CHUNK_LENGTH + 4]; - System.arraycopy(PNG_IMG, PLTE_CHUNK_START, palette, 0, PLTE_CHUNK_LENGTH); - - palette[RED_IDX] = (byte) pRed; - palette[GREEN_IDX] = (byte) pGreen; - palette[BLUE_IDX] = (byte) pBlue; - - updateCRC(palette, 0, PLTE_CHUNK_LENGTH); - - return palette; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.servlet.GenericServlet; + +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.IOException; +import java.util.zip.CRC32; + +/** + * Creates a minimal 1 x 1 pixel PNG image, in a color specified by the + * {@code "color"} parameter. The color is HTML-style #RRGGBB, with two + * digits hex number for red, green and blue (the hash, '#', is optional). + *

      + * The class does only byte manipulation, there is no server-side image + * processing involving AWT ({@code Toolkit} class) of any kind. + *

      + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: ColorServlet.java#2 $ + */ +public class ColorServlet extends GenericServlet { + private final static String RGB_PARAME = "color"; + + // A minimal, one color indexed PNG + private final static byte[] PNG_IMG = new byte[]{ + (byte) 0x89, (byte) 'P', (byte) 'N', (byte) 'G', // PNG signature (8 bytes) + 0x0d, 0x0a, 0x1a, 0x0a, + + 0x00, 0x00, 0x00, 0x0d, // IHDR length (13) + (byte) 'I', (byte) 'H', (byte) 'D', (byte) 'R', // Image header + 0x00, 0x00, 0x00, 0x01, // width + 0x00, 0x00, 0x00, 0x01, // height + 0x01, 0x03, 0x00, 0x00, 0x00, // bits, color type, compression, filter, interlace + 0x25, (byte) 0xdb, 0x56, (byte) 0xca, // IHDR CRC + + 0x00, 0x00, 0x00, 0x03, // PLTE length (3) + (byte) 'P', (byte) 'L', (byte) 'T', (byte) 'E', // Palette + 0x00, 0x00, (byte) 0xff, // red, green, blue (updated by this servlet) + (byte) 0x8a, (byte) 0x78, (byte) 0xd2, 0x57, // PLTE CRC + + 0x00, 0x00, 0x00, 0x0a, // IDAT length (10) + (byte) 'I', (byte) 'D', (byte) 'A', (byte) 'T', // Image data + 0x78, (byte) 0xda, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, + (byte) 0xe5, 0x27, (byte) 0xde, (byte) 0xfc, // IDAT CRC + + + 0x00, 0x00, 0x00, 0x00, // IEND length (0) + (byte) 'I', (byte) 'E', (byte) 'N', (byte) 'D', // Image end + (byte) 0xae, (byte) 0x42, (byte) 0x60, (byte) 0x82 // IEND CRC + }; + + private final static int PLTE_CHUNK_START = 37; // after chunk length + private final static int PLTE_CHUNK_LENGTH = 7; // chunk name & data + + private final static int RED_IDX = 4; + private final static int GREEN_IDX = RED_IDX + 1; + private final static int BLUE_IDX = GREEN_IDX + 1; + + private final CRC32 crc = new CRC32(); + + /** + * Creates a ColorDroplet. + */ + public ColorServlet() { + super(); + } + + /** + * Renders the 1 x 1 single color PNG to the response. + * + * @see ColorServlet class description + * + * @param pRequest the request + * @param pResponse the response + * + * @throws IOException + * @throws ServletException + */ + public void service(ServletRequest pRequest, ServletResponse pResponse) throws IOException, ServletException { + int red = 0; + int green = 0; + int blue = 0; + + // Get color parameter and parse color + String rgb = pRequest.getParameter(RGB_PARAME); + if (rgb != null && rgb.length() >= 6 && rgb.length() <= 7) { + int index = 0; + + // If the hash ('#') character is included, skip it. + if (rgb.length() == 7) { + index++; + } + + try { + // Two digit hex for each color + String r = rgb.substring(index, index += 2); + red = Integer.parseInt(r, 0x10); + + String g = rgb.substring(index, index += 2); + green = Integer.parseInt(g, 0x10); + + String b = rgb.substring(index, index += 2); + blue = Integer.parseInt(b, 0x10); + } + catch (NumberFormatException nfe) { + log("Wrong color format for ColorDroplet: " + rgb + ". Must be RRGGBB."); + } + } + + // Set MIME type for PNG + pResponse.setContentType("image/png"); + ServletOutputStream out = pResponse.getOutputStream(); + + try { + // Write header (and palette chunk length) + out.write(PNG_IMG, 0, PLTE_CHUNK_START); + + // Create palette chunk, excl lenght, and write + byte[] palette = makePalette(red, green, blue); + out.write(palette); + + // Write image data until end + int pos = PLTE_CHUNK_START + PLTE_CHUNK_LENGTH + 4; + out.write(PNG_IMG, pos, PNG_IMG.length - pos); + } + finally { + out.flush(); + } + } + + /** + * Updates the CRC for a byte array. Note that the byte array must be at + * least {@code pOff + pLen + 4} bytes long, as the CRC is stored in the + * 4 last bytes. + * + * @param pBytes the bytes to create CRC for + * @param pOff the offset into the byte array to create CRC for + * @param pLen the length of the byte array to create CRC for + */ + private void updateCRC(byte[] pBytes, int pOff, int pLen) { + int value; + + synchronized (crc) { + crc.reset(); + crc.update(pBytes, pOff, pLen); + value = (int) crc.getValue(); + } + + pBytes[pOff + pLen ] = (byte) ((value >> 24) & 0xff); + pBytes[pOff + pLen + 1] = (byte) ((value >> 16) & 0xff); + pBytes[pOff + pLen + 2] = (byte) ((value >> 8) & 0xff); + pBytes[pOff + pLen + 3] = (byte) ( value & 0xff); + } + + /** + * Creates a PNG palette (PLTE) chunk with one color. + * The palette chunk data is always 3 bytes in length (one byte per color + * component). + * The returned byte array is then {@code 4 + 3 + 4 = 11} bytes, + * including chunk header, data and CRC. + * + * @param pRed the red component + * @param pGreen the reen component + * @param pBlue the blue component + * + * @return the bytes for the PLTE chunk, including CRC (but not length) + */ + private byte[] makePalette(int pRed, int pGreen, int pBlue) { + byte[] palette = new byte[PLTE_CHUNK_LENGTH + 4]; + System.arraycopy(PNG_IMG, PLTE_CHUNK_START, palette, 0, PLTE_CHUNK_LENGTH); + + palette[RED_IDX] = (byte) pRed; + palette[GREEN_IDX] = (byte) pGreen; + palette[BLUE_IDX] = (byte) pBlue; + + updateCRC(palette, 0, PLTE_CHUNK_LENGTH); + + return palette; + } +} 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 1dd725ad..30ce20aa 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ComposeFilter.java @@ -1,61 +1,60 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.servlet.ServletRequest; -import java.awt.image.BufferedImage; -import java.awt.image.RenderedImage; -import java.io.IOException; - -/** - * ComposeFilter - *

      - * - * @author Harald Kuhr - * @version $Id: ComposeFilter.java#1 $ - */ -public class ComposeFilter extends ImageFilter { - protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException { - // 1. Load different image, locally (using ServletContext.getResource) - // - Allow loading other filtered sources, or servlets? For example to - // apply filename or timestamps? - // - Allow applying text directly? Variables? - // 2. Apply transformations from config - // - Relative positioning - // - Relative scaling - // - Repeat (fill-pattern)? - // - Rotation? - // - Transparency? - // - Background or foreground (layers)? - // 3. Apply loaded image to original image (or vice versa?). - return pImage; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.servlet.ServletRequest; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.IOException; + +/** + * ComposeFilter + * + * @author Harald Kuhr + * @version $Id: ComposeFilter.java#1 $ + */ +public class ComposeFilter extends ImageFilter { + protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException { + // 1. Load different image, locally (using ServletContext.getResource) + // - Allow loading other filtered sources, or servlets? For example to + // apply filename or timestamps? + // - Allow applying text directly? Variables? + // 2. Apply transformations from config + // - Relative positioning + // - Relative scaling + // - Repeat (fill-pattern)? + // - Rotation? + // - Transparency? + // - Background or foreground (layers)? + // 3. Apply loaded image to original image (or vice versa?). + return pImage; + } +} 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 8ecf5435..c5cadff4 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ContentNegotiationFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ContentNegotiationFilter.java @@ -1,442 +1,441 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.image.ImageUtil; -import com.twelvemonkeys.lang.StringUtil; - -import javax.imageio.ImageIO; -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.HttpServletResponse; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.image.IndexColorModel; -import java.awt.image.RenderedImage; -import java.io.IOException; -import java.net.HttpURLConnection; -import java.util.HashMap; -import java.util.Map; - -/** - * This filter implements server side content negotiation and transcoding for - * images. - * - * @todo Add support for automatic recognition of known browsers, to avoid - * 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 { - - private final static String MIME_TYPE_IMAGE_PREFIX = "image/"; - private static final String MIME_TYPE_IMAGE_ANY = MIME_TYPE_IMAGE_PREFIX + "*"; - private static final String MIME_TYPE_ANY = "*/*"; - private static final String HTTP_HEADER_ACCEPT = "Accept"; - private static final String HTTP_HEADER_VARY = "Vary"; - protected static final String HTTP_HEADER_USER_AGENT = "User-Agent"; - - private static final String FORMAT_JPEG = "image/jpeg"; - private static final String FORMAT_WBMP = "image/wbmp"; - private static final String FORMAT_GIF = "image/gif"; - private static final String FORMAT_PNG = "image/png"; - - private final static String[] sKnownFormats = new String[] { - FORMAT_JPEG, FORMAT_PNG, FORMAT_GIF, FORMAT_WBMP - }; - private float[] knownFormatQuality = new float[] { - 1f, 1f, 0.99f, 0.5f - }; - - private HashMap formatQuality; // HashMap, as I need to clone this for each request - private final Object lock = new Object(); - - /* - private Pattern[] mKnownAgentPatterns; - private String[] mKnownAgentAccpets; - */ - { - // Hack: Make sure the filter don't trigger all the time - // See: super.trigger(ServletRequest) - triggerParams = new String[] {}; - } - - /* - public void setAcceptMappings(String pPropertiesFile) { - // NOTE: Supposed to be: - // = - // .accept= - - Properties mappings = new Properties(); - try { - mappings.load(getServletContext().getResourceAsStream(pPropertiesFile)); - - List patterns = new ArrayList(); - List accepts = new ArrayList(); - - for (Iterator iterator = mappings.keySet().iterator(); iterator.hasNext();) { - String agent = (String) iterator.next(); - if (agent.endsWith(".accept")) { - continue; - } - - try { - patterns.add(Pattern.compile((String) mappings.get(agent))); - - // TODO: Consider preparsing ACCEPT header?? - accepts.add(mappings.get(agent + ".accept")); - } - catch (PatternSyntaxException e) { - log("Could not parse User-Agent identification for " + agent, e); - } - - mKnownAgentPatterns = (Pattern[]) patterns.toArray(new Pattern[patterns.size()]); - mKnownAgentAccpets = (String[]) accepts.toArray(new String[accepts.size()]); - } - } - catch (IOException e) { - log("Could not read accetp-mappings properties file: " + pPropertiesFile, e); - } - } - */ - - protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { - // NOTE: super invokes trigger() and image specific doFilter() if needed - super.doFilterImpl(pRequest, pResponse, pChain); - - if (pResponse instanceof HttpServletResponse) { - // Update the Vary HTTP header field - ((HttpServletResponse) pResponse).addHeader(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT); - //((HttpServletResponse) pResponse).addHeader(HTTP_HEADER_VARY, HTTP_HEADER_USER_AGENT); - } - } - - /** - * Makes sure the filter triggers for unknown file formats. - * - * @param pRequest the request - * @return {@code true} if the filter should execute, {@code false} - * otherwise - */ - protected boolean trigger(ServletRequest pRequest) { - boolean trigger = false; - - if (pRequest instanceof HttpServletRequest) { - HttpServletRequest request = (HttpServletRequest) pRequest; - String accept = getAcceptedFormats(request); - String originalFormat = getServletContext().getMimeType(request.getRequestURI()); - - //System.out.println("Accept: " + accept); - //System.out.println("Original format: " + originalFormat); - - // Only override original format if it is not accpeted by the client - // Note: Only explicit matches are okay, */* or image/* is not. - if (!StringUtil.contains(accept, originalFormat)) { - trigger = true; - } - } - - // Call super, to allow content negotiation even though format is supported - return trigger || super.trigger(pRequest); - } - - private String getAcceptedFormats(HttpServletRequest pRequest) { - return pRequest.getHeader(HTTP_HEADER_ACCEPT); - } - - /* - private String getAcceptedFormats(HttpServletRequest pRequest) { - String accept = pRequest.getHeader(HTTP_HEADER_ACCEPT); - - // Check if User-Agent is in list of known agents - if (mKnownAgentPatterns != null) { - String agent = pRequest.getHeader(HTTP_HEADER_USER_AGENT); - for (int i = 0; i < mKnownAgentPatterns.length; i++) { - Pattern pattern = mKnownAgentPatterns[i]; - if (pattern.matcher(agent).matches()) { - // Merge known with real accpet, in case plugins add extra capabilities - accept = mergeAccept(mKnownAgentAccpets[i], accept); - System.out.println("--> User-Agent: " + agent + " accepts: " + accept); - return accept; - } - } - } - - System.out.println("No agent match, defaulting to Accept header: " + accept); - return accept; - } - - private String mergeAccept(String pKnown, String pAccept) { - // TODO: Make sure there are no duplicates... - return pKnown + ", " + pAccept; - } - */ - - protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException { - if (pRequest instanceof HttpServletRequest) { - HttpServletRequest request = (HttpServletRequest) pRequest; - - Map formatQuality = getFormatQualityMapping(); - - // TODO: Consider adding original format, and use as fallback in worst case? - // TODO: Original format should have some boost, to avoid unneccesary convertsion? - - // Update source quality settings from image properties - adjustQualityFromImage(formatQuality, pImage); - //System.out.println("Source quality mapping: " + formatQuality); - - adjustQualityFromAccept(formatQuality, request); - //System.out.println("Final media scores: " + formatQuality); - - // Find the formats with the highest quality factor, and use the first (predictable) - String acceptable = findBestFormat(formatQuality); - - //System.out.println("Acceptable: " + acceptable); - - // Send HTTP 406 Not Acceptable - if (acceptable == null) { - if (pResponse instanceof HttpServletResponse) { - ((HttpServletResponse) pResponse).sendError(HttpURLConnection.HTTP_NOT_ACCEPTABLE); - } - return null; - } - else { - // TODO: Only if the format was changed! - // Let other filters/caches/proxies know we changed the image - } - - // Set format - pResponse.setOutputContentType(acceptable); - //System.out.println("Set format: " + acceptable); - } - - return pImage; - } - - private Map getFormatQualityMapping() { - synchronized(lock) { - if (formatQuality == null) { - formatQuality = new HashMap(); - - // Use ImageIO to find formats we can actually write - String[] formats = ImageIO.getWriterMIMETypes(); - - // All known formats qs are initially 1.0 - // Others should be 0.1 or something like that... - for (String format : formats) { - formatQuality.put(format, getKnownFormatQuality(format)); - } - } - } - //noinspection unchecked - return (Map) formatQuality.clone(); - } - - /** - * Finds the best available format. - * - * @param pFormatQuality the format to quality mapping - * @return the mime type of the best available format - */ - private static String findBestFormat(Map pFormatQuality) { - String acceptable = null; - float acceptQuality = 0.0f; - for (Map.Entry entry : pFormatQuality.entrySet()) { - float qValue = entry.getValue(); - if (qValue > acceptQuality) { - acceptQuality = qValue; - acceptable = entry.getKey(); - } - } - - //System.out.println("Accepted format: " + acceptable); - //System.out.println("Accepted quality: " + acceptQuality); - return acceptable; - } - - /** - * Adjust quality from HTTP Accept header - * - * @param pFormatQuality the format to quality mapping - * @param pRequest the request - */ - private void adjustQualityFromAccept(Map pFormatQuality, HttpServletRequest pRequest) { - // Multiply all q factors with qs factors - // No q=.. should be interpreted as q=1.0 - - // Apache does some extras; if both explicit types and wildcards - // (without qaulity factor) are present, */* is interpreted as - // */*;q=0.01 and image/* is interpreted as image/*;q=0.02 - // See: http://httpd.apache.org/docs-2.0/content-negotiation.html - - String accept = getAcceptedFormats(pRequest); - //System.out.println("Accept: " + accept); - - float anyImageFactor = getQualityFactor(accept, MIME_TYPE_IMAGE_ANY); - anyImageFactor = (anyImageFactor == 1) ? 0.02f : anyImageFactor; - - float anyFactor = getQualityFactor(accept, MIME_TYPE_ANY); - anyFactor = (anyFactor == 1) ? 0.01f : anyFactor; - - for (String format : pFormatQuality.keySet()) { - //System.out.println("Trying format: " + format); - - String formatMIME = MIME_TYPE_IMAGE_PREFIX + format; - float qFactor = getQualityFactor(accept, formatMIME); - qFactor = (qFactor == 0f) ? Math.max(anyFactor, anyImageFactor) : qFactor; - adjustQuality(pFormatQuality, format, qFactor); - } - } - - /** - * - * @param pAccept the accpet header value - * @param pContentType the content type to get the quality factor for - * @return the q factor of the given format, according to the accept header - */ - private static float getQualityFactor(String pAccept, String pContentType) { - float qFactor = 0; - int foundIndex = pAccept.indexOf(pContentType); - if (foundIndex >= 0) { - int startQIndex = foundIndex + pContentType.length(); - if (startQIndex < pAccept.length() && pAccept.charAt(startQIndex) == ';') { - while (startQIndex < pAccept.length() && pAccept.charAt(startQIndex++) == ' ') { - // Skip over whitespace - } - - if (pAccept.charAt(startQIndex++) == 'q' && pAccept.charAt(startQIndex++) == '=') { - int endQIndex = pAccept.indexOf(',', startQIndex); - if (endQIndex < 0) { - endQIndex = pAccept.length(); - } - - try { - qFactor = Float.parseFloat(pAccept.substring(startQIndex, endQIndex)); - //System.out.println("Found qFactor " + qFactor); - } - catch (NumberFormatException e) { - // TODO: Determine what to do here.. Maybe use a very low value? - // Ahem.. The specs don't say anything about how to interpret a wrong q factor.. - //System.out.println("Unparseable q setting; " + e.getMessage()); - } - } - // TODO: Determine what to do here.. Maybe use a very low value? - // Unparseable q value, use 0 - } - else { - // Else, assume quality is 1.0 - qFactor = 1; - } - } - return qFactor; - } - - - /** - * Adjusts source quality settings from image properties. - * - * @param pFormatQuality the format to quality mapping - * @param pImage the image - */ - private static void adjustQualityFromImage(Map pFormatQuality, BufferedImage pImage) { - // NOTE: The values are all made-up. May need tuning. - - // If pImage.getColorModel() instanceof IndexColorModel - // JPEG qs*=0.6 - // If NOT binary or 2 color index - // WBMP qs*=0.5 - // Else - // GIF qs*=0.02 - // PNG qs*=0.9 // JPEG is smaller/faster - if (pImage.getColorModel() instanceof IndexColorModel) { - adjustQuality(pFormatQuality, FORMAT_JPEG, 0.6f); - - if (pImage.getType() != BufferedImage.TYPE_BYTE_BINARY || ((IndexColorModel) pImage.getColorModel()).getMapSize() != 2) { - adjustQuality(pFormatQuality, FORMAT_WBMP, 0.5f); - } - } - else { - adjustQuality(pFormatQuality, FORMAT_GIF, 0.01f); - adjustQuality(pFormatQuality, FORMAT_PNG, 0.99f); // JPEG is smaller/faster - } - - // If pImage.getColorModel().hasTransparentPixels() - // JPEG qs*=0.05 - // WBMP qs*=0.05 - // If NOT transparency == BITMASK - // GIF qs*=0.8 - if (ImageUtil.hasTransparentPixels(pImage, true)) { - adjustQuality(pFormatQuality, FORMAT_JPEG, 0.009f); - adjustQuality(pFormatQuality, FORMAT_WBMP, 0.009f); - - if (pImage.getColorModel().getTransparency() != Transparency.BITMASK) { - adjustQuality(pFormatQuality, FORMAT_GIF, 0.8f); - } - } - } - - /** - * Updates the quality in the map. - * - * @param pFormatQuality Map - * @param pFormat the format - * @param pFactor the quality factor - */ - private static void adjustQuality(Map pFormatQuality, String pFormat, float pFactor) { - Float oldValue = pFormatQuality.get(pFormat); - if (oldValue != null) { - pFormatQuality.put(pFormat, oldValue * pFactor); - //System.out.println("New vallue after multiplying with " + pFactor + " is " + pFormatQuality.get(pFormat)); - } - } - - - /** - * Gets the initial quality if this is a known format, otherwise 0.1 - * - * @param pFormat the format name - * @return the q factor of the given format - */ - private float getKnownFormatQuality(String pFormat) { - for (int i = 0; i < sKnownFormats.length; i++) { - if (pFormat.equals(sKnownFormats[i])) { - return knownFormatQuality[i]; - } - } - return 0.1f; - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.image.ImageUtil; +import com.twelvemonkeys.lang.StringUtil; + +import javax.imageio.ImageIO; +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.HttpServletResponse; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.HashMap; +import java.util.Map; + +/** + * This filter implements server side content negotiation and transcoding for + * images. + ** + * @author Harald Kuhr + * @version $Id: ContentNegotiationFilter.java#1 $ + */ +// TODO: Add support for automatic recognition of known browsers, to avoid +// 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). +public class ContentNegotiationFilter extends ImageFilter { + + private final static String MIME_TYPE_IMAGE_PREFIX = "image/"; + private static final String MIME_TYPE_IMAGE_ANY = MIME_TYPE_IMAGE_PREFIX + "*"; + private static final String MIME_TYPE_ANY = "*/*"; + private static final String HTTP_HEADER_ACCEPT = "Accept"; + private static final String HTTP_HEADER_VARY = "Vary"; + protected static final String HTTP_HEADER_USER_AGENT = "User-Agent"; + + private static final String FORMAT_JPEG = "image/jpeg"; + private static final String FORMAT_WBMP = "image/wbmp"; + private static final String FORMAT_GIF = "image/gif"; + private static final String FORMAT_PNG = "image/png"; + + private final static String[] sKnownFormats = new String[] { + FORMAT_JPEG, FORMAT_PNG, FORMAT_GIF, FORMAT_WBMP + }; + private float[] knownFormatQuality = new float[] { + 1f, 1f, 0.99f, 0.5f + }; + + private HashMap formatQuality; // HashMap, as I need to clone this for each request + private final Object lock = new Object(); + + /* + private Pattern[] mKnownAgentPatterns; + private String[] mKnownAgentAccpets; + */ + { + // Hack: Make sure the filter don't trigger all the time + // See: super.trigger(ServletRequest) + triggerParams = new String[] {}; + } + + /* + public void setAcceptMappings(String pPropertiesFile) { + // NOTE: Supposed to be: + // = + // .accept= + + Properties mappings = new Properties(); + try { + mappings.load(getServletContext().getResourceAsStream(pPropertiesFile)); + + List patterns = new ArrayList(); + List accepts = new ArrayList(); + + for (Iterator iterator = mappings.keySet().iterator(); iterator.hasNext();) { + String agent = (String) iterator.next(); + if (agent.endsWith(".accept")) { + continue; + } + + try { + patterns.add(Pattern.compile((String) mappings.get(agent))); + + // TODO: Consider preparsing ACCEPT header?? + accepts.add(mappings.get(agent + ".accept")); + } + catch (PatternSyntaxException e) { + log("Could not parse User-Agent identification for " + agent, e); + } + + mKnownAgentPatterns = (Pattern[]) patterns.toArray(new Pattern[patterns.size()]); + mKnownAgentAccpets = (String[]) accepts.toArray(new String[accepts.size()]); + } + } + catch (IOException e) { + log("Could not read accetp-mappings properties file: " + pPropertiesFile, e); + } + } + */ + + protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { + // NOTE: super invokes trigger() and image specific doFilter() if needed + super.doFilterImpl(pRequest, pResponse, pChain); + + if (pResponse instanceof HttpServletResponse) { + // Update the Vary HTTP header field + ((HttpServletResponse) pResponse).addHeader(HTTP_HEADER_VARY, HTTP_HEADER_ACCEPT); + //((HttpServletResponse) pResponse).addHeader(HTTP_HEADER_VARY, HTTP_HEADER_USER_AGENT); + } + } + + /** + * Makes sure the filter triggers for unknown file formats. + * + * @param pRequest the request + * @return {@code true} if the filter should execute, {@code false} + * otherwise + */ + protected boolean trigger(ServletRequest pRequest) { + boolean trigger = false; + + if (pRequest instanceof HttpServletRequest) { + HttpServletRequest request = (HttpServletRequest) pRequest; + String accept = getAcceptedFormats(request); + String originalFormat = getServletContext().getMimeType(request.getRequestURI()); + + //System.out.println("Accept: " + accept); + //System.out.println("Original format: " + originalFormat); + + // Only override original format if it is not accpeted by the client + // Note: Only explicit matches are okay, */* or image/* is not. + if (!StringUtil.contains(accept, originalFormat)) { + trigger = true; + } + } + + // Call super, to allow content negotiation even though format is supported + return trigger || super.trigger(pRequest); + } + + private String getAcceptedFormats(HttpServletRequest pRequest) { + return pRequest.getHeader(HTTP_HEADER_ACCEPT); + } + + /* + private String getAcceptedFormats(HttpServletRequest pRequest) { + String accept = pRequest.getHeader(HTTP_HEADER_ACCEPT); + + // Check if User-Agent is in list of known agents + if (mKnownAgentPatterns != null) { + String agent = pRequest.getHeader(HTTP_HEADER_USER_AGENT); + for (int i = 0; i < mKnownAgentPatterns.length; i++) { + Pattern pattern = mKnownAgentPatterns[i]; + if (pattern.matcher(agent).matches()) { + // Merge known with real accpet, in case plugins add extra capabilities + accept = mergeAccept(mKnownAgentAccpets[i], accept); + System.out.println("--> User-Agent: " + agent + " accepts: " + accept); + return accept; + } + } + } + + System.out.println("No agent match, defaulting to Accept header: " + accept); + return accept; + } + + private String mergeAccept(String pKnown, String pAccept) { + // TODO: Make sure there are no duplicates... + return pKnown + ", " + pAccept; + } + */ + + protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException { + if (pRequest instanceof HttpServletRequest) { + HttpServletRequest request = (HttpServletRequest) pRequest; + + Map formatQuality = getFormatQualityMapping(); + + // TODO: Consider adding original format, and use as fallback in worst case? + // TODO: Original format should have some boost, to avoid unneccesary convertsion? + + // Update source quality settings from image properties + adjustQualityFromImage(formatQuality, pImage); + //System.out.println("Source quality mapping: " + formatQuality); + + adjustQualityFromAccept(formatQuality, request); + //System.out.println("Final media scores: " + formatQuality); + + // Find the formats with the highest quality factor, and use the first (predictable) + String acceptable = findBestFormat(formatQuality); + + //System.out.println("Acceptable: " + acceptable); + + // Send HTTP 406 Not Acceptable + if (acceptable == null) { + if (pResponse instanceof HttpServletResponse) { + ((HttpServletResponse) pResponse).sendError(HttpURLConnection.HTTP_NOT_ACCEPTABLE); + } + return null; + } + else { + // TODO: Only if the format was changed! + // Let other filters/caches/proxies know we changed the image + } + + // Set format + pResponse.setOutputContentType(acceptable); + //System.out.println("Set format: " + acceptable); + } + + return pImage; + } + + private Map getFormatQualityMapping() { + synchronized(lock) { + if (formatQuality == null) { + formatQuality = new HashMap(); + + // Use ImageIO to find formats we can actually write + String[] formats = ImageIO.getWriterMIMETypes(); + + // All known formats qs are initially 1.0 + // Others should be 0.1 or something like that... + for (String format : formats) { + formatQuality.put(format, getKnownFormatQuality(format)); + } + } + } + //noinspection unchecked + return (Map) formatQuality.clone(); + } + + /** + * Finds the best available format. + * + * @param pFormatQuality the format to quality mapping + * @return the mime type of the best available format + */ + private static String findBestFormat(Map pFormatQuality) { + String acceptable = null; + float acceptQuality = 0.0f; + for (Map.Entry entry : pFormatQuality.entrySet()) { + float qValue = entry.getValue(); + if (qValue > acceptQuality) { + acceptQuality = qValue; + acceptable = entry.getKey(); + } + } + + //System.out.println("Accepted format: " + acceptable); + //System.out.println("Accepted quality: " + acceptQuality); + return acceptable; + } + + /** + * Adjust quality from HTTP Accept header + * + * @param pFormatQuality the format to quality mapping + * @param pRequest the request + */ + private void adjustQualityFromAccept(Map pFormatQuality, HttpServletRequest pRequest) { + // Multiply all q factors with qs factors + // No q=.. should be interpreted as q=1.0 + + // Apache does some extras; if both explicit types and wildcards + // (without qaulity factor) are present, */* is interpreted as + // */*;q=0.01 and image/* is interpreted as image/*;q=0.02 + // See: http://httpd.apache.org/docs-2.0/content-negotiation.html + + String accept = getAcceptedFormats(pRequest); + //System.out.println("Accept: " + accept); + + float anyImageFactor = getQualityFactor(accept, MIME_TYPE_IMAGE_ANY); + anyImageFactor = (anyImageFactor == 1) ? 0.02f : anyImageFactor; + + float anyFactor = getQualityFactor(accept, MIME_TYPE_ANY); + anyFactor = (anyFactor == 1) ? 0.01f : anyFactor; + + for (String format : pFormatQuality.keySet()) { + //System.out.println("Trying format: " + format); + + String formatMIME = MIME_TYPE_IMAGE_PREFIX + format; + float qFactor = getQualityFactor(accept, formatMIME); + qFactor = (qFactor == 0f) ? Math.max(anyFactor, anyImageFactor) : qFactor; + adjustQuality(pFormatQuality, format, qFactor); + } + } + + /** + * + * @param pAccept the accpet header value + * @param pContentType the content type to get the quality factor for + * @return the q factor of the given format, according to the accept header + */ + private static float getQualityFactor(String pAccept, String pContentType) { + float qFactor = 0; + int foundIndex = pAccept.indexOf(pContentType); + if (foundIndex >= 0) { + int startQIndex = foundIndex + pContentType.length(); + if (startQIndex < pAccept.length() && pAccept.charAt(startQIndex) == ';') { + while (startQIndex < pAccept.length() && pAccept.charAt(startQIndex++) == ' ') { + // Skip over whitespace + } + + if (pAccept.charAt(startQIndex++) == 'q' && pAccept.charAt(startQIndex++) == '=') { + int endQIndex = pAccept.indexOf(',', startQIndex); + if (endQIndex < 0) { + endQIndex = pAccept.length(); + } + + try { + qFactor = Float.parseFloat(pAccept.substring(startQIndex, endQIndex)); + //System.out.println("Found qFactor " + qFactor); + } + catch (NumberFormatException e) { + // TODO: Determine what to do here.. Maybe use a very low value? + // Ahem.. The specs don't say anything about how to interpret a wrong q factor.. + //System.out.println("Unparseable q setting; " + e.getMessage()); + } + } + // TODO: Determine what to do here.. Maybe use a very low value? + // Unparseable q value, use 0 + } + else { + // Else, assume quality is 1.0 + qFactor = 1; + } + } + return qFactor; + } + + + /** + * Adjusts source quality settings from image properties. + * + * @param pFormatQuality the format to quality mapping + * @param pImage the image + */ + private static void adjustQualityFromImage(Map pFormatQuality, BufferedImage pImage) { + // NOTE: The values are all made-up. May need tuning. + + // If pImage.getColorModel() instanceof IndexColorModel + // JPEG qs*=0.6 + // If NOT binary or 2 color index + // WBMP qs*=0.5 + // Else + // GIF qs*=0.02 + // PNG qs*=0.9 // JPEG is smaller/faster + if (pImage.getColorModel() instanceof IndexColorModel) { + adjustQuality(pFormatQuality, FORMAT_JPEG, 0.6f); + + if (pImage.getType() != BufferedImage.TYPE_BYTE_BINARY || ((IndexColorModel) pImage.getColorModel()).getMapSize() != 2) { + adjustQuality(pFormatQuality, FORMAT_WBMP, 0.5f); + } + } + else { + adjustQuality(pFormatQuality, FORMAT_GIF, 0.01f); + adjustQuality(pFormatQuality, FORMAT_PNG, 0.99f); // JPEG is smaller/faster + } + + // If pImage.getColorModel().hasTransparentPixels() + // JPEG qs*=0.05 + // WBMP qs*=0.05 + // If NOT transparency == BITMASK + // GIF qs*=0.8 + if (ImageUtil.hasTransparentPixels(pImage, true)) { + adjustQuality(pFormatQuality, FORMAT_JPEG, 0.009f); + adjustQuality(pFormatQuality, FORMAT_WBMP, 0.009f); + + if (pImage.getColorModel().getTransparency() != Transparency.BITMASK) { + adjustQuality(pFormatQuality, FORMAT_GIF, 0.8f); + } + } + } + + /** + * Updates the quality in the map. + * + * @param pFormatQuality Map + * @param pFormat the format + * @param pFactor the quality factor + */ + private static void adjustQuality(Map pFormatQuality, String pFormat, float pFactor) { + Float oldValue = pFormatQuality.get(pFormat); + if (oldValue != null) { + pFormatQuality.put(pFormat, oldValue * pFactor); + //System.out.println("New vallue after multiplying with " + pFactor + " is " + pFormatQuality.get(pFormat)); + } + } + + + /** + * Gets the initial quality if this is a known format, otherwise 0.1 + * + * @param pFormat the format name + * @return the q factor of the given format + */ + private float getKnownFormatQuality(String pFormat) { + for (int i = 0; i < sKnownFormats.length; i++) { + if (pFormat.equals(sKnownFormats[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 a1cf761f..d9936ef7 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/CropFilter.java @@ -1,234 +1,235 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.servlet.ServletUtil; - -import javax.servlet.ServletRequest; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.image.RenderedImage; - -/** - * This Servlet is able to render a cropped part of an image. - * - *


      - * - * Parameters:
      - *

      - *
      {@code cropX}
      - *
      integer, the new left edge of the image. - *
      {@code cropY}
      - *
      integer, the new top of the image. - *
      {@code cropWidth}
      - *
      integer, the new width of the image. - *
      {@code cropHeight}
      - *
      integer, the new height of the image. - *
      {@code cropUniform}
      - *
      boolean, wether or not uniform scalnig should be used. Default is - * {@code true}. - *
      {@code cropUnits}
      - *
      string, one of {@code PIXELS}, {@code PERCENT}. - * {@code PIXELS} is default. - * - * - * - *
      {@code image}
      - *
      string, the URL of the image to scale. - * - *
      {@code scaleX}
      - *
      integer, the new width of the image. - * - *
      {@code scaleY}
      - *
      integer, the new height of the image. - * - *
      {@code scaleUniform}
      - *
      boolean, wether or not uniform scalnig should be used. Default is - * {@code true}. - * - *
      {@code scaleUnits}
      - *
      string, one of {@code PIXELS}, {@code PERCENT}. - * {@code PIXELS} is default. - * - *
      {@code scaleQuality}
      - *
      string, one of {@code SCALE_SMOOTH}, {@code SCALE_FAST}, - * {@code SCALE_REPLICATE}, {@code SCALE_AREA_AVERAGING}. - * {@code SCALE_DEFAULT} is default. - * - *
      - * - * @example - * <IMG src="/crop/test.jpg?image=http://www.iconmedialab.com/images/random/home_image_12.jpg&cropWidth=500&cropUniform=true"> - * - * @example - * <IMG src="/crop/test.png?cache=false&image=http://www.iconmedialab.com/images/random/home_image_12.jpg&cropWidth=50&cropUnits=PERCENT"> - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: CropFilter.java#1 $ - */ -public class CropFilter extends ScaleFilter { - /** {@code cropX}*/ - protected final static String PARAM_CROP_X = "cropX"; - /** {@code cropY}*/ - protected final static String PARAM_CROP_Y = "cropY"; - /** {@code cropWidth}*/ - protected final static String PARAM_CROP_WIDTH = "cropWidth"; - /** {@code cropHeight}*/ - protected final static String PARAM_CROP_HEIGHT = "cropHeight"; - /** {@code cropUniform}*/ - protected final static String PARAM_CROP_UNIFORM = "cropUniform"; - /** {@code cropUnits}*/ - protected final static String PARAM_CROP_UNITS = "cropUnits"; - - /** - * Reads the image from the requested URL, scales it, crops it, and returns - * it in the - * Servlet stream. See above for details on parameters. - */ - protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { - // Get crop coordinates - int x = ServletUtil.getIntParameter(pRequest, PARAM_CROP_X, -1); - int y = ServletUtil.getIntParameter(pRequest, PARAM_CROP_Y, -1); - int width = ServletUtil.getIntParameter(pRequest, PARAM_CROP_WIDTH, -1); - int height = ServletUtil.getIntParameter(pRequest, PARAM_CROP_HEIGHT, -1); - - boolean uniform = - ServletUtil.getBooleanParameter(pRequest, PARAM_CROP_UNIFORM, false); - - int units = getUnits(ServletUtil.getParameter(pRequest, PARAM_CROP_UNITS, null)); - - // Get crop bounds - Rectangle bounds = - getBounds(x, y, width, height, units, uniform, pImage); - - // Return cropped version - return pImage.getSubimage((int) bounds.getX(), (int) bounds.getY(), - (int) bounds.getWidth(), - (int) bounds.getHeight()); - //return scaled.getSubimage(x, y, width, height); - } - - protected Rectangle getBounds(int pX, int pY, int pWidth, int pHeight, - int pUnits, boolean pUniform, - BufferedImage pImg) { - // Algoritm: - // Try to get x and y (default 0,0). - // Try to get width and height (default width-x, height-y) - // - // If percent, get ratio - // - // If uniform - // - - int oldWidth = pImg.getWidth(); - int oldHeight = pImg.getHeight(); - float ratio; - - if (pUnits == UNITS_PERCENT) { - if (pWidth >= 0 && pHeight >= 0) { - // Non-uniform - pWidth = (int) ((float) oldWidth * (float) pWidth / 100f); - pHeight = (int) ((float) oldHeight * (float) pHeight / 100f); - } - else if (pWidth >= 0) { - // Find ratio from pWidth - ratio = (float) pWidth / 100f; - pWidth = (int) ((float) oldWidth * ratio); - pHeight = (int) ((float) oldHeight * ratio); - - } - else if (pHeight >= 0) { - // Find ratio from pHeight - ratio = (float) pHeight / 100f; - pWidth = (int) ((float) oldWidth * ratio); - pHeight = (int) ((float) oldHeight * ratio); - } - // Else: No crop - } - //else if (UNITS_PIXELS.equalsIgnoreCase(pUnits)) { - else if (pUnits == UNITS_PIXELS) { - // Uniform - if (pUniform) { - if (pWidth >= 0 && pHeight >= 0) { - // Compute both ratios - ratio = (float) pWidth / (float) oldWidth; - float heightRatio = (float) pHeight / (float) oldHeight; - - // Find the largest ratio, and use that for both - if (heightRatio < ratio) { - ratio = heightRatio; - pWidth = (int) ((float) oldWidth * ratio); - } - else { - pHeight = (int) ((float) oldHeight * ratio); - } - - } - else if (pWidth >= 0) { - // Find ratio from pWidth - ratio = (float) pWidth / (float) oldWidth; - pHeight = (int) ((float) oldHeight * ratio); - } - else if (pHeight >= 0) { - // Find ratio from pHeight - ratio = (float) pHeight / (float) oldHeight; - pWidth = (int) ((float) oldWidth * ratio); - } - // Else: No crop - } - } - // Else: No crop - - // Not specified, or outside bounds: Use original dimensions - if (pWidth < 0 || (pX < 0 && pWidth > oldWidth) - || (pX >= 0 && (pX + pWidth) > oldWidth)) { - pWidth = (pX >= 0 ? oldWidth - pX : oldWidth); - } - if (pHeight < 0 || (pY < 0 && pHeight > oldHeight) - || (pY >= 0 && (pY + pHeight) > oldHeight)) { - pHeight = (pY >= 0 ? oldHeight - pY : oldHeight); - } - - // Center - if (pX < 0) { - pX = (pImg.getWidth() - pWidth) / 2; - } - if (pY < 0) { - pY = (pImg.getHeight() - pHeight) / 2; - } - - //System.out.println("x: " + pX + " y: " + pY - // + " w: " + pWidth + " h " + pHeight); - - return new Rectangle(pX, pY, pWidth, pHeight); - } +/* + * 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 of the copyright holder 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 HOLDER 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.servlet.ServletUtil; + +import javax.servlet.ServletRequest; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; + +/** + * This Servlet is able to render a cropped part of an image. + * + *
      + * + * Parameters:
      + *
      + *
      {@code cropX}
      + *
      integer, the new left edge of the image. + *
      {@code cropY}
      + *
      integer, the new top of the image. + *
      {@code cropWidth}
      + *
      integer, the new width of the image. + *
      {@code cropHeight}
      + *
      integer, the new height of the image. + *
      {@code cropUniform}
      + *
      boolean, wether or not uniform scalnig should be used. Default is + * {@code true}. + *
      {@code cropUnits}
      + *
      string, one of {@code PIXELS}, {@code PERCENT}. + * {@code PIXELS} is default. + * + * + * + *
      {@code image}
      + *
      string, the URL of the image to scale. + * + *
      {@code scaleX}
      + *
      integer, the new width of the image. + * + *
      {@code scaleY}
      + *
      integer, the new height of the image. + * + *
      {@code scaleUniform}
      + *
      boolean, wether or not uniform scalnig should be used. Default is + * {@code true}. + * + *
      {@code scaleUnits}
      + *
      string, one of {@code PIXELS}, {@code PERCENT}. + * {@code PIXELS} is default. + * + *
      {@code scaleQuality}
      + *
      string, one of {@code SCALE_SMOOTH}, {@code SCALE_FAST}, + * {@code SCALE_REPLICATE}, {@code SCALE_AREA_AVERAGING}. + * {@code SCALE_DEFAULT} is default. + * + *
      + *

      + * Examples: + *
      + * <IMG src="/crop/test.jpg?image=http://www.iconmedialab.com/images/random/home_image_12.jpg&cropWidth=500&cropUniform=true"> + *
      + * <IMG src="/crop/test.png?cache=false&image=http://www.iconmedialab.com/images/random/home_image_12.jpg&cropWidth=50&cropUnits=PERCENT"> + *

      + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: CropFilter.java#1 $ + */ +public class CropFilter extends ScaleFilter { + /** {@code cropX}*/ + protected final static String PARAM_CROP_X = "cropX"; + /** {@code cropY}*/ + protected final static String PARAM_CROP_Y = "cropY"; + /** {@code cropWidth}*/ + protected final static String PARAM_CROP_WIDTH = "cropWidth"; + /** {@code cropHeight}*/ + protected final static String PARAM_CROP_HEIGHT = "cropHeight"; + /** {@code cropUniform}*/ + protected final static String PARAM_CROP_UNIFORM = "cropUniform"; + /** {@code cropUnits}*/ + protected final static String PARAM_CROP_UNITS = "cropUnits"; + + /** + * Reads the image from the requested URL, scales it, crops it, and returns + * it in the + * Servlet stream. See above for details on parameters. + */ + protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { + // Get crop coordinates + int x = ServletUtil.getIntParameter(pRequest, PARAM_CROP_X, -1); + int y = ServletUtil.getIntParameter(pRequest, PARAM_CROP_Y, -1); + int width = ServletUtil.getIntParameter(pRequest, PARAM_CROP_WIDTH, -1); + int height = ServletUtil.getIntParameter(pRequest, PARAM_CROP_HEIGHT, -1); + + boolean uniform = + ServletUtil.getBooleanParameter(pRequest, PARAM_CROP_UNIFORM, false); + + int units = getUnits(ServletUtil.getParameter(pRequest, PARAM_CROP_UNITS, null)); + + // Get crop bounds + Rectangle bounds = + getBounds(x, y, width, height, units, uniform, pImage); + + // Return cropped version + return pImage.getSubimage((int) bounds.getX(), (int) bounds.getY(), + (int) bounds.getWidth(), + (int) bounds.getHeight()); + //return scaled.getSubimage(x, y, width, height); + } + + protected Rectangle getBounds(int pX, int pY, int pWidth, int pHeight, + int pUnits, boolean pUniform, + BufferedImage pImg) { + // Algoritm: + // Try to get x and y (default 0,0). + // Try to get width and height (default width-x, height-y) + // + // If percent, get ratio + // + // If uniform + // + + int oldWidth = pImg.getWidth(); + int oldHeight = pImg.getHeight(); + float ratio; + + if (pUnits == UNITS_PERCENT) { + if (pWidth >= 0 && pHeight >= 0) { + // Non-uniform + pWidth = (int) ((float) oldWidth * (float) pWidth / 100f); + pHeight = (int) ((float) oldHeight * (float) pHeight / 100f); + } + else if (pWidth >= 0) { + // Find ratio from pWidth + ratio = (float) pWidth / 100f; + pWidth = (int) ((float) oldWidth * ratio); + pHeight = (int) ((float) oldHeight * ratio); + + } + else if (pHeight >= 0) { + // Find ratio from pHeight + ratio = (float) pHeight / 100f; + pWidth = (int) ((float) oldWidth * ratio); + pHeight = (int) ((float) oldHeight * ratio); + } + // Else: No crop + } + //else if (UNITS_PIXELS.equalsIgnoreCase(pUnits)) { + else if (pUnits == UNITS_PIXELS) { + // Uniform + if (pUniform) { + if (pWidth >= 0 && pHeight >= 0) { + // Compute both ratios + ratio = (float) pWidth / (float) oldWidth; + float heightRatio = (float) pHeight / (float) oldHeight; + + // Find the largest ratio, and use that for both + if (heightRatio < ratio) { + ratio = heightRatio; + pWidth = (int) ((float) oldWidth * ratio); + } + else { + pHeight = (int) ((float) oldHeight * ratio); + } + + } + else if (pWidth >= 0) { + // Find ratio from pWidth + ratio = (float) pWidth / (float) oldWidth; + pHeight = (int) ((float) oldHeight * ratio); + } + else if (pHeight >= 0) { + // Find ratio from pHeight + ratio = (float) pHeight / (float) oldHeight; + pWidth = (int) ((float) oldWidth * ratio); + } + // Else: No crop + } + } + // Else: No crop + + // Not specified, or outside bounds: Use original dimensions + if (pWidth < 0 || (pX < 0 && pWidth > oldWidth) + || (pX >= 0 && (pX + pWidth) > oldWidth)) { + pWidth = (pX >= 0 ? oldWidth - pX : oldWidth); + } + if (pHeight < 0 || (pY < 0 && pHeight > oldHeight) + || (pY >= 0 && (pY + pHeight) > oldHeight)) { + pHeight = (pY >= 0 ? oldHeight - pY : oldHeight); + } + + // Center + if (pX < 0) { + pX = (pImg.getWidth() - pWidth) / 2; + } + if (pY < 0) { + pY = (pImg.getHeight() - pHeight) / 2; + } + + //System.out.println("x: " + pX + " y: " + pY + // + " w: " + pWidth + " h " + pHeight); + + return new Rectangle(pX, pY, pWidth, pHeight); + } } \ No newline at end of file diff --git a/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java b/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java index b5e5054a..83d887f6 100644 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/IIOProviderContextListener.java @@ -41,11 +41,12 @@ import java.util.List; /** * 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$ 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 d50da14e..1374910c 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageFilter.java @@ -1,219 +1,219 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.image.ImageUtil; -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; - -/** - * Abstract base class for image filters. Automatically decoding and encoding of - * the image is handled in the {@code doFilterImpl} method. - * - * @see #doFilter(java.awt.image.BufferedImage,javax.servlet.ServletRequest,ImageServletResponse) - * - * @author Harald Kuhr - * @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[] 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 #oncePerRequest} member variable. - * - * @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 - * @param pChain the filter chain - * - * @throws IOException - * @throws ServletException - */ - protected void doFilterImpl(final ServletRequest pRequest, final ServletResponse pResponse, final FilterChain pChain) - throws IOException, ServletException { - - //System.out.println("Starting filtering..."); - // Test for trigger params - if (!trigger(pRequest)) { - //System.out.println("Passing request on to next in chain (skipping " + getFilterName() + ")..."); - // Pass the request on - 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 = createImageServletResponse(pRequest, pResponse); - - //System.out.println("Passing request on to next in chain..."); - // Pass the request on - pChain.doFilter(pRequest, imageResponse); - - //System.out.println("Post filtering..."); - - // Get image - //System.out.println("Getting image from ImageServletResponse..."); - // Get the image from the wrapped response - RenderedImage image = imageResponse.getImage(); - //System.out.println("Got image: " + image); - - // Note: Image will be null if this is a HEAD request, the - // If-Modified-Since header is present, or similar. - if (image != null) { - // Do the image filtering - //System.out.println("Filtering image (" + getFilterName() + ")..."); - image = doFilter(ImageUtil.toBuffered(image), pRequest, imageResponse); - //System.out.println("Done filtering."); - - //System.out.println("Making image available..."); - // 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 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 #triggerParams} to test if: - *

      - *
      {@code mTriggerParams == null}
      - *
      {@code return true}
      - *
      {@code mTriggerParams != null}, loop through parameters, and test - * if {@code pRequest} contains the parameter. If match
      - *
      {@code return true}
      - *
      Otherwise
      - *
      {@code return false}
      - *
      - * - * - * @param pRequest the servlet request - * @return {@code true} if the filter should do image filtering - */ - protected boolean trigger(final ServletRequest pRequest) { - // If triggerParams not set, assume always trigger - if (triggerParams == null) { - return true; - } - - // Trigger only for certain request parameters - for (String triggerParam : triggerParams) { - if (pRequest.getParameter(triggerParam) != null) { - return true; - } - } - - // Didn't trigger - return false; - } - - /** - * Sets the trigger parameters. - * The parameter is supposed to be a comma-separated string of parameter - * names. - * - * @param pTriggerParams a comma-separated string of parameter names. - */ - // 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); - } - - /** - * Filters the image for this request. - * - * @param pImage the image to filter - * @param pRequest the servlet request - * @param pResponse the servlet response - * - * @return the filtered image - * @throws java.io.IOException if an I/O error occurs during filtering - */ - protected abstract RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException; -} +/* + * 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 of the copyright holder 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 HOLDER 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.image.ImageUtil; +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; + +/** + * Abstract base class for image filters. Automatically decoding and encoding of + * the image is handled in the {@code doFilterImpl} method. + * + * @see #doFilter(java.awt.image.BufferedImage,javax.servlet.ServletRequest,ImageServletResponse) + * + * @author Harald Kuhr + * @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[] 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 #oncePerRequest} member variable. + * + * @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 + * @param pChain the filter chain + * + * @throws IOException + * @throws ServletException + */ + protected void doFilterImpl(final ServletRequest pRequest, final ServletResponse pResponse, final FilterChain pChain) + throws IOException, ServletException { + + //System.out.println("Starting filtering..."); + // Test for trigger params + if (!trigger(pRequest)) { + //System.out.println("Passing request on to next in chain (skipping " + getFilterName() + ")..."); + // Pass the request on + 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 = createImageServletResponse(pRequest, pResponse); + + //System.out.println("Passing request on to next in chain..."); + // Pass the request on + pChain.doFilter(pRequest, imageResponse); + + //System.out.println("Post filtering..."); + + // Get image + //System.out.println("Getting image from ImageServletResponse..."); + // Get the image from the wrapped response + RenderedImage image = imageResponse.getImage(); + //System.out.println("Got image: " + image); + + // Note: Image will be null if this is a HEAD request, the + // If-Modified-Since header is present, or similar. + if (image != null) { + // Do the image filtering + //System.out.println("Filtering image (" + getFilterName() + ")..."); + image = doFilter(ImageUtil.toBuffered(image), pRequest, imageResponse); + //System.out.println("Done filtering."); + + //System.out.println("Making image available..."); + // 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 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 #triggerParams} to test if: + *

      + *
      + *
      {@code mTriggerParams == null}
      + *
      {@code return true}
      + *
      {@code mTriggerParams != null}, loop through parameters, and test + * if {@code pRequest} contains the parameter. If match
      + *
      {@code return true}
      + *
      Otherwise
      + *
      {@code return false}
      + *
      + * + * @param pRequest the servlet request + * @return {@code true} if the filter should do image filtering + */ + protected boolean trigger(final ServletRequest pRequest) { + // If triggerParams not set, assume always trigger + if (triggerParams == null) { + return true; + } + + // Trigger only for certain request parameters + for (String triggerParam : triggerParams) { + if (pRequest.getParameter(triggerParam) != null) { + return true; + } + } + + // Didn't trigger + return false; + } + + /** + * Sets the trigger parameters. + * The parameter is supposed to be a comma-separated string of parameter + * names. + * + * @param pTriggerParams a comma-separated string of parameter names. + */ + // 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); + } + + /** + * Filters the image for this request. + * + * @param pImage the image to filter + * @param pRequest the servlet request + * @param pResponse the servlet response + * + * @return the filtered image + * @throws java.io.IOException if an I/O error occurs during filtering + */ + protected abstract RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) throws IOException; +} 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 d2d4c067..150de214 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ImageServletResponse.java @@ -1,195 +1,209 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.servlet.ServletResponse; -import java.awt.image.BufferedImage; -import java.awt.image.RenderedImage; -import java.io.IOException; - -/** - * ImageServletResponse. - *

      - * The request attributes regarding image size and source region (AOI) are used - * in the decoding process, and must be set before the first invocation of - * {@link #getImage()} to have any effect. - * - * @author Harald Kuhr - * @version $Id: ImageServletResponse.java#4 $ - */ -public interface ImageServletResponse extends ServletResponse { - /** - * Request attribute of type {@link java.awt.Dimension} controlling image - * size. - * If either {@code width} or {@code height} is negative, the size is - * computed, using uniform scaling. - * Else, if {@code SIZE_UNIFORM} is {@code true}, the size will be - * computed to the largest possible area (with correct aspect ratio) - * fitting inside the target area. - * Otherwise, the image is scaled to the given size, with no regard to - * aspect ratio. - *

      - * Defaults to {@code null} (original image size). - */ - String ATTRIB_SIZE = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE"; - - /** - * Request attribute of type {@link Boolean} controlling image sizing. - *

      - * Defaults to {@code Boolean.TRUE}. - */ - String ATTRIB_SIZE_UNIFORM = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE_UNIFORM"; - - /** - * Request attribute of type {@link Boolean} controlling image sizing. - *

      - * Defaults to {@code Boolean.FALSE}. - */ - String ATTRIB_SIZE_PERCENT = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE_PERCENT"; - - /** - * Request attribute of type {@link java.awt.Rectangle} controlling image - * source region (area of interest). - *

      - * Defaults to {@code null} (the entire image). - */ - String ATTRIB_AOI = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI"; - - /** - * Request attribute of type {@link Boolean} controlling image AOI. - *

      - * Defaults to {@code Boolean.FALSE}. - */ - String ATTRIB_AOI_UNIFORM = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI_UNIFORM"; - - /** - * Request attribute of type {@link Boolean} controlling image AOI. - *

      - * Defaults to {@code Boolean.FALSE}. - */ - String ATTRIB_AOI_PERCENT = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI_PERCENT"; - - /** - * Request attribute of type {@link java.awt.Color} controlling background - * color for any transparent/translucent areas of the image. - *

      - * Defaults to {@code null} (keeps the transparent areas transparent). - */ - String ATTRIB_BG_COLOR = "com.twelvemonkeys.servlet.image.ImageServletResponse.BG_COLOR"; - - /** - * Request attribute of type {@link Float} controlling image output compression/quality. - * Used for formats that accepts compression or quality settings, - * like JPEG (quality), PNG (compression only) etc. - *

      - * Defaults to {@code 0.8f} for JPEG. - */ - String ATTRIB_OUTPUT_QUALITY = "com.twelvemonkeys.servlet.image.ImageServletResponse.OUTPUT_QUALITY"; - - /** - * Request attribute of type {@link Double} controlling image read - * subsampling factor. Controls the maximum sample pixels in each direction, - * that is read per pixel in the output image, if the result will be - * downscaled. - * Larger values will result in better quality, at the expense of higher - * memory consumption and CPU usage. - * However, using values above {@code 3.0} will usually not improve image - * quality. - * Legal values are in the range {@code [1.0 .. positive infinity>}. - *

      - * Defaults to {@code 2.0}. - */ - String ATTRIB_READ_SUBSAMPLING_FACTOR = "com.twelvemonkeys.servlet.image.ImageServletResponse.READ_SUBSAMPLING_FACTOR"; - - /** - * Request attribute of type {@link Integer} controlling image resample - * algorithm. - * Legal values are {@link java.awt.Image#SCALE_DEFAULT SCALE_DEFAULT}, - * {@link java.awt.Image#SCALE_FAST SCALE_FAST} or - * {@link java.awt.Image#SCALE_SMOOTH SCALE_SMOOTH}. - *

      - * Note: When using a value of {@code SCALE_FAST}, you should also use a - * subsampling factor of {@code 1.0}, for fast read/scale. - * Otherwise, use a subsampling factor of {@code 2.0} for better quality. - *

      - * Defaults to {@code SCALE_DEFAULT}. - */ - String ATTRIB_IMAGE_RESAMPLE_ALGORITHM = "com.twelvemonkeys.servlet.image.ImageServletResponse.IMAGE_RESAMPLE_ALGORITHM"; - - /** - * Gets the image format for this response, such as "image/gif" or "image/jpeg". - * If not set, the default format is that of the original image. - * - * @return the image format for this response. - * @see #setOutputContentType(String) - */ - String getOutputContentType(); - - /** - * Sets the image format for this response, such as "image/gif" or "image/jpeg". - *

      - * As an example, a custom filter could do content negotiation based on the - * request header fields and write the image back in an appropriate format. - *

      - * If not set, the default format is that of the original image. - * - * @param pImageFormat the image format for this response. - */ - void setOutputContentType(String pImageFormat); - - //TODO: ?? void setCompressionQuality(float pQualityFactor); - //TODO: ?? float getCompressionQuality(); - - /** - * Writes the image to the original {@code ServletOutputStream}. - * If no format is {@linkplain #setOutputContentType(String) set} in this response, - * the image is encoded in the same format as the original image. - * - * @throws java.io.IOException if an I/O exception occurs during writing - */ - void flush() throws IOException; - - /** - * Gets the decoded image from the response. - * - * @return a {@code BufferedImage} or {@code null} if the image could not be read. - * - * @throws java.io.IOException if an I/O exception occurs during reading - */ - BufferedImage getImage() throws IOException; - - /** - * Sets the image for this response. - * - * @param pImage the new response image. - */ - void setImage(RenderedImage pImage); -} +/* + * 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 of the copyright holder 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 HOLDER 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.servlet.ServletResponse; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.IOException; + +/** + * ImageServletResponse. + *

      + * The request attributes regarding image size and source region (AOI) are used + * in the decoding process, and must be set before the first invocation of + * {@link #getImage()} to have any effect. + *

      + * + * @author Harald Kuhr + * @version $Id: ImageServletResponse.java#4 $ + */ +public interface ImageServletResponse extends ServletResponse { + /** + * Request attribute of type {@link java.awt.Dimension} controlling image + * size. + * If either {@code width} or {@code height} is negative, the size is + * computed, using uniform scaling. + * Else, if {@code SIZE_UNIFORM} is {@code true}, the size will be + * computed to the largest possible area (with correct aspect ratio) + * fitting inside the target area. + * Otherwise, the image is scaled to the given size, with no regard to + * aspect ratio. + *

      + * Defaults to {@code null} (original image size). + *

      + */ + String ATTRIB_SIZE = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE"; + + /** + * Request attribute of type {@link Boolean} controlling image sizing. + *

      + * Defaults to {@code Boolean.TRUE}. + *

      + */ + String ATTRIB_SIZE_UNIFORM = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE_UNIFORM"; + + /** + * Request attribute of type {@link Boolean} controlling image sizing. + *

      + * Defaults to {@code Boolean.FALSE}. + *

      + */ + String ATTRIB_SIZE_PERCENT = "com.twelvemonkeys.servlet.image.ImageServletResponse.SIZE_PERCENT"; + + /** + * Request attribute of type {@link java.awt.Rectangle} controlling image + * source region (area of interest). + *

      + * Defaults to {@code null} (the entire image). + *

      + */ + String ATTRIB_AOI = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI"; + + /** + * Request attribute of type {@link Boolean} controlling image AOI. + *

      + * Defaults to {@code Boolean.FALSE}. + *

      + */ + String ATTRIB_AOI_UNIFORM = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI_UNIFORM"; + + /** + * Request attribute of type {@link Boolean} controlling image AOI. + *

      + * Defaults to {@code Boolean.FALSE}. + *

      + */ + String ATTRIB_AOI_PERCENT = "com.twelvemonkeys.servlet.image.ImageServletResponse.AOI_PERCENT"; + + /** + * Request attribute of type {@link java.awt.Color} controlling background + * color for any transparent/translucent areas of the image. + *

      + * Defaults to {@code null} (keeps the transparent areas transparent). + *

      + */ + String ATTRIB_BG_COLOR = "com.twelvemonkeys.servlet.image.ImageServletResponse.BG_COLOR"; + + /** + * Request attribute of type {@link Float} controlling image output compression/quality. + * Used for formats that accepts compression or quality settings, + * like JPEG (quality), PNG (compression only) etc. + *

      + * Defaults to {@code 0.8f} for JPEG. + *

      + */ + String ATTRIB_OUTPUT_QUALITY = "com.twelvemonkeys.servlet.image.ImageServletResponse.OUTPUT_QUALITY"; + + /** + * Request attribute of type {@link Double} controlling image read + * subsampling factor. Controls the maximum sample pixels in each direction, + * that is read per pixel in the output image, if the result will be + * downscaled. + * Larger values will result in better quality, at the expense of higher + * memory consumption and CPU usage. + * However, using values above {@code 3.0} will usually not improve image + * quality. + * Legal values are in the range {@code [1.0 .. positive infinity>}. + *

      + * Defaults to {@code 2.0}. + *

      + */ + String ATTRIB_READ_SUBSAMPLING_FACTOR = "com.twelvemonkeys.servlet.image.ImageServletResponse.READ_SUBSAMPLING_FACTOR"; + + /** + * Request attribute of type {@link Integer} controlling image resample + * algorithm. + * Legal values are {@link java.awt.Image#SCALE_DEFAULT SCALE_DEFAULT}, + * {@link java.awt.Image#SCALE_FAST SCALE_FAST} or + * {@link java.awt.Image#SCALE_SMOOTH SCALE_SMOOTH}. + *

      + * Note: When using a value of {@code SCALE_FAST}, you should also use a + * subsampling factor of {@code 1.0}, for fast read/scale. + * Otherwise, use a subsampling factor of {@code 2.0} for better quality. + *

      + *

      + * Defaults to {@code SCALE_DEFAULT}. + *

      + */ + String ATTRIB_IMAGE_RESAMPLE_ALGORITHM = "com.twelvemonkeys.servlet.image.ImageServletResponse.IMAGE_RESAMPLE_ALGORITHM"; + + /** + * Gets the image format for this response, such as "image/gif" or "image/jpeg". + * If not set, the default format is that of the original image. + * + * @return the image format for this response. + * @see #setOutputContentType(String) + */ + String getOutputContentType(); + + /** + * Sets the image format for this response, such as "image/gif" or "image/jpeg". + *

      + * As an example, a custom filter could do content negotiation based on the + * request header fields and write the image back in an appropriate format. + *

      + *

      + * If not set, the default format is that of the original image. + *

      + * + * @param pImageFormat the image format for this response. + */ + void setOutputContentType(String pImageFormat); + + //TODO: ?? void setCompressionQuality(float pQualityFactor); + //TODO: ?? float getCompressionQuality(); + + /** + * Writes the image to the original {@code ServletOutputStream}. + * If no format is {@linkplain #setOutputContentType(String) set} in this response, + * the image is encoded in the same format as the original image. + * + * @throws java.io.IOException if an I/O exception occurs during writing + */ + void flush() throws IOException; + + /** + * Gets the decoded image from the response. + * + * @return a {@code BufferedImage} or {@code null} if the image could not be read. + * + * @throws java.io.IOException if an I/O exception occurs during reading + */ + BufferedImage getImage() throws IOException; + + /** + * Sets the image for this response. + * + * @param pImage the new response image. + */ + void setImage(RenderedImage pImage); +} 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 ad71b05d..b3b1f245 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/RotateFilter.java @@ -1,204 +1,203 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.image.ImageUtil; -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.servlet.ServletUtil; - -import javax.servlet.ServletRequest; -import java.awt.*; -import java.awt.geom.AffineTransform; -import java.awt.geom.Rectangle2D; -import java.awt.image.BufferedImage; -import java.awt.image.RenderedImage; - -/** - * This Servlet is able to render a cropped part of an image. - * - *


      - * - * Parameters:
      - *

      - *
      {@code cropX}
      - *
      integer, the new left edge of the image. - *
      {@code cropY}
      - *
      integer, the new top of the image. - *
      {@code cropWidth}
      - *
      integer, the new width of the image. - *
      {@code cropHeight}
      - *
      integer, the new height of the image. - * - * - * - *
      - * - * @example - * JPEG: - * <IMG src="/scale/test.jpg?image=http://www.iconmedialab.com/images/random/home_image_12.jpg&width=500&uniform=true"> - * - * PNG: - * <IMG src="/scale/test.png?cache=false&image=http://www.iconmedialab.com/images/random/home_image_12.jpg&width=50&units=PERCENT"> - * - * @todo Correct rounding errors, resulting in black borders when rotating 90 - * degrees, and one of width or height is odd length... - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @version $Id: RotateFilter.java#1 $ - */ - -public class RotateFilter extends ImageFilter { - /** {@code angle}*/ - protected final static String PARAM_ANGLE = "angle"; - /** {@code angleUnits (RADIANS|DEGREES)}*/ - protected final static String PARAM_ANGLE_UNITS = "angleUnits"; - /** {@code crop}*/ - protected final static String PARAM_CROP = "rotateCrop"; - /** {@code bgcolor}*/ - protected final static String PARAM_BGCOLOR = "rotateBgcolor"; - - /** {@code degrees}*/ - private final static String ANGLE_DEGREES = "degrees"; - /** {@code radians}*/ - //private final static String ANGLE_RADIANS = "radians"; - - /** - * Reads the image from the requested URL, rotates it, and returns - * it in the - * Servlet stream. See above for details on parameters. - */ - - protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { - // Get angle - double ang = getAngle(pRequest); - - // Get bounds - Rectangle2D rect = getBounds(pRequest, pImage, ang); - int width = (int) rect.getWidth(); - int height = (int) rect.getHeight(); - - // Create result image - BufferedImage res = ImageUtil.createTransparent(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = res.createGraphics(); - - // Get background color and clear - String str = pRequest.getParameter(PARAM_BGCOLOR); - if (!StringUtil.isEmpty(str)) { - Color bgcolor = StringUtil.toColor(str); - g.setBackground(bgcolor); - g.clearRect(0, 0, width, height); - } - - // Set mHints (why do I always get jagged edgdes?) - RenderingHints hints = new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); - hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)); - hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)); - hints.add(new RenderingHints(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)); - - g.setRenderingHints(hints); - - // Rotate around center - AffineTransform at = AffineTransform - .getRotateInstance(ang, width / 2.0, height / 2.0); - - // Move to center - at.translate(width / 2.0 - pImage.getWidth() / 2.0, - height / 2.0 - pImage.getHeight() / 2.0); - - // Draw it, centered - g.drawImage(pImage, at, null); - - return res; - } - - /** - * Gets the angle of rotation. - */ - - private double getAngle(ServletRequest pReq) { - double angle = 0.0; - String str = pReq.getParameter(PARAM_ANGLE); - if (!StringUtil.isEmpty(str)) { - angle = Double.parseDouble(str); - - // Convert to radians, if needed - str = pReq.getParameter(PARAM_ANGLE_UNITS); - if (!StringUtil.isEmpty(str) - && ANGLE_DEGREES.equalsIgnoreCase(str)) { - angle = Math.toRadians(angle); - } - } - - return angle; - } - - /** - * Get the bounding rectangle of the rotated image. - */ - - private Rectangle2D getBounds(ServletRequest pReq, BufferedImage pImage, - double pAng) { - // Get dimensions of original image - int width = pImage.getWidth(); // loads the image - int height = pImage.getHeight(); - - // Test if we want to crop image (default) - // if true - // - find the largest bounding box INSIDE the rotated image, - // that matches the original proportions (nearest 90deg) - // (scale up to fit dimensions?) - // else - // - find the smallest bounding box OUTSIDE the rotated image. - // - that matches the original proportions (nearest 90deg) ? - // (scale down to fit dimensions?) - AffineTransform at = - AffineTransform.getRotateInstance(pAng, width / 2.0, height / 2.0); - - Rectangle2D orig = new Rectangle(width, height); - Shape rotated = at.createTransformedShape(orig); - - if (ServletUtil.getBooleanParameter(pReq, PARAM_CROP, false)) { - // TODO: Inside box - return rotated.getBounds2D(); - } - else { - return rotated.getBounds2D(); - } - } -} - +/* + * 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 of the copyright holder 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 HOLDER 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.image.ImageUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.servlet.ServletUtil; + +import javax.servlet.ServletRequest; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; + +/** + * This Servlet is able to render a cropped part of an image. + * + *
      + * + * Parameters:
      + *
      + *
      {@code cropX}
      + *
      integer, the new left edge of the image. + *
      {@code cropY}
      + *
      integer, the new top of the image. + *
      {@code cropWidth}
      + *
      integer, the new width of the image. + *
      {@code cropHeight}
      + *
      integer, the new height of the image. + * + *
      + * + *

      + * Examples: + *
      + * JPEG: + * <IMG src="/scale/test.jpg?image=http://www.iconmedialab.com/images/random/home_image_12.jpg&width=500&uniform=true"> + *
      + * PNG: + * <IMG src="/scale/test.png?cache=false&image=http://www.iconmedialab.com/images/random/home_image_12.jpg&width=50&units=PERCENT"> + *

      + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: RotateFilter.java#1 $ + */ +// TODO: Correct rounding errors, resulting in black borders when rotating 90 +// degrees, and one of width or height is odd length... +public class RotateFilter extends ImageFilter { + /** {@code angle}*/ + protected final static String PARAM_ANGLE = "angle"; + /** {@code angleUnits (RADIANS|DEGREES)}*/ + protected final static String PARAM_ANGLE_UNITS = "angleUnits"; + /** {@code crop}*/ + protected final static String PARAM_CROP = "rotateCrop"; + /** {@code bgcolor}*/ + protected final static String PARAM_BGCOLOR = "rotateBgcolor"; + + /** {@code degrees}*/ + private final static String ANGLE_DEGREES = "degrees"; + /** {@code radians}*/ + //private final static String ANGLE_RADIANS = "radians"; + + /** + * Reads the image from the requested URL, rotates it, and returns + * it in the + * Servlet stream. See above for details on parameters. + */ + + protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { + // Get angle + double ang = getAngle(pRequest); + + // Get bounds + Rectangle2D rect = getBounds(pRequest, pImage, ang); + int width = (int) rect.getWidth(); + int height = (int) rect.getHeight(); + + // Create result image + BufferedImage res = ImageUtil.createTransparent(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = res.createGraphics(); + + // Get background color and clear + String str = pRequest.getParameter(PARAM_BGCOLOR); + if (!StringUtil.isEmpty(str)) { + Color bgcolor = StringUtil.toColor(str); + g.setBackground(bgcolor); + g.clearRect(0, 0, width, height); + } + + // Set mHints (why do I always get jagged edgdes?) + RenderingHints hints = new RenderingHints(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + hints.add(new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY)); + hints.add(new RenderingHints(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)); + hints.add(new RenderingHints(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC)); + + g.setRenderingHints(hints); + + // Rotate around center + AffineTransform at = AffineTransform + .getRotateInstance(ang, width / 2.0, height / 2.0); + + // Move to center + at.translate(width / 2.0 - pImage.getWidth() / 2.0, + height / 2.0 - pImage.getHeight() / 2.0); + + // Draw it, centered + g.drawImage(pImage, at, null); + + return res; + } + + /** + * Gets the angle of rotation. + */ + + private double getAngle(ServletRequest pReq) { + double angle = 0.0; + String str = pReq.getParameter(PARAM_ANGLE); + if (!StringUtil.isEmpty(str)) { + angle = Double.parseDouble(str); + + // Convert to radians, if needed + str = pReq.getParameter(PARAM_ANGLE_UNITS); + if (!StringUtil.isEmpty(str) + && ANGLE_DEGREES.equalsIgnoreCase(str)) { + angle = Math.toRadians(angle); + } + } + + return angle; + } + + /** + * Get the bounding rectangle of the rotated image. + */ + + private Rectangle2D getBounds(ServletRequest pReq, BufferedImage pImage, + double pAng) { + // Get dimensions of original image + int width = pImage.getWidth(); // loads the image + int height = pImage.getHeight(); + + // Test if we want to crop image (default) + // if true + // - find the largest bounding box INSIDE the rotated image, + // that matches the original proportions (nearest 90deg) + // (scale up to fit dimensions?) + // else + // - find the smallest bounding box OUTSIDE the rotated image. + // - that matches the original proportions (nearest 90deg) ? + // (scale down to fit dimensions?) + AffineTransform at = + AffineTransform.getRotateInstance(pAng, width / 2.0, height / 2.0); + + Rectangle2D orig = new Rectangle(width, height); + Shape rotated = at.createTransformedShape(orig); + + if (ServletUtil.getBooleanParameter(pReq, PARAM_CROP, false)) { + // TODO: Inside box + return rotated.getBounds2D(); + } + else { + return rotated.getBounds2D(); + } + } +} + 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 705d1bb2..bf780e64 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/ScaleFilter.java @@ -1,324 +1,331 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.image.ImageUtil; -import com.twelvemonkeys.lang.StringUtil; -import com.twelvemonkeys.servlet.ServletUtil; - -import javax.servlet.ServletRequest; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.image.RenderedImage; -import java.lang.reflect.Field; - - -/** - * This filter renders a scaled version of an image read from a - * given URL. The image can be output as a GIF, JPEG or PNG image - * or similar. - *

      - *


      - *

      - * Parameters:
      - *

      - *
      {@code scaleX}
      - *
      integer, the new width of the image. - *
      {@code scaleY}
      - *
      integer, the new height of the image. - *
      {@code scaleUniform}
      - *
      boolean, wether or not uniform scalnig should be used. Default is - * {@code true}. - *
      {@code scaleUnits}
      - *
      string, one of {@code PIXELS}, {@code PERCENT}. - * {@code PIXELS} is default. - *
      {@code scaleQuality}
      - *
      string, one of {@code SCALE_SMOOTH}, {@code SCALE_FAST}, - * {@code SCALE_REPLICATE}, {@code SCALE_AREA_AVERAGING}. - * {@code SCALE_DEFAULT} is default (see - * {@link java.awt.Image#getScaledInstance(int,int,int)}, {@link java.awt.Image} - * for more details). - *
      - * - * @author Harald Kuhr - * @author last modified by $Author: haku $ - * @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"> - */ -public class ScaleFilter extends ImageFilter { - - /** - * Width and height are absolute pixels. The default. - */ - public static final int UNITS_PIXELS = 1; - /** - * Width and height are percentage of original width and height. - */ - public static final int UNITS_PERCENT = 5; - /** - * Ahh, good choice! - */ - //private static final int UNITS_METRIC = 42; - /** - * The root of all evil... - */ - //private static final int UNITS_INCHES = 666; - /** - * Unknown units. - */ - public static final int UNITS_UNKNOWN = 0; - - /** - * {@code scaleQuality} - */ - protected final static String PARAM_SCALE_QUALITY = "scaleQuality"; - /** - * {@code scaleUnits} - */ - protected final static String PARAM_SCALE_UNITS = "scaleUnits"; - /** - * {@code scaleUniform} - */ - protected final static String PARAM_SCALE_UNIFORM = "scaleUniform"; - /** - * {@code scaleX} - */ - protected final static String PARAM_SCALE_X = "scaleX"; - /** - * {@code scaleY} - */ - protected final static String PARAM_SCALE_Y = "scaleY"; - /** - * {@code image} - */ - protected final static String PARAM_IMAGE = "image"; - - /** */ - protected int defaultScaleQuality = Image.SCALE_DEFAULT; - - /** - * Reads the image from the requested URL, scales it, and returns it in the - * Servlet stream. See above for details on parameters. - */ - protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { - - // Get quality setting - // SMOOTH | FAST | REPLICATE | DEFAULT | AREA_AVERAGING - // See Image (mHints) - int quality = getQuality(pRequest.getParameter(PARAM_SCALE_QUALITY)); - - // Get units, default is pixels - // PIXELS | PERCENT | METRIC | INCHES - int units = getUnits(pRequest.getParameter(PARAM_SCALE_UNITS)); - if (units == UNITS_UNKNOWN) { - log("Unknown units for scale, returning original."); - return pImage; - } - - // Use uniform scaling? Default is true - boolean uniformScale = ServletUtil.getBooleanParameter(pRequest, PARAM_SCALE_UNIFORM, true); - - // Get dimensions - int width = ServletUtil.getIntParameter(pRequest, PARAM_SCALE_X, -1); - int height = ServletUtil.getIntParameter(pRequest, PARAM_SCALE_Y, -1); - - // Get dimensions for scaled image - Dimension dim = getDimensions(pImage, width, height, units, uniformScale); - - width = (int) dim.getWidth(); - height = (int) dim.getHeight(); - - // Return scaled instance directly - return ImageUtil.createScaled(pImage, width, height, quality); - } - - /** - * Gets the quality constant for the scaling, from the string argument. - * - * @param pQualityStr The string representation of the scale quality - * constant. - * @return The matching quality constant, or the default quality if none - * was found. - * @see java.awt.Image - * @see java.awt.Image#getScaledInstance(int,int,int) - */ - protected int getQuality(String pQualityStr) { - if (!StringUtil.isEmpty(pQualityStr)) { - try { - // Get quality constant from Image using reflection - Class cl = Image.class; - Field field = cl.getField(pQualityStr.toUpperCase()); - - return field.getInt(null); - } - catch (IllegalAccessException ia) { - log("Unable to get quality.", ia); - } - catch (NoSuchFieldException nsf) { - log("Unable to get quality.", nsf); - } - } - - return defaultScaleQuality; - } - - public void setDefaultScaleQuality(String pDefaultScaleQuality) { - defaultScaleQuality = getQuality(pDefaultScaleQuality); - } - - /** - * Gets the units constant for the width and height arguments, from the - * given string argument. - * - * @param pUnitStr The string representation of the units constant, - * can be one of "PIXELS" or "PERCENT". - * @return The mathcing units constant, or UNITS_UNKNOWN if none was found. - */ - protected int getUnits(String pUnitStr) { - if (StringUtil.isEmpty(pUnitStr) - || pUnitStr.equalsIgnoreCase("PIXELS")) { - return UNITS_PIXELS; - } - else if (pUnitStr.equalsIgnoreCase("PERCENT")) { - return UNITS_PERCENT; - } - else { - return UNITS_UNKNOWN; - } - } - - /** - * Gets the dimensions (height and width) of the scaled image. The - * dimensions are computed based on the old image's dimensions, the units - * used for specifying new dimensions and whether or not uniform scaling - * should be used (se algorithm below). - * - * @param pImage the image to be scaled - * @param pWidth the new width of the image, or -1 if unknown - * @param pHeight the new height of the image, or -1 if unknown - * @param pUnits the constant specifying units for width and height - * parameter (UNITS_PIXELS or UNITS_PERCENT) - * @param pUniformScale 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 Dimension getDimensions(Image pImage, int pWidth, int pHeight, - int pUnits, boolean pUniformScale) { - - // If uniform, make sure width and height are scaled the same ammount - // (use ONLY height or ONLY width). - // - // Algoritm: - // if uniform - // if newHeight not set - // find ratio newWidth / oldWidth - // oldHeight *= ratio - // else if newWidth not set - // find ratio newWidth / oldWidth - // oldHeight *= ratio - // else - // find both ratios and use the smallest one - // (this will be the largest version of the image that fits - // inside the rectangle given) - // (if PERCENT, just use smallest percentage). - // - // If units is percent, we only need old height and width - - int oldWidth = ImageUtil.getWidth(pImage); - int oldHeight = ImageUtil.getHeight(pImage); - float ratio; - - if (pUnits == UNITS_PERCENT) { - if (pWidth >= 0 && pHeight >= 0) { - // Non-uniform - pWidth = (int) ((float) oldWidth * (float) pWidth / 100f); - pHeight = (int) ((float) oldHeight * (float) pHeight / 100f); - } - else if (pWidth >= 0) { - // Find ratio from pWidth - ratio = (float) pWidth / 100f; - pWidth = (int) ((float) oldWidth * ratio); - pHeight = (int) ((float) oldHeight * ratio); - } - else if (pHeight >= 0) { - // Find ratio from pHeight - ratio = (float) pHeight / 100f; - pWidth = (int) ((float) oldWidth * ratio); - pHeight = (int) ((float) oldHeight * ratio); - } - // Else: No scale - } - else if (pUnits == UNITS_PIXELS) { - if (pUniformScale) { - if (pWidth >= 0 && pHeight >= 0) { - // Compute both ratios - ratio = (float) pWidth / (float) oldWidth; - float heightRatio = (float) pHeight / (float) oldHeight; - - // Find the largest ratio, and use that for both - if (heightRatio < ratio) { - ratio = heightRatio; - pWidth = (int) ((float) oldWidth * ratio); - } - else { - pHeight = (int) ((float) oldHeight * ratio); - } - - } - else if (pWidth >= 0) { - // Find ratio from pWidth - ratio = (float) pWidth / (float) oldWidth; - pHeight = (int) ((float) oldHeight * ratio); - } - else if (pHeight >= 0) { - // Find ratio from pHeight - ratio = (float) pHeight / (float) oldHeight; - pWidth = (int) ((float) oldWidth * ratio); - } - // Else: No scale - } - } - - // Default is no scale, just work as a proxy - if (pWidth < 0) { - pWidth = oldWidth; - } - if (pHeight < 0) { - pHeight = oldHeight; - } - - // Create new Dimension object and return - return new Dimension(pWidth, pHeight); - } -} +/* + * 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 of the copyright holder 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 HOLDER 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.image.ImageUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.servlet.ServletUtil; + +import javax.servlet.ServletRequest; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.lang.reflect.Field; + + +/** + * This filter renders a scaled version of an image read from a + * given URL. The image can be output as a GIF, JPEG or PNG image + * or similar. + * + *
      + * + *

      + * Parameters:
      + *

      + *
      + *
      {@code scaleX}
      + *
      integer, the new width of the image. + *
      {@code scaleY}
      + *
      integer, the new height of the image. + *
      {@code scaleUniform}
      + *
      boolean, wether or not uniform scalnig should be used. Default is + * {@code true}. + *
      {@code scaleUnits}
      + *
      string, one of {@code PIXELS}, {@code PERCENT}. + * {@code PIXELS} is default. + *
      {@code scaleQuality}
      + *
      string, one of {@code SCALE_SMOOTH}, {@code SCALE_FAST}, + * {@code SCALE_REPLICATE}, {@code SCALE_AREA_AVERAGING}. + * {@code SCALE_DEFAULT} is default (see + * {@link java.awt.Image#getScaledInstance(int,int,int)}, {@link java.awt.Image} + * for more details). + *
      + *

      + * Examples: + *

      + *
      + * <IMG src="/scale/test.jpg?scaleX=500&scaleUniform=false">
      + * <IMG src="/scale/test.png?scaleY=50&scaleUnits=PERCENT">
      + * 
      + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: ScaleFilter.java#1 $ + * + */ +public class ScaleFilter extends ImageFilter { + + /** + * Width and height are absolute pixels. The default. + */ + public static final int UNITS_PIXELS = 1; + /** + * Width and height are percentage of original width and height. + */ + public static final int UNITS_PERCENT = 5; + /** + * Ahh, good choice! + */ + //private static final int UNITS_METRIC = 42; + /** + * The root of all evil... + */ + //private static final int UNITS_INCHES = 666; + /** + * Unknown units. + */ + public static final int UNITS_UNKNOWN = 0; + + /** + * {@code scaleQuality} + */ + protected final static String PARAM_SCALE_QUALITY = "scaleQuality"; + /** + * {@code scaleUnits} + */ + protected final static String PARAM_SCALE_UNITS = "scaleUnits"; + /** + * {@code scaleUniform} + */ + protected final static String PARAM_SCALE_UNIFORM = "scaleUniform"; + /** + * {@code scaleX} + */ + protected final static String PARAM_SCALE_X = "scaleX"; + /** + * {@code scaleY} + */ + protected final static String PARAM_SCALE_Y = "scaleY"; + /** + * {@code image} + */ + protected final static String PARAM_IMAGE = "image"; + + /** */ + protected int defaultScaleQuality = Image.SCALE_DEFAULT; + + /** + * Reads the image from the requested URL, scales it, and returns it in the + * Servlet stream. See above for details on parameters. + */ + protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { + + // Get quality setting + // SMOOTH | FAST | REPLICATE | DEFAULT | AREA_AVERAGING + // See Image (mHints) + int quality = getQuality(pRequest.getParameter(PARAM_SCALE_QUALITY)); + + // Get units, default is pixels + // PIXELS | PERCENT | METRIC | INCHES + int units = getUnits(pRequest.getParameter(PARAM_SCALE_UNITS)); + if (units == UNITS_UNKNOWN) { + log("Unknown units for scale, returning original."); + return pImage; + } + + // Use uniform scaling? Default is true + boolean uniformScale = ServletUtil.getBooleanParameter(pRequest, PARAM_SCALE_UNIFORM, true); + + // Get dimensions + int width = ServletUtil.getIntParameter(pRequest, PARAM_SCALE_X, -1); + int height = ServletUtil.getIntParameter(pRequest, PARAM_SCALE_Y, -1); + + // Get dimensions for scaled image + Dimension dim = getDimensions(pImage, width, height, units, uniformScale); + + width = (int) dim.getWidth(); + height = (int) dim.getHeight(); + + // Return scaled instance directly + return ImageUtil.createScaled(pImage, width, height, quality); + } + + /** + * Gets the quality constant for the scaling, from the string argument. + * + * @param pQualityStr The string representation of the scale quality + * constant. + * @return The matching quality constant, or the default quality if none + * was found. + * @see java.awt.Image + * @see java.awt.Image#getScaledInstance(int,int,int) + */ + protected int getQuality(String pQualityStr) { + if (!StringUtil.isEmpty(pQualityStr)) { + try { + // Get quality constant from Image using reflection + Class cl = Image.class; + Field field = cl.getField(pQualityStr.toUpperCase()); + + return field.getInt(null); + } + catch (IllegalAccessException ia) { + log("Unable to get quality.", ia); + } + catch (NoSuchFieldException nsf) { + log("Unable to get quality.", nsf); + } + } + + return defaultScaleQuality; + } + + public void setDefaultScaleQuality(String pDefaultScaleQuality) { + defaultScaleQuality = getQuality(pDefaultScaleQuality); + } + + /** + * Gets the units constant for the width and height arguments, from the + * given string argument. + * + * @param pUnitStr The string representation of the units constant, + * can be one of "PIXELS" or "PERCENT". + * @return The mathcing units constant, or UNITS_UNKNOWN if none was found. + */ + protected int getUnits(String pUnitStr) { + if (StringUtil.isEmpty(pUnitStr) + || pUnitStr.equalsIgnoreCase("PIXELS")) { + return UNITS_PIXELS; + } + else if (pUnitStr.equalsIgnoreCase("PERCENT")) { + return UNITS_PERCENT; + } + else { + return UNITS_UNKNOWN; + } + } + + /** + * Gets the dimensions (height and width) of the scaled image. The + * dimensions are computed based on the old image's dimensions, the units + * used for specifying new dimensions and whether or not uniform scaling + * should be used (se algorithm below). + * + * @param pImage the image to be scaled + * @param pWidth the new width of the image, or -1 if unknown + * @param pHeight the new height of the image, or -1 if unknown + * @param pUnits the constant specifying units for width and height + * parameter (UNITS_PIXELS or UNITS_PERCENT) + * @param pUniformScale 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 Dimension getDimensions(Image pImage, int pWidth, int pHeight, + int pUnits, boolean pUniformScale) { + + // If uniform, make sure width and height are scaled the same ammount + // (use ONLY height or ONLY width). + // + // Algoritm: + // if uniform + // if newHeight not set + // find ratio newWidth / oldWidth + // oldHeight *= ratio + // else if newWidth not set + // find ratio newWidth / oldWidth + // oldHeight *= ratio + // else + // find both ratios and use the smallest one + // (this will be the largest version of the image that fits + // inside the rectangle given) + // (if PERCENT, just use smallest percentage). + // + // If units is percent, we only need old height and width + + int oldWidth = ImageUtil.getWidth(pImage); + int oldHeight = ImageUtil.getHeight(pImage); + float ratio; + + if (pUnits == UNITS_PERCENT) { + if (pWidth >= 0 && pHeight >= 0) { + // Non-uniform + pWidth = (int) ((float) oldWidth * (float) pWidth / 100f); + pHeight = (int) ((float) oldHeight * (float) pHeight / 100f); + } + else if (pWidth >= 0) { + // Find ratio from pWidth + ratio = (float) pWidth / 100f; + pWidth = (int) ((float) oldWidth * ratio); + pHeight = (int) ((float) oldHeight * ratio); + } + else if (pHeight >= 0) { + // Find ratio from pHeight + ratio = (float) pHeight / 100f; + pWidth = (int) ((float) oldWidth * ratio); + pHeight = (int) ((float) oldHeight * ratio); + } + // Else: No scale + } + else if (pUnits == UNITS_PIXELS) { + if (pUniformScale) { + if (pWidth >= 0 && pHeight >= 0) { + // Compute both ratios + ratio = (float) pWidth / (float) oldWidth; + float heightRatio = (float) pHeight / (float) oldHeight; + + // Find the largest ratio, and use that for both + if (heightRatio < ratio) { + ratio = heightRatio; + pWidth = (int) ((float) oldWidth * ratio); + } + else { + pHeight = (int) ((float) oldHeight * ratio); + } + + } + else if (pWidth >= 0) { + // Find ratio from pWidth + ratio = (float) pWidth / (float) oldWidth; + pHeight = (int) ((float) oldHeight * ratio); + } + else if (pHeight >= 0) { + // Find ratio from pHeight + ratio = (float) pHeight / (float) oldHeight; + pWidth = (int) ((float) oldWidth * ratio); + } + // Else: No scale + } + } + + // Default is no scale, just work as a proxy + if (pWidth < 0) { + pWidth = oldWidth; + } + if (pHeight < 0) { + pHeight = oldHeight; + } + + // Create new Dimension object and return + return new Dimension(pWidth, pHeight); + } +} 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 7c6e7c0b..9a7aa74d 100755 --- a/servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java +++ b/servlet/src/main/java/com/twelvemonkeys/servlet/image/SourceRenderFilter.java @@ -1,184 +1,184 @@ -/* - * 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 of the copyright holder 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 HOLDER 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.servlet.ServletUtil; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.awt.image.RenderedImage; -import java.io.IOException; - -/** - * A {@link javax.servlet.Filter} that extracts request parameters, and sets the - * corresponding request attributes from {@link ImageServletResponse}. - * Only affects how the image is decoded, and must be applied before any - * other image filters in the chain. - *

      - * @see ImageServletResponse#ATTRIB_SIZE - * @see ImageServletResponse#ATTRIB_AOI - * - * @author Harald Kuhr - * @version $Id: SourceRenderFilter.java#1 $ - */ -public class SourceRenderFilter extends ImageFilter { - private String sizeWidthParam = "size.w"; - private String sizeHeightParam = "size.h"; - private String sizePercentParam = "size.percent"; - private String sizeUniformParam = "size.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) { - regionHeightParam = pRegionHeightParam; - } - - public void setRegionWidthParam(String pRegionWidthParam) { - regionWidthParam = pRegionWidthParam; - } - - public void setRegionLeftParam(String pRegionLeftParam) { - regionLeftParam = pRegionLeftParam; - } - - public void setRegionTopParam(String pRegionTopParam) { - regionTopParam = pRegionTopParam; - } - - public void setSizeHeightParam(String pSizeHeightParam) { - sizeHeightParam = pSizeHeightParam; - } - - public void setSizeWidthParam(String pSizeWidthParam) { - sizeWidthParam = pSizeWidthParam; - } - - public void setRegionPercentParam(String pRegionPercentParam) { - regionPercentParam = pRegionPercentParam; - } - - public void setRegionUniformParam(String pRegionUniformParam) { - regionUniformParam = pRegionUniformParam; - } - - public void setSizePercentParam(String pSizePercentParam) { - sizePercentParam = pSizePercentParam; - } - - public void setSizeUniformParam(String pSizeUniformParam) { - sizeUniformParam = pSizeUniformParam; - } - - public void init() throws ServletException { - if (triggerParams == null) { - // Add all params as triggers - triggerParams = new String[]{sizeWidthParam, sizeHeightParam, - sizeUniformParam, sizePercentParam, - regionLeftParam, regionTopParam, - regionWidthParam, regionHeightParam, - regionUniformParam, regionPercentParam}; - } - } - - /** - * Extracts request parameters, and sets the corresponding request - * attributes if specified. - * - * @param pRequest - * @param pResponse - * @param pChain - * @throws IOException - * @throws ServletException - */ - protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { - // TODO: Max size configuration, to avoid DOS attacks? OutOfMemory - - // Size parameters - 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, sizeUniformParam, true); - if (!uniform) { - pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.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, 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, regionUniformParam, false); - if (uniform) { - pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.TRUE); - } - percent = ServletUtil.getBooleanParameter(pRequest, regionPercentParam, false); - if (percent) { - pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_PERCENT, Boolean.TRUE); - } - - super.doFilterImpl(pRequest, pResponse, pChain); - } - - /** - * This implementation does no filtering, and simply returns the image - * passed in. - * - * @param pImage - * @param pRequest - * @param pResponse - * @return {@code pImage} - */ - protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { - return pImage; - } +/* + * 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 of the copyright holder 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 HOLDER 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.servlet.ServletUtil; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.RenderedImage; +import java.io.IOException; + +/** + * A {@link javax.servlet.Filter} that extracts request parameters, and sets the + * corresponding request attributes from {@link ImageServletResponse}. + * Only affects how the image is decoded, and must be applied before any + * other image filters in the chain. + * + * @see ImageServletResponse#ATTRIB_SIZE + * @see ImageServletResponse#ATTRIB_AOI + * + * @author Harald Kuhr + * @version $Id: SourceRenderFilter.java#1 $ + */ +public class SourceRenderFilter extends ImageFilter { + private String sizeWidthParam = "size.w"; + private String sizeHeightParam = "size.h"; + private String sizePercentParam = "size.percent"; + private String sizeUniformParam = "size.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) { + regionHeightParam = pRegionHeightParam; + } + + public void setRegionWidthParam(String pRegionWidthParam) { + regionWidthParam = pRegionWidthParam; + } + + public void setRegionLeftParam(String pRegionLeftParam) { + regionLeftParam = pRegionLeftParam; + } + + public void setRegionTopParam(String pRegionTopParam) { + regionTopParam = pRegionTopParam; + } + + public void setSizeHeightParam(String pSizeHeightParam) { + sizeHeightParam = pSizeHeightParam; + } + + public void setSizeWidthParam(String pSizeWidthParam) { + sizeWidthParam = pSizeWidthParam; + } + + public void setRegionPercentParam(String pRegionPercentParam) { + regionPercentParam = pRegionPercentParam; + } + + public void setRegionUniformParam(String pRegionUniformParam) { + regionUniformParam = pRegionUniformParam; + } + + public void setSizePercentParam(String pSizePercentParam) { + sizePercentParam = pSizePercentParam; + } + + public void setSizeUniformParam(String pSizeUniformParam) { + sizeUniformParam = pSizeUniformParam; + } + + public void init() throws ServletException { + if (triggerParams == null) { + // Add all params as triggers + triggerParams = new String[]{sizeWidthParam, sizeHeightParam, + sizeUniformParam, sizePercentParam, + regionLeftParam, regionTopParam, + regionWidthParam, regionHeightParam, + regionUniformParam, regionPercentParam}; + } + } + + /** + * Extracts request parameters, and sets the corresponding request + * attributes if specified. + * + * @param pRequest + * @param pResponse + * @param pChain + * @throws IOException + * @throws ServletException + */ + protected void doFilterImpl(ServletRequest pRequest, ServletResponse pResponse, FilterChain pChain) throws IOException, ServletException { + // TODO: Max size configuration, to avoid DOS attacks? OutOfMemory + + // Size parameters + 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, sizeUniformParam, true); + if (!uniform) { + pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.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, 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, regionUniformParam, false); + if (uniform) { + pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_UNIFORM, Boolean.TRUE); + } + percent = ServletUtil.getBooleanParameter(pRequest, regionPercentParam, false); + if (percent) { + pRequest.setAttribute(ImageServletResponse.ATTRIB_SIZE_PERCENT, Boolean.TRUE); + } + + super.doFilterImpl(pRequest, pResponse, pChain); + } + + /** + * This implementation does no filtering, and simply returns the image + * passed in. + * + * @param pImage + * @param pRequest + * @param pResponse + * @return {@code pImage} + */ + protected RenderedImage doFilter(BufferedImage pImage, ServletRequest pRequest, ImageServletResponse pResponse) { + return pImage; + } } \ No newline at end of file