From 2523f31a8c51d5b881480af6c5f92a328acc9beb Mon Sep 17 00:00:00 2001 From: Harald Kuhr Date: Thu, 3 Sep 2009 20:28:39 +0200 Subject: [PATCH] Adding twelvemonkeys-core --- twelvemonkeys-core/pom.xml | 58 + .../image/AbstractImageSource.java | 104 + .../twelvemonkeys/image/AreaAverageOp.java | 453 ++++ .../image/BrightnessContrastFilter.java | 167 ++ .../image/BufferedImageFactory.java | 543 +++++ .../image/BufferedImageIcon.java | 91 + .../twelvemonkeys/image/ConvolveTester.java | 128 + .../image/ConvolveWithEdgeOp.java | 244 ++ .../com/twelvemonkeys/image/CopyDither.java | 298 +++ .../twelvemonkeys/image/DiffusionDither.java | 465 ++++ .../com/twelvemonkeys/image/EasyImage.java | 78 + .../image/ExtendedImageConsumer.java | 61 + .../com/twelvemonkeys/image/GraphicsUtil.java | 82 + .../twelvemonkeys/image/GrayColorModel.java | 58 + .../com/twelvemonkeys/image/GrayFilter.java | 129 + .../image/ImageConversionException.java | 48 + .../image/ImageFilterException.java | 73 + .../com/twelvemonkeys/image/ImageUtil.java | 2037 ++++++++++++++++ .../com/twelvemonkeys/image/IndexImage.java | 1530 ++++++++++++ .../twelvemonkeys/image/InverseColorMap.java | 211 ++ .../image/InverseColorMapIndexColorModel.java | 310 +++ .../java/com/twelvemonkeys/image/Magick.java | 24 + .../image/MagickAccelerator.java | 184 ++ .../com/twelvemonkeys/image/MagickUtil.java | 549 +++++ .../image/MonochromeColorModel.java | 78 + .../com/twelvemonkeys/image/PixelizeOp.java | 378 +++ .../com/twelvemonkeys/image/ResampleOp.java | 1639 +++++++++++++ .../twelvemonkeys/image/SubsampleTester.java | 163 ++ .../image/SubsamplingFilter.java | 77 + .../java/com/twelvemonkeys/image/inv_cmap.c | 524 ++++ .../com/twelvemonkeys/image/package-info.java | 9 + .../io/AbstractCachedSeekableStream.java | 277 +++ .../com/twelvemonkeys/io/CompoundReader.java | 218 ++ .../com/twelvemonkeys/io/EmptyReader.java | 45 + .../io/FastByteArrayOutputStream.java | 131 + .../io/FileCacheSeekableStream.java | 313 +++ .../twelvemonkeys/io/FileSeekableStream.java | 130 + .../java/com/twelvemonkeys/io/FileSystem.java | 101 + .../java/com/twelvemonkeys/io/FileUtil.java | 1092 +++++++++ .../twelvemonkeys/io/FilenameMaskFilter.java | 239 ++ .../io/FilenameSuffixFilter.java | 93 + .../io/LittleEndianDataInputStream.java | 427 ++++ .../io/LittleEndianDataOutputStream.java | 334 +++ .../io/LittleEndianRandomAccessFile.java | 600 +++++ .../io/MemoryCacheSeekableStream.java | 191 ++ .../com/twelvemonkeys/io/NullInputStream.java | 80 + .../twelvemonkeys/io/NullOutputStream.java | 68 + .../twelvemonkeys/io/RandomAccessStream.java | 240 ++ .../java/com/twelvemonkeys/io/Seekable.java | 184 ++ .../twelvemonkeys/io/SeekableInputStream.java | 224 ++ .../io/SeekableOutputStream.java | 138 ++ .../twelvemonkeys/io/StringArrayReader.java | 185 ++ .../java/com/twelvemonkeys/io/SubStream.java | 135 ++ .../com/twelvemonkeys/io/UnixFileSystem.java | 105 + .../java/com/twelvemonkeys/io/Win32File.java | 190 ++ .../com/twelvemonkeys/io/Win32FileSystem.java | 90 + .../java/com/twelvemonkeys/io/Win32Lnk.java | 472 ++++ .../twelvemonkeys/io/WriterOutputStream.java | 236 ++ .../io/enc/AbstractRLEDecoder.java | 138 ++ .../java/com/twelvemonkeys/io/enc/Base64.java | 671 +++++ .../twelvemonkeys/io/enc/Base64Decoder.java | 187 ++ .../twelvemonkeys/io/enc/Base64Encoder.java | 99 + .../twelvemonkeys/io/enc/DecodeException.java | 54 + .../com/twelvemonkeys/io/enc/Decoder.java | 64 + .../twelvemonkeys/io/enc/DecoderStream.java | 189 ++ .../twelvemonkeys/io/enc/DeflateEncoder.java | 66 + .../com/twelvemonkeys/io/enc/Encoder.java | 65 + .../twelvemonkeys/io/enc/EncoderStream.java | 133 + .../twelvemonkeys/io/enc/InflateDecoder.java | 103 + .../com/twelvemonkeys/io/enc/LZWDecoder.java | 46 + .../com/twelvemonkeys/io/enc/LZWEncoder.java | 46 + .../io/enc/PackBits16Decoder.java | 173 ++ .../twelvemonkeys/io/enc/PackBitsDecoder.java | 188 ++ .../twelvemonkeys/io/enc/PackBitsEncoder.java | 124 + .../com/twelvemonkeys/io/enc/RLE4Decoder.java | 132 + .../com/twelvemonkeys/io/enc/RLE8Decoder.java | 117 + .../twelvemonkeys/io/enc/package-info.java | 14 + .../io/ole2/CompoundDocument.java | 761 ++++++ .../io/ole2/CorruptDocumentException.java | 54 + .../java/com/twelvemonkeys/io/ole2/Entry.java | 340 +++ .../com/twelvemonkeys/io/ole2/SIdChain.java | 104 + .../twelvemonkeys/io/ole2/package-info.java | 11 + .../java/com/twelvemonkeys/io/package.html | 7 + .../main/java/com/twelvemonkeys/io/todo.txt | 7 + .../java/com/twelvemonkeys/lang/BeanUtil.java | 612 +++++ .../java/com/twelvemonkeys/lang/DateUtil.java | 210 ++ .../com/twelvemonkeys/lang/ExceptionUtil.java | 129 + .../java/com/twelvemonkeys/lang/MathUtil.java | 168 ++ .../lang/MostUnfortunateException.java | 55 + .../com/twelvemonkeys/lang/NativeLoader.java | 398 +++ .../twelvemonkeys/lang/NativeResourceSPI.java | 99 + .../java/com/twelvemonkeys/lang/Platform.java | 244 ++ .../com/twelvemonkeys/lang/ReflectUtil.java | 137 ++ .../com/twelvemonkeys/lang/StringUtil.java | 2150 +++++++++++++++++ .../com/twelvemonkeys/lang/SystemUtil.java | 693 ++++++ .../java/com/twelvemonkeys/lang/Validate.java | 124 + .../java/com/twelvemonkeys/lang/package.html | 7 + .../net/AuthenticatorFilter.java | 47 + .../twelvemonkeys/net/HttpURLConnection.java | 1103 +++++++++ .../java/com/twelvemonkeys/net/MIMEUtil.java | 312 +++ .../java/com/twelvemonkeys/net/NetUtil.java | 1423 +++++++++++ .../net/PasswordAuthenticator.java | 45 + .../net/SimpleAuthenticator.java | 326 +++ .../java/com/twelvemonkeys/net/package.html | 7 + .../util/AbstractDecoratedMap.java | 400 +++ .../twelvemonkeys/util/AbstractResource.java | 92 + .../util/AbstractTokenIterator.java | 87 + .../java/com/twelvemonkeys/util/BASE64.java | 149 ++ .../java/com/twelvemonkeys/util/BeanMap.java | 245 ++ .../com/twelvemonkeys/util/BooleanKey.java | 52 + .../twelvemonkeys/util/CollectionUtil.java | 642 +++++ .../com/twelvemonkeys/util/DebugUtil.java | 1757 ++++++++++++++ .../twelvemonkeys/util/DuplicateHandler.java | 151 ++ .../com/twelvemonkeys/util/ExpiringMap.java | 49 + .../com/twelvemonkeys/util/FileResource.java | 78 + .../twelvemonkeys/util/FilterIterator.java | 151 ++ .../java/com/twelvemonkeys/util/FloatKey.java | 52 + .../com/twelvemonkeys/util/IgnoreCaseMap.java | 176 ++ .../com/twelvemonkeys/util/IntegerKey.java | 51 + .../com/twelvemonkeys/util/LRUHashMap.java | 214 ++ .../java/com/twelvemonkeys/util/LRUMap.java | 229 ++ .../com/twelvemonkeys/util/LinkedMap.java | 466 ++++ .../com/twelvemonkeys/util/LinkedSet.java | 82 + .../java/com/twelvemonkeys/util/NullMap.java | 117 + .../java/com/twelvemonkeys/util/PaintKey.java | 53 + .../twelvemonkeys/util/Rectangle2DKey.java | 53 + .../java/com/twelvemonkeys/util/Resource.java | 80 + .../util/ResourceChangeListener.java | 51 + .../twelvemonkeys/util/ResourceMonitor.java | 207 ++ .../com/twelvemonkeys/util/StringKey.java | 52 + .../util/StringTokenIterator.java | 329 +++ .../java/com/twelvemonkeys/util/Time.java | 181 ++ .../com/twelvemonkeys/util/TimeFormat.java | 449 ++++ .../com/twelvemonkeys/util/TimeoutMap.java | 449 ++++ .../com/twelvemonkeys/util/TokenIterator.java | 57 + .../java/com/twelvemonkeys/util/TypedMap.java | 320 +++ .../com/twelvemonkeys/util/URLResource.java | 89 + .../java/com/twelvemonkeys/util/Visitor.java | 19 + .../com/twelvemonkeys/util/WeakWeakMap.java | 216 ++ .../com/twelvemonkeys/util/XMLProperties.java | 1287 ++++++++++ .../util/convert/ConversionException.java | 96 + .../twelvemonkeys/util/convert/Converter.java | 193 ++ .../util/convert/ConverterImpl.java | 135 ++ .../util/convert/DateConverter.java | 152 ++ .../util/convert/DefaultConverter.java | 135 ++ .../util/convert/MissingTypeException.java | 56 + .../NoAvailableConverterException.java | 57 + .../util/convert/NumberConverter.java | 208 ++ .../util/convert/PropertyConverter.java | 75 + .../util/convert/TimeConverter.java | 136 ++ .../util/convert/TypeMismathException.java | 49 + .../twelvemonkeys/util/convert/pacakge.html | 8 + .../java/com/twelvemonkeys/util/package.html | 7 + .../util/regex/REWildcardStringParser.java | 398 +++ .../util/regex/RegExTokenIterator.java | 105 + .../util/regex/WildcardStringParser.java | 803 ++++++ .../com/twelvemonkeys/util/regex/package.html | 10 + .../util/service/RegisterableService.java | 61 + .../service/ServiceConfigurationError.java | 52 + .../util/service/ServiceRegistry.java | 546 +++++ .../twelvemonkeys/util/service/package.html | 10 + .../com/twelvemonkeys/xml/DOMSerializer.java | 182 ++ .../java/com/twelvemonkeys/xml/XMLReader.java | 55 + .../com/twelvemonkeys/xml/XMLSerializer.java | 601 +++++ .../com/twelvemonkeys/net/MIMEUtil.properties | 111 + .../image/ImageUtilTestCase.java | 574 +++++ .../image/ResampleOpTestCase.java | 263 ++ .../io/CompoundReaderTestCase.java | 63 + .../io/FastByteArrayOutputStreamTestCase.java | 31 + .../io/FileCacheSeekableStreamTestCase.java | 26 + .../io/FileSeekableStreamTestCase.java | 72 + .../io/InputStreamAbstractTestCase.java | 399 +++ .../io/MemoryCacheSeekableStreamTestCase.java | 20 + .../io/OutputStreamAbstractTestCase.java | 236 ++ .../io/ReaderAbstractTestCase.java | 215 ++ .../io/SeekableAbstractTestCase.java | 23 + .../SeekableInputStreamAbstractTestCase.java | 480 ++++ .../io/SeekableInterfaceTest.java | 12 + .../io/StringArrayReaderTestCase.java | 52 + .../io/enc/Base64DecoderTestCase.java | 65 + .../io/enc/Base64EncoderTestCase.java | 71 + .../io/enc/DecoderAbstractTestCase.java | 99 + .../io/enc/DeflateDecoderTestCase.java | 23 + .../io/enc/EncoderAbstractTestCase.java | 88 + .../io/enc/InflateDecoderTestCase.java | 23 + .../io/enc/PackBitsDecoderTestCase.java | 23 + .../io/enc/PackBitsEncoderTestCase.java | 23 + .../io/ole2/CompoundDocumentTestCase.java | 77 + .../twelvemonkeys/lang/BeanUtilTestCase.java | 228 ++ .../lang/ObjectAbstractTestCase.java | 321 +++ .../lang/StringUtilTestCase.java | 861 +++++++ .../twelvemonkeys/net/NetUtilTestCase.java | 59 + .../twelvemonkeys/util/BeanMapTestCase.java | 146 ++ .../util/CollectionAbstractTestCase.java | 1308 ++++++++++ .../twelvemonkeys/util/LRUMapTestCase.java | 182 ++ .../twelvemonkeys/util/LinkedMapTestCase.java | 175 ++ .../util/MapAbstractTestCase.java | 1660 +++++++++++++ .../twelvemonkeys/util/NullMapTestCase.java | 187 ++ .../util/ObjectAbstractTestCase.java | 307 +++ .../util/SetAbstractTestCase.java | 183 ++ .../util/StringTokenIteratorTestCase.java | 111 + .../util/TimeoutMapTestCase.java | 632 +++++ .../util/TokenIteratorAbstractTestCase.java | 52 + .../util/convert/ConverterTestCase.java | 18 + .../util/convert/DateConverterTestCase.java | 58 + .../convert/DefaultConverterTestCase.java | 71 + .../util/convert/NumberConverterTestCase.java | 42 + .../PropertyConverterAbstractTestCase.java | 99 + .../util/convert/TimeConverterTestCase.java | 19 + .../regex/RegExTokenIteratorTestCase.java | 122 + .../src/test/resources/Thumbs-camera.db | Bin 0 -> 79360 bytes .../src/test/resources/sunflower.jpg | Bin 0 -> 25094 bytes twelvemonkeys-core/todo.txt | 10 + 213 files changed, 52695 insertions(+) create mode 100644 twelvemonkeys-core/pom.xml create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveTester.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/CopyDither.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayColorModel.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayFilter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageConversionException.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/IndexImage.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/Magick.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MonochromeColorModel.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/PixelizeOp.java create mode 100644 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ResampleOp.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/SubsampleTester.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/SubsamplingFilter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/inv_cmap.c create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/package-info.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/AbstractCachedSeekableStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/CompoundReader.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/EmptyReader.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FastByteArrayOutputStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileCacheSeekableStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSeekableStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSystem.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FilenameMaskFilter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FilenameSuffixFilter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianDataOutputStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianRandomAccessFile.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/MemoryCacheSeekableStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullInputStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullOutputStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/RandomAccessStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Seekable.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableInputStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableOutputStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/StringArrayReader.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SubStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/UnixFileSystem.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32File.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32FileSystem.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32Lnk.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/WriterOutputStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecodeException.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Decoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Encoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWDecoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWEncoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/package-info.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/CorruptDocumentException.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/Entry.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/package-info.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/package.html create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/todo.txt create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/BeanUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/DateUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MathUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MostUnfortunateException.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeLoader.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeResourceSPI.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Platform.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ReflectUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/StringUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/SystemUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Validate.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/package.html create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/MIMEUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/NetUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/package.html create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractResource.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BeanMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BooleanKey.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/CollectionUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/DebugUtil.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/DuplicateHandler.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ExpiringMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FileResource.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FilterIterator.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FloatKey.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/IgnoreCaseMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/IntegerKey.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUHashMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedSet.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/NullMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/PaintKey.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Rectangle2DKey.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Resource.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ResourceChangeListener.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ResourceMonitor.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/StringKey.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/StringTokenIterator.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Time.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeFormat.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeoutMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TokenIterator.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TypedMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/URLResource.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Visitor.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/WeakWeakMap.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/XMLProperties.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/ConversionException.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/Converter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/ConverterImpl.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/DateConverter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/MissingTypeException.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/NoAvailableConverterException.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/NumberConverter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/PropertyConverter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/TimeConverter.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/TypeMismathException.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/pacakge.html create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/package.html create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/REWildcardStringParser.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/RegExTokenIterator.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/WildcardStringParser.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/package.html create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/RegisterableService.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceConfigurationError.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceRegistry.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/package.html create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/DOMSerializer.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/XMLReader.java create mode 100755 twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java create mode 100755 twelvemonkeys-core/src/main/resources/com/twelvemonkeys/net/MIMEUtil.properties create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/image/ResampleOpTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/CompoundReaderTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FastByteArrayOutputStreamTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileCacheSeekableStreamTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileSeekableStreamTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/MemoryCacheSeekableStreamTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/OutputStreamAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/ReaderAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInterfaceTest.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/StringArrayReaderTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/Base64DecoderTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DeflateDecoderTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/PackBitsDecoderTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/PackBitsEncoderTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocumentTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/ObjectAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/StringUtilTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/BeanMapTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/CollectionAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/LRUMapTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/LinkedMapTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/MapAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/NullMapTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/ObjectAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/SetAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/StringTokenIteratorTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TimeoutMapTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TokenIteratorAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/ConverterTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/DateConverterTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/NumberConverterTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/PropertyConverterAbstractTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/TimeConverterTestCase.java create mode 100755 twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/regex/RegExTokenIteratorTestCase.java create mode 100755 twelvemonkeys-core/src/test/resources/Thumbs-camera.db create mode 100755 twelvemonkeys-core/src/test/resources/sunflower.jpg create mode 100755 twelvemonkeys-core/todo.txt diff --git a/twelvemonkeys-core/pom.xml b/twelvemonkeys-core/pom.xml new file mode 100644 index 00000000..86735787 --- /dev/null +++ b/twelvemonkeys-core/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + twelvemonkeys-core + com.twelvemonkeys + 2.2-SNAPSHOT + TwelveMonkeys Core + + The TwelveMonkeys Core library. Contains common utility classes. + + + + com.twelvemonkeys + twelvemonkeys-parent + 2.0 + + + + + jmagick + jmagick + 6.2.4 + provided + true + + + + junit + junit + 4.3.1 + test + + + + jmock + jmock-cglib + 1.0.1 + test + + + + + + + maven-source-plugin + + + + maven-resources-plugin + + UTF-8 + + + + + \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java new file mode 100755 index 00000000..7b8a3b46 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AbstractImageSource.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.image.ImageProducer; +import java.awt.image.ImageConsumer; +import java.util.List; +import java.util.ArrayList; + +/** + * 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 mConsumers = new ArrayList(); + protected int mWidth; + protected int mHeight; + protected int mXOff; + protected int mYOff; + + // ImageProducer interface + public void addConsumer(ImageConsumer pConsumer) { + if (mConsumers.contains(pConsumer)) { + return; + } + mConsumers.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(ImageConsumer pConsumer) { + mConsumers.remove(pConsumer); + } + + /** + * This implementation silently ignores this instruction. If pixeldata is + * not in TDLR order by default, subclasses must override this method. + * + * @param pConsumer the consumer that requested the resend + * + * @see ImageProducer#requestTopDownLeftRightResend(java.awt.image.ImageConsumer) + */ + public void requestTopDownLeftRightResend(ImageConsumer pConsumer) { + // ignore + } + + public void startProduction(ImageConsumer pConsumer) { + addConsumer(pConsumer); + } + + public boolean isConsumer(ImageConsumer pConsumer) { + return mConsumers.contains(pConsumer); + } + + protected abstract void initConsumer(ImageConsumer pConsumer); + + protected abstract void sendPixels(ImageConsumer pConsumer); +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java new file mode 100755 index 00000000..1ece4bd4 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/AreaAverageOp.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.*; +import java.io.File; +import java.io.IOException; + +/** + * AreaAverageOp + * + * @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/AreaAverageOp.java#2 $ + */ +public class AreaAverageOp implements BufferedImageOp, RasterOp { + + final private int mWidth; + final private int mHeight; + + private Rectangle mSourceRegion; + + public AreaAverageOp(final int pWidth, final int pHeight) { + mWidth = pWidth; + mHeight = pHeight; + } + + public Rectangle getSourceRegion() { + if (mSourceRegion == null) { + return null; + } + return new Rectangle(mSourceRegion); + } + + public void setSourceRegion(final Rectangle pSourceRegion) { + if (pSourceRegion == null) { + mSourceRegion = null; + } + else { + if (mSourceRegion == null) { + mSourceRegion = new Rectangle(pSourceRegion); + } + else { + mSourceRegion.setBounds(pSourceRegion); + } + } + } + + public BufferedImage filter(BufferedImage src, BufferedImage dest) { + BufferedImage result = dest != null ? dest : createCompatibleDestImage(src, null); + + // TODO: src and dest can't be the same + + // TODO: Do some type checking here.. + // Should work with + // * all BYTE types, unless sub-byte packed rasters/IndexColorModel + // * all INT types (even custom, as long as they use 8bit/componnet) + // * all USHORT types (even custom) + + // TODO: Also check if the images are really compatible!? + + long start = System.currentTimeMillis(); + // Straight-forward version + //Image scaled = src.getScaledInstance(mWidth, mHeight, Image.SCALE_AREA_AVERAGING); + //ImageUtil.drawOnto(result, scaled); + //result = new BufferedImageFactory(scaled).getBufferedImage(); + + /* + // Try: Use bilinear/bicubic and half the image down until it's less than + // twice as big, then use bicubic for the last step? + BufferedImage temp = null; + AffineTransform xform = null; + int w = src.getWidth(); + int h = src.getHeight(); + while (w / 2 > mWidth && h / 2 > mHeight) { + w /= 2; + h /= 2; + + if (temp == null) { + xform = AffineTransform.getScaleInstance(.5, .5); + ColorModel cm = src.getColorModel(); + temp = new BufferedImage(cm, + ImageUtil.createCompatibleWritableRaster(src, cm, w, h), + cm.isAlphaPremultiplied(), null); + + resample(src, temp, xform); + } + else { + resample(temp, temp, xform); + } + + System.out.println("w: " + w); + System.out.println("h: " + h); + } + + if (temp != null) { + src = temp.getSubimage(0, 0, w, h); + } + + resample(src, result, AffineTransform.getScaleInstance(mWidth / (double) w, mHeight / (double) h)); + */ + + // The real version + filterImpl(src.getRaster(), result.getRaster()); + + long time = System.currentTimeMillis() - start; + System.out.println("time: " + time); + + return result; + } + + private void resample(final BufferedImage pSrc, final BufferedImage pDest, final AffineTransform pXform) { + Graphics2D d = pDest.createGraphics(); + d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + try { + d.drawImage(pSrc, pXform, null); + } + finally { + d.dispose(); + } + } + + public WritableRaster filter(Raster src, WritableRaster dest) { + WritableRaster result = dest != null ? dest : createCompatibleDestRaster(src); + return filterImpl(src, result); + } + + private WritableRaster filterImpl(Raster src, WritableRaster dest) { + //System.out.println("src: " + src); + //System.out.println("dest: " + dest); + if (mSourceRegion != null) { + int cx = mSourceRegion.x; + int cy = mSourceRegion.y; + int cw = mSourceRegion.width; + int ch = mSourceRegion.height; + + boolean same = src == dest; + dest = dest.createWritableChild(cx, cy, cw, ch, 0, 0, null); + src = same ? dest : src.createChild(cx, cy, cw, ch, 0, 0, null); + //System.out.println("src: " + src); + //System.out.println("dest: " + dest); + } + + final int width = src.getWidth(); + final int height = src.getHeight(); + + // TODO: This don't work too well.. + // The thing is that the step length and the scan length will vary, for + // non-even (1/2, 1/4, 1/8 etc) resampling + int widthSteps = (width + mWidth - 1) / mWidth; + int heightSteps = (height + mHeight - 1) / mHeight; + + final boolean oddX = width % mWidth != 0; + final boolean oddY = height % mHeight != 0; + + final int dataElements = src.getNumDataElements(); + final int bands = src.getNumBands(); + final int dataType = src.getTransferType(); + + Object data = null; + int scanW; + int scanH; + + // TYPE_USHORT setup + int[] bitMasks = null; + int[] bitOffsets = null; + if (src.getTransferType() == DataBuffer.TYPE_USHORT) { + if (src.getSampleModel() instanceof SinglePixelPackedSampleModel) { + // DIRECT + SinglePixelPackedSampleModel sampleModel = (SinglePixelPackedSampleModel) src.getSampleModel(); + bitMasks = sampleModel.getBitMasks(); + bitOffsets = sampleModel.getBitOffsets(); + } + else { + // GRAY + bitMasks = new int[]{0xffff}; + bitOffsets = new int[]{0}; + } + } + + for (int y = 0; y < mHeight; y++) { + if (!oddY || y < mHeight) { + scanH = heightSteps; + } + else { + scanH = height - (y * heightSteps); + } + + for (int x = 0; x < mWidth; x++) { + if (!oddX || x < mWidth) { + scanW = widthSteps; + } + else { + scanW = width - (x * widthSteps); + } + final int pixelCount = scanW * scanH; + final int pixelLength = pixelCount * dataElements; + + try { + data = src.getDataElements(x * widthSteps, y * heightSteps, scanW, scanH, data); + } + catch (IndexOutOfBoundsException e) { + // TODO: FixMe! + // The bug is in the steps... + //System.err.println("x: " + x); + //System.err.println("y: " + y); + //System.err.println("widthSteps: " + widthSteps); + //System.err.println("heightSteps: " + heightSteps); + //System.err.println("scanW: " + scanW); + //System.err.println("scanH: " + scanH); + // + //System.err.println("width: " + width); + //System.err.println("height: " + height); + //System.err.println("mWidth: " + mWidth); + //System.err.println("mHeight: " + mHeight); + // + //e.printStackTrace(); + continue; + } + + // TODO: Might need more channels... Use an array? + // NOTE: These are not neccessarily ARGB.. + double valueA = 0.0; + double valueR = 0.0; + double valueG = 0.0; + double valueB = 0.0; + + switch (dataType) { + case DataBuffer.TYPE_BYTE: + // TODO: Doesn't hold for index color models... + // For index color, the best bet is probably convert to + // true color, then convert back to the same index color + // model + byte[] bytePixels = (byte[]) data; + for (int i = 0; i < pixelLength; i += dataElements) { + valueA += bytePixels[i] & 0xff; + if (bands > 1) { + valueR += bytePixels[i + 1] & 0xff; + valueG += bytePixels[i + 2] & 0xff; + if (bands > 3) { + valueB += bytePixels[i + 3] & 0xff; + } + } + } + + // Average + valueA /= pixelCount; + if (bands > 1) { + valueR /= pixelCount; + valueG /= pixelCount; + if (bands > 3) { + valueB /= pixelCount; + } + } + + //for (int i = 0; i < pixelLength; i += dataElements) { + bytePixels[0] = (byte) clamp((int) valueA); + if (bands > 1) { + bytePixels[1] = (byte) clamp((int) valueR); + bytePixels[2] = (byte) clamp((int) valueG); + if (bands > 3) { + bytePixels[3] = (byte) clamp((int) valueB); + } + } + //} + break; + + case DataBuffer.TYPE_INT: + int[] intPixels = (int[]) data; + // TODO: Rewrite to use bit offsets and masks from + // color model (see TYPE_USHORT) in case of a non- + // 888 or 8888 colormodel? + for (int i = 0; i < pixelLength; i += dataElements) { + valueA += (intPixels[i] & 0xff000000) >> 24; + valueR += (intPixels[i] & 0xff0000) >> 16; + valueG += (intPixels[i] & 0xff00) >> 8; + valueB += (intPixels[i] & 0xff); + } + + // Average + valueA /= pixelCount; + valueR /= pixelCount; + valueG /= pixelCount; + valueB /= pixelCount; + + //for (int i = 0; i < pixelLength; i += dataElements) { + intPixels[0] = clamp((int) valueA) << 24; + intPixels[0] |= clamp((int) valueR) << 16; + intPixels[0] |= clamp((int) valueG) << 8; + intPixels[0] |= clamp((int) valueB); + //} + break; + + case DataBuffer.TYPE_USHORT: + if (bitMasks != null) { + short[] shortPixels = (short[]) data; + for (int i = 0; i < pixelLength; i += dataElements) + { + valueA += (shortPixels[i] & bitMasks[0]) >> bitOffsets[0]; + if (bitMasks.length > 1) { + valueR += (shortPixels[i] & bitMasks[1]) >> bitOffsets[1]; + valueG += (shortPixels[i] & bitMasks[2]) >> bitOffsets[2]; + if (bitMasks.length > 3) { + valueB += (shortPixels[i] & bitMasks[3]) >> bitOffsets[3]; + } + } + } + + // Average + valueA /= pixelCount; + valueR /= pixelCount; + valueG /= pixelCount; + valueB /= pixelCount; + + //for (int i = 0; i < pixelLength; i += dataElements) { + shortPixels[0] = (short) (((int) valueA << bitOffsets[0]) & bitMasks[0]); + if (bitMasks.length > 1) { + shortPixels[0] |= (short) (((int) valueR << bitOffsets[1]) & bitMasks[1]); + shortPixels[0] |= (short) (((int) valueG << bitOffsets[2]) & bitMasks[2]); + if (bitMasks.length > 3) { + shortPixels[0] |= (short) (((int) valueB << bitOffsets[3]) & bitMasks[3]); + } + } + //} + break; + } + default: + throw new IllegalArgumentException("TransferType not supported: " + dataType); + + } + + dest.setDataElements(x, y, 1, 1, data); + } + } + + return dest; + } + + private static int clamp(final int pValue) { + return pValue > 255 ? 255 : pValue; + } + + public RenderingHints getRenderingHints() { + return null; + } + + // TODO: Refactor boilerplate to AbstractBufferedImageOp or use a delegate? + // Delegate is maybe better as we won't always implement both BIOp and RasterOP + // (but are there ever any time we want to implemnet RasterOp and not BIOp?) + public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { + ColorModel cm = destCM != null ? destCM : src.getColorModel(); + return new BufferedImage(cm, + ImageUtil.createCompatibleWritableRaster(src, cm, mWidth, mHeight), + cm.isAlphaPremultiplied(), null); + } + + public WritableRaster createCompatibleDestRaster(Raster src) { + return src.createCompatibleWritableRaster(mWidth, mHeight); + } + + public Rectangle2D getBounds2D(Raster src) { + return new Rectangle(mWidth, mHeight); + } + + public Rectangle2D getBounds2D(BufferedImage src) { + return new Rectangle(mWidth, mHeight); + } + + public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { + // TODO: This is wrong! + if (dstPt == null) { + if (srcPt instanceof Point2D.Double) { + dstPt = new Point2D.Double(); + } + else { + dstPt = new Point2D.Float(); + } + } + dstPt.setLocation(srcPt); + + return dstPt; + } + + public static void main(String[] pArgs) throws IOException { + BufferedImage image = ImageIO.read(new File("2006-Lamborghini-Gallardo-Spyder-Y-T-1600x1200.png")); + //BufferedImage image = ImageIO.read(new File("focus-rs.jpg")); + //BufferedImage image = ImageIO.read(new File("blauesglas_16_bitmask444.bmp")); + //image = ImageUtil.toBuffered(image, BufferedImage.TYPE_USHORT_GRAY); + + for (int i = 0; i < 100; i++) { + //new PixelizeOp(10).filter(image, null); + //new AffineTransformOp(AffineTransform.getScaleInstance(.1, .1), AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(image, null); + //ImageUtil.toBuffered(image.getScaledInstance(image.getWidth() / 4, image.getHeight() / 4, Image.SCALE_AREA_AVERAGING)); + //new ResampleOp(image.getWidth() / 10, image.getHeight() / 10, ResampleOp.FILTER_BOX).filter(image, null); + //new ResampleOp(image.getWidth() / 10, image.getHeight() / 10, ResampleOp.FILTER_QUADRATIC).filter(image, null); + //new AreaAverageOp(image.getWidth() / 10, image.getHeight() / 10).filter(image, null); + } + + long start = System.currentTimeMillis(); + //PixelizeOp pixelizer = new PixelizeOp(image.getWidth() / 10, 1); + //pixelizer.setSourceRegion(new Rectangle(0, 2 * image.getHeight() / 3, image.getWidth(), image.getHeight() / 4)); + //PixelizeOp pixelizer = new PixelizeOp(4); + //image = pixelizer.filter(image, image); // Filter in place, that's cool + //image = new AffineTransformOp(AffineTransform.getScaleInstance(.25, .25), AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(image, null); + //image = ImageUtil.toBuffered(image.getScaledInstance(image.getWidth() / 4, image.getHeight() / 4, Image.SCALE_AREA_AVERAGING)); + //image = new ResampleOp(image.getWidth() / 4, image.getHeight() / 4, ResampleOp.FILTER_BOX).filter(image, null); + //image = new ResampleOp(image.getWidth() / 4, image.getHeight() / 4, ResampleOp.FILTER_QUADRATIC).filter(image, null); + //image = new AreaAverageOp(image.getWidth() / 7, image.getHeight() / 4).filter(image, null); + image = new AreaAverageOp(500, 600).filter(image, null); + //image = new ResampleOp(500, 600, ResampleOp.FILTER_BOX).filter(image, null); + long time = System.currentTimeMillis() - start; + + System.out.println("time: " + time + " ms"); + + JFrame frame = new JFrame("Test"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setContentPane(new JScrollPane(new JLabel(new BufferedImageIcon(image)))); + frame.pack(); + frame.setVisible(true); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java new file mode 100755 index 00000000..97664a38 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BrightnessContrastFilter.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.image.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 { + + // This filter can filter IndexColorModel, as it is does not depend on + // the pixels' location + { + canFilterIndexColorModel = true; + } + + // Use a precalculated lookup table for performace + private int[] mLUT = null; + + /** + * 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) { + mLUT = 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 = mLUT[r]; + g = mLUT[g]; + b = mLUT[b]; + + // Return ARGB pixel, leave transparency as is + return (pARGB & 0xFF000000) | (r << 16) | (g << 8) | b; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java new file mode 100755 index 00000000..06154aef --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageFactory.java @@ -0,0 +1,543 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.*; +import java.awt.image.*; +import java.util.*; +import java.util.List; +import java.lang.reflect.Array; + +/** + * A faster, lighter and easier way to convert an {@code Image} to a + * {@code BufferedImage} than using a {@code PixelGrabber}. + * Clients may provide progress listeners to monitor conversion progress. + *

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

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

{@code + * short value = (short) (intValue & 0x0000ffff); + * }
+ * + * @param pPixels the pixel data to convert + * @return an array of {@code short}s, same lenght as {@code pPixels} + */ + private static short[] toShortPixels(int[] pPixels) { + short[] pixels = new short[pPixels.length]; + for (int i = 0; i < pixels.length; i++) { + pixels[i] = (short) (pPixels[i] & 0xffff); + } + return pixels; + } + + /** + * This interface allows clients of a {@code BufferedImageFactory} to + * receive notifications of decoding progress. + * + * @see BufferedImageFactory#addProgressListener + * @see BufferedImageFactory#removeProgressListener + */ + public static interface ProgressListener extends EventListener { + + /** + * Reports progress to this listener. + * Invoked by the {@code BufferedImageFactory} to report progress in + * the image decoding. + * + * @param pFactory the factory reporting the progress + * @param pPercentage the perccentage of progress + */ + void progress(BufferedImageFactory pFactory, float pPercentage); + } + + private class Consumer implements ImageConsumer { + /** + * Implementation of all setPixels methods. + * Note that this implementation assumes that all invocations for one + * image uses the same color model, and that the pixel data has the + * same type. + * + * @param pX x coordinate of pixel data region + * @param pY y coordinate of pixel data region + * @param pWidth width of pixel data region + * @param pHeight height of pixel data region + * @param pModel the color model of the pixel data + * @param pPixels the pixel data array + * @param pOffset the offset into the pixel data array + * @param pScanSize the scan size of the pixel data array + */ + private void setPixelsImpl(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, Object pPixels, int pOffset, int pScanSize) { + setColorModelOnce(pModel); + + if (pPixels == null) { + return; + } + + //System.out.println("Setting " + pPixels.getClass().getComponentType() + " pixels: " + Array.getLength(pPixels)); + + // Allocate array if neccessary + if (mSourcePixels == null) { + /* + System.out.println("ColorModel: " + pModel); + System.out.println("Scansize: " + pScanSize + " TrasferType: " + ImageUtil.getTransferType(pModel)); + System.out.println("Creating " + pPixels.getClass().getComponentType() + " array of length " + (mWidth * mHeight)); + */ + // Allocate a suitable source pixel array + // TODO: Should take pixel "width" into consideration, for byte packed rasters?! + // OR... Is anything but single-pixel models really supported by the API? + mSourcePixels = Array.newInstance(pPixels.getClass().getComponentType(), mWidth * mHeight); + mScanSize = mWidth; + mOffset = 0; + } + else if (mSourcePixels.getClass() != pPixels.getClass()) { + throw new IllegalStateException("Only one pixel type allowed"); + } + + // AOI stuff + if (pY < mY) { + int diff = mY - pY; + if (diff >= pHeight) { + return; + } + pOffset += pScanSize * diff; + pY += diff; + pHeight -= diff; + } + if (pY + pHeight > mY + mHeight) { + pHeight = (mY + mHeight) - pY; + if (pHeight <= 0) { + return; + } + } + + if (pX < mX) { + int diff = mX - pX; + if (diff >= pWidth) { + return; + } + pOffset += diff; + pX += diff; + pWidth -= diff; + } + if (pX + pWidth > mX + mWidth) { + pWidth = (mX + mWidth) - pX; + if (pWidth <= 0) { + return; + } + } + + int dstOffset = mOffset + (pY - mY) * mScanSize + (pX - mX); + + // Do the pixel copying + for (int i = pHeight; i > 0; i--) { + System.arraycopy(pPixels, pOffset, mSourcePixels, dstOffset, pWidth); + pOffset += pScanSize; + dstOffset += mScanSize; + } + + processProgress(pY + pHeight); + } + + /** {@code ImageConsumer} implementation, do not invoke directly */ + public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, short[] pPixels, int pOffset, int pScanSize) { + setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize); + } + + private void setColorModelOnce(ColorModel pModel) { + // NOTE: There seems to be a "bug" in AreaAveragingScaleFilter, as it + // first passes the original colormodel through in setColorModel, then + // later replaces it with the default RGB in the first setPixels call + // (this is probably allowed according to the spec, but it's a waste of + // time and space). + if (mSourceColorModel != pModel) { + if (/*mSourceColorModel == null ||*/ mSourcePixels == null) { + mSourceColorModel = pModel; + } + else { + throw new IllegalStateException("Change of ColorModel after pixel delivery not supported"); + } + } + + // If color model is all we ask for, stop now + if (mReadColorModelOnly) { + mConsumer.imageComplete(ImageConsumer.IMAGEABORTED); + } + } + + /** {@code ImageConsumer} implementation, do not invoke */ + public void imageComplete(int pStatus) { + mFetching = false; + + if (mProducer != null) { + mProducer.removeConsumer(this); + } + + switch (pStatus) { + case IMAGEERROR: + new Error().printStackTrace(); + mError = true; + break; + } + + synchronized (BufferedImageFactory.this) { + BufferedImageFactory.this.notifyAll(); + } + } + + /** {@code ImageConsumer} implementation, do not invoke directly */ + public void setColorModel(ColorModel pModel) { + //System.out.println("SetColorModel: " + pModel); + setColorModelOnce(pModel); + } + + /** {@code ImageConsumer} implementation, do not invoke directly */ + public void setDimensions(int pWidth, int pHeight) { + //System.out.println("Setting dimensions: " + pWidth + ", " + pHeight); + if (mWidth < 0) { + mWidth = pWidth - mX; + } + if (mHeight < 0) { + mHeight = pHeight - mY; + } + + // Hmm.. Special case, but is it a good idea? + if (mWidth <= 0 || mHeight <= 0) { + imageComplete(STATICIMAGEDONE); + } + } + + /** {@code ImageConsumer} implementation, do not invoke directly */ + public void setHints(int pHintflags) { + // ignore + } + + /** {@code ImageConsumer} implementation, do not invoke directly */ + public void setPixels(int pX, int pY, int pWidth, int pHeight, ColorModel pModel, byte[] pPixels, int pOffset, int pScanSize) { + /*if (pModel.getPixelSize() < 8) { + // Byte packed + setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toBytePackedPixels(pPixels, pModel.getPixelSize()), pOffset, pScanSize); + } + /* + else if (pModel.getPixelSize() > 8) { + // Byte interleaved + setPixelsImpl(pX, pY, pWidth, pHeight, pModel, toByteInterleavedPixels(pPixels), pOffset, pScanSize); + } + */ + //else { + // Default, pixelSize == 8, one byte pr pixel + setPixelsImpl(pX, pY, pWidth, pHeight, pModel, pPixels, pOffset, pScanSize); + //} + } + + /** {@code ImageConsumer} implementation, do not invoke directly */ + public void setPixels(int pX, int pY, int pWeigth, int pHeight, ColorModel pModel, int[] pPixels, int pOffset, int pScanSize) { + if (ImageUtil.getTransferType(pModel) == DataBuffer.TYPE_USHORT) { + // NOTE: Workaround for limitation in ImageConsumer API + // Convert int[] to short[], to be compatible with the ColorModel + setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, toShortPixels(pPixels), pOffset, pScanSize); + } + else { + setPixelsImpl(pX, pY, pWeigth, pHeight, pModel, pPixels, pOffset, pScanSize); + } + } + + /** {@code ImageConsumer} implementation, do not invoke directly */ + public void setProperties(Hashtable pProperties) { + mSourceProperties = pProperties; + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java new file mode 100755 index 00000000..d254cdff --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/BufferedImageIcon.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import javax.swing.Icon; +import java.awt.image.BufferedImage; +import java.awt.*; +import java.awt.geom.AffineTransform; + +/** + * 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 mImage; + private int mWidth; + private int mHeight; + private final boolean mFast; + + public BufferedImageIcon(BufferedImage pImage) { + this(pImage, pImage.getWidth(), pImage.getHeight()); + } + + public BufferedImageIcon(BufferedImage pImage, int pWidth, int pHeight) { + if (pImage == null) { + throw new IllegalArgumentException("image == null"); + } + if (pWidth <= 0 || pHeight <= 0) { + throw new IllegalArgumentException("Icon size must be positive"); + } + + mImage = pImage; + mWidth = pWidth; + mHeight = pHeight; + + mFast = pImage.getWidth() == mWidth && pImage.getHeight() == mHeight; + } + + public int getIconHeight() { + return mHeight; + } + + public int getIconWidth() { + return mWidth; + } + + public void paintIcon(Component c, Graphics g, int x, int y) { + if (mFast || !(g instanceof Graphics2D)) { + //System.out.println("Scaling fast"); + g.drawImage(mImage, x, y, mWidth, mHeight, null); + } + else { + //System.out.println("Scaling using interpolation"); + Graphics2D g2 = (Graphics2D) g; + AffineTransform xform = AffineTransform.getTranslateInstance(x, y); + xform.scale(mWidth / (double) mImage.getWidth(), mHeight / (double) mImage.getHeight()); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(mImage, xform, null); + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveTester.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveTester.java new file mode 100755 index 00000000..437c6449 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveTester.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.Kernel; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; + +/** + * ConvolveTester + * + * @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/ConvolveTester.java#1 $ + */ +public class ConvolveTester { + + // Initial sample timings (avg, 1000 iterations) + // PNG, type 0: JPEG, type 3: + // ZERO_FILL: 5.4 ms 4.6 ms + // NO_OP: 5.4 ms 4.6 ms + // REFLECT: 42.4 ms 24.9 ms + // WRAP: 86.9 ms 29.5 ms + + final static int ITERATIONS = 1000; + + public static void main(String[] pArgs) throws IOException { + File input = new File(pArgs[0]); + BufferedImage image = ImageIO.read(input); + BufferedImage result = null; + + System.out.println("image: " + image); + + if (pArgs.length > 1) { + float ammount = Float.parseFloat(pArgs[1]); + + int edgeOp = pArgs.length > 2 ? Integer.parseInt(pArgs[2]) : ImageUtil.EDGE_REFLECT; + + long start = System.currentTimeMillis(); + for (int i = 0; i < ITERATIONS; i++) { + result = sharpen(image, ammount, edgeOp); + } + long end = System.currentTimeMillis(); + System.out.println("Time: " + ((end - start) / (double) ITERATIONS) + "ms"); + + showIt(result, "Sharpened " + ammount + " " + input.getName()); + } + else { + showIt(image, "Original " + input.getName()); + } + + } + + public static void showIt(final BufferedImage pImage, final String pTitle) { + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + JFrame frame = new JFrame(pTitle); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setLocationByPlatform(true); + JPanel pane = new JPanel(new BorderLayout()); + GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); + BufferedImageIcon icon = new BufferedImageIcon(ImageUtil.accelerate(pImage, gc)); + JScrollPane scroll = new JScrollPane(new JLabel(icon)); + scroll.setBorder(null); + pane.add(scroll); + frame.setContentPane(pane); + frame.pack(); + frame.setVisible(true); + } + }); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + static BufferedImage sharpen(BufferedImage pOriginal, final float pAmmount, int pEdgeOp) { + if (pAmmount == 0f) { + return pOriginal; + } + + // Create the convolution matrix + float[] data = new float[]{ + 0.0f, -pAmmount, 0.0f, + -pAmmount, 4f * pAmmount + 1f, -pAmmount, + 0.0f, -pAmmount, 0.0f + }; + + // Do the filtering + return ImageUtil.convolve(pOriginal, new Kernel(3, 3, data), pEdgeOp); + + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java new file mode 100755 index 00000000..0e377f6c --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ConvolveWithEdgeOp.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.image.*; +import java.awt.*; +import java.awt.geom.Rectangle2D; +import java.awt.geom.Point2D; + +/** + * This class implements a convolution from the source + * to the destination. + * + * @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/ConvolveWithEdgeOp.java#1 $ + * + * @see java.awt.image.ConvolveOp + */ +public class ConvolveWithEdgeOp implements BufferedImageOp, RasterOp { + + /** + * Alias for {@link ConvolveOp#EDGE_ZERO_FILL}. + * @see #EDGE_REFLECT + */ + public static final int EDGE_ZERO_FILL = ConvolveOp.EDGE_ZERO_FILL; + /** + * Alias for {@link ConvolveOp#EDGE_NO_OP}. + * @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. + */ + 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 #EDGE_REFLECT + */ + public static final int EDGE_WRAP = 3; // as JAI BORDER_WRAP + + private final Kernel mKernel; + private final int mEdgeCondition; + + private final ConvolveOp mConvolve; + + public ConvolveWithEdgeOp(final Kernel pKernel, final int pEdgeCondition, final RenderingHints pHints) { + // Create convolution operation + int edge; + switch (pEdgeCondition) { + case EDGE_REFLECT: + case EDGE_WRAP: + edge = ConvolveOp.EDGE_NO_OP; + break; + default: + edge = pEdgeCondition; + break; + } + mKernel = pKernel; + mEdgeCondition = pEdgeCondition; + mConvolve = new ConvolveOp(pKernel, edge, pHints); + } + + public ConvolveWithEdgeOp(final Kernel pKernel) { + this(pKernel, EDGE_ZERO_FILL, null); + } + + public BufferedImage filter(BufferedImage pSource, BufferedImage pDestination) { + if (pSource == null) { + throw new NullPointerException("source image is null"); + } + if (pSource == pDestination) { + throw new IllegalArgumentException("source image cannot be the same as the destination image"); + } + + int borderX = mKernel.getWidth() / 2; + int borderY = mKernel.getHeight() / 2; + + BufferedImage original = addBorder(pSource, borderX, borderY); + + // 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 destination = pDestination; + if (original.getType() == BufferedImage.TYPE_3BYTE_BGR) { + destination = ImageUtil.createBuffered( + pSource.getWidth(), pSource.getHeight(), + pSource.getType(), pSource.getColorModel().getTransparency(), + null + ); + } + + // Do the filtering (if destination is null, a new image will be created) + destination = mConvolve.filter(original, destination); + + if (pSource != original) { + // Remove the border + destination = destination.getSubimage(borderX, borderY, pSource.getWidth(), pSource.getHeight()); + } + + return destination; + } + + private BufferedImage addBorder(final BufferedImage pOriginal, final int pBorderX, final int pBorderY) { + if ((mEdgeCondition & 2) == 0) { + return pOriginal; + } + + // 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 (mEdgeCondition) { + 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 " + mEdgeCondition); + } + + } + finally { + g.dispose(); + } + + return bordered; + } + + /** + * Returns the edge condition. + * @return the edge condition of this {@code ConvolveOp}. + * @see #EDGE_NO_OP + * @see #EDGE_ZERO_FILL + * @see #EDGE_REFLECT + * @see #EDGE_WRAP + */ + public int getEdgeCondition() { + return mEdgeCondition; + } + + public WritableRaster filter(final Raster pSource, final WritableRaster pDestination) { + return mConvolve.filter(pSource, pDestination); + } + + public BufferedImage createCompatibleDestImage(final BufferedImage pSource, final ColorModel pDesinationColorModel) { + return mConvolve.createCompatibleDestImage(pSource, pDesinationColorModel); + } + + public WritableRaster createCompatibleDestRaster(final Raster pSource) { + return mConvolve.createCompatibleDestRaster(pSource); + } + + public Rectangle2D getBounds2D(final BufferedImage pSource) { + return mConvolve.getBounds2D(pSource); + } + + public Rectangle2D getBounds2D(final Raster pSource) { + return mConvolve.getBounds2D(pSource); + } + + public Point2D getPoint2D(final Point2D pSourcePoint, final Point2D pDestinationPoint) { + return mConvolve.getPoint2D(pSourcePoint, pDestinationPoint); + } + + public RenderingHints getRenderingHints() { + return mConvolve.getRenderingHints(); + } + + public Kernel getKernel() { + return mConvolve.getKernel(); + } + +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/CopyDither.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/CopyDither.java new file mode 100755 index 00000000..0e4b3e6e --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/CopyDither.java @@ -0,0 +1,298 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.*; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.awt.image.Raster; +import java.awt.image.RasterOp; +import java.awt.image.WritableRaster; + +/** + * This BufferedImageOp simply copies pixels, converting to a + * {@code IndexColorModel}. + + * @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/CopyDither.java#1 $ + * + */ +public class CopyDither implements BufferedImageOp, RasterOp { + + protected IndexColorModel mIndexColorModel = null; + + /** + * Creates a {@code CopyDither}, using the given + * {@code IndexColorModel} for dithering into. + * + * @param pICM an IndexColorModel. + */ + public CopyDither(IndexColorModel pICM) { + // Store colormodel + mIndexColorModel = pICM; + } + + /** + * Creates a {@code CopyDither}, with no fixed + * {@code IndexColorModel}. The colormodel will be generated for each + * filtering, unless the dest image allready has an + * {@code IndexColorModel}. + */ + public CopyDither() { + } + + + /** + * 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, + mIndexColorModel); + } + 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 + * + * @return a {@code WritableRaster} + */ + public final WritableRaster createCompatibleDestRaster(Raster pSrc) { + return createCompatibleDestRaster(pSrc, getICM(pSrc)); + } + + public final WritableRaster createCompatibleDestRaster(Raster pSrc, + IndexColorModel pIndexColorModel) { + /* + return new BufferedImage(pSrc.getWidth(), pSrc.getHeight(), + BufferedImage.TYPE_BYTE_INDEXED, + pIndexColorModel).getRaster(); + */ + 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 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 destiantion 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 + * @param pDest + * + * @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 (mIndexColorModel != null ? mIndexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK | IndexImage.COLOR_SELECTION_QUALITY)); + } + private IndexColorModel getICM(Raster pSource) { + return (mIndexColorModel != null ? mIndexColorModel : 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 | IndexImage.COLOR_SELECTION_QUALITY); + } + + /** + * Performs a single-input/single-output pixel copy operation. + * + * @param pSource + * @param pDest + * @param pColorModel + * + * @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(); + + if (pDest == null) { + pDest = createCompatibleDestRaster(pSource, pColorModel); + } + + // temp buffers + final int[] inRGB = new int[4]; + Object pixel = null; + + // TODO: Use getPixels instead of getPixel for better performance? + + // Loop through image data + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // Get rgb from original raster + // DON'T KNOW IF THIS WILL WORK FOR ALL TYPES..? + pSource.getPixel(x, y, inRGB); + + // Get pixel value... + // It is VERY important that we are using an IndexColorModel that + // support reverse color lookup for speed. + pixel = pColorModel.getDataElements(toIntARGB(inRGB), pixel); + + // And set it + pDest.setDataElements(x, y, pixel); + } + } + return pDest; + } +} + diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java new file mode 100755 index 00000000..5d9c4e31 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java @@ -0,0 +1,465 @@ +package com.twelvemonkeys.image; + + +import java.awt.*; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.awt.image.Raster; +import java.awt.image.RasterOp; +import java.awt.image.WritableRaster; +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 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: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/DiffusionDither.java#1 $ + * + */ +public class DiffusionDither implements BufferedImageOp, RasterOp { + + protected IndexColorModel mIndexColorModel = null; + private boolean mAlternateScans = true; + private static final int FS_SCALE = 1 << 8; + private static final Random RANDOM = new Random(); + + /** + * Creates a {@code DiffusionDither}, using the given + * {@code IndexColorModel} for dithering into. + * + * @param pICM an IndexColorModel. + */ + public DiffusionDither(IndexColorModel pICM) { + // Store colormodel + mIndexColorModel = pICM; + } + + /** + * Creates a {@code DiffusionDither}, with no fixed + * {@code IndexColorModel}. The colormodel will be generated for each + * filtering, unless the dest image allready has an + * {@code IndexColorModel}. + */ + public DiffusionDither() { + } + + /** + * 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. + * + * @param pUse {@code true} if scan mode should be alternating left/right + */ + public void setAlternateScans(boolean pUse) { + mAlternateScans = 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 + * + * @return a {@code WritableRaster} + */ + public final WritableRaster createCompatibleDestRaster(Raster pSrc) { + return createCompatibleDestRaster(pSrc, getICM(pSrc)); + } + + public final WritableRaster createCompatibleDestRaster(Raster pSrc, + IndexColorModel pIndexColorModel) { + return pIndexColorModel.createCompatibleWritableRaster(pSrc.getWidth(), pSrc.getHeight()); + /* + return new BufferedImage(pSrc.getWidth(), pSrc.getHeight(), + BufferedImage.TYPE_BYTE_INDEXED, + pIndexColorModel).getRaster(); + */ + } + + + /** + * 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 destiantion 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 + * @param pDest + * + * @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 (mIndexColorModel != null ? mIndexColorModel : IndexImage.getIndexColorModel(pSource, 256, IndexImage.TRANSPARENCY_BITMASK)); + } + private IndexColorModel getICM(Raster pSource) { + return (mIndexColorModel != null ? mIndexColorModel : 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 + * @param pDest + * @param pColorModel + * + * @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[][] mCurrErr = new int[width + 2][3]; + int[][] mNextErr = 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 + /* + mCurrErr[i][0] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE; + mCurrErr[i][1] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE; + mCurrErr[i][2] = (Math.abs(RANDOM.nextInt()) % (FS_SCALE * 2)) - FS_SCALE; + */ + mCurrErr[i][0] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; + mCurrErr[i][1] = RANDOM.nextInt(FS_SCALE * 2) - FS_SCALE; + mCurrErr[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 = mNextErr.length; --i >= 0;) { + mNextErr[i][0] = 0; + mNextErr[i][1] = 0; + mNextErr[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) + mCurrErr[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) + mCurrErr[x + 2][0] += diff[0] * 7; + mCurrErr[x + 2][1] += diff[1] * 7; + mCurrErr[x + 2][2] += diff[2] * 7; + + // Row 2 (y + 1) + // Update error in this pixel (x - 1) + mNextErr[x][0] += diff[0] * 3; + mNextErr[x][1] += diff[1] * 3; + mNextErr[x][2] += diff[2] * 3; + // Update error in this pixel (x) + mNextErr[x + 1][0] += diff[0] * 5; + mNextErr[x + 1][1] += diff[1] * 5; + mNextErr[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 + mNextErr[x + 2][0] += diff[0]; // * 1; + mNextErr[x + 2][1] += diff[1]; // * 1; + mNextErr[x + 2][2] += diff[2]; // * 1; + + // Next + x++; + + // Done? + if (x >= limit) { + break; + } + + } + else { + // Row 1 (y) + // Update error in this pixel (x - 1) + mCurrErr[x][0] += diff[0] * 7; + mCurrErr[x][1] += diff[1] * 7; + mCurrErr[x][2] += diff[2] * 7; + + // Row 2 (y + 1) + // Update error in this pixel (x + 1) + mNextErr[x + 2][0] += diff[0] * 3; + mNextErr[x + 2][1] += diff[1] * 3; + mNextErr[x + 2][2] += diff[2] * 3; + // Update error in this pixel (x) + mNextErr[x + 1][0] += diff[0] * 5; + mNextErr[x + 1][1] += diff[1] * 5; + mNextErr[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 + mNextErr[x][0] += diff[0]; // * 1; + mNextErr[x][1] += diff[1]; // * 1; + mNextErr[x][2] += diff[2]; // * 1; + + // Previous + x--; + + // Done? + if (x <= limit) { + break; + } + } + } + + // Make next error info current for next iteration + int[][] temperr; + temperr = mCurrErr; + mCurrErr = mNextErr; + mNextErr = temperr; + + // Toggle direction + if (mAlternateScans) { + forward = !forward; + } + } + return pDest; + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java new file mode 100755 index 00000000..71427244 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.*; +import java.awt.image.renderable.RenderableImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * EasyImage + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/EasyImage.java#1 $ + */ +public class EasyImage extends BufferedImage { + public EasyImage(InputStream pInput) throws IOException { + this(ImageIO.read(pInput)); + } + + public EasyImage(BufferedImage pImage) { + this(pImage.getColorModel(), pImage.getRaster()); + } + + public EasyImage(RenderableImage pImage) { + this(pImage.createDefaultRendering()); + } + + public EasyImage(RenderedImage pImage) { + this(pImage.getColorModel(), pImage.copyData(pImage.getColorModel().createCompatibleWritableRaster(pImage.getWidth(), pImage.getHeight()))); + } + + public EasyImage(ImageProducer pImage) { + this(new BufferedImageFactory(pImage).getBufferedImage()); + } + + public EasyImage(Image pImage) { + this(new BufferedImageFactory(pImage).getBufferedImage()); + } + + private EasyImage(ColorModel cm, WritableRaster raster) { + super(cm, raster, cm.isAlphaPremultiplied(), null); + } + + public boolean write(String pFormat, OutputStream pOutput) throws IOException { + return ImageIO.write(this, pFormat, pOutput); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java new file mode 100755 index 00000000..949d6932 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.image.ImageConsumer; +import java.awt.image.ColorModel; + +/** + * ExtendedImageConsumer + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ExtendedImageConsumer.java#1 $ + */ +public interface ExtendedImageConsumer extends ImageConsumer { + /** + * + * @param pX + * @param pY + * @param pWidth + * @param pHeight + * @param pModel + * @param pPixels + * @param pOffset + * @param pScanSize + */ + public void setPixels(int pX, int pY, int pWidth, int pHeight, + ColorModel pModel, + short[] pPixels, int pOffset, int pScanSize); + + // Allow for packed and interleaved models + public void setPixels(int pX, int pY, int pWidth, int pHeight, + ColorModel pModel, + byte[] pPixels, int pOffset, int pScanSize); +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java new file mode 100755 index 00000000..f221bdb5 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GraphicsUtil.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.*; + +/** + * 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayColorModel.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayColorModel.java new file mode 100755 index 00000000..c636e9fc --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayColorModel.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.image.*; + +/** + * This class represents a 256 color fixed grayscale IndexColorModel. + * + * @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/GrayColorModel.java#1 $ + * + */ +public class GrayColorModel extends IndexColorModel { + + private final static byte[] sGrays = createGrayScale(); + + public GrayColorModel() { + super(8, sGrays.length, sGrays, sGrays, sGrays); + } + + private static byte[] createGrayScale() { + byte[] grays = new byte[256]; + for (int i = 0; i < 256; i++) { + grays[i] = (byte) i; + } + return grays; + } + +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayFilter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayFilter.java new file mode 100755 index 00000000..dd4a8ef2 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/GrayFilter.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.image.*; + +/** + * 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 mLow = 0; + private float mRange = 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; + } + + mLow = (int) (pLow * 255f); + mRange = 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 (mRange != 1.0f) { + // Apply range + gray = mLow + (int) (gray * mRange); + } + + // Return ARGB pixel + return (pARGB & 0xFF000000) | (gray << 16) | (gray << 8) | gray; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageConversionException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageConversionException.java new file mode 100755 index 00000000..aefa2f2c --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageConversionException.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +/** + * This class wraps IllegalArgumentException, and is thrown by the ImageUtil + * class, when trying to convert images read from {@code null}-sources etc. + * + * @author Harald Kuhr + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageConversionException.java#1 $ + */ +public class ImageConversionException extends ImageFilterException { + + public ImageConversionException(String pMessage) { + super(pMessage); + } + + public ImageConversionException(String pMessage, Throwable pCause) { + super(pMessage, pCause); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java new file mode 100755 index 00000000..d91bf053 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +/** + * This class wraps IllegalArgumentException as thrown by the + * BufferedImageOp interface for more fine-grained control. + * + * @author Harald Kuhr + * + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageFilterException.java#1 $ + */ +public class ImageFilterException extends IllegalArgumentException { + private Throwable mCause = null; + + public ImageFilterException(String pStr) { + super(pStr); + } + + public ImageFilterException(Throwable pT) { + initCause(pT); + } + + public ImageFilterException(String pStr, Throwable pT) { + super(pStr); + initCause(pT); + } + + public Throwable initCause(Throwable pThrowable) { + if (mCause != null) { + // May only be called once + throw new IllegalStateException(); + } + else if (pThrowable == this) { + throw new IllegalArgumentException(); + } + + mCause = pThrowable; + + // Hmmm... + return this; + } + + public Throwable getCause() { + return mCause; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java new file mode 100755 index 00000000..8c2ae031 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java @@ -0,0 +1,2037 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.*; + +import java.util.Hashtable; + +/** + * This class contains methods for basic image manipulation and conversion. + * + * @todo Split palette generation out, into ColorModel classes. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ImageUtil.java#3 $ + */ +public final class ImageUtil { + + 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); + //private static Object sTrackerMutex = new Object(); + + /** Image id used by the image tracker */ + //private static int sTrackerId = 0; + + /** */ + protected static final AffineTransform IDENTITY_TRANSFORM = new AffineTransform(); + /** */ + protected static final Point LOCATION_UPPER_LEFT = new Point(0, 0); + + /** */ + private static final boolean COLORMODEL_TRANSFERTYPE_SUPPORTED = isColorModelTransferTypeSupported(); + + /** */ + 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() { + } + + /** + * Tests if {@code ColorModel} has a {@code getTransferType} method. + * + * @return {@code true} if {@code ColorModel} has a + * {@code getTransferType} method + */ + private static boolean isColorModelTransferTypeSupported() { + try { + ColorModel.getRGBdefault().getTransferType(); + return true; + } + catch (Throwable t) { + return false; + } + } + + /** + * 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 allready 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 allready 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 allready 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 allready 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 allready 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 allready 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 allready 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 copy of the given image. The image will have the same + * colormodel and raster type, but will not share image (pixel) data. + * + * @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(pImage, img); + + 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 with more + * resource-friendly than using + * {@code ColorModel.createCompatibleWritableRaster(w, h)}. + *

+ * For unknown 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 with the array from grabber.getPixels() + // directly, and creating a raster based on the 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 = data.length / (w * h); + bands = pColorModel.getNumComponents(); + } + else if (pPixels instanceof short[]) { + short[] data = (short[]) pPixels; + buffer = new DataBufferUShort(data, data.length); + bands = data.length / (pWidth * pHeight); + //bands = cm.getNumComponents(); + } + 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); + } + + //bands = pColorModel.getNumComponents(); + //System.out.println("Pixels: " + data.length + " (" + buffer.getSize() + ")"); + //System.out.println("w*h*bands: " + (pWidth * pHeight * bands)); + //System.out.println("Bands: " + bands); + //System.out.println("Numcomponents: " + pColorModel.getNumComponents()); + } + else { + //System.out.println("Fallback!"); + // Fallback mode, slower & requires more memory, but compatible + bands = -1; + + // Create raster from colormodel, w and h + raster = pColorModel.createCompatibleWritableRaster(pWidth, pHeight); + raster.setDataElements(0, 0, pWidth, pHeight, pPixels); // Note: This is known to throw ClassCastExceptions.. + } + + //System.out.println("Bands: " + bands); + //System.out.println("Pixels: " + pixels.getClass() + " length: " + buffer.getSize()); + //System.out.println("Needed Raster: " + cm.createCompatibleWritableRaster(1, 1)); + + if (raster == null) { + //int bits = cm.getPixelSize(); + //if (bits > 4) { + if (pColorModel instanceof IndexColorModel && isIndexedPacked((IndexColorModel) pColorModel)) { + //System.out.println("Creating packed indexed model"); + raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pColorModel.getPixelSize(), LOCATION_UPPER_LEFT); + } + else if (pColorModel instanceof PackedColorModel) { + //System.out.println("Creating packed model"); + PackedColorModel pcm = (PackedColorModel) pColorModel; + raster = Raster.createPackedRaster(buffer, pWidth, pHeight, pWidth, pcm.getMasks(), LOCATION_UPPER_LEFT); + } + else { + //System.out.println("Creating interleaved model"); + // (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); + } + //System.out.println("zzz Data array: " + buffer.getSize()); + + 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 mWidth the requested width of the raster + * @param mHeight the requested height of the raster + * + * @return a new WritableRaster + */ + static WritableRaster createCompatibleWritableRaster(BufferedImage pOriginal, ColorModel pModel, int mWidth, int mHeight) { + if (pModel == null || equals(pOriginal.getColorModel(), pModel)) { + switch (pOriginal.getType()) { + case BufferedImage.TYPE_3BYTE_BGR: + int[] bOffs = {2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return + return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, + mWidth, mHeight, + mWidth * 3, 3, + bOffs, null); + case BufferedImage.TYPE_4BYTE_ABGR: + case BufferedImage.TYPE_4BYTE_ABGR_PRE: + bOffs = new int[] {3, 2, 1, 0}; // NOTE: These are reversed from what the cm.createCompatibleWritableRaster would return + return Raster.createInterleavedRaster(DataBuffer.TYPE_BYTE, + mWidth, mHeight, + mWidth * 4, 4, + bOffs, null); + default: + return pOriginal.getColorModel().createCompatibleWritableRaster(mWidth, mHeight); + } + } + return pModel.createCompatibleWritableRaster(mWidth, mHeight); + } + + /** + * 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 allready 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 allready 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 pImage the image to draw on + * @param pSource the source image to draw + * + * @throws NullPointerException if {@code pImage} or {@code pSource} is + * {@code null} + */ + static void drawOnto(final BufferedImage pImage, final Image pSource) { + Graphics2D g = pImage.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); + //AffineTransformOp transformOp = new AffineTransformOp( + // transform, fast ? AffineTransformOp.TYPE_NEAREST_NEIGHBOR : 3 // 3 == TYPE_BICUBIC + //); + // + //return transformOp.filter(pSource, null); + + // TODO: Figure out if this is correct + BufferedImage dest = createTransparent(newW, newH); + //ColorModel cm = pSource.getColorModel(); + //new BufferedImage(cm, + // createCompatibleWritableRaster(pSource, cm, newW, newH), + // cm.isAlphaPremultiplied(), null); + + // 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) { + // Clear with all transparent + //Composite normal = g.getComposite(); + //g.setComposite(AlphaComposite.Clear); + //g.fillRect(0, 0, newW, newH); + //g.setComposite(normal); + + // 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 colormodel. 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 colormodels 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) { + 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) { + // TODO: These conversions are broken! + // box == area average + // point == replicate (or..?) + 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 pAmmount}0.0
-{@code pAmmount}4.0 * {@code pAmmount} + 1.0-{@code pAmmount}
0.0-{@code pAmmount}0.0
+ * + * @param pOriginal the BufferedImage to sharpen + * @param pAmmount the ammount of sharpening + * + * @return a BufferedImage, containing the sharpened image. + */ + public static BufferedImage sharpen(BufferedImage pOriginal, float pAmmount) { + if (pAmmount == 0f) { + return pOriginal; + } + + // Create the convolution matrix + float[] data = new float[] { + 0.0f, -pAmmount, 0.0f, -pAmmount, 4f * pAmmount + 1f, -pAmmount, 0.0f, -pAmmount, 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 ammount 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 ammount and scale matrix instead? +// pAmmount = 1f - pAmmount; +// float pAmmount = 1f - pRadius; +// +// // Normalize ammount +// float normAmt = (1f - pAmmount) / 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, pAmmount, 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 pAmmount the ammount of contrast in the range [-1.0..1.0]. + * + * @return an {@code Image}, containing the contrasted image. + */ + public static Image contrast(Image pOriginal, float pAmmount) { + // No change, return original + if (pAmmount == 0f) { + return pOriginal; + } + + // Create filter + RGBImageFilter filter = new BrightnessContrastFilter(0f, pAmmount); + + // Return contrast adjusted image + return filter(pOriginal, filter); + } + + + /** + * Changes the brightness of the original image. + * + * @param pOriginal the {@code Image} to change + * @param pAmmount the ammount of brightness in the range [-2.0..2.0]. + * + * @return an {@code Image} + */ + public static Image brightness(Image pOriginal, float pAmmount) { + // No change, return original + if (pAmmount == 0f) { + return pOriginal; + } + + // Create filter + RGBImageFilter filter = new BrightnessContrastFilter(pAmmount, 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-accellerated 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-accellerated 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-accellerated 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 + * occured, 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 + * occured, 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 + * occured, 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: The synchronization throws IllegalMonitorStateException if + // using JIT on J2SE 1.2 (tested version Sun JRE 1.2.2_017). + // Works perfectly interpreted... Hmmm... + //synchronized (sTrackerMutex) { + //imageId = ++sTrackerId; + //} + + // 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 wether 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 inbetween. + * + * @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 inbetween. + * + * @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 blendfactor. + * 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; + } + /** + * PixelGrabber subclass that stores any potential properties from an image. + */ + /* + private static class MyPixelGrabber extends PixelGrabber { + private Hashtable mProps = null; + + public MyPixelGrabber(Image pImage) { + // Simply grab all pixels, do not convert to default RGB space + super(pImage, 0, 0, -1, -1, false); + } + + // Default implementation does not store the properties... + public void setProperties(Hashtable pProps) { + super.setProperties(pProps); + mProps = pProps; + } + + public Hashtable getProperties() { + return mProps; + } + } + */ + + /** + * Gets the transfer type from the given {@code ColorModel}. + *

+ * NOTE: This is a workaround for missing functionality in JDK 1.2. + * + * @param pModel the color model + * @return the transfer type + * + * @throws NullPointerException if {@code pModel} is {@code null}. + * + * @see java.awt.image.ColorModel#getTransferType() + */ + public static int getTransferType(ColorModel pModel) { + if (COLORMODEL_TRANSFERTYPE_SUPPORTED) { + return pModel.getTransferType(); + } + else { + // Stupid workaround + // TODO: Create something that performs better + return pModel.createCompatibleSampleModel(1, 1).getDataType(); + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/IndexImage.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/IndexImage.java new file mode 100755 index 00000000..950b195a --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/IndexImage.java @@ -0,0 +1,1530 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* +****************************************************************************** +* +* ============================================================================ +* 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.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * This class implements an adaptive pallete 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: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/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 colorspace. The cube can be split + * approximagely in half to generate two cubes. + */ + private static class Cube { + int[] min = {0, 0, 0}, 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; + } + Iterator itr = v.iterator(); + Counter c; + + while (itr.hasNext()) { + c = (Counter) itr.next(); + 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; + } + Iterator itr = v.iterator(); + Counter c; + + while (itr.hasNext()) { + c = (Counter) itr.next(); + 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 pallete. + * + * @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); + + //System.out.println("IndexColorModel created from colors."); + } + else if (!(icm instanceof InverseColorMapIndexColorModel)) { + // If possible, use faster code + //System.out.println("Wrappimg IndexColorModel in InverseColorMapIndexColorModel"); + icm = new InverseColorMapIndexColorModel(icm); + } + //else { + //System.out.println("Allredy InverseColorMapIndexColorModel"); + //} + return icm; + } + + /** + * Creates an {@code IndexColorModel} from the given image, using an adaptive + * pallete. + * + * @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. + 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++; + } + for (int i = numberOfCubes; i > j; i--) { + cubes[i] = cubes[i - 1]; + } + 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 IndexColorModel(numOfBits, r.length, r, g, b, r.length - 1); + icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b, r.length - 1); + } + else { + //icm = new IndexColorModel(numOfBits, r.length, r, g, b); + icm = new InverseColorMapIndexColorModel(numOfBits, r.length, r, g, b); + } + return icm; + } + + /** + * Converts the input image (must be {@code TYPE_INT_RGB} or + * {@code TYPE_INT_ARGB}) to an indexed image. Generating an adaptive + * pallete (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 pallete (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 mHints 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 + * pallete 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 mHints 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 colormodel, 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 pallete. + * 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 transparancy? + 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). + // Got anything to do with isPremultiplied? Hmm... + 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 + * pallete 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 mHints 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 pallete. + * 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 pallete (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 mHints 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 transparancy, 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 + java.io.File in = new java.io.File(pArgs[argIdx++]); + + if (!in.exists()) { + System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!"); + System.exit(5); + } + + // Read palette if needed + java.io.File paletteFile = null; + + if (paletteFileName != null) { + paletteFile = new java.io.File(paletteFileName); + if (!paletteFile.exists()) { + System.err.println("File \"" + in.getAbsolutePath() + "\" does not exist!"); + System.exit(5); + } + } + + // Make sure we can write + java.io.File out; + + if (argIdx < pArgs.length) { + out = new java.io.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 java.io.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 (java.io.IOException ioe) { + ioe.printStackTrace(System.err); + System.exit(5); + } + + // Create mHints + 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; + long end; + + 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 + end = System.currentTimeMillis(); + System.out.println("Color selection + dither: " + (end - 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 (java.io.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); + end = System.currentTimeMillis(); + time += (end - 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMap.java new file mode 100755 index 00000000..5b334244 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMap.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +/** + * 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[] mRGBMapByte; + int[] mRGBMapInt; + int mNumColors; + int mMaxColor; + byte[] mInverseRGB; // inverse rgb color map + int mTransparentIndex = -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) { + mRGBMapByte = pRGBColorMap; + mNumColors = mRGBMapByte.length / 4; + mTransparentIndex = pTransparent; + + mInverseRGB = 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) { + mRGBMapInt = pRGBColorMap; + mNumColors = mRGBMapInt.length; + mTransparentIndex = pTransparent; + + mInverseRGB = 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 < mNumColors; ++i) { + if (i == mTransparentIndex) { + // 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 (mRGBMapByte != null) { + red = mRGBMapByte[i * 4] & 0xFF; + green = mRGBMapByte[i * 4 + 1] & 0xFF; + blue = mRGBMapByte[i * 4 + 2] & 0xFF; + } + else if (mRGBMapInt != null) { + red = (mRGBMapInt[i] >> 16) & 0xFF; + green = (mRGBMapInt[i] >> 8) & 0xFF; + blue = mRGBMapInt[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; + mInverseRGB[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 mInverseRGB[((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 mInverseRGB[((pRed << (2 * QUANTBITS - TRUNCBITS)) & QUANTMASK_RED) + + ((pGreen << (/* 1 * */ QUANTBITS - TRUNCBITS)) & QUANTMASK_GREEN) + + ((pBlue >> (TRUNCBITS)) & QUANTMASK_BLUE)] & 0xFF; + } +} + diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java new file mode 100755 index 00000000..41fae813 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java @@ -0,0 +1,310 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +package com.twelvemonkeys.image; + +import com.twelvemonkeys.lang.StringUtil; + +import java.awt.*; +import java.awt.image.DataBuffer; +import java.awt.image.IndexColorModel; + +/** + * A faster implementation of {@code IndexColorModel}, that is backed by an + * inverse color-map, for fast lookups. + * + * @author Harald Kuhr + * @author $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/InverseColorMapIndexColorModel.java#1 $ + * + */ +public class InverseColorMapIndexColorModel extends IndexColorModel { + + protected int mRGBs[]; + protected int mMapSize; + + protected InverseColorMap mInverseMap = null; + private final static int ALPHA_THRESHOLD = 0x80; + + private int mWhiteIndex = -1; + private final static int WHITE = 0x00FFFFFF; + private final static int RGB_MASK = 0x00FFFFFF; + + /** + * Creates an {@code InverseColorMapIndexColorModel} from an existing + * {@code IndexColorModel}. + * + * @param pColorModel the colormodel to create from + */ + public InverseColorMapIndexColorModel(IndexColorModel pColorModel) { + this(pColorModel, getRGBs(pColorModel)); + } + + // NOTE: The pRGBs parameter is used to get around invoking getRGBs two + // times. What is wrong with protected?! + private InverseColorMapIndexColorModel(IndexColorModel pColorModel, int[] pRGBs) { + super(pColorModel.getComponentSize()[0], pColorModel.getMapSize(), + pRGBs, 0, + ImageUtil.getTransferType(pColorModel), + pColorModel.getValidPixels()); + + mRGBs = pRGBs; + mMapSize = mRGBs.length; + + mInverseMap = new InverseColorMap(mRGBs); + mWhiteIndex = getWhiteIndex(); + } + + /** + * Creates a defensive copy of the RGB colormap in the given + * {@code IndexColorModel}. + * + * @param pColorModel the indec colormodel to get RGB values from + * @return the RGB colormap + */ + private static int[] getRGBs(IndexColorModel pColorModel) { + int[] rgb = new int[pColorModel.getMapSize()]; + pColorModel.getRGBs(rgb); + return rgb; + } + + /** + * Creates an {@code InverseColorMapIndexColorModel} from the given array + * of RGB components, plus one transparent index. + * + * @param pNumBits the number of bits each pixel occupies + * @param pSize the size of the color component arrays + * @param pRGBs the array of packed RGB color components + * @param pStart the starting offset of the first color component + * @param pAlpha indicates whether alpha values are contained in {@code pRGBs} + * @param pTransparentIndex the index of the transparent pixel + * @param pTransferType the data type of the array used to represent pixels + * + * @throws IllegalArgumentException if bits is less than 1 or greater than 16, + * or if size is less than 1 + * + * @see IndexColorModel#IndexColorModel(int, int, int[], int, boolean, int, int) + */ + public InverseColorMapIndexColorModel(int pNumBits, int pSize, int[] pRGBs, + int pStart, boolean pAlpha, int pTransparentIndex, + int pTransferType) { + super(pNumBits, pSize, pRGBs, pStart, pAlpha, pTransparentIndex, pTransferType); + mRGBs = getRGBs(this); + mMapSize = mRGBs.length; + + mInverseMap = new InverseColorMap(mRGBs, pTransparentIndex); + mWhiteIndex = getWhiteIndex(); + } + + /** + * Creates an {@code InverseColorMapIndexColorModel} from the given arrays + * of red, green, and blue components, plus one transparent index. + * + * @param pNumBits the number of bits each pixel occupies + * @param pSize the size of the color component arrays + * @param pReds the array of red color components + * @param pGreens the array of green color components + * @param pBlues the array of blue color components + * @param pTransparentIndex the index of the transparent pixel + * + * @throws IllegalArgumentException if bits is less than 1 or greater than 16, + * or if size is less than 1 + * + * @see IndexColorModel#IndexColorModel(int, int, byte[], byte[], byte[], int) + */ + public InverseColorMapIndexColorModel(int pNumBits, int pSize, + byte[] pReds, byte[] pGreens, byte[] pBlues, + int pTransparentIndex) { + super(pNumBits, pSize, pReds, pGreens, pBlues, pTransparentIndex); + mRGBs = getRGBs(this); + mMapSize = mRGBs.length; + + mInverseMap = new InverseColorMap(mRGBs, pTransparentIndex); + mWhiteIndex = getWhiteIndex(); + } + + /** + * Creates an {@code InverseColorMapIndexColorModel} from the given arrays + * of red, green, and blue components. + * + * @param pNumBits the number of bits each pixel occupies + * @param pSize the size of the color component arrays + * @param pReds the array of red color components + * @param pGreens the array of green color components + * @param pBlues the array of blue color components + * + * @throws IllegalArgumentException if bits is less than 1 or greater than 16, + * or if size is less than 1 + * + * @see IndexColorModel#IndexColorModel(int, int, byte[], byte[], byte[]) + */ + public InverseColorMapIndexColorModel(int pNumBits, int pSize, + byte[] pReds, byte[] pGreens, byte[] pBlues) { + super(pNumBits, pSize, pReds, pGreens, pBlues); + mRGBs = getRGBs(this); + mMapSize = mRGBs.length; + + mInverseMap = new InverseColorMap(mRGBs); + mWhiteIndex = getWhiteIndex(); + } + + private int getWhiteIndex() { + for (int i = 0; i < mRGBs.length; i++) { + int color = mRGBs[i]; + if ((color & RGB_MASK) == WHITE) { + return i; + } + } + return -1; + } + + /** + * Creates an {@code IndexColorModel} optimized for the given {@code Image}. + * + * @param pImage the {@code Image} containing the RGB samples + * @param pNumCols the maximum number of colors in the {@code IndexColorModel} + * @param pFlags flags + * + * @return a new optimized {@code IndexColorModel} + */ + public static IndexColorModel create(Image pImage, int pNumCols, int pFlags) { + // TODO: Inline and deprecate IndexImage.getIndexColorModel!? + IndexColorModel icm = IndexImage.getIndexColorModel(pImage, pNumCols, pFlags); + + InverseColorMapIndexColorModel cm; + if (icm instanceof InverseColorMapIndexColorModel) { + cm = (InverseColorMapIndexColorModel) icm; + } + else { + cm = new InverseColorMapIndexColorModel(icm); + } + + return cm; + } + + /** + * Returns a data element array representation of a pixel in this + * ColorModel, given an integer pixel representation in the + * default RGB color model. This array can then be passed to the + * {@link java.awt.image.WritableRaster#setDataElements(int, int, Object) setDataElements} + * method of a {@link java.awt.image.WritableRaster} object. If the pixel variable is + * {@code null}, a new array is allocated. If {@code pixel} + * is not {@code null}, it must be + * a primitive array of type {@code transferType}; otherwise, a + * {@code ClassCastException} is thrown. An + * {@code ArrayIndexOutOfBoundsException} is + * thrown if {@code pixel} is not large enough to hold a pixel + * value for this {@code ColorModel}. The pixel array is returned. + *

+ * Since {@code OpaqueIndexColorModel} can be subclassed, subclasses + * inherit the implementation of this method and if they don't + * override it then they throw an exception if they use an + * unsupported {@code transferType}. + * + * #param rgb the integer pixel representation in the default RGB + * color model + * #param pixel the specified pixel + * #return an array representation of the specified pixel in this + * {@code OpaqueIndexColorModel}. + * #throws ClassCastException if {@code pixel} + * is not a primitive array of type {@code transferType} + * #throws ArrayIndexOutOfBoundsException if + * {@code pixel} is not large enough to hold a pixel value + * for this {@code ColorModel} + * #throws UnsupportedOperationException if {@code transferType} + * is invalid + * @see java.awt.image.WritableRaster#setDataElements + * @see java.awt.image.SampleModel#setDataElements + * + */ + public Object getDataElements(int rgb, Object pixel) { + + int alpha = (rgb>>>24); + + int pix; + if (alpha < ALPHA_THRESHOLD && getTransparentPixel() != -1) { + pix = getTransparentPixel(); + } + else { + int color = rgb & RGB_MASK; + if (color == WHITE && mWhiteIndex != -1) { + pix = mWhiteIndex; + } + else { + pix = mInverseMap.getIndexNearest(color); + } + } + + return installpixel(pixel, pix); + } + + private Object installpixel(Object pixel, int pix) { + switch (transferType) { + case DataBuffer.TYPE_INT: + int[] intObj; + if (pixel == null) { + pixel = intObj = new int[1]; + } + else { + intObj = (int[]) pixel; + } + intObj[0] = pix; + break; + case DataBuffer.TYPE_BYTE: + byte[] byteObj; + if (pixel == null) { + pixel = byteObj = new byte[1]; + } + else { + byteObj = (byte[]) pixel; + } + byteObj[0] = (byte) pix; + break; + case DataBuffer.TYPE_USHORT: + short[] shortObj; + if (pixel == null) { + pixel = shortObj = new short[1]; + } + else { + shortObj = (short[]) pixel; + } + shortObj[0] = (short) pix; + break; + default: + throw new UnsupportedOperationException("This method has not been " + + "implemented for transferType " + transferType); + } + return pixel; + } + + public String toString() { + // Just a workaround to ease debugging + return StringUtil.replace(super.toString(), "IndexColorModel: ", getClass().getName() + ": "); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/Magick.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/Magick.java new file mode 100755 index 00000000..48165b7e --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/Magick.java @@ -0,0 +1,24 @@ +package com.twelvemonkeys.image; + +/** + * Magick + * + * @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/Magick.java#1 $ + */ +final class Magick { + static final boolean DEBUG = useDebug(); + + private static boolean useDebug() { + try { + return "TRUE".equalsIgnoreCase(System.getProperty("com.twelvemonkeys.image.magick.debug")); + } + catch (Throwable t) { + // Most probably in case of a SecurityManager + return false; + } + } + + private Magick() {} +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java new file mode 100755 index 00000000..9f8b8647 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickAccelerator.java @@ -0,0 +1,184 @@ +package com.twelvemonkeys.image; + +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import com.twelvemonkeys.lang.SystemUtil; + +import magick.MagickImage; + +import java.awt.image.*; + +/** + * 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[] sNativeOp = new Class[1]; + + static { + try { + sNativeOp[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 < sNativeOp.length; i++) { + if (pOpClass == sNativeOp[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.mWidth, resample.mHeight, resample.mFilterType); + + // 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickUtil.java new file mode 100755 index 00000000..491fdaf1 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MagickUtil.java @@ -0,0 +1,549 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import magick.*; + +import java.awt.*; +import java.awt.color.ColorSpace; +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: + case ImageType.ColorSeparationMatteType: + 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); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MonochromeColorModel.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MonochromeColorModel.java new file mode 100755 index 00000000..9f4894f9 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/MonochromeColorModel.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.image.*; + +/** + * Monochrome B/W color model. + * + * @author Harald Kuhr + */ +public class MonochromeColorModel extends IndexColorModel { + + private final static int[] MONO_PALETTE = {0x00000000, 0x00FFFFFF}; + + private static MonochromeColorModel sInstance = new MonochromeColorModel(); + + private MonochromeColorModel() { + super(1, 2, MONO_PALETTE, 0, false, -1, DataBuffer.TYPE_BYTE); + } + + public static IndexColorModel getInstance() { + return sInstance; + } + + public synchronized Object getDataElements(int pRGB, Object pPixel) { + // Get color components + int r = pRGB >> 16 & 0xFF; + int g = pRGB >> 8 & 0xFF; + int b = pRGB & 0xFF; + + // ITU standard: Gray scale=(222*Red+707*Green+71*Blue)/1000 + int gray = (222 * r + 707 * g + 71 * b) / 1000; + + byte[] pixel; + if (pPixel != null) { + pixel = (byte[]) pPixel; + } + else { + pixel = new byte[1]; + } + + if (gray <= 0x80) { + pixel[0] = 0; + } + else { + pixel[0] = 1; + } + + return pixel; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/PixelizeOp.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/PixelizeOp.java new file mode 100755 index 00000000..c73efd84 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/PixelizeOp.java @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.*; +import java.io.File; +import java.io.IOException; + +/** + * PixelizeOp + * + * @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/PixelizeOp.java#2 $ + */ +public class PixelizeOp implements BufferedImageOp, RasterOp { + // TODO: support more raster types/color models + // TODO: This is actually an implementation of Area Averaging, without the scale... Let's extract it... + + final private int mPixelSizeX; + final private int mPixelSizeY; + + private Rectangle mSourceRegion; + + public PixelizeOp(final int pPixelSize) { + this(pPixelSize, pPixelSize); + } + + public PixelizeOp(final int pPixelSizeX, final int pPixelSizeY) { + mPixelSizeX = pPixelSizeX; + mPixelSizeY = pPixelSizeY; + } + + public Rectangle getSourceRegion() { + if (mSourceRegion == null) { + return null; + } + return new Rectangle(mSourceRegion); + } + + public void setSourceRegion(final Rectangle pSourceRegion) { + if (pSourceRegion == null) { + mSourceRegion = null; + } + else { + if (mSourceRegion == null) { + mSourceRegion = new Rectangle(pSourceRegion); + } + else { + mSourceRegion.setBounds(pSourceRegion); + } + } + } + + public BufferedImage filter(BufferedImage src, BufferedImage dest) { + BufferedImage result = dest != null ? dest : createCompatibleDestImage(src, null); + + // TODO: Do some type checking here.. + // Should work with + // * all BYTE types, unless sub-byte packed rasters/IndexColorModel + // * all INT types (even custom, as long as they use 8bit/componnet) + // * all USHORT types (even custom) + + // TODO: Also check if the images are really compatible!? + + filterImpl(src.getRaster(), result.getRaster()); + + return result; + } + + public WritableRaster filter(Raster src, WritableRaster dest) { + WritableRaster result = dest != null ? dest : createCompatibleDestRaster(src); + return filterImpl(src, result); + } + + private WritableRaster filterImpl(Raster src, WritableRaster dest) { + //System.out.println("src: " + src); + //System.out.println("dest: " + dest); + if (mSourceRegion != null) { + int cx = mSourceRegion.x; + int cy = mSourceRegion.y; + int cw = mSourceRegion.width; + int ch = mSourceRegion.height; + + boolean same = src == dest; + dest = dest.createWritableChild(cx, cy, cw, ch, 0, 0, null); + src = same ? dest : src.createChild(cx, cy, cw, ch, 0, 0, null); + //System.out.println("src: " + src); + //System.out.println("dest: " + dest); + } + + final int width = src.getWidth(); + final int height = src.getHeight(); + int w = (width + mPixelSizeX - 1) / mPixelSizeX; + int h = (height + mPixelSizeY - 1) / mPixelSizeY; + + final boolean oddX = width % w != 0; + final boolean oddY = height % h != 0; + + final int dataElements = src.getNumDataElements(); + final int bands = src.getNumBands(); + final int dataType = src.getTransferType(); + + Object data = null; + int scanW; + int scanH; + + + // TYPE_USHORT setup + int[] bitMasks = null; + int[] bitOffsets = null; + if (src.getTransferType() == DataBuffer.TYPE_USHORT) { + if (src.getSampleModel() instanceof SinglePixelPackedSampleModel) { + // DIRECT + SinglePixelPackedSampleModel sampleModel = (SinglePixelPackedSampleModel) src.getSampleModel(); + bitMasks = sampleModel.getBitMasks(); + bitOffsets = sampleModel.getBitOffsets(); + } + else { + // GRAY + bitMasks = new int[] {0xffff}; + bitOffsets = new int[] {0}; + } + } + + for (int y = 0; y < h; y++) { + if (!oddY || y + 1 < h) { + scanH = mPixelSizeY; + } + else { + scanH = height - (y * mPixelSizeY); + } + + for (int x = 0; x < w; x++) { + if (!oddX || x + 1 < w) { + scanW = mPixelSizeX; + } + else { + scanW = width - (x * mPixelSizeX); + } + final int pixelCount = scanW * scanH; + final int pixelLength = pixelCount * dataElements; + + data = src.getDataElements(x * mPixelSizeX, y * mPixelSizeY, scanW, scanH, data); + + // NOTE: These are not neccessarily ARGB.. + double valueA = 0.0; + double valueR = 0.0; + double valueG = 0.0; + double valueB = 0.0; + + switch (dataType) { + case DataBuffer.TYPE_BYTE: + // TODO: Doesn't hold for index color models... + byte[] bytePixels = (byte[]) data; + for (int i = 0; i < pixelLength; i += dataElements) { + valueA += bytePixels[i] & 0xff; + if (bands > 1) { + valueR += bytePixels[i + 1] & 0xff; + valueG += bytePixels[i + 2] & 0xff; + if (bands > 3) { + valueB += bytePixels[i + 3] & 0xff; + } + } + } + + // Average + valueA /= pixelCount; + if (bands > 1) { + valueR /= pixelCount; + valueG /= pixelCount; + if (bands > 3) { + valueB /= pixelCount; + } + } + + for (int i = 0; i < pixelLength; i += dataElements) { + bytePixels[i] = (byte) clamp((int) valueA); + if (bands > 1) { + bytePixels[i + 1] = (byte) clamp((int) valueR); + bytePixels[i + 2] = (byte) clamp((int) valueG); + if (bands > 3) { + bytePixels[i + 3] = (byte) clamp((int) valueB); + } + } + } + break; + + case DataBuffer.TYPE_INT: + int[] intPixels = (int[]) data; + for (int i = 0; i < pixelLength; i += dataElements) { + valueA += (intPixels[i] & 0xff000000) >> 24; + valueR += (intPixels[i] & 0xff0000) >> 16; + valueG += (intPixels[i] & 0xff00) >> 8; + valueB += (intPixels[i] & 0xff); + } + + // Average + valueA /= pixelCount; + valueR /= pixelCount; + valueG /= pixelCount; + valueB /= pixelCount; + + for (int i = 0; i < pixelLength; i += dataElements) { + intPixels[i] = clamp((int) valueA) << 24; + intPixels[i] |= clamp((int) valueR) << 16; + intPixels[i] |= clamp((int) valueG) << 8; + intPixels[i] |= clamp((int) valueB); + } + break; + + case DataBuffer.TYPE_USHORT: + if (bitMasks != null) { + short[] shortPixels = (short[]) data; + for (int i = 0; i < pixelLength; i += dataElements) { + valueA += (shortPixels[i] & bitMasks[0]) >> bitOffsets[0]; + if (bitMasks.length > 1) { + valueR += (shortPixels[i] & bitMasks[1]) >> bitOffsets[1]; + valueG += (shortPixels[i] & bitMasks[2]) >> bitOffsets[2]; + if (bitMasks.length > 3) { + valueB += (shortPixels[i] & bitMasks[3]) >> bitOffsets[3]; + } + } + } + + // Average + valueA /= pixelCount; + valueR /= pixelCount; + valueG /= pixelCount; + valueB /= pixelCount; + + for (int i = 0; i < pixelLength; i += dataElements) { + shortPixels[i] = (short) (((int) valueA << bitOffsets[0]) & bitMasks[0]); + if (bitMasks.length > 1) { + shortPixels[i] |= (short) (((int) valueR << bitOffsets[1]) & bitMasks[1]); + shortPixels[i] |= (short) (((int) valueG << bitOffsets[2]) & bitMasks[2]); + if (bitMasks.length > 3) { + shortPixels[i] |= (short) (((int) valueB << bitOffsets[3]) & bitMasks[3]); + } + } + } + break; + } + default: + throw new IllegalArgumentException("TransferType not supported: " + dataType); + + } + + dest.setDataElements(x * mPixelSizeX, y * mPixelSizeY, scanW, scanH, data); + } + } + /*/ + // This is a very naive way of pixelizing (but it works)... + // Thanks to the awsome speed of AffineTransformOp, it's also fast + double sx = w / (double) src.getWidth(); + double sy = h / (double) src.getHeight(); + + WritableRaster temp = src.createCompatibleWritableRaster(w, h); + + new AffineTransformOp(AffineTransform.getScaleInstance(sx, sy), 3) + .filter(src, temp); + new AffineTransformOp(AffineTransform.getScaleInstance(1 / sx, 1 / sy), + AffineTransformOp.TYPE_NEAREST_NEIGHBOR) + .filter(temp, dest); + //*/ + + return dest; + } + + private static int clamp(final int pValue) { + return pValue > 255 ? 255 : pValue; + } + + public RenderingHints getRenderingHints() { + return null; + } + + // TODO: Refactor boilerplate to AbstractBufferedImageOp or use a delegate? + // Delegate is maybe better as we won't always implement both BIOp and RasterOP + // (but are there ever any time we want to implemnet RasterOp and not BIOp?) + public BufferedImage createCompatibleDestImage(BufferedImage src, ColorModel destCM) { + ColorModel cm = destCM != null ? destCM : src.getColorModel(); + return new BufferedImage(cm, + ImageUtil.createCompatibleWritableRaster(src, cm, src.getWidth(), src.getHeight()), + cm.isAlphaPremultiplied(), null); + } + + public WritableRaster createCompatibleDestRaster(Raster src) { + return src.createCompatibleWritableRaster(); + } + + public Rectangle2D getBounds2D(Raster src) { + return new Rectangle(src.getWidth(), src.getHeight()); + } + + public Rectangle2D getBounds2D(BufferedImage src) { + return new Rectangle(src.getWidth(), src.getHeight()); + } + + public Point2D getPoint2D(Point2D srcPt, Point2D dstPt) { + if (dstPt == null) { + if (srcPt instanceof Point2D.Double) { + dstPt = new Point2D.Double(); + } + else { + dstPt = new Point2D.Float(); + } + } + dstPt.setLocation(srcPt); + return dstPt; + } + + public static void main(String[] pArgs) throws IOException { + BufferedImage image = ImageIO.read(new File("2006-Lamborghini-Gallardo-Spyder-Y-T-1600x1200.png")); + //BufferedImage image = ImageIO.read(new File("focus-rs.jpg")); + //BufferedImage image = ImageIO.read(new File("blauesglas_16_bitmask444.bmp")); + //image = ImageUtil.toBuffered(image, BufferedImage.TYPE_USHORT_GRAY); + + for (int i = 0; i < 10; i++) { + //new PixelizeOp(10).filter(image, null); + //new AffineTransformOp(AffineTransform.getScaleInstance(.1, .1), AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(image, null); + //ImageUtil.toBuffered(image.getScaledInstance(image.getWidth() / 4, image.getHeight() / 4, Image.SCALE_AREA_AVERAGING)); + //new ResampleOp(image.getWidth() / 10, image.getHeight() / 10, ResampleOp.FILTER_BOX).filter(image, null); + new ResampleOp(image.getWidth() / 10, image.getHeight() / 10, ResampleOp.FILTER_QUADRATIC).filter(image, null); + } + + + long start = System.currentTimeMillis(); + //PixelizeOp pixelizer = new PixelizeOp(image.getWidth() / 10, 1); + //pixelizer.setSourceRegion(new Rectangle(0, 2 * image.getHeight() / 3, image.getWidth(), image.getHeight() / 4)); + //PixelizeOp pixelizer = new PixelizeOp(4); + //image = pixelizer.filter(image, image); // Filter in place, that's cool + //image = new AffineTransformOp(AffineTransform.getScaleInstance(.25, .25), AffineTransformOp.TYPE_NEAREST_NEIGHBOR).filter(image, null); + //image = ImageUtil.toBuffered(image.getScaledInstance(image.getWidth() / 4, image.getHeight() / 4, Image.SCALE_AREA_AVERAGING)); + //image = new ResampleOp(image.getWidth() / 4, image.getHeight() / 4, ResampleOp.FILTER_BOX).filter(image, null); + image = new ResampleOp(image.getWidth() / 4, image.getHeight() / 4, ResampleOp.FILTER_QUADRATIC).filter(image, null); + long time = System.currentTimeMillis() - start; + + System.out.println("time: " + time + " ms"); + + JFrame frame = new JFrame("Test"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setContentPane(new JScrollPane(new JLabel(new BufferedImageIcon(image)))); + frame.pack(); + frame.setVisible(true); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ResampleOp.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ResampleOp.java new file mode 100644 index 00000000..205f0ca4 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/ResampleOp.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + ******************************************************************************* + * + * 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 com.twelvemonkeys.lang.SystemUtil; + +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 mWidth; + int mHeight; + + int mFilterType; + private static final boolean TRANSFORM_OP_BICUBIC_SUPPORT = SystemUtil.isFieldAvailable(AffineTransformOp.class.getName(), "TYPE_BICUBIC"); + + /** + * 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 mName; + + public Key(final String pName) { + super(sIndex++); + mName = pName; + } + + public boolean isCompatibleValue(Object pValue) { + return pValue instanceof Value && ((Value) pValue).isCompatibleKey(this); + } + + public String toString() { + return mName; + } + } + + /** + * RenderingHints value implementaion, works with Key keys. + */ + // TODO: Extract abstract Value class, and move to AbstractBufferedImageOp + static final class Value { + final private RenderingHints.Key mKey; + final private String mName; + final private int mType; + + public Value(final RenderingHints.Key pKey, final String pName, final int pType) { + mKey = pKey; + mName = pName; + validateFilterType(pType); + mType = pType;// TODO: test for duplicates + } + + public boolean isCompatibleKey(Key pKey) { + return pKey == mKey; + } + + public int getFilterType() { + return mType; + } + + public String toString() { + return mName; + } + } + + /** + * Creates a {@code ResampleOp} that will resample input images to the + * given width and height, using the default interpolation filter. + * + * @param pWidth width of the resampled image + * @param pHeight height of the resampled image + */ + public ResampleOp(int pWidth, int pHeight) { + this(pWidth, pHeight, 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 pWidth width of the resampled image + * @param pHeight height of the resampled image + * @param pHints rendering hints, affecting interpolation algorithm + * @see #KEY_RESAMPLE_INTERPOLATION + * @see RenderingHints#KEY_INTERPOLATION + * @see RenderingHints#KEY_RENDERING + * @see RenderingHints#KEY_COLOR_RENDERING + */ + public ResampleOp(int pWidth, int pHeight, RenderingHints pHints) { + this(pWidth, pHeight, getFilterType(pHints)); + } + + /** + * Creates a {@code ResampleOp} that will resample input images to the + * given width and height, using the given interpolation filter. + * + * @param pWidth width of the resampled image + * @param pHeight height of the resampled image + * @param pFilterType interpolation filter algorithm + * @see filter type constants + */ + public ResampleOp(int pWidth, int pHeight, int pFilterType) { + if (pWidth <= 0 || pHeight <= 0) { + // NOTE: w/h == 0 makes the Magick DLL crash and the JVM dies.. :-P + throw new IllegalArgumentException("width and height must be positive"); + } + + mWidth = pWidth; + mHeight = pHeight; + + validateFilterType(pFilterType); + mFilterType = pFilterType; + } + + private static void 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: + break; + 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 prioritze 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; + } + + /** + * Resamples (scales) the image to the size, and using the algorithm + * specified in the constructor. + * + * @param pInput The {@code BufferedImage} to be filtered + * @param pOutput The {@code BufferedImage} in which to store the resampled + * image + * @return The resampled {@code BufferedImage}. + * @throws NullPointerException if {@code pInput} is {@code null} + * @throws IllegalArgumentException if {@code pInput == pOutput}. + * @see #ResampleOp(int,int,int) + */ + public final BufferedImage filter(final BufferedImage pInput, final BufferedImage pOutput) { + if (pInput == null) { + throw new NullPointerException("Input == null"); + } + if (pInput == pOutput) { + throw new IllegalArgumentException("Output image cannot be the same as the input image"); + } + + InterpolationFilter filter; + + + // Special case for POINT, TRIANGLE and QUADRATIC filter, as standard + // Java implementation is very fast (possibly H/W accellerated) + switch (mFilterType) { + case FILTER_POINT: + return fastResample(pInput, pOutput, mWidth, mHeight, AffineTransformOp.TYPE_NEAREST_NEIGHBOR); + case FILTER_TRIANGLE: + return fastResample(pInput, pOutput, mWidth, mHeight, AffineTransformOp.TYPE_BILINEAR); + case FILTER_QUADRATIC: + if (TRANSFORM_OP_BICUBIC_SUPPORT) { + return fastResample(pInput, pOutput, mWidth, mHeight, 3); // AffineTransformOp.TYPE_BICUBIC + } + // Fall through + default: + filter = createFilter(mFilterType); + // NOTE: Workaround for filter throwing exceptions when input or output is less than support... + if (Math.min(pInput.getWidth(), pInput.getHeight()) <= filter.support() || Math.min(mWidth, mHeight) <= filter.support()) { + return fastResample(pInput, pOutput, mWidth, mHeight, AffineTransformOp.TYPE_BILINEAR); + } + // Fall through + } + + + // Try to use native ImageMagick code + BufferedImage result = MagickAccelerator.filter(this, pInput, pOutput); + if (result != null) { + return result; + } + + // Otherwise, continue in pure Java mode + + // TODO: What if pOutput != null and wrong size? Create new? Render on only a part? Document? + + // If filter type != POINT or BOX an input has IndexColorModel, convert + // to true color, with alpha reflecting that of the original colormodel. + BufferedImage input; + ColorModel cm; + if (mFilterType != FILTER_BOX && (cm = pInput.getColorModel()) instanceof IndexColorModel) { + // TODO: OPTIMIZE: If colormodel has only b/w or gray, we could skip color info + input = ImageUtil.toBuffered(pInput, cm.hasAlpha() ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_3BYTE_BGR); + } + else { + input = pInput; + } + + // Create or convert output to a suitable image + // TODO: OPTIMIZE: Don't really need to convert all types to same as input + result = pOutput != null ? /*pOutput*/ ImageUtil.toBuffered(pOutput, input.getType()) : createCompatibleDestImage(input, null); + + resample(input, result, filter); + + // If pOutput != null and needed to be converted, draw it back + if (pOutput != null && pOutput != result) { + //pOutput.setData(output.getRaster()); + ImageUtil.drawOnto(pOutput, result); + result = pOutput; + } + + 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 forget to 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 pInput, final BufferedImage pOutput, final int pWidth, final int pHeight, final int pType) { + BufferedImage temp = pInput; + + double xScale; + double yScale; + + AffineTransform transform; + AffineTransformOp scale; + + if (pType > AffineTransformOp.TYPE_NEAREST_NEIGHBOR) { + // Initially scale so all remaining operations will halve the image + if (pWidth < pInput.getWidth() || pHeight < pInput.getHeight()) { + int w = pWidth; + int h = pHeight; + while (w < pInput.getWidth() / 2) { + w *= 2; + } + while (h < pInput.getHeight() / 2) { + h *= 2; + } + + xScale = w / (double) pInput.getWidth(); + yScale = h / (double) pInput.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 = pWidth / (double) temp.getWidth(); + yScale = pHeight / (double) temp.getHeight(); + + if (pType > 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, pType); + + return scale.filter(temp, pOutput); + + } + + /** + * Returns the current filter type constant. + * + * @return the current filter type constant. + * @see filter type constants + */ + public int getFilterType() { + return mFilterType; + } + + 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: // Should never happen + 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, mWidth, mHeight), + cm.isAlphaPremultiplied(), null); + + } + + public RenderingHints getRenderingHints() { + Object value; + switch (mFilterType) { + 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: " + mFilterType); + } + + return new RenderingHints(KEY_RESAMPLE_INTERPOLATION, value); + } + + public Rectangle2D getBounds2D(BufferedImage src) { + return new Rectangle(mWidth, mHeight); + } + + 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 + */ + + static 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)]; + + 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)]; + + 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. + Returns -1 if error, 0 if success. + */ + private BufferedImage resample(BufferedImage pSource, BufferedImage pDest, InterpolationFilter pFilter) { + // TODO: Don't work... Could fix by creating a temporary image in filter method + final int dstWidth = pDest.getWidth(); + final int dstHeight = pDest.getHeight(); + + 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); + + 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)]; + + 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)]; + + 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 qTODO?) + 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 horz 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/SubsampleTester.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/SubsampleTester.java new file mode 100755 index 00000000..be8e52f2 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/SubsampleTester.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.Iterator; + +/** + * SubsampleTester + * + * @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/SubsampleTester.java#1 $ + */ +public class SubsampleTester { + + // Initial testing shows we need at least 9 pixels (sampleFactor == 3) to make a good looking image.. + // Also, using Lanczos is much better than (and allmost as fast as) halving using AffineTransform + // - But I guess those numbers depend on the data type of the input image... + + public static void main(String[] pArgs) throws IOException { + // To/from larger than or equal to 4x4 + //ImageUtil.createResampled(new BufferedImage(5, 5, BufferedImage.TYPE_INT_ARGB), 4, 4, BufferedImage.SCALE_SMOOTH); + //ImageUtil.createResampled(new BufferedImage(4, 4, BufferedImage.TYPE_INT_ARGB), 5, 5, BufferedImage.SCALE_SMOOTH); + + // To/from smaller than or equal to 4x4 with fast scale + //ImageUtil.createResampled(new BufferedImage(3, 3, BufferedImage.TYPE_INT_ARGB), 10, 10, BufferedImage.SCALE_FAST); + //ImageUtil.createResampled(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB), 3, 3, BufferedImage.SCALE_FAST); + + // To/from smaller than or equal to 4x4 with default scale + //ImageUtil.createResampled(new BufferedImage(3, 3, BufferedImage.TYPE_INT_ARGB), 10, 10, BufferedImage.SCALE_DEFAULT); + //ImageUtil.createResampled(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB), 3, 3, BufferedImage.SCALE_DEFAULT); + + // To/from smaller than or equal to 4x4 with smooth scale + try { + ImageUtil.createResampled(new BufferedImage(3, 3, BufferedImage.TYPE_INT_ARGB), 10, 10, BufferedImage.SCALE_SMOOTH); + } + catch (IndexOutOfBoundsException e) { + e.printStackTrace(); + } + //try { + // ImageUtil.createResampled(new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB), 3, 3, BufferedImage.SCALE_SMOOTH); + //} + //catch (IndexOutOfBoundsException e) { + // e.printStackTrace(); + // return; + //} + + File input = new File(pArgs[0]); + ImageInputStream stream = ImageIO.createImageInputStream(input); + + Iterator readers = ImageIO.getImageReaders(stream); + if (readers.hasNext()) { + if (stream == null) { + return; + } + ImageReader reader = readers.next(); + reader.setInput(stream); + + ImageReadParam param = reader.getDefaultReadParam(); + + for (int i = 0; i < 25; i++) { + //readImage(pArgs, reader, param); + } + + long start = System.currentTimeMillis(); + + BufferedImage image = readImage(pArgs, reader, param); + + long end = System.currentTimeMillis(); + + System.out.println("elapsed time: " + (end - start) + " ms"); + + int subX = param.getSourceXSubsampling(); + int subY = param.getSourceYSubsampling(); + + System.out.println("image: " + image); + + //ImageIO.write(image, "png", new File(input.getParentFile(), input.getName().replace('.', '_') + "_new.png")); + + ConvolveTester.showIt(image, input.getName() + (subX > 1 || subY > 1 ? " (subsampled " + subX + " by " + subY + ")" : "")); + } + else { + System.err.println("No reader found for input: " + input.getAbsolutePath()); + } + } + + private static BufferedImage readImage(final String[] pArgs, final ImageReader pReader, final ImageReadParam pParam) throws IOException { + double sampleFactor; // Minimum number of samples (in each dimension) pr pixel in output + + int width = pArgs.length > 1 ? Integer.parseInt(pArgs[1]) : 300; + int height = pArgs.length > 2 ? Integer.parseInt(pArgs[2]) : 200; + + if (pArgs.length > 3 && (sampleFactor = Double.parseDouble(pArgs[3])) > 0) { + int originalWidth = pReader.getWidth(0); + int originalHeight = pReader.getHeight(0); + + System.out.println("originalWidth: " + originalWidth); + System.out.println("originalHeight: " + originalHeight); + + int subX = (int) Math.max(originalWidth / (double) (width * sampleFactor), 1.0); + int subY = (int) Math.max(originalHeight / (double) (height * sampleFactor), 1.0); + + if (subX > 1 || subY > 1) { + System.out.println("subX: " + subX); + System.out.println("subY: " + subY); + pParam.setSourceSubsampling(subX, subY, subX > 1 ? subX / 2 : 0, subY > 1 ? subY / 2 : 0); + } + } + + BufferedImage image = pReader.read(0, pParam); + + System.out.println("image: " + image); + + int algorithm = BufferedImage.SCALE_DEFAULT; + if (pArgs.length > 4) { + if ("smooth".equals(pArgs[4].toLowerCase())) { + algorithm = BufferedImage.SCALE_SMOOTH; + } + else if ("fast".equals(pArgs[4].toLowerCase())) { + algorithm = BufferedImage.SCALE_FAST; + } + } + + if (image.getWidth() != width || image.getHeight() != height) { + image = ImageUtil.createScaled(image, width, height, algorithm); + } + + return image; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/SubsamplingFilter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/SubsamplingFilter.java new file mode 100755 index 00000000..26aecd6a --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/SubsamplingFilter.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.image; + +import java.awt.image.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 mXSub; + private int mYSub; + + /** + * 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."); + } + + mXSub = pXSub; + mYSub = pYSub; + } + + /** {@code ImageFilter} implementation, do not invoke. */ + public void setDimensions(int pWidth, int pHeight) { + destWidth = (pWidth + mXSub - 1) / mXSub; + destHeight = (pHeight + mYSub - 1) / mYSub; + + //System.out.println("Subsampling: " + mXSub + "," + mYSub + "-> " + destWidth + ", " + destHeight); + super.setDimensions(pWidth, pHeight); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/inv_cmap.c b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/inv_cmap.c new file mode 100755 index 00000000..efdc5600 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/inv_cmap.c @@ -0,0 +1,524 @@ +/* + * This software is copyrighted as noted below. It may be freely copied, + * modified, and redistributed, provided that the copyright notice is + * preserved on all copies. + * + * There is no warranty or other guarantee of fitness for this software, + * it is provided solely "as is". Bug reports or fixes may be sent + * to the author, who may or may not act on them as he desires. + * + * You may not include this software in a program or other software product + * without supplying the source, or without informing the end-user that the + * source is available for no extra charge. + * + * If you modify this software, you should include a notice giving the + * name of the person performing the modification, the date of modification, + * and the reason for such modification. + */ +/* + * inv_cmap.c - Compute an inverse colormap. + * + * Author: Spencer W. Thomas + * EECS Dept. + * University of Michigan + * Date: Thu Sep 20 1990 + * Copyright (c) 1990, University of Michigan + * + * $Id: inv_cmap.c,v 3.0.1.3 1992/04/30 14:07:28 spencer Exp $ + */ + +#include +#include + + +static int bcenter, gcenter, rcenter; +static long gdist, rdist, cdist; +static long cbinc, cginc, crinc; +static unsigned long *gdp, *rdp, *cdp; +static unsigned char *grgbp, *rrgbp, *crgbp; +static gstride, rstride; +static long x, xsqr, colormax; +static int cindex; + +#ifdef USE_PROTOTYPES +static void maxfill( unsigned long *, long ); +static int redloop( void ); +static int greenloop( int ); +static int blueloop( int ); +#else +static void maxfill(); +static int redloop(); +static int greenloop(); +static int blueloop(); +#endif + + +/***************************************************************** + * TAG( inv_cmap ) + * + * Compute an inverse colormap efficiently. + * Inputs: + * colors: Number of colors in the forward colormap. + * colormap: The forward colormap. + * bits: Number of quantization bits. The inverse + * colormap will have (2^bits)^3 entries. + * dist_buf: An array of (2^bits)^3 long integers to be + * used as scratch space. + * Outputs: + * rgbmap: The output inverse colormap. The entry + * rgbmap[(r<<(2*bits)) + (g<> nbits; + gcenter = colormap[1][cindex] >> nbits; + bcenter = colormap[2][cindex] >> nbits; + + rdist = colormap[0][cindex] - (rcenter * x + x/2); + gdist = colormap[1][cindex] - (gcenter * x + x/2); + cdist = colormap[2][cindex] - (bcenter * x + x/2); + cdist = rdist*rdist + gdist*gdist + cdist*cdist; + + crinc = 2 * ((rcenter + 1) * xsqr - (colormap[0][cindex] * x)); + cginc = 2 * ((gcenter + 1) * xsqr - (colormap[1][cindex] * x)); + cbinc = 2 * ((bcenter + 1) * xsqr - (colormap[2][cindex] * x)); + + /* Array starting points. */ + cdp = dist_buf + rcenter * rstride + gcenter * gstride + bcenter; + crgbp = rgbmap + rcenter * rstride + gcenter * gstride + bcenter; + + (void)redloop(); + } +} + +/* redloop -- loop up and down from red center. */ +static int +redloop() +{ + int detect; + int r; + int first; + long txsqr = xsqr + xsqr; + static long rxx; + + detect = 0; + + /* Basic loop up. */ + for ( r = rcenter, rdist = cdist, rxx = crinc, + rdp = cdp, rrgbp = crgbp, first = 1; + r < colormax; + r++, rdp += rstride, rrgbp += rstride, + rdist += rxx, rxx += txsqr, first = 0 ) + { + if ( greenloop( first ) ) + detect = 1; + else if ( detect ) + break; + } + + /* Basic loop down. */ + for ( r = rcenter - 1, rxx = crinc - txsqr, rdist = cdist - rxx, + rdp = cdp - rstride, rrgbp = crgbp - rstride, first = 1; + r >= 0; + r--, rdp -= rstride, rrgbp -= rstride, + rxx -= txsqr, rdist -= rxx, first = 0 ) + { + if ( greenloop( first ) ) + detect = 1; + else if ( detect ) + break; + } + + return detect; +} + +/* greenloop -- loop up and down from green center. */ +static int +greenloop( restart ) +int restart; +{ + int detect; + int g; + int first; + long txsqr = xsqr + xsqr; + static int here, min, max; + static long ginc, gxx, gcdist; /* "gc" variables maintain correct */ + static unsigned long *gcdp; /* values for bcenter position, */ + static unsigned char *gcrgbp; /* despite modifications by blueloop */ + /* to gdist, gdp, grgbp. */ + + if ( restart ) + { + here = gcenter; + min = 0; + max = colormax - 1; + ginc = cginc; + } + + detect = 0; + + /* Basic loop up. */ + for ( g = here, gcdist = gdist = rdist, gxx = ginc, + gcdp = gdp = rdp, gcrgbp = grgbp = rrgbp, first = 1; + g <= max; + g++, gdp += gstride, gcdp += gstride, grgbp += gstride, gcrgbp += gstride, + gdist += gxx, gcdist += gxx, gxx += txsqr, first = 0 ) + { + if ( blueloop( first ) ) + { + if ( !detect ) + { + /* Remember here and associated data! */ + if ( g > here ) + { + here = g; + rdp = gcdp; + rrgbp = gcrgbp; + rdist = gcdist; + ginc = gxx; + } + detect = 1; + } + } + else if ( detect ) + { + break; + } + } + + /* Basic loop down. */ + for ( g = here - 1, gxx = ginc - txsqr, gcdist = gdist = rdist - gxx, + gcdp = gdp = rdp - gstride, gcrgbp = grgbp = rrgbp - gstride, + first = 1; + g >= min; + g--, gdp -= gstride, gcdp -= gstride, grgbp -= gstride, gcrgbp -= gstride, + gxx -= txsqr, gdist -= gxx, gcdist -= gxx, first = 0 ) + { + if ( blueloop( first ) ) + { + if ( !detect ) + { + /* Remember here! */ + here = g; + rdp = gcdp; + rrgbp = gcrgbp; + rdist = gcdist; + ginc = gxx; + detect = 1; + } + } + else if ( detect ) + { + break; + } + } + + + return detect; +} + +/* blueloop -- loop up and down from blue center. */ +static int +blueloop( restart ) +int restart; +{ + int detect; + register unsigned long *dp; + register unsigned char *rgbp; + register long bdist, bxx; + register int b, i = cindex; + register long txsqr = xsqr + xsqr; + register int lim; + static int here, min, max; + static long binc; + + if ( restart ) + { + here = bcenter; + min = 0; + max = colormax - 1; + binc = cbinc; + } + + detect = 0; + + /* Basic loop up. */ + /* First loop just finds first applicable cell. */ + for ( b = here, bdist = gdist, bxx = binc, dp = gdp, rgbp = grgbp, lim = max; + b <= lim; + b++, dp++, rgbp++, + bdist += bxx, bxx += txsqr ) + { + if ( *dp > bdist ) + { + /* Remember new 'here' and associated data! */ + if ( b > here ) + { + here = b; + gdp = dp; + grgbp = rgbp; + gdist = bdist; + binc = bxx; + } + detect = 1; + break; + } + } + /* Second loop fills in a run of closer cells. */ + for ( ; + b <= lim; + b++, dp++, rgbp++, + bdist += bxx, bxx += txsqr ) + { + if ( *dp > bdist ) + { + *dp = bdist; + *rgbp = i; + } + else + { + break; + } + } + + /* Basic loop down. */ + /* Do initializations here, since the 'find' loop might not get + * executed. + */ + lim = min; + b = here - 1; + bxx = binc - txsqr; + bdist = gdist - bxx; + dp = gdp - 1; + rgbp = grgbp - 1; + /* The 'find' loop is executed only if we didn't already find + * something. + */ + if ( !detect ) + for ( ; + b >= lim; + b--, dp--, rgbp--, + bxx -= txsqr, bdist -= bxx ) + { + if ( *dp > bdist ) + { + /* Remember here! */ + /* No test for b against here necessary because b < + * here by definition. + */ + here = b; + gdp = dp; + grgbp = rgbp; + gdist = bdist; + binc = bxx; + detect = 1; + break; + } + } + /* The 'update' loop. */ + for ( ; + b >= lim; + b--, dp--, rgbp--, + bxx -= txsqr, bdist -= bxx ) + { + if ( *dp > bdist ) + { + *dp = bdist; + *rgbp = i; + } + else + { + break; + } + } + + + /* If we saw something, update the edge trackers. */ + + return detect; +} + +static void +maxfill( buffer, side ) +unsigned long *buffer; +long side; +{ + register unsigned long maxv = ~0L; + register long i; + register unsigned long *bp; + + for ( i = side * side * side, bp = buffer; + i > 0; + i--, bp++ ) + *bp = maxv; +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/package-info.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/package-info.java new file mode 100755 index 00000000..e344c3a8 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/image/package-info.java @@ -0,0 +1,9 @@ +/** + * Classes for image manipulation. + *

+ * See the class {@link com.twelvemonkeys.image.ImageUtil}. + * + * @version 1.0 + * @author Harald Kuhr + */ +package com.twelvemonkeys.image; \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/AbstractCachedSeekableStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/AbstractCachedSeekableStream.java new file mode 100755 index 00000000..ba23b5ea --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/AbstractCachedSeekableStream.java @@ -0,0 +1,277 @@ +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.Validate; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Represents a cached seekable stream, that reads through a cache. + * + * @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/AbstractCachedSeekableStream.java#2 $ + */ +abstract class AbstractCachedSeekableStream extends SeekableInputStream { + /** The backing stream */ + protected final InputStream mStream; + + /** The stream positon in the backing stream (mStream) */ + protected long mStreamPosition; + + private StreamCache mCache; + + protected AbstractCachedSeekableStream(final InputStream pStream, final StreamCache pCache) { + Validate.notNull(pStream, "stream"); + Validate.notNull(pCache, "cache"); + + mStream = pStream; + mCache = pCache; + } + + protected final StreamCache getCache() { + return mCache; + } + + @Override + public int available() throws IOException { + long avail = mStreamPosition - mPosition + mStream.available(); + return avail > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) avail; + } + + public int read() throws IOException { + checkOpen(); + int read; + + if (mPosition == mStreamPosition) { + // TODO: Read more bytes here! + // TODO: Use buffer if not in-memory cache? (See FileCacheSeekableStream overrides). + // Read a byte from the stream + read = mStream.read(); + + if (read >= 0) { + mStreamPosition++; + mCache.write(read); + } + } + else { + // ..or read byte from the cache + syncPosition(); + read = mCache.read(); + } + + // TODO: This field is not REALLY considered accessible.. :-P + if (read != -1) { + mPosition++; + } + + return read; + } + + @Override + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + checkOpen(); + int length; + + if (mPosition == mStreamPosition) { + // Read bytes from the stream + length = mStream.read(pBytes, pOffset, pLength); + + if (length > 0) { + mStreamPosition += length; + mCache.write(pBytes, pOffset, length); + } + } + else { + // ...or read bytes from the cache + syncPosition(); + length = mCache.read(pBytes, pOffset, pLength); + } + + // TODO: This field is not REALLY considered accessible.. :-P + if (length > 0) { + mPosition += length; + } + + return length; + } + + protected final void syncPosition() throws IOException { + if (mCache.getPosition() != mPosition) { + mCache.seek(mPosition); // Assure EOF is correctly thrown + } + } + + public final boolean isCached() { + return true; + } + + public abstract boolean isCachedMemory(); + + public abstract boolean isCachedFile(); + + protected void seekImpl(long pPosition) throws IOException { + if (mStreamPosition < pPosition) { + // Make sure we append at end of cache + if (mCache.getPosition() != mStreamPosition) { + mCache.seek(mStreamPosition); + } + + // Read diff from stream into cache + long left = pPosition - mStreamPosition; + + // TODO: Use fixed buffer, instead of allocating here... + int bufferLen = left > 1024 ? 1024 : (int) left; + byte[] buffer = new byte[bufferLen]; + + while (left > 0) { + int length = buffer.length < left ? buffer.length : (int) left; + int read = mStream.read(buffer, 0, length); + + if (read > 0) { + mCache.write(buffer, 0, read); + mStreamPosition += read; + left -= read; + } + else if (read < 0) { + break; + } + } + } + else if (mStreamPosition >= pPosition) { + // Seek backwards into the cache + mCache.seek(pPosition); + } + +// System.out.println("pPosition: " + pPosition); +// System.out.println("mPosition: " + mPosition); +// System.out.println("mStreamPosition: " + mStreamPosition); +// System.out.println("mCache.mPosition: " + mCache.getPosition()); + + // NOTE: If mPosition == pPosition then we're good to go + } + + protected void flushBeforeImpl(long pPosition) { + mCache.flush(pPosition); + } + + protected void closeImpl() throws IOException { + mCache.flush(mPosition); + mCache = null; + mStream.close(); + } + + /** + * An abstract stream cache. + * + * @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/AbstractCachedSeekableStream.java#2 $ + */ + public static abstract class StreamCache { + + /** + * Creates a {@code StreamCache}. + */ + protected StreamCache() { + } + + /** + * Writes a single byte at the current read/write position. The read/write position will be increased by one. + * + * @param pByte the byte value to write. + * + * @throws IOException if an I/O exception occurs in the cache backing mechanism. + */ + abstract void write(int pByte) throws IOException; + + /** + * 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. + * @param pLength the number of bytes to write from the buffer. + * + * @throws IOException if an I/O exception occurs in the cache backing mechanism. + */ + void write(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { + for (int i = 0; i < pLength; i++) { + write(pBuffer[pOffset + i]); + } + } + + /** + * Reads a single byte a the current read/write position. The read/write position will be increased by one. + * + * @return the value read, or {@code -1} to indicate EOF. + * + * @throws IOException if an I/O exception occurs in the cache backing mechanism. + */ + abstract int read() throws IOException; + + /** + * 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. + * @param pLength the number of bytes to write from the buffer. + * @return the number of bytes read, or {@code -1} to indicate EOF. + * + * @throws IOException if an I/O exception occurs in the cache backing mechanism. + */ + int read(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { + int count = 0; + for (int i = 0; i < pLength; i++) { + int read = read(); + if (read >= 0) { + pBuffer[pOffset + i] = (byte) read; + count++; + } + else { + break; + } + } + return count; + } + + /** + * Repositions the current cache read/write position to the given position. + * + * @param pPosition the new read/write position + * + * @throws IOException if an I/O exception occurs in the cache backing mechanism. + */ + abstract void seek(long pPosition) throws IOException; + + /** + * 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. + */ + void flush(final long pPosition) { + } + + /** + * Returns the current cache read/write position. + * + * @return the current cache read/write postion. + * + * @throws IOException if the position can't be determined because of a problem in the cache backing mechanism. + */ + abstract long getPosition() throws IOException; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/CompoundReader.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/CompoundReader.java new file mode 100755 index 00000000..35114414 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/CompoundReader.java @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.Validate; + +import java.io.IOException; +import java.io.Reader; +import java.util.Iterator; +import java.util.ArrayList; +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 mCurrent; + private List mReaders; + protected final Object mLock; + + protected final boolean mMarkSupported; + + private int mCurrentReader; + private int mMarkedReader; + private int mMark; + 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")); + + mLock = pReaders; // NOTE: It's ok to sync on pReaders, as the + // reference can't change, only it's elements + + mReaders = new ArrayList(); + + boolean markSupported = true; + while (pReaders.hasNext()) { + Reader reader = pReaders.next(); + if (reader == null) { + throw new NullPointerException("readers cannot contain null-elements"); + } + mReaders.add(reader); + markSupported = markSupported && reader.markSupported(); + } + mMarkSupported = markSupported; + + mCurrent = nextReader(); + } + + protected final Reader nextReader() { + if (mCurrentReader >= mReaders.size()) { + mCurrent = new EmptyReader(); + } + else { + mCurrent = mReaders.get(mCurrentReader++); + } + + // NOTE: Reset mNext for every reader, and record marked reader in mark/reset methods! + mNext = 0; + return mCurrent; + } + + /** + * 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 (mReaders == null) { + throw new IOException("Stream closed"); + } + } + + public void close() throws IOException { + // Close all readers + for (Reader reader : mReaders) { + reader.close(); + } + mReaders = 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 (mLock) { + ensureOpen(); + mMark = mNext; + mMarkedReader = mCurrentReader; + + mCurrent.mark(pReadLimit); + } + } + + @Override + public void reset() throws IOException { + synchronized (mLock) { + ensureOpen(); + + if (mCurrentReader != mMarkedReader) { + // Reset any reader before this + for (int i = mCurrentReader; i >= mMarkedReader; i--) { + mReaders.get(i).reset(); + } + + mCurrentReader = mMarkedReader - 1; + nextReader(); + } + mCurrent.reset(); + + mNext = mMark; + } + } + + @Override + public boolean markSupported() { + return mMarkSupported; + } + + @Override + public int read() throws IOException { + synchronized (mLock) { + int read = mCurrent.read(); + + if (read < 0 && mCurrentReader < mReaders.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 (mLock) { + int read = mCurrent.read(pBuffer, pOffset, pLength); + + if (read < 0 && mCurrentReader < mReaders.size()) { + nextReader(); + return read(pBuffer, pOffset, pLength); // In case of 0-length readers + } + + mNext += read; + + return read; + } + } + + @Override + public boolean ready() throws IOException { + return mCurrent.ready(); + } + + @Override + public long skip(long pChars) throws IOException { + synchronized (mLock) { + long skipped = mCurrent.skip(pChars); + + if (skipped == 0 && mCurrentReader < mReaders.size()) { + nextReader(); + return skip(pChars); // In case of 0-length readers + } + + mNext += skipped; + + return skipped; + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/EmptyReader.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/EmptyReader.java new file mode 100755 index 00000000..d83c9053 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/EmptyReader.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FastByteArrayOutputStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FastByteArrayOutputStream.java new file mode 100755 index 00000000..da7a2d1a --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FastByteArrayOutputStream.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.ByteArrayInputStream; + +/** + * 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: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FastByteArrayOutputStream.java#2 $ + */ +public final class FastByteArrayOutputStream extends ByteArrayOutputStream { + /** Max grow size (unless if writing more than this ammount of bytes) */ + protected int mMaxGrowSize = 1024 * 1024; // 1 MB + + /** + * 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 synchronized 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 synchronized 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 + mMaxGrowSize), 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 behviour. + * + * @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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileCacheSeekableStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileCacheSeekableStream.java new file mode 100755 index 00000000..c064b394 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileCacheSeekableStream.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.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 final InputStream mStream; +// private final RandomAccessFile mCache; + private byte[] mBuffer; + + /** The stream positon in the backing stream (mStream) */ +// private long mStreamPosition; + + // TODO: getStreamPosition() should always be the same as + // mCache.getFilePointer() + // otherwise there's some inconsistency here... Enforce this? + + /** + * 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? + mBuffer = new byte[1024]; + } + + public final boolean isCachedMemory() { + return false; + } + + public final boolean isCachedFile() { + return true; + } + + @Override + protected void closeImpl() throws IOException { + super.closeImpl(); + mBuffer = null; + } +/* + public final boolean isCached() { + return true; + } + + // InputStream overrides + @Override + public int available() throws IOException { + long avail = mStreamPosition - mPosition + mStream.available(); + return avail > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) avail; + } + + public void closeImpl() throws IOException { + mStream.close(); + mCache.close(); + + // TODO: Delete cache file here? + // ThreadPool.invokeLater(new DeleteFileAction(mCacheFile)); + } + */ + + @Override + public int read() throws IOException { + checkOpen(); + + int read; + if (mPosition == mStreamPosition) { + // Read ahead into buffer, for performance + read = readAhead(mBuffer, 0, mBuffer.length); + if (read >= 0) { + read = mBuffer[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) { + mPosition++; + } + return read; + } + + @Override + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + checkOpen(); + + int length; + if (mPosition == mStreamPosition) { + // 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, mStreamPosition - mPosition)); + + //System.out.println("Read " + length + " byte from cache"); + } + + // TODO: This field is not REALLY considered accessible.. :-P + if (length > 0) { + mPosition += length; + } + return length; + } + + private int readAhead(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { + int length; + length = mStream.read(pBytes, pOffset, pLength); + + if (length > 0) { + mStreamPosition += length; + getCache().write(pBytes, pOffset, length); + } + return length; + } + + /* + private void syncPosition() throws IOException { + if (mCache.getFilePointer() != mPosition) { + mCache.seek(mPosition); // Assure EOF is correctly thrown + } + } + + // Seekable overrides + + protected void flushBeforeImpl(long pPosition) { + // TODO: Implement + // For now, it's probably okay to do nothing, this is just for + // performance (as long as people follow spec, not behaviour) + } + + protected void seekImpl(long pPosition) throws IOException { + if (mStreamPosition < pPosition) { + // Make sure we append at end of cache + if (mCache.getFilePointer() != mStreamPosition) { + mCache.seek(mStreamPosition); + } + + // Read diff from stream into cache + long left = pPosition - mStreamPosition; + int bufferLen = left > 1024 ? 1024 : (int) left; + byte[] buffer = new byte[bufferLen]; + + while (left > 0) { + int length = buffer.length < left ? buffer.length : (int) left; + int read = mStream.read(buffer, 0, length); + + if (read > 0) { + mCache.write(buffer, 0, read); + mStreamPosition += read; + left -= read; + } + else if (read < 0) { + break; + } + } + } + else if (mStreamPosition >= pPosition) { + // Seek backwards into the cache + mCache.seek(pPosition); + } + +// System.out.println("pPosition: " + pPosition); +// System.out.println("mStreamPosition: " + mStreamPosition); +// System.out.println("mCache.getFilePointer(): " + mCache.getFilePointer()); + + // NOTE: If mPosition == pPosition then we're good to go + } + */ + + final static class FileCache extends StreamCache { + private RandomAccessFile mCacheFile; + + public FileCache(final File pFile) throws FileNotFoundException { + Validate.notNull(pFile, "file"); + mCacheFile = new RandomAccessFile(pFile, "rw"); + } + + public void write(final int pByte) throws IOException { + mCacheFile.write(pByte); + } + + @Override + public void write(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { + mCacheFile.write(pBuffer, pOffset, pLength); + } + + public int read() throws IOException { + return mCacheFile.read(); + } + + @Override + public int read(final byte[] pBuffer, final int pOffset, final int pLength) throws IOException { + return mCacheFile.read(pBuffer, pOffset, pLength); + } + + public void seek(final long pPosition) throws IOException { + mCacheFile.seek(pPosition); + } + + public long getPosition() throws IOException { + return mCacheFile.getFilePointer(); + } + } + +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSeekableStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSeekableStream.java new file mode 100755 index 00000000..87bcee47 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSeekableStream.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import java.io.*; + +/** + * 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() - mPosition; + 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) { + mPosition++; + } + 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) { + mPosition += 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSystem.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSystem.java new file mode 100755 index 00000000..7670ac3e --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileSystem.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * FileSystem + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/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.indexOf("windows") != -1) { + return new Win32FileSystem(); + } + else if (os.indexOf("linux") != -1 || + os.indexOf("sun os") != -1 || + os.indexOf("sunos") != -1 || + os.indexOf("solaris") != -1 || + os.indexOf("mpe/ix") != -1 || + os.indexOf("hp-ux") != -1 || + os.indexOf("aix") != -1 || + os.indexOf("freebsd") != -1 || + os.indexOf("irix") != -1 || + os.indexOf("digital unix") != -1 || + os.indexOf("unix") != -1 || + os.indexOf("mac os x") != -1) { + return new UnixFileSystem(); + } + else { + return new UnknownFileSystem(os); + } + } + + private static class UnknownFileSystem extends FileSystem { + private final String mOSName; + + UnknownFileSystem(String pOSName) { + mOSName = pOSName; + } + + long getFreeSpace(File pPath) { + return 0l; + } + + long getTotalSpace(File pPath) { + return 0l; + } + + String getName() { + return "Unknown (" + mOSName + ")"; + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileUtil.java new file mode 100755 index 00000000..defb8d04 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FileUtil.java @@ -0,0 +1,1092 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.Validate; +import com.twelvemonkeys.util.Visitor; + +import java.io.*; +import java.net.URL; +import java.text.NumberFormat; +import java.lang.reflect.Method; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.UndeclaredThrowableException; + +/** + * 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's 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"); + + // 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's probably okay.. ;-) + } + + /* + // Consider using the example from + // http://developer.java.sun.com/developer/Books/performance/ch04.pdf + // Test if this is really faster. And what about a lot of concurrence? + // Have a pool of buffers? :-) + + static final int BUFF_SIZE = 100000; + static final byte[] buffer = new byte[BUFF_SIZE]; + + public static void copy(InputStream in, OutputStream out) throws IOException { + while (true) { + synchronized (buffer) { + int amountRead = in.read(buffer); + if (amountRead == -1) { + break; + } + out.write(buffer, 0, amountRead); + } + } + } + */ + + /** + * Gets the file (type) extension of the given file. + * A file extension is the part of the filename, after the last occurence + * 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 bytearray + 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 mFailedCount = 0; + private IOException mException = null; + + public void visit(final File pFile) { + try { + if (!delete(pFile, true)) { + mFailedCount++; + } + } + catch (IOException e) { + mFailedCount++; + if (mException == null) { + mException = e; + } + } + } + + boolean succeeded() throws IOException { + if (mException != null) { + throw mException; + } + return mFailedCount == 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 implictly called by isDirectory + throw new FileNotFoundException("\"" + pFolder + "\" is not a directory or is not readable."); + } + + if (StringUtil.isEmpty(pFilenameMask)) { + return folder.listFiles(); + } + + 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) { + 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/platfor 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 + */ + 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FilenameMaskFilter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FilenameMaskFilter.java new file mode 100755 index 00000000..1626affc --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FilenameMaskFilter.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import com.twelvemonkeys.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 + */ +public class FilenameMaskFilter implements FilenameFilter { + + // Members + private String[] mFilenameMasksForInclusion; + private String[] mFilenameMasksForExclusion; + private boolean mInclusion = 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) { + mFilenameMasksForInclusion = pFilenameMasksForInclusion; + } + + /** + * @return the current inclusion masks + */ + public String[] getFilenameMasksForInclusion() { + return mFilenameMasksForInclusion.clone(); + } + + /** + * @param pFilenameMasksForExclusion the filename masks to exclude + */ + public void setFilenameMasksForExclusion(String[] pFilenameMasksForExclusion) { + mFilenameMasksForExclusion = pFilenameMasksForExclusion; + mInclusion = false; + } + + /** + * @return the current exclusion masks + */ + public String[] getFilenameMasksForExclusion() { + return mFilenameMasksForExclusion.clone(); + } + + /** + * This method implements the {@code java.io.FilenameFilter} interface. + * + * @param pDir the directory in which the file was found. + * @param pName the pName of the file. + * @return {@code true} if the pName should be included in the file + * list; {@code false} otherwise. + */ + public boolean accept(File pDir, String pName) { + WildcardStringParser parser; + + // Check each filename string mask whether the file is to be accepted + if (mInclusion) { // Inclusion + for (String mask : mFilenameMasksForInclusion) { + 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 : mFilenameMasksForExclusion) { + 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 (mInclusion) { + // Inclusion + if (mFilenameMasksForInclusion == null) { + retVal.append("No filename masks set - property mFilenameMasksForInclusion is null!"); + } + else { + retVal.append(mFilenameMasksForInclusion.length); + retVal.append(" filename mask(s) - "); + for (i = 0; i < mFilenameMasksForInclusion.length; i++) { + retVal.append("\""); + retVal.append(mFilenameMasksForInclusion[i]); + retVal.append("\", \""); + } + } + } + else { + // Exclusion + if (mFilenameMasksForExclusion == null) { + retVal.append("No filename masks set - property mFilenameMasksForExclusion is null!"); + } + else { + retVal.append(mFilenameMasksForExclusion.length); + retVal.append(" exclusion filename mask(s) - "); + for (i = 0; i < mFilenameMasksForExclusion.length; i++) { + retVal.append("\""); + retVal.append(mFilenameMasksForExclusion[i]); + retVal.append("\", \""); + } + } + } + return retVal.toString(); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FilenameSuffixFilter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FilenameSuffixFilter.java new file mode 100755 index 00000000..fc91efd9 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/FilenameSuffixFilter.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + + +import com.twelvemonkeys.lang.StringUtil; + +import java.io.File; +import java.io.FilenameFilter; + + +/** + * A Java Bean used for approving file names which are to be included in a + * {@code java.io.File} listing. The file name suffixes are used as a + * filter input and is given to the class via the string array property:
+ *

{@code filenameSuffixesToExclude} + *

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

+ * + * @param pDir the directory in which the file was found. + * @param pName the pName of the file. + * @return {@code true} if the pName should be included in the file list; + * {@code false} otherwise. + */ + public boolean accept(final File pDir, final String pName) { + if (StringUtil.isEmpty(mFilenameSuffixesToExclude)) { + return true; + } + + for (String aMFilenameSuffixesToExclude : mFilenameSuffixesToExclude) { + // -- Edit by haraldK, to make interfaces more consistent + // if (StringUtil.filenameSuffixIs(pName, mFilenameSuffixesToExclude[i])) { + if (aMFilenameSuffixesToExclude.equals(FileUtil.getExtension(pName))) { + return false; + } + } + return true; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java new file mode 100755 index 00000000..0cfabfda --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianDataInputStream.java @@ -0,0 +1,427 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * 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 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 + * @version 1.0.3, 28 December 2002 + */ +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(pStream); + if (pStream == null) { + throw new IllegalArgumentException("stream == null"); + } + } + + /** + * Reads a 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 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 byte from the underlying input stream + * with value between -128 and 127 + * + * @return the 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 byte from the underlying + * input stream with value between 0 and 255 + * + * @return the 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 short from the underlying + * input stream in little endian order, low byte first. + * + * @return the 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 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 << 24) >> 16) + ((byte1 << 24) >> 24); + return (byte2 << 8) + byte1; + } + + /** + * Reads a two byte Unicode 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 int from the underlying + * input stream in little endian order, low byte first. + * + * @return the 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 int from the underlying + * input stream in little endian order, low byte first. + * + * @return the 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 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 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 skipBytes + * method of 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 readFully + * method of 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 readFully + * method of 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 readLine + * method of 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(); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianDataOutputStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianDataOutputStream.java new file mode 100755 index 00000000..343c887c --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianDataOutputStream.java @@ -0,0 +1,334 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * 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 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 mWritten; + + /** + * 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(pStream); + if (pStream == null) { + throw new IllegalArgumentException("stream == null"); + } + } + + /** + * Writes the specified byte value to the underlying output stream. + * + * @param pByte the 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); + mWritten++; + } + + /** + * Writes pLength bytes from the specified byte array + * starting at 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); + mWritten += pLength; + } + + + /** + * Writes a 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 0 in written. + * + * @param pBoolean the 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 byte to the underlying output stream + * + * @param pByte the byte value to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeByte(int pByte) throws IOException { + out.write(pByte); + mWritten++; + } + + /** + * Writes a two byte short to the underlying output stream in + * little endian order, low byte first. + * + * @param pShort the 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); + mWritten += 2; + } + + /** + * Writes a two byte char to the underlying output stream + * in little endian order, low byte first. + * + * @param pChar the 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); + mWritten += 2; + } + + /** + * Writes a four-byte int to the underlying output stream + * in little endian order, low byte first, high byte last + * + * @param pInt the 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); + mWritten += 4; + + } + + /** + * Writes an eight-byte long to the underlying output stream + * in little endian order, low byte first, high byte last + * + * @param pLong the 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); + mWritten += 8; + } + + /** + * Writes a 4 byte Java float to the underlying output stream in + * little endian order. + * + * @param f the 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 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 writeByte() method. + * + * @param pString the 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)); + } + mWritten += 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 writeChar method. + * + * @param pString a 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); + } + mWritten += 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)); + mWritten += 2; + } + else { + out.write(0xC0 | ((c >> 6) & 0x1F)); + out.write(0x80 | (c & 0x3F)); + mWritten += 1; + } + } + + mWritten += 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 written field. + * @see #mWritten + */ + public int size() { + return mWritten; + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianRandomAccessFile.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianRandomAccessFile.java new file mode 100755 index 00000000..6a560b9c --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/LittleEndianRandomAccessFile.java @@ -0,0 +1,600 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import 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 mFile; + + public LittleEndianRandomAccessFile(final String pName, final String pMode) throws FileNotFoundException { + this(FileUtil.resolve(pName), pMode); + } + + public LittleEndianRandomAccessFile(final File pFile, final String pMode) throws FileNotFoundException { + mFile = new RandomAccessFile(pFile, pMode); + } + + public void close() throws IOException { + mFile.close(); + } + + public FileChannel getChannel() { + return mFile.getChannel(); + } + + public FileDescriptor getFD() throws IOException { + return mFile.getFD(); + } + + public long getFilePointer() throws IOException { + return mFile.getFilePointer(); + } + + public long length() throws IOException { + return mFile.length(); + } + + public int read() throws IOException { + return mFile.read(); + } + + public int read(final byte[] b) throws IOException { + return mFile.read(b); + } + + public int read(final byte[] b, final int off, final int len) throws IOException { + return mFile.read(b, off, len); + } + + public void readFully(final byte[] b) throws IOException { + mFile.readFully(b); + } + + public void readFully(final byte[] b, final int off, final int len) throws IOException { + mFile.readFully(b, off, len); + } + + public String readLine() throws IOException { + return mFile.readLine(); + } + + /** + * Reads a 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 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 = mFile.read(); + if (b < 0) { + throw new EOFException(); + } + return b != 0; + } + + /** + * Reads a signed byte from the underlying input stream + * with value between -128 and 127 + * + * @return the 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 = mFile.read(); + if (b < 0) { + throw new EOFException(); + } + return (byte) b; + + } + + /** + * Reads an unsigned byte from the underlying + * input stream with value between 0 and 255 + * + * @return the 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 = mFile.read(); + if (b < 0) { + throw new EOFException(); + } + return b; + } + + /** + * Reads a two byte signed short from the underlying + * input stream in little endian order, low byte first. + * + * @return the 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 = mFile.read(); + int byte2 = mFile.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 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 = mFile.read(); + int byte2 = mFile.read(); + if (byte2 < 0) { + throw new EOFException(); + } + //return ((byte2 << 24) >> 16) + ((byte1 << 24) >> 24); + return (byte2 << 8) + byte1; + } + + /** + * Reads a two byte Unicode 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 = mFile.read(); + int byte2 = mFile.read(); + if (byte2 < 0) { + throw new EOFException(); + } + return (char) (((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24)); + } + + + /** + * Reads a four byte signed int from the underlying + * input stream in little endian order, low byte first. + * + * @return the 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 = mFile.read(); + int byte2 = mFile.read(); + int byte3 = mFile.read(); + int byte4 = mFile.read(); + + if (byte4 < 0) { + throw new EOFException(); + } + return (byte4 << 24) + ((byte3 << 24) >>> 8) + + ((byte2 << 24) >>> 16) + ((byte1 << 24) >>> 24); + } + + /** + * Reads an eight byte signed int from the underlying + * input stream in little endian order, low byte first. + * + * @return the 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 = mFile.read(); + long byte2 = mFile.read(); + long byte3 = mFile.read(); + long byte4 = mFile.read(); + long byte5 = mFile.read(); + long byte6 = mFile.read(); + long byte7 = mFile.read(); + long byte8 = mFile.read(); + + 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 = mFile.read(); + int byte2 = mFile.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 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 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 pos is less than + * 0 or if an I/O error occurs. + */ + public void seek(final long pos) throws IOException { + mFile.seek(pos); + } + + public void setLength(final long newLength) throws IOException { + mFile.setLength(newLength); + } + + public int skipBytes(final int n) throws IOException { + return mFile.skipBytes(n); + } + + public void write(final byte[] b) throws IOException { + mFile.write(b); + } + + public void write(final byte[] b, final int off, final int len) throws IOException { + mFile.write(b, off, len); + } + + public void write(final int b) throws IOException { + mFile.write(b); + } + + /** + * Writes a 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 0 in written. + * + * @param pBoolean the 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 byte to the underlying output stream + * + * @param pByte the byte value to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeByte(int pByte) throws IOException { + mFile.write(pByte); + } + + /** + * Writes a two byte short to the underlying output stream in + * little endian order, low byte first. + * + * @param pShort the short to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeShort(int pShort) throws IOException { + mFile.write(pShort & 0xFF); + mFile.write((pShort >>> 8) & 0xFF); + } + + /** + * Writes a two byte char to the underlying output stream + * in little endian order, low byte first. + * + * @param pChar the char value to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeChar(int pChar) throws IOException { + mFile.write(pChar & 0xFF); + mFile.write((pChar >>> 8) & 0xFF); + } + + /** + * Writes a four-byte int to the underlying output stream + * in little endian order, low byte first, high byte last + * + * @param pInt the int to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeInt(int pInt) throws IOException { + mFile.write(pInt & 0xFF); + mFile.write((pInt >>> 8) & 0xFF); + mFile.write((pInt >>> 16) & 0xFF); + mFile.write((pInt >>> 24) & 0xFF); + + } + + /** + * Writes an eight-byte long to the underlying output stream + * in little endian order, low byte first, high byte last + * + * @param pLong the long to be written. + * @throws IOException if the underlying stream throws an IOException. + */ + public void writeLong(long pLong) throws IOException { + mFile.write((int) pLong & 0xFF); + mFile.write((int) (pLong >>> 8) & 0xFF); + mFile.write((int) (pLong >>> 16) & 0xFF); + mFile.write((int) (pLong >>> 24) & 0xFF); + mFile.write((int) (pLong >>> 32) & 0xFF); + mFile.write((int) (pLong >>> 40) & 0xFF); + mFile.write((int) (pLong >>> 48) & 0xFF); + mFile.write((int) (pLong >>> 56) & 0xFF); + } + + /** + * Writes a 4 byte Java float to the underlying output stream in + * little endian order. + * + * @param f the 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 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 writeByte() method. + * + * @param pString the String value to be written. + * @throws IOException if the underlying stream throws an IOException. + * @see #writeByte(int) + * @see #mFile + */ + public void writeBytes(String pString) throws IOException { + int length = pString.length(); + for (int i = 0; i < length; i++) { + mFile.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 writeChar method. + * + * @param pString a String value to be written. + * @throws IOException if the underlying stream throws an IOException. + * @see #writeChar(int) + * @see #mFile + */ + public void writeChars(String pString) throws IOException { + int length = pString.length(); + for (int i = 0; i < length; i++) { + int c = pString.charAt(i); + mFile.write(c & 0xFF); + mFile.write((c >>> 8) & 0xFF); + } + } + + /** + * 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(); + } + + mFile.write((numbytes >>> 8) & 0xFF); + mFile.write(numbytes & 0xFF); + for (int i = 0; i < numchars; i++) { + int c = pString.charAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { + mFile.write(c); + } + else if (c > 0x07FF) { + mFile.write(0xE0 | ((c >> 12) & 0x0F)); + mFile.write(0x80 | ((c >> 6) & 0x3F)); + mFile.write(0x80 | (c & 0x3F)); + } + else { + mFile.write(0xC0 | ((c >> 6) & 0x1F)); + mFile.write(0x80 | (c & 0x3F)); + } + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/MemoryCacheSeekableStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/MemoryCacheSeekableStream.java new file mode 100755 index 00000000..7a0631c3 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/MemoryCacheSeekableStream.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import 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 mCache = new ArrayList(); + private long mLength; + private long mPosition; + private long mStart; + + private byte[] getBlock() throws IOException { + final long currPos = mPosition - mStart; + 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 >= mCache.size()) { + try { + mCache.add(new byte[BLOCK_SIZE]); +// System.out.println("Allocating new block, size: " + BLOCK_SIZE); +// System.out.println("New total size: " + mCache.size() * BLOCK_SIZE + " (" + mCache.size() + " blocks)"); + } + catch (OutOfMemoryError e) { + throw new IOException("No more memory for cache: " + mCache.size() * BLOCK_SIZE); + } + } + + //System.out.println("index: " + index); + + return mCache.get((int) index); + } + + public void write(final int pByte) throws IOException { + byte[] buffer = getBlock(); + + int idx = (int) (mPosition % BLOCK_SIZE); + buffer[idx] = (byte) pByte; + mPosition++; + + if (mPosition > mLength) { + mLength = mPosition; + } + } + + // 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) mPosition % BLOCK_SIZE; + if (index == 0) { + buffer = getBlock(); + } + buffer[index] = pBuffer[pOffset + i]; + + mPosition++; + } + if (mPosition > mLength) { + mLength = mPosition; + } + } + + public int read() throws IOException { + if (mPosition >= mLength) { + return -1; + } + + byte[] buffer = getBlock(); + + int idx = (int) (mPosition % BLOCK_SIZE); + mPosition++; + + return buffer[idx] & 0xff; + } + + // TODO: OptimizeMe!!! + @Override + public int read(final byte[] pBytes, final int pOffset, final int pLength) throws IOException { + if (mPosition >= mLength) { + return -1; + } + + byte[] buffer = getBlock(); + + int bufferPos = (int) (mPosition % BLOCK_SIZE); + + // Find maxIdx and simplify test in for-loop + int maxLen = (int) Math.min(Math.min(pLength, buffer.length - bufferPos), mLength - mPosition); + + int i; + //for (i = 0; i < pLength && i < buffer.length - idx && i < mLength - mPosition; i++) { + for (i = 0; i < maxLen; i++) { + pBytes[pOffset + i] = buffer[bufferPos + i]; + } + + mPosition += i; + + return i; + } + + public void seek(final long pPosition) throws IOException { + if (pPosition < mStart) { + throw new IOException("Seek before flush position"); + } + mPosition = pPosition; + } + + @Override + public void flush(final long pPosition) { + int firstPos = (int) (pPosition / BLOCK_SIZE) - 1; + + for (int i = 0; i < firstPos; i++) { + mCache.remove(0); + } + + mStart = pPosition; + } + + public long getPosition() { + return mPosition; + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullInputStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullInputStream.java new file mode 100755 index 00000000..aa27ac30 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullInputStream.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import java.io.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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullOutputStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullOutputStream.java new file mode 100755 index 00000000..ad99120b --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/NullOutputStream.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/RandomAccessStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/RandomAccessStream.java new file mode 100755 index 00000000..e717cbd2 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/RandomAccessStream.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.io.EOFException; + +/** + * 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 mPosition = -1; + + /** This random access stream, wrapped in an {@code InputStream} */ + SeekableInputStream mInputView = null; + /** This random access stream, wrapped in an {@code OutputStream} */ + SeekableOutputStream mOutputView = 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 (mInputView == null) { + mInputView = new InputStreamView(this); + } + return mInputView; + } + + /** + * 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 (mOutputView == null) { + mOutputView = new OutputStreamView(this); + } + return mOutputView; + } + + static final class InputStreamView extends SeekableInputStream { + // TODO: Consider adding synchonization (on mStream) for all operations + // TODO: Is is a good thing that close/flush etc works on mStream? + // - 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 mStream) for all operations + // TODO: Is is a good thing that close/flush etc works on mStream? + // - 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Seekable.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Seekable.java new file mode 100755 index 00000000..b84d5d3e --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Seekable.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableInputStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableInputStream.java new file mode 100755 index 00000000..1ff511a2 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableInputStream.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import 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 mPosition. mPosition needs to be + // updated from the read/read/read methods... + + /** The stream position in this stream */ + long mPosition; + long mFlushedPosition; + boolean mClosed; + + protected Stack mMarkedPositions = 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(long pLength) throws IOException { + long pos = mPosition; + if (pos + pLength < mFlushedPosition) { + throw new IOException("position < flushedPosition"); + } + + // Stop at stream length for compatibility, even though it's allowed + // to seek past end of stream + seek(Math.min(pos + pLength, pos + available())); + + return mPosition - 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(mPosition - pLimit, mFlushedPosition)); + } + 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 < mFlushedPosition) { + throw new IndexOutOfBoundsException("position < flushedPosition"); + } + + seekImpl(pPosition); + mPosition = pPosition; + } + + protected abstract void seekImpl(long pPosition) throws IOException; + + public final void mark() { + mMarkedPositions.push(mPosition); + } + + @Override + public final void reset() throws IOException { + checkOpen(); + if (!mMarkedPositions.isEmpty()) { + long newPos = mMarkedPositions.pop(); + + // NOTE: This is correct according to javax.imageio (IOException), + // but it's kind of inconsistent with seek that throws IndexOutOfBoundsException... + if (newPos < mFlushedPosition) { + 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 < mFlushedPosition) { + throw new IndexOutOfBoundsException("position < flushedPosition"); + } + if (pPosition > getStreamPosition()) { + throw new IndexOutOfBoundsException("position > stream position"); + } + checkOpen(); + flushBeforeImpl(pPosition); + mFlushedPosition = 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(mFlushedPosition); + } + + public final long getFlushedPosition() throws IOException { + checkOpen(); + return mFlushedPosition; + } + + public final long getStreamPosition() throws IOException { + checkOpen(); + return mPosition; + } + + protected final void checkOpen() throws IOException { + if (mClosed) { + throw new IOException("closed"); + } + } + + @Override + public final void close() throws IOException { + checkOpen(); + mClosed = 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 (!mClosed) { + try { + close(); + } + catch (IOException ignore) { + // Ignroe + } + } + super.finalize(); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableOutputStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableOutputStream.java new file mode 100755 index 00000000..2061e833 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SeekableOutputStream.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import java.io.OutputStream; +import java.io.IOException; +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 mPosition; + long mFlushedPosition; + boolean mClosed; + + protected Stack mMarkedPositions = 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 < mFlushedPosition) { + throw new IndexOutOfBoundsException("position < flushedPosition!"); + } + + seekImpl(pPosition); + mPosition = pPosition; + } + + protected abstract void seekImpl(long pPosition) throws IOException; + + public final void mark() { + mMarkedPositions.push(mPosition); + } + + public final void reset() throws IOException { + checkOpen(); + if (!mMarkedPositions.isEmpty()) { + long newPos = mMarkedPositions.pop(); + + // TODO: This is correct according to javax.imageio (IOException), + // but it's inconsistent with seek that throws IndexOutOfBoundsException... + if (newPos < mFlushedPosition) { + throw new IOException("Previous marked position has been discarded!"); + } + + seek(newPos); + } + } + + public final void flushBefore(long pPosition) throws IOException { + if (pPosition < mFlushedPosition) { + throw new IndexOutOfBoundsException("position < flushedPosition!"); + } + if (pPosition > getStreamPosition()) { + throw new IndexOutOfBoundsException("position > getStreamPosition()!"); + } + checkOpen(); + flushBeforeImpl(pPosition); + mFlushedPosition = pPosition; + } + + protected abstract void flushBeforeImpl(long pPosition) throws IOException; + + @Override + public final void flush() throws IOException { + flushBefore(mFlushedPosition); + } + + public final long getFlushedPosition() throws IOException { + checkOpen(); + return mFlushedPosition; + } + + public final long getStreamPosition() throws IOException { + checkOpen(); + return mPosition; + } + + protected final void checkOpen() throws IOException { + if (mClosed) { + throw new IOException("closed"); + } + } + + @Override + public final void close() throws IOException { + checkOpen(); + mClosed = true; + closeImpl(); + } + + protected abstract void closeImpl() throws IOException; +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/StringArrayReader.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/StringArrayReader.java new file mode 100755 index 00000000..8f2dbb55 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/StringArrayReader.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import java.io.StringReader; +import java.io.IOException; +import java.io.Reader; + +/** + * 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 mCurrent; + private String[] mStrings; + protected final Object mLock; + private int mCurrentSting; + private int mMarkedString; + private int mMark; + private int mNext; + + /** + * Create a new string array reader. + * + * @param pStrings Strings providing the character stream. + */ + public StringArrayReader(final String[] pStrings) { + super(""); + if (pStrings == null) { + throw new NullPointerException("strings == null"); + } + + mLock = lock = pStrings; // NOTE: It's ok to sync on pStrings, as the + // reference can't change, only it's elements + + mStrings = pStrings.clone(); // Defensive copy for content + nextReader(); + } + + protected final Reader nextReader() { + if (mCurrentSting >= mStrings.length) { + mCurrent = new EmptyReader(); + } + else { + mCurrent = new StringReader(mStrings[mCurrentSting++]); + } + // NOTE: Reset mNext for every reader, and record marked reader in mark/reset methods! + mNext = 0; + + return mCurrent; + } + + /** + * 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 (mStrings == null) { + throw new IOException("Stream closed"); + } + } + + public void close() { + super.close(); + mStrings = null; + mCurrent.close(); + } + + public void mark(int pReadLimit) throws IOException { + if (pReadLimit < 0){ + throw new IllegalArgumentException("Read limit < 0"); + } + + synchronized (mLock) { + ensureOpen(); + mMark = mNext; + mMarkedString = mCurrentSting; + + mCurrent.mark(pReadLimit); + } + } + + public void reset() throws IOException { + synchronized (mLock) { + ensureOpen(); + + if (mCurrentSting != mMarkedString) { + mCurrentSting = mMarkedString - 1; + nextReader(); + mCurrent.skip(mMark); + } + else { + mCurrent.reset(); + } + + mNext = mMark; + } + } + + public boolean markSupported() { + return true; + } + + public int read() throws IOException { + synchronized (mLock) { + int read = mCurrent.read(); + + if (read < 0 && mCurrentSting < mStrings.length) { + nextReader(); + return read(); // In case of empty strings + } + + mNext++; + + return read; + } + } + + public int read(char pBuffer[], int pOffset, int pLength) throws IOException { + synchronized (mLock) { + int read = mCurrent.read(pBuffer, pOffset, pLength); + + if (read < 0 && mCurrentSting < mStrings.length) { + nextReader(); + return read(pBuffer, pOffset, pLength); // In case of empty strings + } + + mNext += read; + + return read; + } + } + + public boolean ready() throws IOException { + return mCurrent.ready(); + } + + public long skip(long pChars) throws IOException { + synchronized (mLock) { + long skipped = mCurrent.skip(pChars); + + if (skipped == 0 && mCurrentSting < mStrings.length) { + nextReader(); + return skip(pChars); + } + + mNext += skipped; + + return skipped; + } + } + +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SubStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SubStream.java new file mode 100755 index 00000000..e611ba8f --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/SubStream.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.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 mLeft; + private int mMarkLimit; + + /** + * 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")); + mLeft = 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 (mLeft > 0) { + //noinspection ResultOfMethodCallIgnored + skip(mLeft); + } + } + + @Override + public int available() throws IOException { + return (int) Math.min(super.available(), mLeft); + } + + @Override + public void mark(int pReadLimit) { + super.mark(pReadLimit);// This either succeeds or does nothing... + mMarkLimit = pReadLimit; + } + + @Override + public void reset() throws IOException { + super.reset();// This either succeeds or throws IOException + mLeft += mMarkLimit; + } + + @Override + public int read() throws IOException { + if (mLeft-- <= 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 (mLeft <= 0) { + return -1; + } + + int read = super.read(pBytes, pOffset, (int) findMaxLen(pLength)); + mLeft = read < 0 ? 0 : mLeft - 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 (mLeft < pLength) { + return (int) Math.max(mLeft, 0); + } + else { + return pLength; + } + } + + @Override + public long skip(long pLength) throws IOException { + long skipped = super.skip(findMaxLen(pLength));// Skips 0 or more, never -1 + mLeft -= skipped; + return skipped; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/UnixFileSystem.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/UnixFileSystem.java new file mode 100755 index 00000000..3675fee5 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/UnixFileSystem.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import com.twelvemonkeys.util.StringTokenIterator; + +import java.io.File; +import java.io.IOException; +import java.io.BufferedReader; + +/** + * 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32File.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32File.java new file mode 100755 index 00000000..393ab6c0 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32File.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import java.io.*; + +/** + * 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 File object pointing to a Windows symbolic link + * (.lnk file) in a Win32Lnk. + * If the operating system is not Windows, the + * pPath parameter is returned unwrapped. + * + * @param pPath any path, possibly pointing to a Windows symbolic link file. + * May be null, in which case null is returned. + * + * @return a new Win32Lnk object if the current os is Windows, and + * the file is a Windows symbolic link (.lnk file), otherwise + * 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 File array, possibly pointing to Windows symbolic links + * (.lnk files) in Win32Lnks. + * + * @param pPaths an array of Files, possibly pointing to Windows + * symbolic link files. + * May be null, in which case null is returned. + * + * @return pPaths, with any File representing a Windows + * symbolic link (.lnk file) wrapped in a 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32FileSystem.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32FileSystem.java new file mode 100755 index 00000000..8e8ff5a9 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32FileSystem.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import java.io.File; +import java.io.BufferedReader; +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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32Lnk.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32Lnk.java new file mode 100755 index 00000000..d2e1191b --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/Win32Lnk.java @@ -0,0 +1,472 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import 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 mTarget; + 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 + // mTarget = this causes infinite loops in some methods + target = new File(pPath); + } + mTarget = 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 mTarget; + } + + // java.io.File overrides below + + @Override + public boolean isDirectory() { + return mTarget.isDirectory(); + } + + @Override + public boolean canRead() { + return mTarget.canRead(); + } + + @Override + public boolean canWrite() { + return mTarget.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 mTarget.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 mTarget.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 mTarget.getCanonicalFile(); + } + + @Override + public String getCanonicalPath() throws IOException { + return mTarget.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 mTarget.isFile(); + } + + @Override + public boolean isHidden() { + return mTarget.isHidden(); + } + + @Override + public long lastModified() { + return mTarget.lastModified(); + } + + @Override + public long length() { + return mTarget.length(); + } + + @Override + public String[] list() { + return mTarget.list(); + } + + @Override + public String[] list(final FilenameFilter filter) { + return mTarget.list(filter); + } + + @Override + public File[] listFiles() { + return Win32File.wrap(mTarget.listFiles()); + } + + @Override + public File[] listFiles(final FileFilter filter) { + return Win32File.wrap(mTarget.listFiles(filter)); + } + + @Override + public File[] listFiles(final FilenameFilter filter) { + return Win32File.wrap(mTarget.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 mTarget.setLastModified(time); + } + + @Override + public boolean setReadOnly() { + return mTarget.setReadOnly(); + } + + @Override + public String toString() { + if (mTarget.equals(this)) { + return super.toString(); + } + return super.toString() + " -> " + mTarget.toString(); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/WriterOutputStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/WriterOutputStream.java new file mode 100755 index 00000000..3aa4a41d --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/WriterOutputStream.java @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.DateUtil; + +import java.io.*; +import java.nio.charset.Charset; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; + +/** + * 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 mWriter; + final protected Decoder mDecoder; + final ByteArrayOutputStream mBufferStream = new FastByteArrayOutputStream(1024); + private volatile boolean mIsFlushing = 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) { + mWriter = pWriter; + mDecoder = 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(); + mWriter.close(); + mWriter = null; + } + + @Override + public void flush() throws IOException { + flushBuffer(); + mWriter.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(); + mDecoder.decodeTo(mWriter, pBytes, pOffset, pLength); + } + + @Override + public final void write(int pByte) { + // TODO: Is it possible to know if this is a good place in the stream to + // flush? It might be in the middle of a multi-byte encoded character.. + mBufferStream.write(pByte); + } + + private void flushBuffer() throws IOException { + if (!mIsFlushing && mBufferStream.size() > 0) { + mIsFlushing = true; + mBufferStream.writeTo(this); // NOTE: Avoids cloning buffer array + mBufferStream.reset(); + mIsFlushing = 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java new file mode 100755 index 00000000..105339a6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * Abstract base class for RLE decoding as specifed by in the Windows BMP (aka DIB) file format. + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/AbstractRLEDecoder.java#1 $ + */ +// TODO: Move to other package or make public +abstract class AbstractRLEDecoder implements Decoder { + protected final byte[] mRow; + protected final int mWidth; + protected int mSrcX; + protected int mSrcY; + protected int mDstX; + protected int mDstY; + + /** + * Creates an RLEDecoder. As RLE encoded BMP's may contain x and y deltas, + * etc, we need to know height and width of the image. + * + * @param pWidth width of the image + * @param pHeight heigth of the image + */ + AbstractRLEDecoder(int pWidth, int pHeight) { + mWidth = pWidth; + int bytesPerRow = mWidth; + int mod = bytesPerRow % 4; + if (mod != 0) { + bytesPerRow += 4 - mod; + } + mRow = new byte[bytesPerRow]; + + mSrcX = 0; + mSrcY = pHeight - 1; + + mDstX = mSrcX; + mDstY = mSrcY; + } + + /** + * Decodes one full row of image data. + * + * @param pStream the input stream containint RLE data + * + * @throws IOException if an I/O related exception ocurs while reading + */ + protected abstract void decodeRow(InputStream pStream) throws IOException; + + /** + * Decodes as much data as possible, from the stream into the buffer. + * + * @param pStream the input stream containing RLE data + * @param pBuffer tge buffer to decode the data to + * + * @return the number of bytes decoded from the stream, to the buffer + * + * @throws IOException if an I/O related exception ocurs while reading + */ + public final int decode(InputStream pStream, byte[] pBuffer) throws IOException { + int decoded = 0; + + while (decoded < pBuffer.length && mDstY >= 0) { + // NOTE: Decode only full rows, don't decode if y delta + if (mDstX == 0 && mSrcY == mDstY) { + decodeRow(pStream); + } + + int length = Math.min(mRow.length - mDstX, pBuffer.length - decoded); + System.arraycopy(mRow, mDstX, pBuffer, decoded, length); + mDstX += length; + decoded += length; + + if (mDstX == mRow.length) { + mDstX = 0; + mDstY--; + + // NOTE: If src Y is < dst Y, we have a delta, and have to fill the + // gap with zero-bytes + if (mDstY > mSrcY) { + for (int i = 0; i < mRow.length; i++) { + mRow[i] = 0x00; + } + } + } + } + + return decoded; + } + + /** + * Checks a read byte for EOF marker. + * + * @param pByte the byte to check + * @return the value of {@code pByte} if positive. + * + * @throws EOFException if {@code pByte} is negative + */ + protected static int checkEOF(int pByte) throws EOFException { + if (pByte < 0) { + throw new EOFException("Premature end of file"); + } + return pByte; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64.java new file mode 100755 index 00000000..061a124b --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64.java @@ -0,0 +1,671 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +/* + * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom . com) + * All rights reserved. + *

+ * Redistribution and use in source and binary forms, with or without modification, + * are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this list + * of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above 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 MiG InfoCom AB nor the names of its contributors may be + * used to endorse or promote products derived from this software without specific + * prior written permission. + *

+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY + * OF SUCH DAMAGE. + */ +package com.twelvemonkeys.io.enc; + +import java.util.Arrays; + +/** + * A very fast and memory efficient class to encode and decode to and from + * BASE64 in full accordance with RFC 2045. + *

+ * On Windows XP sp1 with 1.4.2_04 and later ;), this encoder and decoder is + * about 10 times faster on small arrays (10 - 1000 bytes) and 2-3 times as fast + * on larger arrays (10000 - 1000000 bytes) compared to + * {@code sun.misc.Encoder()/Decoder()}. + *

+ * On byte arrays the encoder is about 20% faster than + * Jakarta Commons Base64 Codec + * for encode and about 50% faster for decoding large arrays. This + * implementation is about twice as fast on very small arrays (< 30 bytes). + * If source/destination is a {@code String} this version is about three times + * as fast due to the fact that the Commons Codec result has to be recoded + * to a {@code String} from {@code byte[]}, which is very expensive. + *

+ * This encode/decode algorithm doesn't create any temporary arrays as many + * other codecs do, it only allocates the resulting array. This produces less + * garbage and it is possible to handle arrays twice as large as algorithms that + * create a temporary array. (E.g. Jakarta Commons Codec). It is unknown + * whether Sun's {@code sun.misc.Encoder()/Decoder()} produce temporary arrays + * but since performance is quite low it probably does. + *

+ * The encoder produces the same output as the Sun one except that Sun's encoder + * appends a trailing line separator if the last character isn't a pad. + * Unclear why but it only adds to the length and is probably a side effect. + * Both are in conformance with RFC 2045 though.
+ * Commons codec seem to always add a trailing line separator. + *

+ * Note! + * The encode/decode method pairs (types) come in three versions with the + * exact same algorithm and thus a lot of code redundancy. This is to not + * create any temporary arrays for transcoding to/from different + * format types. The methods not used can simply be commented out. + *

+ * There is also a "fast" version of all decode methods that works the same way + * as the normal ones, but har a few demands on the decoded input. Normally + * though, these fast verions should be used if the source if + * the input is known and it hasn't bee tampered with. + *

+ * If you find the code useful or you find a bug, please send me a note at + * base64 @ miginfocom . com. + *

+ * + * @author Mikael Grev, 2004-aug-02 11:31:11 + * @version 2.2 + */ +final class Base64 { + private static final char[] CA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); + private static final int[] IA = new int[256]; + + static { + Arrays.fill(IA, -1); + for (int i = 0, iS = CA.length; i < iS; i++) { + IA[CA[i]] = i; + } + IA['='] = 0; + } + + // **************************************************************************************** + // * char[] version + // **************************************************************************************** + + /** + * Encodes a raw byte array into a BASE64 {@code char[]} representation im + * accordance with RFC 2045. + * + * @param sArr The bytes to convert. If {@code null} or length 0 an + * empty array will be returned. + * @param lineSep Optional "\r\n" after 76 characters, unless end of file. + *
+ * No line separator will be in breach of RFC 2045 which + * specifies max 76 per line but will be a little faster. + * @return A BASE64 encoded array. Never {@code null}. + */ + public static char[] encodeToChar(byte[] sArr, boolean lineSep) { + // Check special case + int sLen = sArr != null ? sArr.length : 0; + if (sLen == 0) { + return new char[0]; + } + + int eLen = (sLen / 3) * 3;// Length of even 24-bits. + int cCnt = ((sLen - 1) / 3 + 1) << 2;// Returned character count + int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0);// Length of returned array + char[] dArr = new char[dLen]; + + // Encode even 24-bits + for (int s = 0, d = 0, cc = 0; s < eLen;) { + // Copy next three bytes into lower 24 bits of int, paying attension to sign. + int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); + + // Encode the int into four chars + dArr[d++] = CA[(i >>> 18) & 0x3f]; + dArr[d++] = CA[(i >>> 12) & 0x3f]; + dArr[d++] = CA[(i >>> 6) & 0x3f]; + dArr[d++] = CA[i & 0x3f]; + + // Add optional line separator + if (lineSep && ++cc == 19 && d < dLen - 2) { + dArr[d++] = '\r'; + dArr[d++] = '\n'; + cc = 0; + } + } + + // Pad and encode last bits if source isn't even 24 bits. + int left = sLen - eLen;// 0 - 2. + if (left > 0) { + // Prepare the int + int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); + + // Set last four chars + dArr[dLen - 4] = CA[i >> 12]; + dArr[dLen - 3] = CA[(i >>> 6) & 0x3f]; + dArr[dLen - 2] = left == 2 ? CA[i & 0x3f] : '='; + dArr[dLen - 1] = '='; + } + return dArr; + } + + /** + * Decodes a BASE64 encoded char array. All illegal characters will be + * ignored and can handle both arrays with and without line separators. + * + * @param sArr The source array. {@code null} or length 0 will return + * an empty array. + * @return The decoded array of bytes. May be of length 0. Will be + * {@code null} if the legal characters (including '=') isn't + * divideable by 4. (I.e. definitely corrupted). + */ + public static byte[] decode(char[] sArr) { + // Check special case + int sLen = sArr != null ? sArr.length : 0; + if (sLen == 0) { + return new byte[0]; + } + + // Count illegal characters (including '\r', '\n') to know what size the returned array will be, + // so we don't have to reallocate & copy it later. + int sepCnt = 0;// Number of separator characters. (Actually illegal characters, but that's a bonus...) + for (int i = 0; i < sLen; i++)// If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. + { + if (IA[sArr[i]] < 0) { + sepCnt++; + } + } + + // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. + if ((sLen - sepCnt) % 4 != 0) { + return null; + } + + int pad = 0; + for (int i = sLen; i > 1 && IA[sArr[--i]] <= 0;) { + if (sArr[i] == '=') { + pad++; + } + } + + int len = ((sLen - sepCnt) * 6 >> 3) - pad; + + byte[] dArr = new byte[len];// Preallocate byte[] of exact length + + for (int s = 0, d = 0; d < len;) { + // Assemble three bytes into an int from four "valid" characters. + int i = 0; + for (int j = 0; j < 4; j++) + {// j only increased if a valid char was found. + int c = IA[sArr[s++]]; + if (c >= 0) { + i |= c << (18 - j * 6); + } + else { + j--; + } + } + // Add the bytes + dArr[d++] = (byte) (i >> 16); + if (d < len) { + dArr[d++] = (byte) (i >> 8); + if (d < len) { + dArr[d++] = (byte) i; + } + } + } + return dArr; + } + + /** + * Decodes a BASE64 encoded char array that is known to be resonably well formatted. The method is about twice as + * fast as {@link #decode(char[])}. The preconditions are:
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).
+ * + Line separator must be "\r\n", as specified in RFC 2045 + * + The array must not contain illegal characters within the encoded string
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
+ * + * @param sArr The source array. Length 0 will return an empty array. {@code null} will throw an exception. + * @return The decoded array of bytes. May be of length 0. + */ + public static byte[] decodeFast(char[] sArr) { + // Check special case + int sLen = sArr.length; + if (sLen == 0) { + return new byte[0]; + } + + int sIx = 0, eIx = sLen - 1;// Start and end index after trimming. + + // Trim illegal chars from start + while (sIx < eIx && IA[sArr[sIx]] < 0) { + sIx++; + } + + // Trim illegal chars from end + while (eIx > 0 && IA[sArr[eIx]] < 0) { + eIx--; + } + + // get the padding count (=) (0, 1 or 2) + int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0;// Count '=' at end. + int cCnt = eIx - sIx + 1;// Content count including possible separators + int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; + + int len = ((cCnt - sepCnt) * 6 >> 3) - pad;// The number of decoded bytes + byte[] dArr = new byte[len];// Preallocate byte[] of exact length + + // Decode all but the last 0 - 2 bytes. + int d = 0; + for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { + // Assemble three bytes into an int from four "valid" characters. + int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]]; + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + dArr[d++] = (byte) (i >> 8); + dArr[d++] = (byte) i; + + // If line separator, jump over it. + if (sepCnt > 0 && ++cc == 19) { + sIx += 2; + cc = 0; + } + } + + if (d < len) { + // Decode last 1-3 bytes (incl '=') into 1-3 bytes + int i = 0; + for (int j = 0; sIx <= eIx - pad; j++) { + i |= IA[sArr[sIx++]] << (18 - j * 6); + } + + for (int r = 16; d < len; r -= 8) { + dArr[d++] = (byte) (i >> r); + } + } + + return dArr; + } + + // **************************************************************************************** + // * byte[] version + // **************************************************************************************** + + /** + * Encodes a raw byte array into a BASE64 {@code byte[]} representation i accordance with RFC 2045. + * + * @param sArr The bytes to convert. If {@code null} or length 0 an empty array will be returned. + * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a + * little faster. + * @return A BASE64 encoded array. Never {@code null}. + */ + public static byte[] encodeToByte(byte[] sArr, boolean lineSep) { + // Check special case + int sLen = sArr != null ? sArr.length : 0; + if (sLen == 0) { + return new byte[0]; + } + + int eLen = (sLen / 3) * 3;// Length of even 24-bits. + int cCnt = ((sLen - 1) / 3 + 1) << 2;// Returned character count + int dLen = cCnt + (lineSep ? (cCnt - 1) / 76 << 1 : 0);// Length of returned array + byte[] dArr = new byte[dLen]; + + // Encode even 24-bits + for (int s = 0, d = 0, cc = 0; s < eLen;) { + // Copy next three bytes into lower 24 bits of int, paying attension to sign. + int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); + + // Encode the int into four chars + dArr[d++] = (byte) CA[(i >>> 18) & 0x3f]; + dArr[d++] = (byte) CA[(i >>> 12) & 0x3f]; + dArr[d++] = (byte) CA[(i >>> 6) & 0x3f]; + dArr[d++] = (byte) CA[i & 0x3f]; + + // Add optional line separator + if (lineSep && ++cc == 19 && d < dLen - 2) { + dArr[d++] = '\r'; + dArr[d++] = '\n'; + cc = 0; + } + } + + // Pad and encode last bits if source isn't an even 24 bits. + int left = sLen - eLen;// 0 - 2. + if (left > 0) { + // Prepare the int + int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); + + // Set last four chars + dArr[dLen - 4] = (byte) CA[i >> 12]; + dArr[dLen - 3] = (byte) CA[(i >>> 6) & 0x3f]; + dArr[dLen - 2] = left == 2 ? (byte) CA[i & 0x3f] : (byte) '='; + dArr[dLen - 1] = '='; + } + return dArr; + } + + /** + * Decodes a BASE64 encoded byte array. All illegal characters will be ignored and can handle both arrays with + * and without line separators. + * + * @param sArr The source array. Length 0 will return an empty array. {@code null} will throw an exception. + * @return The decoded array of bytes. May be of length 0. Will be {@code null} if the legal characters + * (including '=') isn't divideable by 4. (I.e. definitely corrupted). + */ + public static byte[] decode(byte[] sArr) { + // Check special case + int sLen = sArr.length; + + // Count illegal characters (including '\r', '\n') to know what size the returned array will be, + // so we don't have to reallocate & copy it later. + int sepCnt = 0;// Number of separator characters. (Actually illegal characters, but that's a bonus...) + for (int i = 0; i < sLen; i++)// If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. + { + if (IA[sArr[i] & 0xff] < 0) { + sepCnt++; + } + } + + // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. + if ((sLen - sepCnt) % 4 != 0) { + return null; + } + + int pad = 0; + for (int i = sLen; i > 1 && IA[sArr[--i] & 0xff] <= 0;) { + if (sArr[i] == '=') { + pad++; + } + } + + int len = ((sLen - sepCnt) * 6 >> 3) - pad; + + byte[] dArr = new byte[len];// Preallocate byte[] of exact length + + for (int s = 0, d = 0; d < len;) { + // Assemble three bytes into an int from four "valid" characters. + int i = 0; + for (int j = 0; j < 4; j++) + {// j only increased if a valid char was found. + int c = IA[sArr[s++] & 0xff]; + if (c >= 0) { + i |= c << (18 - j * 6); + } + else { + j--; + } + } + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + if (d < len) { + dArr[d++] = (byte) (i >> 8); + if (d < len) { + dArr[d++] = (byte) i; + } + } + } + + return dArr; + } + + /** + * Decodes a BASE64 encoded byte array that is known to be resonably well formatted. The method is about twice as + * fast as {@link #decode(byte[])}. The preconditions are:
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).
+ * + Line separator must be "\r\n", as specified in RFC 2045 + * + The array must not contain illegal characters within the encoded string
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
+ * + * @param sArr The source array. Length 0 will return an empty array. {@code null} will throw an exception. + * @return The decoded array of bytes. May be of length 0. + */ + public static byte[] decodeFast(byte[] sArr) { + // Check special case + int sLen = sArr.length; + if (sLen == 0) { + return new byte[0]; + } + + int sIx = 0, eIx = sLen - 1;// Start and end index after trimming. + + // Trim illegal chars from start + while (sIx < eIx && IA[sArr[sIx] & 0xff] < 0) { + sIx++; + } + + // Trim illegal chars from end + while (eIx > 0 && IA[sArr[eIx] & 0xff] < 0) { + eIx--; + } + + // get the padding count (=) (0, 1 or 2) + int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0;// Count '=' at end. + int cCnt = eIx - sIx + 1;// Content count including possible separators + int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; + + int len = ((cCnt - sepCnt) * 6 >> 3) - pad;// The number of decoded bytes + byte[] dArr = new byte[len];// Preallocate byte[] of exact length + + // Decode all but the last 0 - 2 bytes. + int d = 0; + for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { + // Assemble three bytes into an int from four "valid" characters. + int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]]; + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + dArr[d++] = (byte) (i >> 8); + dArr[d++] = (byte) i; + + // If line separator, jump over it. + if (sepCnt > 0 && ++cc == 19) { + sIx += 2; + cc = 0; + } + } + + if (d < len) { + // Decode last 1-3 bytes (incl '=') into 1-3 bytes + int i = 0; + for (int j = 0; sIx <= eIx - pad; j++) { + i |= IA[sArr[sIx++]] << (18 - j * 6); + } + + for (int r = 16; d < len; r -= 8) { + dArr[d++] = (byte) (i >> r); + } + } + + return dArr; + } + + // **************************************************************************************** + // * String version + // **************************************************************************************** + + /** + * Encodes a raw byte array into a BASE64 {@code String} representation i accordance with RFC 2045. + * + * @param sArr The bytes to convert. If {@code null} or length 0 an empty array will be returned. + * @param lineSep Optional "\r\n" after 76 characters, unless end of file.
+ * No line separator will be in breach of RFC 2045 which specifies max 76 per line but will be a + * little faster. + * @return A BASE64 encoded array. Never {@code null}. + */ + public static String encodeToString(byte[] sArr, boolean lineSep) { + // Reuse char[] since we can't create a String incrementally anyway and StringBuffer/Builder would be slower. + return new String(encodeToChar(sArr, lineSep)); + } + + /** + * Decodes a BASE64 encoded {@code String}. All illegal characters will be ignored and can handle both strings with + * and without line separators.
+ * Note! It can be up to about 2x the speed to call {@code decode(str.toCharArray())} instead. That + * will create a temporary array though. This version will use {@code str.charAt(i)} to iterate the string. + * + * @param str The source string. {@code null} or length 0 will return an empty array. + * @return The decoded array of bytes. May be of length 0. Will be {@code null} if the legal characters + * (including '=') isn't divideable by 4. (I.e. definitely corrupted). + */ + public static byte[] decode(String str) { + // Check special case + int sLen = str != null ? str.length() : 0; + if (sLen == 0) { + return new byte[0]; + } + + // Count illegal characters (including '\r', '\n') to know what size the returned array will be, + // so we don't have to reallocate & copy it later. + int sepCnt = 0;// Number of separator characters. (Actually illegal characters, but that's a bonus...) + for (int i = 0; i < sLen; i++)// If input is "pure" (I.e. no line separators or illegal chars) base64 this loop can be commented out. + { + if (IA[str.charAt(i)] < 0) { + sepCnt++; + } + } + + // Check so that legal chars (including '=') are evenly divideable by 4 as specified in RFC 2045. + if ((sLen - sepCnt) % 4 != 0) { + return null; + } + + // Count '=' at end + int pad = 0; + for (int i = sLen; i > 1 && IA[str.charAt(--i)] <= 0;) { + if (str.charAt(i) == '=') { + pad++; + } + } + + int len = ((sLen - sepCnt) * 6 >> 3) - pad; + + byte[] dArr = new byte[len];// Preallocate byte[] of exact length + + for (int s = 0, d = 0; d < len;) { + // Assemble three bytes into an int from four "valid" characters. + int i = 0; + for (int j = 0; j < 4; j++) + {// j only increased if a valid char was found. + int c = IA[str.charAt(s++)]; + if (c >= 0) { + i |= c << (18 - j * 6); + } + else { + j--; + } + } + // Add the bytes + dArr[d++] = (byte) (i >> 16); + if (d < len) { + dArr[d++] = (byte) (i >> 8); + if (d < len) { + dArr[d++] = (byte) i; + } + } + } + return dArr; + } + + /** + * Decodes a BASE64 encoded string that is known to be resonably well formatted. The method is about twice as + * fast as {@link #decode(String)}. The preconditions are:
+ * + The array must have a line length of 76 chars OR no line separators at all (one line).
+ * + Line separator must be "\r\n", as specified in RFC 2045 + * + The array must not contain illegal characters within the encoded string
+ * + The array CAN have illegal characters at the beginning and end, those will be dealt with appropriately.
+ * + * @param s The source string. Length 0 will return an empty array. {@code null} will throw an exception. + * @return The decoded array of bytes. May be of length 0. + */ + public static byte[] decodeFast(String s) { + // Check special case + int sLen = s.length(); + if (sLen == 0) { + return new byte[0]; + } + + int sIx = 0, eIx = sLen - 1;// Start and end index after trimming. + + // Trim illegal chars from start + while (sIx < eIx && IA[s.charAt(sIx) & 0xff] < 0) { + sIx++; + } + + // Trim illegal chars from end + while (eIx > 0 && IA[s.charAt(eIx) & 0xff] < 0) { + eIx--; + } + + // get the padding count (=) (0, 1 or 2) + int pad = s.charAt(eIx) == '=' ? (s.charAt(eIx - 1) == '=' ? 2 : 1) : 0;// Count '=' at end. + int cCnt = eIx - sIx + 1;// Content count including possible separators + int sepCnt = sLen > 76 ? (s.charAt(76) == '\r' ? cCnt / 78 : 0) << 1 : 0; + + int len = ((cCnt - sepCnt) * 6 >> 3) - pad;// The number of decoded bytes + byte[] dArr = new byte[len];// Preallocate byte[] of exact length + + // Decode all but the last 0 - 2 bytes. + int d = 0; + for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { + // Assemble three bytes into an int from four "valid" characters. + int i = IA[s.charAt(sIx++)] << 18 | IA[s.charAt(sIx++)] << 12 | IA[s.charAt(sIx++)] << 6 | IA[s.charAt(sIx++)]; + + // Add the bytes + dArr[d++] = (byte) (i >> 16); + dArr[d++] = (byte) (i >> 8); + dArr[d++] = (byte) i; + + // If line separator, jump over it. + if (sepCnt > 0 && ++cc == 19) { + sIx += 2; + cc = 0; + } + } + + if (d < len) { + // Decode last 1-3 bytes (incl '=') into 1-3 bytes + int i = 0; + for (int j = 0; sIx <= eIx - pad; j++) { + i |= IA[s.charAt(sIx++)] << (18 - j * 6); + } + + for (int r = 16; d < len; r -= 8) { + dArr[d++] = (byte) (i >> r); + } + } + + return dArr; + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java new file mode 100755 index 00000000..3e736445 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Decoder.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import com.twelvemonkeys.io.FastByteArrayOutputStream; + +import java.io.*; + +/** + * {@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 class Base64Decoder implements Decoder { + /** + * This array maps the characters to their 6 bit values + */ + final static char[] 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[] decode_buffer = new byte[4]; + private ByteArrayOutputStream mWrapped; + private Object mWrappedObject; + + 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(InputStream pStream, byte pBytes[], + int pOffset, 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(InputStream pInput, OutputStream pOutput, 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 linefeeds + do { + read = pInput.read(); + if (read == -1) { + return false; + } + } while (read == 10 || read == 13); + + decode_buffer[0] = (byte) read; + read = readFully(pInput, decode_buffer, 1, pLength - 1); + + if (read == -1) { + return false; + } + if (pLength > 3 && decode_buffer[3] == 61) { + pLength = 3; + } + if (pLength > 2 && decode_buffer[2] == 61) { + pLength = 2; + } + + switch (pLength) { + case 4: + byte3 = PEM_CONVERT_ARRAY[decode_buffer[3] & 255]; + // fall through + case 3: + byte2 = PEM_CONVERT_ARRAY[decode_buffer[2] & 255]; + // fall through + case 2: + byte1 = PEM_CONVERT_ARRAY[decode_buffer[1] & 255]; + byte0 = PEM_CONVERT_ARRAY[decode_buffer[0] & 255]; + // fall through + default: + switch (pLength) { + case 2: + pOutput.write((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); + break; + case 3: + pOutput.write((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); + pOutput.write((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15)); + break; + case 4: + pOutput.write((byte) (byte0 << 2 & 252 | byte1 >>> 4 & 3)); + pOutput.write((byte) (byte1 << 4 & 240 | byte2 >>> 2 & 15)); + pOutput.write((byte) (byte2 << 6 & 192 | byte3 & 63)); + break; + } + break; + } + + return true; + } + + void decodeBuffer(InputStream pInput, ByteArrayOutputStream pOutput, int pLength) throws IOException { + do { + int k = 72; + int i; + for (i = 0; i + 4 < k; i += 4) { + if(!decodeAtom(pInput, pOutput, 4)) { + break; + } + } + + if (!decodeAtom(pInput, pOutput, k - i)) { + break; + } + } + while (true); + } + + public int decode(InputStream pStream, byte[] pBuffer) throws IOException { + if (mWrappedObject != pBuffer) { + // NOTE: Array not cloned in FastByteArrayOutputStream + mWrapped = new FastByteArrayOutputStream(pBuffer); + mWrappedObject = pBuffer; + } + mWrapped.reset(); // NOTE: This only resets count to 0 + + decodeBuffer(pStream, mWrapped, pBuffer.length); + + return mWrapped.size(); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java new file mode 100755 index 00000000..e03a1f55 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Base64Encoder.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.OutputStream; +import java.io.IOException; + +/** + * {@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(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) 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 offset = pOffset; + + // TODO: Temp impl, will only work for single writes + while ((pBuffer.length - offset) > 0) { + byte a, b, c; + if ((pBuffer.length - offset) > 2) { + pLength = 3; + } + else { + pLength = pBuffer.length - offset; + } + + switch (pLength) { + case 1: + a = pBuffer[offset]; + b = 0; + pStream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); + pStream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + pStream.write('='); + pStream.write('='); + offset++; + break; + case 2: + a = pBuffer[offset]; + b = pBuffer[offset + 1]; + c = 0; + pStream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); + pStream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + pStream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + pStream.write('='); + offset += offset + 2; // ??? + break; + default: + a = pBuffer[offset]; + b = pBuffer[offset + 1]; + c = pBuffer[offset + 2]; + pStream.write(Base64Decoder.PEM_ARRAY[(a >>> 2) & 0x3F]); + pStream.write(Base64Decoder.PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + pStream.write(Base64Decoder.PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + pStream.write(Base64Decoder.PEM_ARRAY[c & 0x3F]); + offset = offset + 3; + break; + } + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecodeException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecodeException.java new file mode 100755 index 00000000..7f49b9f6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecodeException.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.IOException; + +/** + * Thrown by {@code Decoder}s when encoded data is not decodable. + *

+ * + * @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(String pMessage) { + super(pMessage); + } + + public DecodeException(String pMessage, Throwable pCause) { + super(pMessage); + initCause(pCause); + } + + public DecodeException(Throwable pCause) { + this(pCause.getMessage(), pCause); + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Decoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Decoder.java new file mode 100755 index 00000000..44752512 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Decoder.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.InputStream; +import java.io.IOException; + +/** + * 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 pBuffer.length} bytes from the given inputstream, + * into the given buffer. + * + * @param pStream the inputstream to decode data from + * @param pBuffer buffer to store the read data + * + * @return the total number of bytes read into the buffer, or {@code -1} + * 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 + */ + int decode(InputStream pStream, byte[] pBuffer) throws IOException; +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java new file mode 100755 index 00000000..2c13a433 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DecoderStream.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.InputStream; +import java.io.IOException; +import java.io.FilterInputStream; + +/** + * An {@code InputStream} that provides on-the-fly deoding 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 int mBufferPos; + protected int mBufferLimit; + protected final byte[] mBuffer; + protected final Decoder mDecoder; + + /** + * 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 + * + * @see java.io.FilterInputStream#in + */ + public DecoderStream(InputStream pStream, Decoder pDecoder) { + super(pStream); + mDecoder = pDecoder; + mBuffer = new byte[1024]; + mBufferPos = 0; + mBufferLimit = 0; + } + + public int available() throws IOException { + return mBufferLimit - mBufferPos + super.available(); + } + + public int read() throws IOException { + if (mBufferPos == mBufferLimit) { + mBufferLimit = fill(); + } + + if (mBufferLimit < 0) { + return -1; + } + + return mBuffer[mBufferPos++] & 0xff; + } + + public int read(byte pBytes[]) throws IOException { + return read(pBytes, 0, pBytes.length); + } + + public int read(byte pBytes[], int pOffset, 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 + " lenght=" + pLength); + } + else if (pLength == 0) { + return 0; + } + + // End of file? + if ((mBufferLimit - mBufferPos) < 0) { + return -1; + } + + // Read until we have read pLength bytes, or have reached EOF + int count = 0; + int off = pOffset; + while (pLength > count) { + int avail = mBufferLimit - mBufferPos; + + if (avail <= 0) { + mBufferLimit = fill(); + + if (mBufferLimit < 0) { + break; + } + } + + // Copy as many bytes as possible + int dstLen = Math.min(pLength - count, avail); + System.arraycopy(mBuffer, mBufferPos, pBytes, off, dstLen); + mBufferPos += dstLen; + + // Update offset (rest) + off += dstLen; + + // Inrease count + count += dstLen; + } + + /* + for (int i = 0; i < count; i++) { + byte b = pBytes[pOffset + i]; + System.out.print("0x" + Integer.toHexString(b & 0xff)); + } + */ + + return count; + } + + public long skip(long pLength) throws IOException { + // End of file? + if (mBufferLimit - mBufferPos < 0) { + return 0; + } + + // Skip until we have skipped pLength bytes, or have reached EOF + long total = 0; + while (total < pLength) { + int avail = mBufferLimit - mBufferPos; + + if (avail == 0) { + mBufferLimit = fill(); + + if (mBufferLimit < 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, avail); + + mBufferPos += skipped; // Just skip these bytes + 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 IO error occurs + */ + protected int fill() throws IOException { + int read = mDecoder.decode(in, mBuffer); + mBufferPos = 0; + + if (read == 0) { + return -1; + } + return read; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java new file mode 100755 index 00000000..ff30241c --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.OutputStream; +import java.io.IOException; +import java.util.zip.Deflater; + +/** + * {@code Encoder} implementation for standard DEFLATE encoding. + *

+ * + * @see RFC 1951 + * + * @see Deflater + * @see InflateDecoder + * @see java.util.zip.DeflaterOutputStream + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/DeflateEncoder.java#2 $ + */ +public final class DeflateEncoder implements Encoder { + + private final Deflater mDeflater; + + public DeflateEncoder() { + this(new Deflater()); + } + + public DeflateEncoder(Deflater pDeflater) { + if (pDeflater == null) { + throw new IllegalArgumentException("deflater == null"); + } + mDeflater = pDeflater; + } + + public void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException { + throw new InternalError("not implemented: encode()"); // TODO: Implement + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Encoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Encoder.java new file mode 100755 index 00000000..b5d67adf --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/Encoder.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Interface for endcoders. + * An {@code Encoder} may be used with an {@code EncoderStream}, to perform + * on-the-fly enoding 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 pBuffer.length} bytes into the given inputstream, + * from the given buffer. + * + * @param pStream the outputstream to encode data to + * @param pBuffer buffer to read data from + * @param pOffset offset into the buffer array + * @param pLength length of data in the buffer + * + * @throws java.io.IOException if an I/O error occurs + */ + void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) + throws IOException; + + //TODO: int requiredBufferSize(): -1 == any, otherwise, use this buffer size + // void flush()? +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java new file mode 100755 index 00000000..5377b1ad --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/EncoderStream.java @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.FilterOutputStream; +import java.io.OutputStream; +import java.io.IOException; + +/** + * 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 { + + protected final Encoder mEncoder; + private final boolean mFlushOnWrite; + + protected int mBufferPos; + protected final byte[] mBuffer; + + /** + * Creates an output stream filter built on top of the specified + * underlying output stream. + * + * @param pStream the underlying output stream + * @param pEncoder + */ + public EncoderStream(OutputStream pStream, 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 + * @param pFlushOnWrite if {@code true}, calls to the byte-array + * {@code write} methods will automatically flush the buffer. + */ + public EncoderStream(OutputStream pStream, Encoder pEncoder, boolean pFlushOnWrite) { + super(pStream); + + mEncoder = pEncoder; + mFlushOnWrite = pFlushOnWrite; + + mBuffer = new byte[1024]; + mBufferPos = 0; + } + + public void close() throws IOException { + flush(); + super.close(); + } + + public void flush() throws IOException { + encodeBuffer(); + super.flush(); + } + + private void encodeBuffer() throws IOException { + if (mBufferPos != 0) { + // Make sure all remaining data in buffer is written to the stream + mEncoder.encode(out, mBuffer, 0, mBufferPos); + // Reset buffer + mBufferPos = 0; + } + } + + public final void write(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(byte[] pBytes, int pOffset, int pLength) throws IOException { + if (!mFlushOnWrite && mBufferPos + pLength < mBuffer.length) { + // Buffer data + System.arraycopy(pBytes, pOffset, mBuffer, mBufferPos, pLength); + mBufferPos += pLength; + } + else { + // Encode data allready in the buffer + if (mBufferPos != 0) { + encodeBuffer(); + } + + // Encode rest without buffering + mEncoder.encode(out, pBytes, pOffset, pLength); + } + } + + public void write(int pByte) throws IOException { + if (mBufferPos >= mBuffer.length - 1) { + encodeBuffer(); // Resets mBufferPos to 0 + } + mBuffer[mBufferPos++] = (byte) pByte; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java new file mode 100755 index 00000000..9315ec42 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * {@code Decoder} implementation for standard DEFLATE encoding. + *

+ * + * @see RFC 1951 + * + * @see Inflater + * @see DeflateEncoder + * @see java.util.zip.InflaterInputStream + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/InflateDecoder.java#2 $ + */ +public final class InflateDecoder implements Decoder { + + private final Inflater mInflater; + + private final byte[] mBuffer; + + /** + * Creates an {@code InflateDecoder} + * + */ + public InflateDecoder() { + this(new Inflater(true)); + } + + /** + * Creates an {@code InflateDecoder} + * + * @param pInflater the inflater instance to use + */ + public InflateDecoder(Inflater pInflater) { + if (pInflater == null) { + throw new IllegalArgumentException("inflater == null"); + } + mInflater = pInflater; + mBuffer = new byte[1024]; + } + + public int decode(InputStream pStream, byte[] pBuffer) throws IOException { + try { + int decoded; + while ((decoded = mInflater.inflate(pBuffer, 0, pBuffer.length)) == 0) { + if (mInflater.finished() || mInflater.needsDictionary()) { + return 0; + } + if (mInflater.needsInput()) { + fill(pStream); + } + } + return decoded; + } + catch (DataFormatException e) { + String message = e.getMessage(); + throw new DecodeException(message != null ? message : "Invalid ZLIB data format", e); + } + } + + private void fill(InputStream pStream) throws IOException { + int available = pStream.read(mBuffer, 0, mBuffer.length); + if (available == -1) { + throw new EOFException("Unexpected end of ZLIB stream"); + } + mInflater.setInput(mBuffer, 0, available); + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWDecoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWDecoder.java new file mode 100755 index 00000000..f2230829 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWDecoder.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.InputStream; +import java.io.IOException; + +/** + * LZWDecoder. + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWDecoder.java#2 $ + */ +public class LZWDecoder implements Decoder { + public int decode(InputStream pStream, byte[] pBuffer) throws IOException { + return 0; // TODO: Implement + // TODO: We probably need a GIF specific subclass + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWEncoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWEncoder.java new file mode 100755 index 00000000..4962b383 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWEncoder.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.OutputStream; +import java.io.IOException; + +/** + * LZWEncoder. + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/LZWEncoder.java#2 $ + */ +public class LZWEncoder implements Encoder { + public void encode(OutputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException { + // TODO: Implement + // TODO: We probably need a GIF specific subclass + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java new file mode 100755 index 00000000..bfef3691 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.InputStream; +import java.io.IOException; +import java.io.EOFException; + +/** + * Decoder implementation for 16 bit-chunked Apple PackBits-like run-length + * encoding. + *

+ * This version of the decoder decodes chunk of 16 bit, instead of 8 bit. + * This format is used in certain PICT files. + * + * @see PackBitsDecoder + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBits16Decoder.java#2 $ + */ +public final class PackBits16Decoder implements Decoder { + // TODO: Refactor this into an option for the PackBitsDecoder? + private final boolean mDisableNoop; + + private int mLeftOfRun; + private boolean mSplitRun; + private boolean mEOF; + + /** + * Creates a {@code PackBitsDecoder}. + */ + public PackBits16Decoder() { + this(false); + } + + /** + * Creates a {@code PackBitsDecoder}. + *

+ * As some implementations of PackBits-like encoders treat -128 as lenght 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 pDisableNoop + */ + public PackBits16Decoder(boolean pDisableNoop) { + mDisableNoop = pDisableNoop; + } + + /** + * Decodes bytes from the given input stream, to the given buffer. + * + * @param pStream + * @param pBuffer a byte array, minimum 128 (or 129 if no-op is disabled) + * bytes long + * @return The number of bytes decoded + * + * @throws java.io.IOException + */ + public int decode(InputStream pStream, byte[] pBuffer) throws IOException { + if (mEOF) { + throw new EOFException("Unexpected end of PackBits stream"); + } + + int read = 0; + final int max = pBuffer.length; + + while (read < max) { + int n; + if (mSplitRun) { + // Continue run + n = mLeftOfRun; + mSplitRun = false; + } + else { + // Start new run + int b = pStream.read(); + if (b < 0) { + mEOF = true; + break; + } + n = (byte) b; + } + + // Split run at or before max + if (n >= 0 && 2 * (n + 1) + read > max) { + mLeftOfRun = n; + mSplitRun = true; + break; + } + else if (n < 0 && 2 * (-n + 1) + read > max) { + mLeftOfRun = n; + mSplitRun = true; + break; + } + + try { + if (n >= 0) { + // Copy next n + 1 shorts literally + int len = 2 * (n + 1); + readFully(pStream, pBuffer, read, len); + read += len; + } + // Allow -128 for compatibility, see above + else if (mDisableNoop || n != -128) { + // Replicate the next short -n + 1 times + byte value1 = readByte(pStream); + byte value2 = readByte(pStream); + + for (int i = -n + 1; i > 0; i--) { + pBuffer[read++] = value1; + pBuffer[read++] = value2; + } + } + // else NOOP (-128) + } + catch (IndexOutOfBoundsException e) { + throw new DecodeException("Error in PackBits decompression, data seems corrupt", e); + } + } + + return read; + } + + private static byte readByte(InputStream pStream) throws IOException { + int read = pStream.read(); + if (read < 0) { + throw new EOFException("Unexpected end of PackBits stream"); + } + return (byte) read; + } + + private static void readFully(InputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException { + if (pLength < 0) { + throw new IndexOutOfBoundsException(); + } + int read = 0; + while (read < pLength) { + int count = pStream.read(pBuffer, pOffset + read, pLength - read); + if (count < 0) { + throw new EOFException("Unexpected end of PackBits stream"); + } + read += count; + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java new file mode 100755 index 00000000..7a27b0ef --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsDecoder.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.IOException; +import java.io.InputStream; +import java.io.EOFException; + +/** + * Decoder 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/PackBitsDecoder.java#1 $ + */ +public final class PackBitsDecoder implements Decoder { + private final boolean mDisableNoop; + + private int mLeftOfRun; + private boolean mSplitRun; + private boolean mEOF; + + /** + * Creates a {@code PackBitsDecoder}. + */ + public PackBitsDecoder() { + this(false); + } + + /** + * Creates a {@code PackBitsDecoder}. + *

+ * As some implementations of PackBits-like encoders treat -128 as lenght 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 pDisableNoop + */ + public PackBitsDecoder(boolean pDisableNoop) { + mDisableNoop = pDisableNoop; + } + + /** + * Decodes bytes from the given input stream, to the given buffer. + * + * @param pStream + * @param pBuffer a byte array, minimum 128 (or 129 if no-op is disabled) + * bytes long + * @return The number of bytes decoded + * + * @throws IOException + */ + public int decode(InputStream pStream, byte[] pBuffer) throws IOException { + if (mEOF) { + throw new EOFException("Unexpected end of PackBits stream"); + } + + int read = 0; + final int max = pBuffer.length; + + while (read < max) { + int n; + if (mSplitRun) { + // Continue run + n = mLeftOfRun; + mSplitRun = false; + } + else { + // Start new run + int b = pStream.read(); + if (b < 0) { + mEOF = true; + break; + } + n = (byte) b; + } + + // Split run at or before max + if (n >= 0 && n + 1 + read > max) { + mLeftOfRun = n; + mSplitRun = true; + break; + } + else if (n < 0 && -n + 1 + read > max) { + mLeftOfRun = n; + mSplitRun = true; + break; + } + + try { + if (n >= 0) { + // Copy next n + 1 bytes literally + readFully(pStream, pBuffer, read, n + 1); + + read += n + 1; + } + // Allow -128 for compatibility, see above + else if (mDisableNoop || n != -128) { + // Replicate the next byte -n + 1 times + byte value = readByte(pStream); + + for (int i = -n + 1; i > 0; i--) { + pBuffer[read++] = value; + } + } + // else NOOP (-128) + } + catch (IndexOutOfBoundsException e) { + throw new DecodeException("Error in PackBits decompression, data seems corrupt", e); + } + } + + return read; + } + + private static byte readByte(InputStream pStream) throws IOException { + int read = pStream.read(); + if (read < 0) { + throw new EOFException("Unexpected end of PackBits stream"); + } + return (byte) read; + } + + private static void readFully(InputStream pStream, byte[] pBuffer, int pOffset, int pLength) throws IOException { + if (pLength < 0) { + throw new IndexOutOfBoundsException(); + } + int read = 0; + while (read < pLength) { + int count = pStream.read(pBuffer, pOffset + read, pLength - read); + if (count < 0) { + throw new EOFException("Unexpected end of PackBits stream"); + } + read += count; + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java new file mode 100755 index 00000000..6e775984 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/PackBitsEncoder.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.OutputStream; +import java.io.IOException; + +/** + * 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[] mBuffer = new byte[128]; + + /** + * Creates a {@code PackBitsEncoder}. + */ + public PackBitsEncoder() { + } + + public 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])))) { + mBuffer[run++] = pBuffer[offset++]; + } + + // If last byte, include it in literal run, if space + if (offset == max && run > 0 && run < 128) { + mBuffer[run++] = pBuffer[offset++]; + } + + if (run > 0) { + pStream.write(run - 1); + pStream.write(mBuffer, 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java new file mode 100755 index 00000000..9ff89f0c --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.InputStream; +import java.io.IOException; + +/** + * Implements 4 bit RLE decoding as specifed by in the Windows BMP (aka DIB) file format. + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/RLE4Decoder.java#1 $ + */ +// TODO: Move to other package or make public +final class RLE4Decoder extends AbstractRLEDecoder { + + public RLE4Decoder(int pWidth, int pHeight) { + super((pWidth + 1) / 2, pHeight); + } + + protected void decodeRow(final InputStream pInput) throws IOException { + int deltaX = 0; + int deltaY = 0; + + while (mSrcY >= 0) { + int byte1 = pInput.read(); + int byte2 = checkEOF(pInput.read()); + if (byte1 == 0x00) { + switch (byte2) { + case 0x00: + // End of line + // NOTE: Some BMPs have double EOLs.. + if (mSrcX != 0) { + mSrcX = mRow.length; + } + break; + case 0x01: + // End of bitmap + mSrcX = mRow.length; + mSrcY = 0; + break; + case 0x02: + // Delta + deltaX = mSrcX + pInput.read(); + deltaY = mSrcY - checkEOF(pInput.read()); + mSrcX = mRow.length; + break; + default: + // Absolute mode + // Copy the next byte2 (3..255) bytes from file to output + // Two samples are packed into one byte + // If the number of bytes used to pack is not a mulitple of 2, + // an additional padding byte is in the stream and must be skipped + boolean paddingByte = (((byte2 + 1) / 2) % 2) != 0; + while (byte2 > 1) { + int packed = checkEOF(pInput.read()); + mRow[mSrcX++] = (byte) packed; + byte2 -= 2; + } + if (byte2 == 1) { + // TODO: Half byte alignment? Seems to be ok... + int packed = checkEOF(pInput.read()); + mRow[mSrcX++] = (byte) (packed & 0xf0); + } + if (paddingByte) { + checkEOF(pInput.read()); + } + break; + } + } + else { + // Encoded mode + // Replicate the two samples in byte2 as many times as byte1 says + while (byte1 > 1) { + mRow[mSrcX++] = (byte) byte2; + byte1 -= 2; + } + if (byte1 == 1) { + // TODO: Half byte alignment? Seems to be ok... + mRow[mSrcX++] = (byte) (byte2 & 0xf0); + } + } + + // If we're done with a complete row, copy the data + if (mSrcX == mRow.length) { + + // Move to new position, either absolute (delta) or next line + if (deltaX != 0 || deltaY != 0) { + mSrcX = (deltaX + 1) / 2; + if (deltaY > mSrcY) { + mSrcY = deltaY; + break; + } + deltaX = 0; + deltaY = 0; + } + else { + mSrcX = 0; + mSrcY--; + break; + } + } + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java new file mode 100755 index 00000000..55e58886 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.enc; + +import java.io.InputStream; +import java.io.IOException; + +/** + * Implements 8 bit RLE decoding as specifed by in the Windows BMP (aka DIB) file format. + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/RLE8Decoder.java#1 $ + */ +// TODO: Move to other package or make public +final class RLE8Decoder extends AbstractRLEDecoder { + + public RLE8Decoder(int pWidth, int pHeight) { + super(pWidth, pHeight); + } + + protected void decodeRow(final InputStream pInput) throws IOException { + int deltaX = 0; + int deltaY = 0; + + while (mSrcY >= 0) { + int byte1 = pInput.read(); + int byte2 = checkEOF(pInput.read()); + if (byte1 == 0x00) { + switch (byte2) { + case 0x00: + // End of line + // NOTE: Some BMPs have double EOLs.. + if (mSrcX != 0) { + mSrcX = mRow.length; + } + break; + case 0x01: + // End of bitmap + mSrcX = mRow.length; + mSrcY = 0; + break; + case 0x02: + // Delta + deltaX = mSrcX + pInput.read(); + deltaY = mSrcY - checkEOF(pInput.read()); + mSrcX = mRow.length; + break; + default: + // Absolute mode + // Copy the next byte2 (3..255) bytes from file to output + boolean paddingByte = (byte2 % 2) != 0; + while (byte2-- > 0) { + mRow[mSrcX++] = (byte) checkEOF(pInput.read()); + } + if (paddingByte) { + checkEOF(pInput.read()); + } + } + } + else { + // Encoded mode + // Replicate byte2 as many times as byte1 says + byte value = (byte) byte2; + while (byte1-- > 0) { + mRow[mSrcX++] = value; + } + } + + // If we're done with a complete row, copy the data + if (mSrcX == mRow.length) { + + // Move to new position, either absolute (delta) or next line + if (deltaX != 0 || deltaY != 0) { + mSrcX = deltaX; + if (deltaY != mSrcY) { + mSrcY = deltaY; + break; + } + deltaX = 0; + deltaY = 0; + } + else { + mSrcX = 0; + mSrcY--; + break; + } + } + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/package-info.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/package-info.java new file mode 100755 index 00000000..d69ccb65 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/enc/package-info.java @@ -0,0 +1,14 @@ +/** + * Contains customized stream classes, that can read or write compressed data on the fly, + * along with encoders and decoders for popular stream formats, such as ZIP (deflate), LZW, PackBits etc.. + * + * @see com.twelvemonkeys.io.enc.DecoderStream + * @see com.twelvemonkeys.io.enc.EncoderStream + * @see com.twelvemonkeys.io.enc.Decoder + * @see com.twelvemonkeys.io.enc.Encoder + * @see com.twelvemonkeys.io.enc.DecodeException + * + * @version 2.0 + * @author Harald Kuhr + */ +package com.twelvemonkeys.io.enc; \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java new file mode 100755 index 00000000..952e6d6e --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/CompoundDocument.java @@ -0,0 +1,761 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.ole2; + +import com.twelvemonkeys.io.*; +import com.twelvemonkeys.lang.StringUtil; + +import javax.imageio.stream.ImageInputStream; +import java.io.*; +import java.util.Arrays; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.UUID; + +/** + * 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 { + // TODO: Write support... + // TODO: Properties: http://support.microsoft.com/kb/186898 + + private static final byte[] MAGIC = new byte[]{ + (byte) 0xD0, (byte) 0xCF, (byte) 0x11, (byte) 0xE0, + (byte) 0xA1, (byte) 0xB1, (byte) 0x1A, (byte) 0xE1, + }; + public static final int HEADER_SIZE = 512; + + private final DataInput mInput; + + private UUID mUID; + + private int mSectorSize; + private int mShortSectorSize; + + private int mDirectorySId; + + private int mMinStreamSize; + + private int mShortSATSID; + private int mShortSATSize; + + // Master Sector Allocation Table + private int[] mMasterSAT; + private int[] mSAT; + private int[] mShortSAT; + + private Entry mRootEntry; + private SIdChain mShortStreamSIdChain; + private SIdChain mDirectorySIdChain; + + private static final int END_OF_CHAIN_SID = -2; + private static final int FREE_SID = -1; + + /** The epoch offset of CompoundDocument time stamps */ + public final static long EPOCH_OFFSET = -11644477200000L; + + /** + * Creates a (for now) read only {@code CompoundDocument}. + * + * @param pFile the file to read from + * + * @throws IOException if an I/O exception occurs while reading the header + */ + public CompoundDocument(final File pFile) throws IOException { + mInput = new LittleEndianRandomAccessFile(FileUtil.resolve(pFile), "r"); + + // 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 FileCacheSeekableStream(pInput)); + } + + // For testing only, consider exposing later + CompoundDocument(final SeekableInputStream pInput) throws IOException { + mInput = new SeekableLittleEndianDataInputStream(pInput); + + // TODO: Might be better to read header on first read operation?! + // OTOH: It's also good to be fail-fast, so at least we should make + // 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 ImageInputStream pInput) throws IOException { + mInput = pInput; + + // TODO: Might be better to read header on first read operation?! + // OTOH: It's also good to be fail-fast, so at least we should make + // sure we're reading a valid document + readHeader(); + } + + 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 (mMasterSAT != null) { + return; + } + + if (!canRead(mInput, false)) { + throw new CorruptDocumentException("Not an OLE 2 Compound Document"); + } + + // UID (seems to be all 0s) + mUID = new UUID(mInput.readLong(), mInput.readLong()); + + /*int version = */mInput.readUnsignedShort(); + //System.out.println("version: " + version); + /*int revision = */mInput.readUnsignedShort(); + //System.out.println("revision: " + revision); + + int byteOrder = mInput.readUnsignedShort(); + if (byteOrder != 0xfffe) { + // Reversed, as I'm allready reading little-endian + throw new CorruptDocumentException("Cannot read big endian OLE 2 Compound Documents"); + } + + mSectorSize = 1 << mInput.readUnsignedShort(); + //System.out.println("sectorSize: " + mSectorSize + " bytes"); + mShortSectorSize = 1 << mInput.readUnsignedShort(); + //System.out.println("shortSectorSize: " + mShortSectorSize + " bytes"); + + // Reserved + if (mInput.skipBytes(10) != 10) { + throw new CorruptDocumentException(); + } + + int SATSize = mInput.readInt(); + //System.out.println("normalSATSize: " + mSATSize); + + mDirectorySId = mInput.readInt(); + //System.out.println("directorySId: " + mDirectorySId); + + // Reserved + if (mInput.skipBytes(4) != 4) { + throw new CorruptDocumentException(); + } + + mMinStreamSize = mInput.readInt(); + //System.out.println("minStreamSize: " + mMinStreamSize + " bytes"); + + mShortSATSID = mInput.readInt(); + //System.out.println("shortSATSID: " + mShortSATSID); + mShortSATSize = mInput.readInt(); + //System.out.println("shortSATSize: " + mShortSATSize); + int masterSATSId = mInput.readInt(); + //System.out.println("masterSATSId: " + mMasterSATSID); + int masterSATSize = mInput.readInt(); + //System.out.println("masterSATSize: " + mMasterSATSize); + + // Read masterSAT: 436 bytes, containing up to 109 SIDs + //System.out.println("MSAT:"); + mMasterSAT = new int[SATSize]; + final int headerSIds = Math.min(SATSize, 109); + for (int i = 0; i < headerSIds; i++) { + mMasterSAT[i] = mInput.readInt(); + //System.out.println("\tSID(" + i + "): " + mMasterSAT[i]); + } + + if (masterSATSId == END_OF_CHAIN_SID) { + // End of chain + int freeSIdLength = 436 - (SATSize * 4); + if (mInput.skipBytes(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 = mInput.readInt(); + switch (sid) { + case FREE_SID:// Free + break; + default: + mMasterSAT[index++] = sid; + break; + } + } + + int next = mInput.readInt(); + if (next == END_OF_CHAIN_SID) {// End of chain + break; + } + + seekToSId(next, FREE_SID); + } + } + } + + private void readSAT() throws IOException { + if (mSAT != null) { + return; + } + + final int intsPerSector = mSectorSize / 4; + + // Read the Sector Allocation Table + mSAT = new int[mMasterSAT.length * intsPerSector]; + + for (int i = 0; i < mMasterSAT.length; i++) { + seekToSId(mMasterSAT[i], FREE_SID); + + for (int j = 0; j < intsPerSector; j++) { + int nextSID = mInput.readInt(); + int index = (j + (i * intsPerSector)); + + mSAT[index] = nextSID; + } + } + + // Read the short-stream Sector Allocation Table + SIdChain chain = getSIdChain(mShortSATSID, FREE_SID); + mShortSAT = new int[mShortSATSize * intsPerSector]; + for (int i = 0; i < mShortSATSize; i++) { + seekToSId(chain.get(i), FREE_SID); + + for (int j = 0; j < intsPerSector; j++) { + int nextSID = mInput.readInt(); + int index = (j + (i * intsPerSector)); + + mShortSAT[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) ? mShortSAT : mSAT; + + 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 < mMinStreamSize; + } + + /** + * 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 continouos... + Entry root = getRootEntry(); + if (mShortStreamSIdChain == null) { + mShortStreamSIdChain = getSIdChain(root.startSId, root.streamSize); + } + + int shortPerStd = mSectorSize / mShortSectorSize; + int offset = pSId / shortPerStd; + int shortOffset = pSId - (offset * shortPerStd); + + pos = HEADER_SIZE + + (mShortStreamSIdChain.get(offset) * (long) mSectorSize) + + (shortOffset * (long) mShortSectorSize); + } + else { + pos = HEADER_SIZE + pSId * (long) mSectorSize; + } + + if (mInput instanceof LittleEndianRandomAccessFile) { + ((LittleEndianRandomAccessFile) mInput).seek(pos); + } + else if (mInput instanceof ImageInputStream) { + ((ImageInputStream) mInput).seek(pos); + } + else { + ((SeekableLittleEndianDataInputStream) mInput).seek(pos); + } + } + + private void seekToDId(final int pDId) throws IOException { + if (mDirectorySIdChain == null) { + mDirectorySIdChain = getSIdChain(mDirectorySId, FREE_SID); + } + + int dIdsPerSId = mSectorSize / Entry.LENGTH; + + int sIdOffset = pDId / dIdsPerSId; + int dIdOffset = pDId - (sIdOffset * dIdsPerSId); + + int sId = mDirectorySIdChain.get(sIdOffset); + + seekToSId(sId, FREE_SID); + if (mInput instanceof LittleEndianRandomAccessFile) { + LittleEndianRandomAccessFile input = (LittleEndianRandomAccessFile) mInput; + input.seek(input.getFilePointer() + dIdOffset * Entry.LENGTH); + } + else if (mInput instanceof ImageInputStream) { + ImageInputStream input = (ImageInputStream) mInput; + input.seek(input.getStreamPosition() + dIdOffset * Entry.LENGTH); + } + else { + SeekableLittleEndianDataInputStream input = (SeekableLittleEndianDataInputStream) mInput; + 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 < mMinStreamSize ? mShortSectorSize : mSectorSize; + + return 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); + mInput.readFully(bytes); + + return new ByteArrayInputStream(bytes); + } + + Entry getEntry(final int pDirectoryId, Entry pParent) throws IOException { + Entry entry = Entry.readEntry(new LittleEndianDataInputStream( + getDirectoryStreamForDId(pDirectoryId) + )); + entry.mParent = pParent; + entry.mDocument = 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 (mRootEntry == null) { + readSAT(); + + mRootEntry = getEntry(0, null); + + if (mRootEntry.type != Entry.ROOT_STORAGE) { + throw new CorruptDocumentException("Invalid root storage type: " + mRootEntry.type); + } + } + return mRootEntry; + } + +// @Override +// public int hashCode() { +// return mUID.hashCode(); +// } +// +// @Override +// public boolean equals(final Object pOther) { +// if (pOther == this) { +// return true; +// } +// +// if (pOther == null) { +// return true; +// } +// +// if (pOther.getClass() == getClass()) { +// return mUID.equals(((CompoundDocument) pOther).mUID); +// } +// +// 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(), mUID, mSectorSize, mShortSectorSize, mDirectorySId, mMasterSAT.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; + } + + // TODO: Enforce stream length! + static class Stream extends SeekableInputStream { + private SIdChain mChain; + int mNextSectorPos; + byte[] mBuffer; + int mBufferPos; + + private final CompoundDocument mDocument; + private final long mLength; + + public Stream(final SIdChain pChain, final long pLength, final int pSectorSize, final CompoundDocument pDocument) { + mChain = pChain; + mLength = pLength; + + mBuffer = new byte[pSectorSize]; + mBufferPos = mBuffer.length; + + mDocument = pDocument; + } + + @Override + public int available() throws IOException { + return (int) Math.min(mBuffer.length - mBufferPos, mLength - getStreamPosition()); + } + + public int read() throws IOException { + if (available() <= 0) { + if (!fillBuffer()) { + return -1; + } + } + + return mBuffer[mBufferPos++] & 0xff; + } + + private boolean fillBuffer() throws IOException { + if (mNextSectorPos < mChain.length()) { + // TODO: Sync on mDocument.mInput here, and we are completely detached... :-) + // TODO: We also need to sync other places... + synchronized (mDocument) { + mDocument.seekToSId(mChain.get(mNextSectorPos), mLength); + mDocument.mInput.readFully(mBuffer); + } + + mNextSectorPos++; + mBufferPos = 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(mBuffer, mBufferPos, b, off, toRead); + mBufferPos += toRead; + + return toRead; + } + + public boolean isCached() { + return true; + } + + public boolean isCachedMemory() { + return false; + } + + public boolean isCachedFile() { + return true; + } + + protected void closeImpl() throws IOException { + mBuffer = null; + mChain = null; + } + + protected void seekImpl(final long pPosition) throws IOException { + long pos = getStreamPosition(); + + if (pos - mBufferPos >= pPosition && pPosition <= pos + available()) { + // Skip inside buffer only + mBufferPos += (pPosition - pos); + } + else { + // Skip outside buffer + mNextSectorPos = (int) (pPosition / mBuffer.length); + if (!fillBuffer()) { + throw new EOFException(); + } + mBufferPos = (int) (pPosition % mBuffer.length); + } + } + + protected void flushBeforeImpl(long pPosition) throws IOException { + // No need to do anything here + } + } + + // TODO: Add test case for this class!!! + static class SeekableLittleEndianDataInputStream extends LittleEndianDataInputStream implements Seekable { + private final SeekableInputStream mSeekable; + + public SeekableLittleEndianDataInputStream(final SeekableInputStream pInput) { + super(pInput); + mSeekable = pInput; + } + + public void seek(final long pPosition) throws IOException { + mSeekable.seek(pPosition); + } + + public boolean isCachedFile() { + return mSeekable.isCachedFile(); + } + + public boolean isCachedMemory() { + return mSeekable.isCachedMemory(); + } + + public boolean isCached() { + return mSeekable.isCached(); + } + + public long getStreamPosition() throws IOException { + return mSeekable.getStreamPosition(); + } + + public long getFlushedPosition() throws IOException { + return mSeekable.getFlushedPosition(); + } + + public void flushBefore(final long pPosition) throws IOException { + mSeekable.flushBefore(pPosition); + } + + public void flush() throws IOException { + mSeekable.flush(); + } + + @Override + public void reset() throws IOException { + mSeekable.reset(); + } + + public void mark() { + mSeekable.mark(); + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/CorruptDocumentException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/CorruptDocumentException.java new file mode 100755 index 00000000..e7340407 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/CorruptDocumentException.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.ole2; + +import java.io.IOException; + +/** + * Thrown when an OLE 2 compound document is considered corrupt. + * + * @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/CorruptDocumentException.java#3 $ + * @see com.twelvemonkeys.io.ole2.CompoundDocument + */ +public class CorruptDocumentException extends IOException { + public CorruptDocumentException() { + this("Corrupt OLE 2 Compound Document"); + } + + public CorruptDocumentException(final String pMessage) { + super(pMessage); + } + + public CorruptDocumentException(final Throwable pCause) { + super(pCause.getMessage()); + initCause(pCause); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/Entry.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/Entry.java new file mode 100755 index 00000000..29790583 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/Entry.java @@ -0,0 +1,340 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.ole2; + +import com.twelvemonkeys.io.SeekableInputStream; + +import java.io.DataInput; +import java.io.IOException; +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 mDocument; + Entry mParent; + SortedSet mChildren; + + 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 { + char[] chars = new char[32]; + for (int i = 0; i < chars.length; i++) { + chars[i] = pInput.readChar(); + } + + // NOTE: Length is in bytes, including the null-terminator... + int nameLength = pInput.readShort(); + name = new String(chars, 0, (nameLength - 1) / 2); + //System.out.println("name: " + name); + + 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 (isDirectory()) { + return null; + } + + return mDocument.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 (isDirectory()) { + 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 mParent; + } + + /** + * 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.mParent = 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 (mChildren == null) { + if (isFile() || rootNodeDId == -1) { + mChildren = NO_CHILDREN; + } + else { + // Start at root node in R/B tree, and raed to the left and right, + // re-build tree, according to the docs + mChildren = mDocument.getEntries(rootNodeDId, this); + } + } + + return mChildren; + } + + @Override + public String toString() { + return "\"" + name + "\"" + + " (" + (isFile() ? "Document" : (isDirectory() ? "Directory" : "Root")) + + (mParent != null ? ", parent: \"" + mParent.getName() + "\"" : "") + + (isFile() ? "" : ", children: " + (mChildren != null ? String.valueOf(mChildren.size()) : "(unknown)")) + + ", 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) && (mParent == other.mParent + || (mParent != null && mParent.equals(other.mParent))); + } + + @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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java new file mode 100755 index 00000000..a918c28b --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/SIdChain.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.io.ole2; + +import java.util.NoSuchElementException; + +/** + * SIdChain + * + * @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/SIdChain.java#1 $ + */ +class SIdChain { + int[] chain; + int size = 0; + int next = 0; + + public SIdChain() { + chain = new int[16]; + } + + void addSID(int pSID) { + ensureCapacity(); + chain[size++] = pSID; + } + + private void ensureCapacity() { + if (chain.length == size) { + int[] temp = new int[size << 1]; + System.arraycopy(chain, 0, temp, 0, size); + chain = temp; + } + } + + public int[] getChain() { + int[] result = new int[size]; + System.arraycopy(chain, 0, result, 0, size); + return result; + } + + public void reset() { + next = 0; + } + + public boolean hasNext() { + return next < size; + } + + public int next() { + if (next >= size) { + throw new NoSuchElementException("No element"); + } + return chain[next++]; + } + + public int get(final int pIndex) { + return chain[pIndex]; + } + + public int length() { + return size; + } + + public String toString() { + StringBuilder buf = new StringBuilder(size * 5); + buf.append('['); + for (int i = 0; i < size; i++) { + if (i != 0) { + buf.append(','); + } + buf.append(chain[i]); + } + buf.append(']'); + + return buf.toString(); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/package-info.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/package-info.java new file mode 100755 index 00000000..0220f950 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/ole2/package-info.java @@ -0,0 +1,11 @@ +/** + * Contains classes for reading the contents of the + * Microsoft OLE 2 compound document format. + * + * @see com.twelvemonkeys.io.ole2.CompoundDocument + * @see OpenOffice.org's documentation + * + * @version 2.0 + * @author Harald Kuhr + */ +package com.twelvemonkeys.io.ole2; \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/package.html b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/package.html new file mode 100755 index 00000000..a8fbf2d6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/package.html @@ -0,0 +1,7 @@ + + + +Provides for system input and output through data streams, serialization and the file system. + + + \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/todo.txt b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/todo.txt new file mode 100755 index 00000000..0b072c62 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/io/todo.txt @@ -0,0 +1,7 @@ +- Remove util.BASE64, make clients use io.Base64. +- Create subpackages + io.base64 + io.lzw + io.packbits + io.zip + \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/BeanUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/BeanUtil.java new file mode 100755 index 00000000..15e9b050 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/BeanUtil.java @@ -0,0 +1,612 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.lang; + +import 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.Map; +import java.util.Arrays; + +/** + * 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 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 != null && paramClass.length > 0) { + 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 != null && paramClass.length > 0) ? 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 excption, it works + method = pObject.getClass().getMethod(pName, pParams); + } + catch (Throwable t) { + // Ignore + } + + // 2: Try any supertypes of paramType, to see if we have a match + if (method == null) { + while ((paramType = paramType.getSuperclass()) != null) { + pParams[0] = paramType; + try { + // If this does not throw an excption, 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 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 Object 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 Object createInstance(Class pClass, Object... pParams) + throws InvocationTargetException { + Object 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.getDeclaredConstructor(paramTypes); + 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 invocaton 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 invocaton 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 Map.Entry in Map.values(), + * a method named + * set + capitalize(entry.getKey()) is called on the bean, + * with 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 Map.Entry in Map.values(), + * a method named + * set + capitalize(entry.getKey()) is called on the bean, + * with 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/DateUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/DateUtil.java new file mode 100755 index 00000000..9f4d6987 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/DateUtil.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.lang; + +import java.util.Date; +import java.util.TimeZone; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.text.ParseException; + +/** + * A utility class with useful date manipulation methods and constants. + *

+ * + * @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 + * 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 + * 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 + * 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 + * 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(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(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(long pTime) { + // TODO: What if timezone offset is sub hour? Are there any? I think so... + return ((pTime / HOUR) * HOUR); + } + + /** + * 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(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(long pTime, TimeZone pTimeZone) { + int offset = pTimeZone.getOffset(pTime); + return (((pTime + offset) / DAY) * DAY) - offset; + } + + public static void main(String[] pArgs) throws ParseException { + DateFormat format = new SimpleDateFormat("yyyy.MM.dd HH.mm.ss S"); + + long time = pArgs.length > 0 ? format.parse(pArgs[0]).getTime() : System.currentTimeMillis(); + + System.out.println(time + ": " + format.format(new Date(time))); + time = roundToSecond(time); + System.out.println(time + ": " + format.format(new Date(time))); + time = roundToMinute(time); + System.out.println(time + ": " + format.format(new Date(time))); + time = roundToHour(time); + System.out.println(time + ": " + format.format(new Date(time))); + time = roundToDay(time); + System.out.println(time + ": " + format.format(new Date(time))); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java new file mode 100755 index 00000000..fe9cd78c --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java @@ -0,0 +1,129 @@ +package com.twelvemonkeys.lang; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.UndeclaredThrowableException; +import java.sql.SQLException; + +/** + * ExceptionUtil + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ExceptionUtil.java#2 $ + */ +public final class ExceptionUtil { + + /*public*/ static void launder(final Throwable pThrowable, Class... pExpectedTypes) { + if (pThrowable instanceof Error) { + throw (Error) pThrowable; + } + if (pThrowable instanceof RuntimeException) { + throw (RuntimeException) pThrowable; + } + + for (Class expectedType : pExpectedTypes) { + if (expectedType.isInstance(pThrowable)) { + throw new RuntimeException(pThrowable); + } + } + + throw new UndeclaredThrowableException(pThrowable); + } + + @SuppressWarnings({"unchecked", "UnusedDeclaration"}) + static void throwAs(final Class pType, final Throwable pThrowable) throws T { + throw (T) pThrowable; + } + + public static void throwUnchecked(final Throwable pThrowable) { + throwAs(RuntimeException.class, pThrowable); + } + + /*public*/ static void handle(final Throwable pThrowable, final ThrowableHandler... pHandler) { + handleImpl(pThrowable, pHandler); + } + + @SuppressWarnings({"unchecked"}) + private static void handleImpl(final Throwable pThrowable, final ThrowableHandler... pHandler) { + // TODO: Sort more specific throwable handlers before less specific? + for (ThrowableHandler handler : pHandler) { + if (handler.handles(pThrowable)) { + handler.handle((T) pThrowable); + return; + } + } + throwUnchecked(pThrowable); + } + + public static abstract class ThrowableHandler { + private Class[] mThrowables; + + protected ThrowableHandler(final Class... pThrowables) { + // TODO: Assert not null + mThrowables = pThrowables.clone(); + } + + final public boolean handles(final Throwable pThrowable) { + for (Class throwable : mThrowables) { + if (throwable.isAssignableFrom(pThrowable.getClass())) { + return true; + } + } + return false; + } + + public abstract void handle(T pThrowable); + } + + @SuppressWarnings({"InfiniteLoopStatement"}) + public static void main(String[] pArgs) { + while (true) { + foo(); + } + } + + private static void foo() { + try { + bar(); + } + catch (Throwable t) { + handle(t, + new ThrowableHandler(IOException.class) { + public void handle(final IOException pThrowable) { + System.out.println("IOException: " + pThrowable + " handled"); + } + }, + new ThrowableHandler(SQLException.class, NumberFormatException.class) { + public void handle(final Exception pThrowable) { + System.out.println("Exception: " + pThrowable + " handled"); + } + }, + new ThrowableHandler(Throwable.class) { + public void handle(final Throwable pThrowable) { + System.err.println("Generic throwable: " + pThrowable + " NOT handled"); + throwUnchecked(pThrowable); + } + } + ); + } + } + + private static void bar() { + baz(); + } + + @SuppressWarnings({"ThrowableInstanceNeverThrown"}) + private static void baz() { + double random = Math.random(); + if (random < (1.0 / 3.0)) { + throwUnchecked(new FileNotFoundException("FNF Boo")); + } + if (random < (2.0 / 3.0)) { + throwUnchecked(new SQLException("SQL Boo")); + } + else { + throwUnchecked(new Exception("Some Boo")); + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MathUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MathUtil.java new file mode 100755 index 00000000..02a16c49 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MathUtil.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.lang; + +/** + * The class MathUtil contains methods for performing basic numeric operations + * such as the elementary exponential, logarithm, square root, and + * trigonometric functions. + * + * @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/MathUtil.java#1 $ + */ +public final class MathUtil { + + /** */ + private MathUtil() { + } + + /** + * Converts an angle measured in degrees to the equivalent angle measured + * in radians. + * This method is a replacement for the Math.toRadians() method in + * Java versions < 1.2 (typically for Applets). + * + * @param pAngDeg an angle, in degrees + * @return the measurement of the angle angdeg in radians. + * + * @see java.lang.Math#toRadians(double) + */ + public static double toRadians(final double pAngDeg) { + return pAngDeg * Math.PI / 180.0; + } + + /** + * Converts an angle measured in radians to the equivalent angle measured + * in degrees. + * This method is a replacement for the Math.toDegrees() method in + * Java versions < 1.2 (typically for Applets). + * + * @param pAngRad an angle, in radians + * @return the measurement of the angle angrad in degrees. + * + * @see java.lang.Math#toDegrees(double) + */ + public static double toDegrees(final double pAngRad) { + return pAngRad * 180.0 / Math.PI; + } + + /** + * Returns the natural logarithm (base e) of a double value. + * Equivalent to java.lang.Math.log, just with a proper name. + * + * @param pArg a number greater than 0.0. + * @return the value ln pArg, the natural logarithm of + * pArg. + * + * @see java.lang.Math#log(double) + */ + public static double ln(final double pArg) { + return Math.log(pArg); + } + + private final static double LN_10 = Math.log(10); + + /** + * Returns the base 10 logarithm of a double value. + * + * @param pArg a number greater than 0.0. + * @return the value log pArg, the base 10 logarithm of + * pArg. + */ + public static double log(final double pArg) { + return Math.log(pArg) / LN_10; + } + + private final static double LN_2 = Math.log(10); + + /** + * Returns the base 2 logarithm of a double value. + * + * @param pArg a number greater than 0.0. + * @return the value log2 pArg, the base 2 + * logarithm of pArg. + */ + public static double log2(final double pArg) { + return Math.log(pArg) / LN_2; + } + + /** + * Returns the base N logarithm of a double value, for a given base + * N. + * + * @param pArg a number greater than 0.0. + * @param pBase a number greater than 0.0. + * + * @return the value logpBase pArg, the base + * pBase logarithm of pArg. + */ + public static double log(final double pArg, final double pBase) { + return Math.log(pArg) / Math.log(pBase); + } + + /** + * A replacement for Math.abs, that never returns negative values. + * Math.abs(long) does this for Long.MIN_VALUE. + * + * @see Math#abs(long) + * @see Long#MIN_VALUE + * + * @param pNumber + * @return the absolute value of pNumber + * + * @throws ArithmeticException if pNumber == Long.MIN_VALUE + */ + public static long abs(final long pNumber) { + if (pNumber == Long.MIN_VALUE) { + throw new ArithmeticException("long overflow: 9223372036854775808"); + } + return (pNumber < 0) ? -pNumber : pNumber; + } + + /** + * A replacement for Math.abs, that never returns negative values. + * Math.abs(int) does this for Integer.MIN_VALUE. + * + * @see Math#abs(int) + * @see Integer#MIN_VALUE + * + * @param pNumber + * @return the absolute value of pNumber + * + * @throws ArithmeticException if pNumber == Integer.MIN_VALUE + */ + public static int abs(final int pNumber) { + if (pNumber == Integer.MIN_VALUE) { + throw new ArithmeticException("int overflow: 2147483648"); + } + return (pNumber < 0) ? -pNumber : pNumber; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MostUnfortunateException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MostUnfortunateException.java new file mode 100755 index 00000000..120f1ff1 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MostUnfortunateException.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.lang; + +/** + * MostUnfortunateException + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/MostUnfortunateException.java#1 $ + */ +class MostUnfortunateException extends RuntimeException { + public MostUnfortunateException() { + this("Most unfortunate."); + } + + public MostUnfortunateException(Throwable pCause) { + this(pCause.getMessage(), pCause); + } + + public MostUnfortunateException(String pMessage, Throwable pCause) { + this(pMessage); + initCause(pCause); + } + + public MostUnfortunateException(String pMessage) { + super("A most unfortunate exception has occured: " + pMessage); + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeLoader.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeLoader.java new file mode 100755 index 00000000..572edeba --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeLoader.java @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.lang; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.util.FilterIterator; +import com.twelvemonkeys.util.service.ServiceRegistry; + +import java.io.*; +import java.util.Iterator; +import java.util.Arrays; + +/** + * NativeLoader + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeLoader.java#2 $ + */ +final class NativeLoader { + // TODO: Considerations: + // - Rename all libs like the current code, to .(so|dll|dylib)? + // - Keep library filename from jar, and rather store a separate + // properties-file with the library->library-file mappings? + // - As all invocations are with library file name, we could probably skip + // both renaming and properties-file altogether... + + // TODO: The real trick here, is how to load the correct library for the + // current platform... + // - Change String pResource to String[] pResources? + // - NativeResource class, that has a list of multiple resources? + // NativeResource(Map) os->native lib mapping + + // TODO: Consider exposing the method from SystemUtil + + // TODO: How about a SPI based solution?! + // public interface com.twelvemonkeys.lang.NativeResourceProvider + // + // imlementations return a pointer to the correct resource for a given (by + // this class) OS. + // + // String getResourceName(...) + // + // See http://tolstoy.com/samizdat/sysprops.html + // System properties: + // "os.name" + // Windows, Linux, Solaris/SunOS, + // Mac OS/Mac OS X/Rhapsody (aka Mac OS X Server) + // General Unix (AIX, Digital Unix, FreeBSD, HP-UX, Irix) + // OS/2 + // "os.arch" + // Windows: x86 + // Linux: x86, i386, i686, x86_64, ia64, + // Solaris: sparc, sparcv9, x86 + // Mac OS: PowerPC, ppc, i386 + // AIX: x86, ppc + // Digital Unix: alpha + // FreeBSD: x86, sparc + // HP-UX: PA-RISC + // Irix: mips + // OS/2: x86 + // "os.version" + // Windows: 4.0 -> NT/95, 5.0 -> 2000, 5.1 -> XP (don't care about old versions, CE etc) + // Mac OS: 8.0, 8.1, 10.0 -> OS X, 10.x.x -> OS X, 5.6 -> Rhapsody (!) + // + // Normalize os.name, os.arch and os.version?! + + + ///** Normalized operating system constant */ + //static final OperatingSystem OS_NAME = normalizeOperatingSystem(); + // + ///** Normalized system architecture constant */ + //static final Architecture OS_ARCHITECTURE = normalizeArchitecture(); + // + ///** Unormalized operating system version constant (for completeness) */ + //static final String OS_VERSION = System.getProperty("os.version"); + + static final NativeResourceRegistry sRegistry = new NativeResourceRegistry(); + + private NativeLoader() { + } + +/* + private static Architecture normalizeArchitecture() { + String arch = System.getProperty("os.arch"); + if (arch == null) { + throw new IllegalStateException("System property \"os.arch\" == null"); + } + + arch = arch.toLowerCase(); + if (OS_NAME == OperatingSystem.Windows + && (arch.startsWith("x86") || arch.startsWith("i386"))) { + return Architecture.X86; + // TODO: 64 bit + } + else if (OS_NAME == OperatingSystem.Linux) { + if (arch.startsWith("x86") || arch.startsWith("i386")) { + return Architecture.I386; + } + else if (arch.startsWith("i686")) { + return Architecture.I686; + } + // TODO: More Linux options? + // TODO: 64 bit + } + else if (OS_NAME == OperatingSystem.MacOS) { + if (arch.startsWith("power") || arch.startsWith("ppc")) { + return Architecture.PPC; + } + else if (arch.startsWith("i386")) { + return Architecture.I386; + } + } + else if (OS_NAME == OperatingSystem.Solaris) { + if (arch.startsWith("sparc")) { + return Architecture.SPARC; + } + if (arch.startsWith("x86")) { + // TODO: Should we use i386 as Linux and Mac does? + return Architecture.X86; + } + // TODO: 64 bit + } + + return Architecture.Unknown; + } +*/ + +/* + private static OperatingSystem normalizeOperatingSystem() { + String os = System.getProperty("os.name"); + if (os == null) { + throw new IllegalStateException("System property \"os.name\" == null"); + } + + os = os.toLowerCase(); + if (os.startsWith("windows")) { + return OperatingSystem.Windows; + } + else if (os.startsWith("linux")) { + return OperatingSystem.Linux; + } + else if (os.startsWith("mac os")) { + return OperatingSystem.MacOS; + } + else if (os.startsWith("solaris") || os.startsWith("sunos")) { + return OperatingSystem.Solaris; + } + + return OperatingSystem.Unknown; + } +*/ + + // TODO: We could actually have more than one resource for each lib... + private static String getResourceFor(String pLibrary) { + Iterator providers = sRegistry.providers(pLibrary); + while (providers.hasNext()) { + NativeResourceSPI resourceSPI = providers.next(); + try { + return resourceSPI.getClassPathResource(Platform.get()); + } + catch (Throwable t) { + // Dergister and try next + sRegistry.deregister(resourceSPI); + } + } + + return null; + } + + /** + * Loads a native library. + * + * @param pLibrary name of the library + * + * @throws UnsatisfiedLinkError + */ + public static void loadLibrary(String pLibrary) { + loadLibrary0(pLibrary, null, null); + } + + /** + * Loads a native library. + * + * @param pLibrary name of the library + * @param pLoader the class loader to use + * + * @throws UnsatisfiedLinkError + */ + public static void loadLibrary(String pLibrary, ClassLoader pLoader) { + loadLibrary0(pLibrary, null, pLoader); + } + + /** + * Loads a native library. + * + * @param pLibrary name of the library + * @param pResource name of the resource + * @param pLoader the class loader to use + * + * @throws UnsatisfiedLinkError + */ + static void loadLibrary0(String pLibrary, String pResource, ClassLoader pLoader) { + if (pLibrary == null) { + throw new IllegalArgumentException("library == null"); + } + + // Try loading normal way + UnsatisfiedLinkError unsatisfied; + try { + System.loadLibrary(pLibrary); + return; + } + catch (UnsatisfiedLinkError err) { + // Ignore + unsatisfied = err; + } + + final ClassLoader loader = pLoader != null ? pLoader : Thread.currentThread().getContextClassLoader(); + final String resource = pResource != null ? pResource : getResourceFor(pLibrary); + + // TODO: resource may be null, and that MIGHT be okay, IFF the resource + // is allready unpacked to the user dir... However, we then need another + // way to resolve the library extension... + // Right now we just fail in a predictable way (no NPE)! + if (resource == null) { + throw unsatisfied; + } + + // Default to load/store from user.home + File dir = new File(System.getProperty("user.home") + "/.twelvemonkeys/lib"); + dir.mkdirs(); + //File libraryFile = new File(dir.getAbsolutePath(), pLibrary + LIBRARY_EXTENSION); + File libraryFile = new File(dir.getAbsolutePath(), pLibrary + "." + FileUtil.getExtension(resource)); + + if (!libraryFile.exists()) { + try { + extractToUserDir(resource, libraryFile, loader); + } + catch (IOException ioe) { + UnsatisfiedLinkError err = new UnsatisfiedLinkError("Unable to extract resource to dir: " + libraryFile.getAbsolutePath()); + err.initCause(ioe); + throw err; + } + } + + // Try to load the library from the file we just wrote + System.load(libraryFile.getAbsolutePath()); + } + + private static void extractToUserDir(String pResource, File pLibraryFile, ClassLoader pLoader) throws IOException { + // Get resource from classpath + InputStream in = pLoader.getResourceAsStream(pResource); + if (in == null) { + throw new FileNotFoundException("Unable to locate classpath resource: " + pResource); + } + + // Write to file in user dir + FileOutputStream fileOut = null; + try { + fileOut = new FileOutputStream(pLibraryFile); + + byte[] tmp = new byte[1024]; + // copy the contents of our resource out to the destination + // dir 1K at a time. 1K may seem arbitrary at first, but today + // is a Tuesday, so it makes perfect sense. + int bytesRead = in.read(tmp); + while (bytesRead != -1) { + fileOut.write(tmp, 0, bytesRead); + bytesRead = in.read(tmp); + } + } + finally { + FileUtil.close(fileOut); + FileUtil.close(in); + } + } + + // TODO: Validate OS names? + // Windows + // Linux + // Solaris + // Mac OS (OSX+) + // Generic Unix? + // Others? + + // TODO: OSes that support different architectures might require different + // resources for each architecture.. Need a namespace/flavour system + // TODO: 32 bit/64 bit issues? + // Eg: Windows, Windows/32, Windows/64, Windows/Intel/64? + // Solaris/Sparc, Solaris/Intel/64 + // MacOS/PowerPC, MacOS/Intel + /* + public static class NativeResource { + private Map mMap; + + public NativeResource(String[] pOSNames, String[] pReourceNames) { + if (pOSNames == null) { + throw new IllegalArgumentException("osNames == null"); + } + if (pReourceNames == null) { + throw new IllegalArgumentException("resourceNames == null"); + } + if (pOSNames.length != pReourceNames.length) { + throw new IllegalArgumentException("osNames.length != resourceNames.length"); + } + + Map map = new HashMap(); + for (int i = 0; i < pOSNames.length; i++) { + map.put(pOSNames[i], pReourceNames[i]); + } + + mMap = Collections.unmodifiableMap(map); + } + + public NativeResource(Map pMap) { + if (pMap == null) { + throw new IllegalArgumentException("map == null"); + } + + Map map = new HashMap(pMap); + + Iterator it = map.keySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + if (!(entry.getKey() instanceof String && entry.getValue() instanceof String)) { + throw new IllegalArgumentException("map contains non-string entries: " + entry); + } + } + + mMap = Collections.unmodifiableMap(map); + } + + protected NativeResource() { + } + + public final String resourceForCurrentOS() { + throw new UnsupportedOperationException(); + } + + protected String getResourceName(String pOSName) { + return (String) mMap.get(pOSName); + } + } + */ + + private static class NativeResourceRegistry extends ServiceRegistry { + public NativeResourceRegistry() { + super(Arrays.asList(NativeResourceSPI.class).iterator()); + registerApplicationClasspathSPIs(); + } + + Iterator providers(String pNativeResource) { + return new FilterIterator(providers(NativeResourceSPI.class), + new NameFilter(pNativeResource)); + } + } + + private static class NameFilter implements FilterIterator.Filter { + private final String mName; + + NameFilter(String pName) { + if (pName == null) { + throw new IllegalArgumentException("name == null"); + } + mName = pName; + } + public boolean accept(NativeResourceSPI pElement) { + return mName.equals(pElement.getResourceName()); + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeResourceSPI.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeResourceSPI.java new file mode 100755 index 00000000..2290ef2a --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/NativeResourceSPI.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.lang; + +/** + * Abstract base class for native reource providers to iplement. + *

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

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

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

+ * NOTE: This method is intended for the SPI framework, and should not be + * invoked by client code. + * + * @param pPlatform the current platform + * @return a {@code String} containing the path to a classpath resource or + * {@code null} if no resource is available. + * + * @see com.twelvemonkeys.lang.Platform.OperatingSystem + * @see com.twelvemonkeys.lang.Platform.Architecture + * @see System#getProperties() + */ + public abstract String getClassPathResource(final Platform pPlatform); +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Platform.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Platform.java new file mode 100755 index 00000000..76c85eab --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Platform.java @@ -0,0 +1,244 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.lang; + +/** + * Platform + * + * @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/Platform.java#1 $ + */ +public final class Platform { + /** + * Normalized operating system constant + */ + final OperatingSystem mOS; + + /** + * Unormalized operating system version constant (for completeness) + */ + final String mVersion; + + /** + * Normalized system architecture constant + */ + final Architecture mArchitecture; + + static final private Platform INSTANCE = new Platform(); + + private Platform() { + mOS = normalizeOperatingSystem(); + mVersion = System.getProperty("os.version"); + mArchitecture = normalizeArchitecture(mOS); + } + + private static OperatingSystem normalizeOperatingSystem() { + String os = System.getProperty("os.name"); + if (os == null) { + throw new IllegalStateException("System property \"os.name\" == null"); + } + + os = os.toLowerCase(); + if (os.startsWith("windows")) { + return OperatingSystem.Windows; + } + else if (os.startsWith("linux")) { + return OperatingSystem.Linux; + } + else if (os.startsWith("mac os")) { + return OperatingSystem.MacOS; + } + else if (os.startsWith("solaris") || os.startsWith("sunos")) { + return OperatingSystem.Solaris; + } + + return OperatingSystem.Unknown; + } + + private static Architecture normalizeArchitecture(final OperatingSystem pOsName) { + String arch = System.getProperty("os.arch"); + if (arch == null) { + throw new IllegalStateException("System property \"os.arch\" == null"); + } + + arch = arch.toLowerCase(); + if (pOsName == OperatingSystem.Windows + && (arch.startsWith("x86") || arch.startsWith("i386"))) { + return Architecture.X86; + // TODO: 64 bit + } + else if (pOsName == OperatingSystem.Linux) { + if (arch.startsWith("x86") || arch.startsWith("i386")) { + return Architecture.I386; + } + else if (arch.startsWith("i686")) { + return Architecture.I686; + } + // TODO: More Linux options? + // TODO: 64 bit + } + else if (pOsName == OperatingSystem.MacOS) { + if (arch.startsWith("power") || arch.startsWith("ppc")) { + return Architecture.PPC; + } + else if (arch.startsWith("i386")) { + return Architecture.I386; + } + } + else if (pOsName == OperatingSystem.Solaris) { + if (arch.startsWith("sparc")) { + return Architecture.SPARC; + } + if (arch.startsWith("x86")) { + // TODO: Should we use i386 as Linux and Mac does? + return Architecture.X86; + } + // TODO: 64 bit + } + + return Architecture.Unknown; + } + + /** + * Returns the current {@code Platform}. + * @return the current {@code Platform}. + */ + public static Platform get() { + return INSTANCE; + } + + /** + * @return this platform's OS. + */ + public OperatingSystem getOS() { + return mOS; + } + + /** + * @return this platform's OS version. + */ + public String getVersion() { + return mVersion; + } + + /** + * @return this platform's architecture. + */ + public Architecture getArchitecture() { + return mArchitecture; + } + + /** + * Shorthand for {@code Platform.get().getOS()}. + * @return the current {@code OperatingSystem}. + */ + public static OperatingSystem os() { + return INSTANCE.mOS; + } + + /** + * Shorthand for {@code Platform.get().getVersion()}. + * @return the current OS version. + */ + public static String version() { + return INSTANCE.mVersion; + } + + /** + * Shorthand for {@code Platform.get().getArchitecture()}. + * @return the current {@code Architecture}. + */ + public static Architecture arch() { + return INSTANCE.mArchitecture; + } + + /** + * 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 $ + */ + public static enum Architecture { + X86("x86"), + I386("i386"), + I686("i686"), + PPC("ppc"), + SPARC("sparc"), + + Unknown(System.getProperty("os.arch")); + + final String mName;// for debug only + + private Architecture(String pName) { + mName = pName; + } + + public String toString() { + return mName; + } + } + + /** + * 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 $ + */ + public static enum OperatingSystem { + Windows("Windows", "win"), + Linux("Linux", "lnx"), + Solaris("Solaris", "sun"), + MacOS("Mac OS", "osx"), + + Unknown(System.getProperty("os.name"), ""); + + final String mId; + final String mName;// for debug only + + private OperatingSystem(String pName, String pId) { + mName = pName; + mId = pId; + } + + public String getName() { + return mName; + } + + public String toString() { + return mId; + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ReflectUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ReflectUtil.java new file mode 100755 index 00000000..d414fef3 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/ReflectUtil.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.lang; + +/** + * 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 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 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 true if the given type is a primitive wrapper. + * + * @param pType + * + * @return true if the given type is a primitive wrapper, otherwise + * 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/StringUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/StringUtil.java new file mode 100755 index 00000000..c1ecda91 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/StringUtil.java @@ -0,0 +1,2150 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.lang; + +import com.twelvemonkeys.util.StringTokenIterator; + +import java.awt.*; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.sql.Timestamp; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.ArrayList; +import java.util.regex.PatternSyntaxException; +import java.util.regex.Pattern; +import java.io.UnsupportedEncodingException; +import java.nio.charset.UnsupportedCharsetException; + +/** + * 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 subarray 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(String[] pStringArray) { + return toCSVString(pStringArray, ", "); + } + + /** + * Converts a string array to a string separated by the given delimiter. + * + * @param pStringArray the string array + * @param pDelimiterString the delimter string + * @return string of delimiter separated values + * @throws IllegalArgumentException if {@code pDelimiterString == null} + */ + public static String toCSVString(String[] 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); + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/SystemUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/SystemUtil.java new file mode 100755 index 00000000..7adad953 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/SystemUtil.java @@ -0,0 +1,693 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.lang; + +import com.twelvemonkeys.util.XMLProperties; + +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; + 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 classloader, as SystemUtil's + // classloader may have a totally different classloader 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Validate.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Validate.java new file mode 100755 index 00000000..1fe91775 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Validate.java @@ -0,0 +1,124 @@ +package com.twelvemonkeys.lang; + +import java.util.Arrays; +import java.util.Collection; +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 $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/Validate.java#1 $ + */ +public final class Validate { + private static final String UNSPECIFIED_PARAM_NAME = "method parameter"; + + private Validate() {} + + // Not null... + + public static T notNull(final T pParameter) { + return notNull(pParameter, null); + } + + public static T notNull(final T pParameter, final String pParamName) { + if (pParameter == null) { + throw new IllegalArgumentException(String.format("%s may not be null", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); + } + return pParameter; + } + + // Not empty... + + public static T notEmpty(final T pParameter) { + return notEmpty(pParameter, null); + } + + public static T notEmpty(final T pParameter, final String pParamName) { + if (pParameter == null || pParameter.length() == 0) { + throw new IllegalArgumentException(String.format("%s may not be empty", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); + } + + return pParameter; + } + + public static T[] notEmpty(final T[] pParameter) { + return notEmpty(pParameter, null); + } + + public static T[] notEmpty(final T[] pParameter, final String pParamName) { + if (pParameter == null || pParameter.length == 0) { + throw new IllegalArgumentException(String.format("%s may not be empty", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); + } + + return pParameter; + } + + public static Collection notEmpty(final Collection pParameter) { + return notEmpty(pParameter, null); + } + + public static Collection notEmpty(final Collection pParameter, final String pParamName) { + if (pParameter == null || pParameter.isEmpty()) { + throw new IllegalArgumentException(String.format("%s may not be empty", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); + } + + return pParameter; + } + + public static Map notEmpty(final Map pParameter) { + return notEmpty(pParameter, null); + } + + public static Map notEmpty(final Map pParameter, final String pParamName) { + if (pParameter == null || pParameter.isEmpty()) { + throw new IllegalArgumentException(String.format("%s may not be empty", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); + } + + return pParameter; + } + + // No null elements + + public static T[] noNullElements(final T[] pParameter) { + return noNullElements(pParameter, null); + } + + public static T[] noNullElements(final T[] pParameter, final String pParamName) { + noNullElements(Arrays.asList(pParameter), pParamName); + return pParameter; + } + + public static Collection noNullElements(final Collection pParameter) { + return noNullElements(pParameter, null); + } + + public static Collection noNullElements(final Collection pParameter, final String pParamName) { + for (T element : pParameter) { + if (element == null) { + throw new IllegalArgumentException(String.format("%s may not contain null elements", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); + } + } + + return pParameter; + } + + public static Map noNullElements(final Map pParameter) { + return noNullElements(pParameter, null); + } + + public static Map noNullElements(final Map pParameter, final String pParamName) { + for (V element : pParameter.values()) { + if (element == null) { + throw new IllegalArgumentException(String.format("%s may not contain null elements", pParamName == null ? UNSPECIFIED_PARAM_NAME : pParamName)); + } + } + + return pParameter; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/package.html b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/package.html new file mode 100755 index 00000000..01784187 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/lang/package.html @@ -0,0 +1,7 @@ + + + +Contains utils/helpers for classes in {@code java.lang}. + + + diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java new file mode 100755 index 00000000..26e9d8c9 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/AuthenticatorFilter.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.net; + +import java.net.*; + +/** + * Interface for filtering Authenticator requests, used by the + * SimpleAuthenticator. + * + * @see SimpleAuthenticator + * @see java.net.Authenticator + * + * @author Harald Kuhr (haraldk@iconmedialab.no), + * @version 1.0 + */ +public interface AuthenticatorFilter { + public boolean accept(InetAddress pAddress, int pPort, String pProtocol, + String pPrompt, String pScheme); + +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java new file mode 100755 index 00000000..a597a501 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/HttpURLConnection.java @@ -0,0 +1,1103 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.net; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.util.BASE64; + +import java.io.*; +import java.net.*; +import java.util.*; + +/** + * A URLConnection with support for HTTP-specific features. See + * the spec for details. + * This version also supports read and connect timeouts, making it more useful + * for clients with limitted time. + *

+ * Note that the timeouts are created on the socket level, and that + *

+ * Note: This class should now work as expected, but it need more testing before + * it can enter production release. + *
+ * --.k + * + * @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/net/HttpURLConnection.java#1 $ + * @todo Write JUnit TestCase + * @todo ConnectionMananger! + * @see RFC 2616 + */ +public class HttpURLConnection extends java.net.HttpURLConnection { + + /** + * HTTP Status-Code 307: Temporary Redirect + */ + public final static int HTTP_REDIRECT = 307; + private final static int HTTP_DEFAULT_PORT = 80; + private final static String HTTP_HEADER_END = "\r\n\r\n"; + private static final String HEADER_WWW_AUTH = "WWW-Authenticate"; + private final static int BUF_SIZE = 8192; + private int mMaxRedirects = (System.getProperty("http.maxRedirects") != null) + ? Integer.parseInt(System.getProperty("http.maxRedirects")) + : 20; + protected int mTimeout = -1; + protected int mConnectTimeout = -1; + private Socket mSocket = null; + protected InputStream mErrorStream = null; + protected InputStream mInputStream = null; + protected OutputStream mOutputStream = null; + private String[] mResponseHeaders = null; + protected Properties mResponseHeaderFields = null; + protected Properties mRequestProperties = new Properties(); + + /** + * Creates a HttpURLConnection. + * + * @param pURL the URL to connect to. + */ + protected HttpURLConnection(URL pURL) { + this(pURL, 0, 0); + } + + /** + * Creates a HttpURLConnection with a given read and connect timeout. + * A timeout value of zero is interpreted as an + * infinite timeout. + * + * @param pURL the URL to connect to. + * @param pTimeout the maximum time the socket will block for read + * and connect operations. + */ + protected HttpURLConnection(URL pURL, int pTimeout) { + this(pURL, pTimeout, pTimeout); + } + + /** + * Creates a HttpURLConnection with a given read and connect timeout. + * A timeout value of zero is interpreted as an + * infinite timeout. + * + * @param pURL the URL to connect to. + * @param pTimeout the maximum time the socket will block for read + * operations. + * @param pConnectTimeout the maximum time the socket will block for + * connection. + */ + protected HttpURLConnection(URL pURL, int pTimeout, int pConnectTimeout) { + super(pURL); + setTimeout(pTimeout); + mConnectTimeout = pConnectTimeout; + } + + /** + * Sets the general request property. If a property with the key already + * exists, overwrite its value with the new value. + *

+ *

NOTE: HTTP requires all request properties which can + * legally have multiple instances with the same key + * to use a comma-seperated list syntax which enables multiple + * properties to be appended into a single property. + * + * @param pKey the keyword by which the request is known + * (e.g., "accept"). + * @param pValue the value associated with it. + * @see #getRequestProperty(java.lang.String) + */ + public void setRequestProperty(String pKey, String pValue) { + if (connected) { + throw new IllegalAccessError("Already connected"); + } + String oldValue = mRequestProperties.getProperty(pKey); + + if (oldValue == null) { + mRequestProperties.setProperty(pKey, pValue); + } + else { + mRequestProperties.setProperty(pKey, oldValue + ", " + pValue); + } + } + + /** + * Returns the value of the named general request property for this + * connection. + * + * @param pKey the keyword by which the request is known (e.g., "accept"). + * @return the value of the named general request property for this + * connection. + * @see #setRequestProperty(java.lang.String, java.lang.String) + */ + public String getRequestProperty(String pKey) { + if (connected) { + throw new IllegalAccessError("Already connected"); + } + return mRequestProperties.getProperty(pKey); + } + + /** + * Gets HTTP response status from responses like: + *

+     * HTTP/1.0 200 OK
+     * HTTP/1.0 401 Unauthorized
+     * 
+ * Extracts the ints 200 and 401 respectively. + * Returns -1 if none can be discerned + * from the response (i.e., the response is not valid HTTP). + *

+ * + * + * @return the HTTP Status-Code + * @throws IOException if an error occurred connecting to the server. + */ + public int getResponseCode() throws IOException { + if (responseCode != -1) { + return responseCode; + } + + // Make sure we've gotten the headers + getInputStream(); + String resp = getHeaderField(0); + + // should have no leading/trailing LWS + // expedite the typical case by assuming it has the + // form "HTTP/1.x 2XX " + int ind; + + try { + ind = resp.indexOf(' '); + while (resp.charAt(ind) == ' ') { + ind++; + } + responseCode = Integer.parseInt(resp.substring(ind, ind + 3)); + responseMessage = resp.substring(ind + 4).trim(); + return responseCode; + } + catch (Exception e) { + return responseCode; + } + } + + /** + * Returns the name of the specified header field. + * + * @param pName the name of a header field. + * @return the value of the named header field, or null + * if there is no such field in the header. + */ + public String getHeaderField(String pName) { + return mResponseHeaderFields.getProperty(StringUtil.toLowerCase(pName)); + } + + /** + * Returns the value for the nth header field. + * It returns null if there are fewer than + * n fields. + *

+ * This method can be used in conjunction with the + * getHeaderFieldKey method to iterate through all + * the headers in the message. + * + * @param pIndex an index. + * @return the value of the nth header field. + * @see java.net.URLConnection#getHeaderFieldKey(int) + */ + public String getHeaderField(int pIndex) { + // TODO: getInputStream() first, to make sure we have header fields + if (pIndex >= mResponseHeaders.length) { + return null; + } + String field = mResponseHeaders[pIndex]; + + // pIndex == 0, means the response code etc (i.e. "HTTP/1.1 200 OK"). + if ((pIndex == 0) || (field == null)) { + return field; + } + int idx = field.indexOf(':'); + + return ((idx > 0) + ? field.substring(idx).trim() + : ""); // TODO: "" or null? + } + + /** + * Returns the key for the nth header field. + * + * @param pIndex an index. + * @return the key for the nth header field, + * or null if there are fewer than n + * fields. + */ + public String getHeaderFieldKey(int pIndex) { + // TODO: getInputStream() first, to make sure we have header fields + if (pIndex >= mResponseHeaders.length) { + return null; + } + String field = mResponseHeaders[pIndex]; + + if (StringUtil.isEmpty(field)) { + return null; + } + int idx = field.indexOf(':'); + + return StringUtil.toLowerCase(((idx > 0) + ? field.substring(0, idx) + : field)); + } + + /** + * Sets the read timeout for the undelying socket. + * A timeout of zero is interpreted as an + * infinite timeout. + * + * @param pTimeout the maximum time the socket will block for read + * operations, in milliseconds. + */ + public void setTimeout(int pTimeout) { + if (pTimeout < 0) { // Must be positive + throw new IllegalArgumentException("Timeout must be positive."); + } + mTimeout = pTimeout; + if (mSocket != null) { + try { + mSocket.setSoTimeout(pTimeout); + } + catch (SocketException se) { + // Not much to do about that... + } + } + } + + /** + * Gets the read timeout for the undelying socket. + * + * @return the maximum time the socket will block for read operations, in + * milliseconds. + * The default value is zero, which is interpreted as an + * infinite timeout. + */ + public int getTimeout() { + + try { + return ((mSocket != null) + ? mSocket.getSoTimeout() + : mTimeout); + } + catch (SocketException se) { + return mTimeout; + } + } + + /** + * Returns an input stream that reads from this open connection. + * + * @return an input stream that reads from this open connection. + * @throws IOException if an I/O error occurs while + * creating the input stream. + */ + public synchronized InputStream getInputStream() throws IOException { + if (!connected) { + connect(); + } + + // Nothing to return + if (responseCode == HTTP_NOT_FOUND) { + throw new FileNotFoundException(url.toString()); + } + int length; + + if (mInputStream == null) { + return null; + } + + // "De-chunk" the output stream + else if ("chunked".equalsIgnoreCase(getHeaderField("Transfer-Encoding"))) { + if (!(mInputStream instanceof ChunkedInputStream)) { + mInputStream = new ChunkedInputStream(mInputStream); + } + } + + // Make sure we don't wait forever, if the content-length is known + else if ((length = getHeaderFieldInt("Content-Length", -1)) >= 0) { + if (!(mInputStream instanceof FixedLengthInputStream)) { + mInputStream = new FixedLengthInputStream(mInputStream, length); + } + } + return mInputStream; + } + + /** + * Returns an output stream that writes to this connection. + * + * @return an output stream that writes to this connection. + * @throws IOException if an I/O error occurs while + * creating the output stream. + */ + public synchronized OutputStream getOutputStream() throws IOException { + + if (!connected) { + connect(); + } + return mOutputStream; + } + + /** + * Indicates that other requests to the server + * are unlikely in the near future. Calling disconnect() + * should not imply that this HttpURLConnection + * instance can be reused for other requests. + */ + public void disconnect() { + if (mSocket != null) { + try { + mSocket.close(); + } + catch (IOException ioe) { + + // Does not matter, I guess. + } + mSocket = null; + } + connected = false; + } + + /** + * Internal connect method. + */ + private void connect(final URL pURL, PasswordAuthentication pAuth, String pAuthType, int pRetries) throws IOException { + // Find correct port + final int port = (pURL.getPort() > 0) + ? pURL.getPort() + : HTTP_DEFAULT_PORT; + + // Create socket if we don't have one + if (mSocket == null) { + //mSocket = new Socket(pURL.getHost(), port); // Blocks... + mSocket = createSocket(pURL, port, mConnectTimeout); + mSocket.setSoTimeout(mTimeout); + } + + // Get Socket output stream + OutputStream os = mSocket.getOutputStream(); + + // Connect using HTTP + writeRequestHeaders(os, pURL, method, mRequestProperties, usingProxy(), pAuth, pAuthType); + + // Get response input stream + InputStream sis = mSocket.getInputStream(); + BufferedInputStream is = new BufferedInputStream(sis); + + // Detatch reponse headers from reponse input stream + InputStream header = detatchResponseHeader(is); + + // Parse headers and set response code/message + mResponseHeaders = parseResponseHeader(header); + mResponseHeaderFields = parseHeaderFields(mResponseHeaders); + + //System.err.println("Headers fields:"); + //mResponseHeaderFields.list(System.err); + // Test HTTP response code, to see if further action is needed + switch (getResponseCode()) { + case HTTP_OK: + // 200 OK + mInputStream = is; + mErrorStream = null; + break; + + /* + case HTTP_PROXY_AUTH: + // 407 Proxy Authentication Required + */ + case HTTP_UNAUTHORIZED: + // 401 Unauthorized + // Set authorization and try again.. Slightly more compatible + responseCode = -1; + + // IS THIS REDIRECTION?? + //if (instanceFollowRedirects) { ??? + String auth = getHeaderField(HEADER_WWW_AUTH); + + // Missing WWW-Authenticate header for 401 response is an error + if (StringUtil.isEmpty(auth)) { + throw new ProtocolException("Missing \"" + HEADER_WWW_AUTH + "\" header for response: 401 " + responseMessage); + } + + // Get real mehtod from WWW-Authenticate header + int SP = auth.indexOf(" "); + String method; + String realm = null; + + if (SP >= 0) { + method = auth.substring(0, SP); + if (auth.length() >= SP + 7) { + realm = auth.substring(SP + 7); // " realm=".lenght() == 7 + } + + // else no realm + } + else { + // Default mehtod is Basic + method = SimpleAuthenticator.BASIC; + } + + // Get PasswordAuthentication + PasswordAuthentication pa = Authenticator.requestPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), port, + pURL.getProtocol(), realm, method); + + // Avoid infinite loop + if (pRetries++ <= 0) { + throw new ProtocolException("Server redirected too many times (" + mMaxRedirects + ") (Authentication required: " + auth + ")"); // This is what sun.net.www.protocol.http.HttpURLConnection does + } + else if (pa != null) { + connect(pURL, pa, method, pRetries); + } + break; + case HTTP_MOVED_PERM: + // 301 Moved Permanently + case HTTP_MOVED_TEMP: + // 302 Found + case HTTP_SEE_OTHER: + // 303 See Other + /* + case HTTP_USE_PROXY: + // 305 Use Proxy + // How do we handle this? + */ + case HTTP_REDIRECT: + // 307 Temporary Redirect + //System.err.println("Redirecting " + getResponseCode()); + if (instanceFollowRedirects) { + // Redirect + responseCode = -1; // Because of the java.net.URLConnection + + // getResponseCode implementation... + // --- + // I think redirects must be get? + //setRequestMethod("GET"); + // --- + String location = getHeaderField("Location"); + URL newLoc = new URL(pURL, location); + + // Test if we can reuse the Socket + if (!(newLoc.getAuthority().equals(pURL.getAuthority()) && (newLoc.getPort() == pURL.getPort()))) { + mSocket.close(); // Close the socket, won't need it anymore + mSocket = null; + } + if (location != null) { + //System.err.println("Redirecting to " + location); + // Avoid infinite loop + if (--pRetries <= 0) { + throw new ProtocolException("Server redirected too many times (5)"); + } + else { + connect(newLoc, pAuth, pAuthType, pRetries); + } + } + break; + } + + // ...else, fall through default (if no Location: header) + default : + // Not 200 OK, or any of the redirect responses + // Probably an error... + mErrorStream = is; + mInputStream = null; + } + + // --- Need rethinking... + // No further questions, let the Socket wait forever (until the server + // closes the connection) + //mSocket.setSoTimeout(0); + // Probably not... The timeout should only kick if the read BLOCKS. + // Shutdown output, meaning any writes to the outputstream below will + // probably fail... + //mSocket.shutdownOutput(); + // Not a good idea at all... POSTs need the outputstream to send the + // form-data. + // --- /Need rethinking. + mOutputStream = os; + } + + private static interface SocketConnector extends Runnable { + + /** + * Method getSocket + * + * @return the socket + * @throws IOException + */ + public Socket getSocket() throws IOException; + } + + /** + * Creates a socket to the given URL and port, with the given connect + * timeout. If the socket waits more than the given timout to connect, + * an ConnectException is thrown. + * + * @param pURL the URL to connect to + * @param pPort the port to connect to + * @param pConnectTimeout the connect timeout + * @return the created Socket. + * @throws ConnectException if the connection is refused or otherwise + * times out. + * @throws UnknownHostException if the IP address of the host could not be + * determined. + * @throws IOException if an I/O error occurs when creating the socket. + * @todo Move this code to a SocetImpl or similar? + * @see Socket#Socket(String,int) + */ + private Socket createSocket(final URL pURL, final int pPort, int pConnectTimeout) throws IOException { + Socket socket; + final Object current = this; + SocketConnector connector; + Thread t = new Thread(connector = new SocketConnector() { + + private IOException mConnectException = null; + private Socket mLocalSocket = null; + + public Socket getSocket() throws IOException { + + if (mConnectException != null) { + throw mConnectException; + } + return mLocalSocket; + } + + // Run method + public void run() { + + try { + mLocalSocket = new Socket(pURL.getHost(), pPort); // Blocks... + } + catch (IOException ioe) { + + // Store this exception for later + mConnectException = ioe; + } + + // Signal that we are done + synchronized (current) { + current.notify(); + } + } + }); + + t.start(); + + // Wait for connect + synchronized (this) { + try { + + /// Only wait if thread is alive! + if (t.isAlive()) { + if (pConnectTimeout > 0) { + wait(pConnectTimeout); + } + else { + wait(); + } + } + } + catch (InterruptedException ie) { + + // Continue excecution on interrupt? Hmmm.. + } + } + + // Throw exception if the socket didn't connect fast enough + if ((socket = connector.getSocket()) == null) { + throw new ConnectException("Socket connect timed out!"); + } + return socket; + } + + /** + * Opens a communications link to the resource referenced by this + * URL, if such a connection has not already been established. + *

+ * If the connect method is called when the connection + * has already been opened (indicated by the connected + * field having the value true), the call is ignored. + *

+ * URLConnection objects go through two phases: first they are + * created, then they are connected. After being created, and + * before being connected, various options can be specified + * (e.g., doInput and UseCaches). After connecting, it is an + * error to try to set them. Operations that depend on being + * connected, like getContentLength, will implicitly perform the + * connection, if necessary. + * + * @throws IOException if an I/O error occurs while opening the + * connection. + * @see java.net.URLConnection#connected + * @see RFC 2616 + */ + public void connect() throws IOException { + if (connected) { + return; // Ignore + } + connected = true; + connect(url, null, null, mMaxRedirects); + } + + /** + * TODO: Proxy support is still missing. + * + * @return this method returns false, as proxy suport is not implemented. + */ + public boolean usingProxy() { + return false; + } + + /** + * Writes the HTTP request headers, for HTTP GET method. + * + * @see RFC 2616 + */ + private static void writeRequestHeaders(OutputStream pOut, URL pURL, String pMethod, Properties pProps, boolean pUsingProxy, + PasswordAuthentication pAuth, String pAuthType) { + PrintWriter out = new PrintWriter(pOut, true); // autoFlush + + if (!pUsingProxy) { + out.println(pMethod + " " + (!StringUtil.isEmpty(pURL.getPath()) + ? pURL.getPath() + : "/") + ((pURL.getQuery() != null) + ? "?" + pURL.getQuery() + : "") + " HTTP/1.1"); // HTTP/1.1 + + // out.println("Connection: close"); // No persistent connections yet + + /* + System.err.println(pMethod + " " + + (!StringUtil.isEmpty(pURL.getPath()) ? pURL.getPath() : "/") + + (pURL.getQuery() != null ? "?" + pURL.getQuery() : "") + + " HTTP/1.1"); // HTTP/1.1 + */ + + // Authority (Host: HTTP/1.1 field, but seems to work for HTTP/1.0) + out.println("Host: " + pURL.getHost() + ((pURL.getPort() != -1) + ? ":" + pURL.getPort() + : "")); + + /* + System.err.println("Host: " + pURL.getHost() + + (pURL.getPort() != -1 ? ":" + pURL.getPort() : "")); + */ + } + else { + + ////-- PROXY (absolute) VERSION + out.println(pMethod + " " + pURL.getProtocol() + "://" + pURL.getHost() + ((pURL.getPort() != -1) + ? ":" + pURL.getPort() + : "") + pURL.getPath() + ((pURL.getQuery() != null) + ? "?" + pURL.getQuery() + : "") + " HTTP/1.1"); + } + + // Check if we have authentication + if (pAuth != null) { + + // If found, set Authorization header + byte[] userPass = (pAuth.getUserName() + ":" + new String(pAuth.getPassword())).getBytes(); + + // "Authorization" ":" credentials + out.println("Authorization: " + pAuthType + " " + BASE64.encode(userPass)); + + /* + System.err.println("Authorization: " + pAuthType + " " + + BASE64.encode(userPass)); + */ + } + + // Iterate over properties + + for (Map.Entry property : pProps.entrySet()) { + out.println(property.getKey() + ": " + property.getValue()); + + //System.err.println(property.getKey() + ": " + property.getValue()); + } + out.println(); // Empty line, marks end of request-header + } + + /** + * Finds the end of the HTTP response header in an array of bytes. + * + * @todo This one's a little dirty... + */ + private static int findEndOfHeader(byte[] pBytes, int pEnd) { + byte[] header = HTTP_HEADER_END.getBytes(); + + // Normal condition, check all bytes + for (int i = 0; i < pEnd - 4; i++) { // Need 4 bytes to match + if ((pBytes[i] == header[0]) && (pBytes[i + 1] == header[1]) && (pBytes[i + 2] == header[2]) && (pBytes[i + 3] == header[3])) { + + //System.err.println("FOUND END OF HEADER!"); + return i + 4; + } + } + + // Check last 3 bytes, to check if we have a partial match + if ((pEnd - 1 >= 0) && (pBytes[pEnd - 1] == header[0])) { + + //System.err.println("FOUND LAST BYTE"); + return -2; // LAST BYTE + } + else if ((pEnd - 2 >= 0) && (pBytes[pEnd - 2] == header[0]) && (pBytes[pEnd - 1] == header[1])) { + + //System.err.println("FOUND LAST TWO BYTES"); + return -3; // LAST TWO BYTES + } + else if ((pEnd - 3 >= 0) && (pBytes[pEnd - 3] == header[0]) && (pBytes[pEnd - 2] == header[1]) && (pBytes[pEnd - 1] == header[2])) { + + //System.err.println("FOUND LAST THREE BYTES"); + return -4; // LAST THREE BYTES + } + return -1; // NO BYTES MATCH + } + + /** + * Reads the header part of the response, and copies it to a different + * InputStream. + */ + private static InputStream detatchResponseHeader(BufferedInputStream pIS) throws IOException { + // Store header in byte array + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + pIS.mark(BUF_SIZE); + byte[] buffer = new byte[BUF_SIZE]; + int length; + int headerEnd; + + // Read from iput, store in bytes + while ((length = pIS.read(buffer)) != -1) { + + // End of header? + headerEnd = findEndOfHeader(buffer, length); + if (headerEnd >= 0) { + + // Write rest + bytes.write(buffer, 0, headerEnd); + + // Go back to last mark + pIS.reset(); + + // Position stream to right after header, and exit loop + pIS.skip(headerEnd); + break; + } + else if (headerEnd < -1) { + + // Write partial (except matching header bytes) + bytes.write(buffer, 0, length - 4); + + // Go back to last mark + pIS.reset(); + + // Position stream to right before potential header end + pIS.skip(length - 4); + } + else { + + // Write all + bytes.write(buffer, 0, length); + } + + // Can't read more than BUF_SIZE ahead anyway + pIS.mark(BUF_SIZE); + } + return new ByteArrayInputStream(bytes.toByteArray()); + } + + /** + * Pareses the response header fields. + */ + private static Properties parseHeaderFields(String[] pHeaders) { + Properties headers = new Properties(); + + // Get header information + int split; + String field; + String value; + + for (String header : pHeaders) { + //System.err.println(pHeaders[i]); + if ((split = header.indexOf(":")) > 0) { + + // Read & parse..? + field = header.substring(0, split); + value = header.substring(split + 1); + + //System.err.println(field + ": " + value.trim()); + headers.setProperty(StringUtil.toLowerCase(field), value.trim()); + } + } + return headers; + } + + /** + * Parses the response headers. + */ + private static String[] parseResponseHeader(InputStream pIS) throws IOException { + List headers = new ArrayList(); + + // Wrap Stream in Reader + BufferedReader in = new BufferedReader(new InputStreamReader(pIS)); + + // Get response status + String header; + + while ((header = in.readLine()) != null) { + //System.err.println(header); + headers.add(header); + } + return headers.toArray(new String[headers.size()]); + } + + /** + * A FilterInputStream that wraps HTTP streams, with given content-length. + */ + protected static class FixedLengthInputStream extends FilterInputStream { + + private int mBytesLeft = 0; + + protected FixedLengthInputStream(InputStream pIS, int pLength) { + super(pIS); + mBytesLeft = pLength; + } + + public int available() throws IOException { + int available = in.available(); + + return ((available < mBytesLeft) + ? available + : mBytesLeft); + } + + public int read() throws IOException { + if (mBytesLeft-- > 0) { + return in.read(); + } + return -1; + } + + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + int read; + + if (mBytesLeft <= 0) { + return -1; // EOF + } + else if (mBytesLeft < pLength) { + + // Read all available + read = in.read(pBytes, pOffset, mBytesLeft); + + //System.err.println("Reading partial: " + read); + mBytesLeft -= read; + return read; + } + + // Just read + read = in.read(pBytes, pOffset, pLength); + + //System.err.println("Reading all avail: " + read); + mBytesLeft -= read; + return read; + } + } + + /** + * A FilterInputStream that wraps HTTP 1.1 "chunked" transfer mode. + */ + protected static class ChunkedInputStream extends FilterInputStream { + + private int mAvailableInCurrentChunk = 0; + + /** + * Creates an input streams that removes the "chunk-headers" and + * makes it look like any other input stream. + */ + protected ChunkedInputStream(InputStream pIS) { + + super(pIS); + if (pIS == null) { + throw new IllegalArgumentException("InputStream may not be null!"); + } + } + + /** + * Returns the number of bytes that can be read from this input stream + * without blocking. + *

+ * This version returns whatever is less of in.available() and the + * length of the current chunk. + * + * @return the number of bytes that can be read from the input stream + * without blocking. + * @throws IOException if an I/O error occurs. + * @see #in + */ + public int available() throws IOException { + + if (mAvailableInCurrentChunk == 0) { + mAvailableInCurrentChunk = parseChunkSize(); + } + int realAvail = in.available(); + + return (mAvailableInCurrentChunk < realAvail) + ? mAvailableInCurrentChunk + : realAvail; + } + + /** + * Reads up to len bytes of data from this input stream into an array + * of bytes. This method blocks until some input is available. + *

+ * This version will read up to len bytes of data, or as much as is + * available in the current chunk. If there is no more data in the + * curernt chunk, the method will read the size of the next chunk, and + * read from that, until the last chunk is read (a chunk with a size of + * 0). + * + * @param pBytes the buffer into which the data is read. + * @param pOffset the start offset of the data. + * @param pLength the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or -1 if + * there is no more data because the end of the stream has been + * reached. + * @throws IOException if an I/O error occurs. + * @see #in + */ + public int read(byte[] pBytes, int pOffset, int pLength) throws IOException { + + //System.err.println("Avail: " + mAvailableInCurrentChunk + // + " length: " + pLength); + int read; + + if (mAvailableInCurrentChunk == -1) { + return -1; // EOF + } + if (mAvailableInCurrentChunk == 0) { + + //System.err.println("Nothing to read, parsing size!"); + // If nothing is read so far, read chunk header + mAvailableInCurrentChunk = parseChunkSize(); + return read(pBytes, pOffset, pLength); + } + else if (mAvailableInCurrentChunk < pLength) { + + // Read all available + read = in.read(pBytes, pOffset, mAvailableInCurrentChunk); + + //System.err.println("Reading partial: " + read); + mAvailableInCurrentChunk -= read; + return read; + } + + // Just read + read = in.read(pBytes, pOffset, pLength); + + //System.err.println("Reading all avail: " + read); + mAvailableInCurrentChunk -= read; + return read; + } + + /** + * Reads the next byte of data from this input stream. The value byte + * is returned as an int in the range 0 to 255. If no byte is available + * because the end of the stream has been reached, the value -1 is + * returned. This method blocks until input data is available, the end + * of the stream is detected, or an exception is thrown. + *

+ * This version reads one byte of data from the current chunk as long + * as there is more data in the chunk. If there is no more data in the + * curernt chunk, the method will read the size of the next chunk, and + * read from that, until the last chunk is read (a chunk with a size of + * 0). + * + * @return the next byte of data, or -1 if the end of the stream is + * reached. + * @see #in + */ + public int read() throws IOException { + + // We have no data, parse chunk header + if (mAvailableInCurrentChunk == -1) { + return -1; + } + else if (mAvailableInCurrentChunk == 0) { + + // Next chunk! + mAvailableInCurrentChunk = parseChunkSize(); + return read(); + } + mAvailableInCurrentChunk--; + return in.read(); + } + + /** + * Reads the chunk size from the chunk header + * chunk-size [SP chunk-extension] CRLF. + * The chunk-extension is simply discarded. + * + * @return the length of the current chunk, or -1 if the current chunk + * is the last-chunk (a chunk with the size of 0). + */ + protected int parseChunkSize() throws IOException { + + StringBuilder buf = new StringBuilder(); + int b; + + // read chunk-size, chunk-extension (if any) and CRLF + while ((b = in.read()) > 0) { + if ((b == '\r') && (in.read() == '\n')) { // Should be no CR or LF + break; // except for this one... + } + buf.append((char) b); + } + String line = buf.toString(); + + // Happens, as we don't read CRLF off the end of the chunk data... + if (line.length() == 0) { + return 0; + } + + // Discard any chunk-extensions, and read size (HEX). + int spIdx = line.indexOf(' '); + int size = Integer.parseInt(((spIdx >= 0) + ? line.substring(0, spIdx) + : line), 16); + + // This is the last chunk (=EOF) + if (size == 0) { + return -1; + } + return size; + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/MIMEUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/MIMEUtil.java new file mode 100755 index 00000000..a576a5e4 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/MIMEUtil.java @@ -0,0 +1,312 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/NetUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/NetUtil.java new file mode 100755 index 00000000..d2637fcb --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/NetUtil.java @@ -0,0 +1,1423 @@ +package com.twelvemonkeys.net; + +import com.twelvemonkeys.io.FileUtil; +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.lang.DateUtil; +import com.twelvemonkeys.util.BASE64; +import com.twelvemonkeys.util.CollectionUtil; + +import java.io.*; +import java.net.*; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Utility class with network related methods. + * + * @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/net/NetUtil.java#2 $ + */ +public final class NetUtil { + + private final static String VERSION_ID = "NetUtil/2.1"; + + private static Authenticator sAuthenticator = null; + + private final static int BUF_SIZE = 8192; + private final static String HTTP = "http://"; + private final static String HTTPS = "https://"; + + /** + * Field HTTP_PROTOCOL + */ + public final static String HTTP_PROTOCOL = "http"; + + /** + * Field HTTPS_PROTOCOL + */ + public final static String HTTPS_PROTOCOL = "https"; + + /** + * Field HTTP_GET + */ + public final static String HTTP_GET = "GET"; + + /** + * Field HTTP_POST + */ + public final static String HTTP_POST = "POST"; + + /** + * Field HTTP_HEAD + */ + public final static String HTTP_HEAD = "HEAD"; + + /** + * Field HTTP_OPTIONS + */ + public final static String HTTP_OPTIONS = "OPTIONS"; + + /** + * Field HTTP_PUT + */ + public final static String HTTP_PUT = "PUT"; + + /** + * Field HTTP_DELETE + */ + public final static String HTTP_DELETE = "DELETE"; + + /** + * Field HTTP_TRACE + */ + public final static String HTTP_TRACE = "TRACE"; + + /** + * RFC 1123 date format, as reccomended by RFC 2616 (HTTP/1.1), sec 3.3 + * NOTE: All date formats are private, to ensure synchronized access. + */ + private static final SimpleDateFormat HTTP_RFC1123_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US); + static { + HTTP_RFC1123_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + } + + /** + * RFC 850 date format, (allmost) as described in RFC 2616 (HTTP/1.1), sec 3.3 + * USE FOR PARSING ONLY (format is not 100% correct, to be more robust). + */ + private static final SimpleDateFormat HTTP_RFC850_FORMAT = new SimpleDateFormat("EEE, dd-MMM-yy HH:mm:ss z", Locale.US); + /** + * ANSI C asctime() date format, (allmost) as described in RFC 2616 (HTTP/1.1), sec 3.3. + * USE FOR PARSING ONLY (format is not 100% correct, to be more robust). + */ + private static final SimpleDateFormat HTTP_ASCTIME_FORMAT = new SimpleDateFormat("EEE MMM d HH:mm:ss yy", Locale.US); + + private static long sNext50YearWindowChange = DateUtil.currentTimeDay(); + static { + HTTP_RFC850_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + HTTP_ASCTIME_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT")); + + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.3: + // - HTTP/1.1 clients and caches SHOULD assume that an RFC-850 date + // which appears to be more than 50 years in the future is in fact + // in the past (this helps solve the "year 2000" problem). + update50YearWindowIfNeeded(); + } + + private static void update50YearWindowIfNeeded() { + // Avoid class synchronization + long next = sNext50YearWindowChange; + + if (next < System.currentTimeMillis()) { + // Next check in one day + next += DateUtil.DAY; + sNext50YearWindowChange = next; + + Date startDate = new Date(next - (50l * DateUtil.CALENDAR_YEAR)); + //System.out.println("next test: " + new Date(next) + ", 50 year start: " + startDate); + synchronized (HTTP_RFC850_FORMAT) { + HTTP_RFC850_FORMAT.set2DigitYearStart(startDate); + } + synchronized (HTTP_ASCTIME_FORMAT) { + HTTP_ASCTIME_FORMAT.set2DigitYearStart(startDate); + } + } + } + + /** + * Creates a NetUtil. + * This class has only static methods and members, and should not be + * instantiated. + */ + private NetUtil() { + } + + public static void main1(String[] args) { + String timeStr = (args.length > 0 && !StringUtil.isNumber(args[0])) ? args[0] : null; + + long time = args.length > 0 ? + (timeStr != null ? parseHTTPDate(timeStr) : Long.parseLong(args[0])) + : System.currentTimeMillis(); + System.out.println(timeStr + " --> " + time + " --> " + formatHTTPDate(time)); + } + + /** + * Main method, reads data from a URL and, optionally, writes it to stdout or a file. + * @param pArgs command line arguemnts + * @throws java.io.IOException if an I/O exception occurs + */ + public static void main(String[] pArgs) throws IOException { + // params: + int timeout = 0; + boolean followRedirects = true; + boolean debugHeaders = false; + String requestPropertiesFile = null; + String requestHeaders = null; + String postData = null; + File putData = null; + int argIdx = 0; + boolean errArgs = false; + boolean writeToFile = false; + boolean writeToStdOut = false; + String outFileName = null; + + while ((argIdx < pArgs.length) && (pArgs[argIdx].charAt(0) == '-') && (pArgs[argIdx].length() >= 2)) { + if ((pArgs[argIdx].charAt(1) == 't') || pArgs[argIdx].equals("--timeout")) { + argIdx++; + try { + timeout = Integer.parseInt(pArgs[argIdx++]); + } + catch (NumberFormatException nfe) { + errArgs = true; + break; + } + } + else if ((pArgs[argIdx].charAt(1) == 'd') || pArgs[argIdx].equals("--debugheaders")) { + debugHeaders = true; + argIdx++; + } + else if ((pArgs[argIdx].charAt(1) == 'n') || pArgs[argIdx].equals("--nofollowredirects")) { + followRedirects = false; + argIdx++; + } + else if ((pArgs[argIdx].charAt(1) == 'r') || pArgs[argIdx].equals("--requestproperties")) { + argIdx++; + requestPropertiesFile = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'p') || pArgs[argIdx].equals("--postdata")) { + argIdx++; + postData = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'u') || pArgs[argIdx].equals("--putdata")) { + argIdx++; + putData = new File(pArgs[argIdx++]); + if (!putData.exists()) { + errArgs = true; + break; + } + } + else if ((pArgs[argIdx].charAt(1) == 'h') || pArgs[argIdx].equals("--header")) { + argIdx++; + requestHeaders = pArgs[argIdx++]; + } + else if ((pArgs[argIdx].charAt(1) == 'f') || pArgs[argIdx].equals("--file")) { + argIdx++; + writeToFile = true; + + // Get optional file name + if (!((argIdx >= (pArgs.length - 1)) || (pArgs[argIdx].charAt(0) == '-'))) { + outFileName = pArgs[argIdx++]; + } + } + else if ((pArgs[argIdx].charAt(1) == 'o') || pArgs[argIdx].equals("--output")) { + argIdx++; + writeToStdOut = true; + } + else { + System.err.println("Unknown option \"" + pArgs[argIdx++] + "\""); + } + } + if (errArgs || (pArgs.length < (argIdx + 1))) { + System.err.println("Usage: java NetUtil [-f|--file []] [-d|--debugheaders] [-h|--header

] [-p|--postdata ] [-u|--putdata ] [-r|--requestProperties ] [-t|--timeout ] [-n|--nofollowredirects] fromUrl"); + System.exit(5); + } + String url = pArgs[argIdx/*++*/]; + + // DONE ARGS + // Get request properties + Properties requestProperties = new Properties(); + + if (requestPropertiesFile != null) { + + // Just read, no exception handling... + requestProperties.load(new FileInputStream(new File(requestPropertiesFile))); + } + if (requestHeaders != null) { + + // Get request headers + String[] headerPairs = StringUtil.toStringArray(requestHeaders, ","); + + for (String headerPair : headerPairs) { + String[] pair = StringUtil.toStringArray(headerPair, ":"); + String key = (pair.length > 0) + ? pair[0].trim() + : null; + String value = (pair.length > 1) + ? pair[1].trim() + : ""; + + if (key != null) { + requestProperties.setProperty(key, value); + } + } + } + java.net.HttpURLConnection conn; + + // Create connection + URL reqURL = getURLAndSetAuthorization(url, requestProperties); + + conn = createHttpURLConnection(reqURL, requestProperties, followRedirects, timeout); + + // POST + if (postData != null) { + // HTTP POST method + conn.setRequestMethod(HTTP_POST); + + // Set entity headers + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Content-Length", String.valueOf(postData.length())); + conn.setRequestProperty("Content-Encoding", "ISO-8859-1"); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + + System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); + OutputStreamWriter writer = new OutputStreamWriter(os, "ISO-8859-1"); + + // Write post data to the stream + writer.write(postData); + writer.write("\r\n"); + + //writer.flush(); + writer.close(); // Does this close the underlying stream? + } + // PUT + else if (putData != null) { + // HTTP PUT method + conn.setRequestMethod(HTTP_PUT); + + // Set entity headers + //conn.setRequestProperty("Content-Type", "???"); + // TODO: Set Content-Type to correct type? + // TODO: Set content-encoding? Or can binary data be sent directly? + conn.setRequestProperty("Content-Length", String.valueOf(putData.length())); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + + System.err.println("OutputStream: " + os.getClass().getName() + "@" + System.identityHashCode(os)); + + // Write put data to the stream + FileUtil.copy(new FileInputStream(putData), os); + + os.close(); + } + + // + InputStream is; + + if (conn.getResponseCode() == 200) { + + // Connect and get stream + is = conn.getInputStream(); + } + else { + is = conn.getErrorStream(); + } + + // + if (debugHeaders) { + System.err.println("Request (debug):"); + System.err.println(conn.getClass()); + System.err.println("Response (debug):"); + + // Headerfield 0 is response code + System.err.println(conn.getHeaderField(0)); + + // Loop from 1, as headerFieldKey(0) == null... + for (int i = 1; ; i++) { + String key = conn.getHeaderFieldKey(i); + + // Seems to be the way to loop through them all... + if (key == null) { + break; + } + System.err.println(key + ": " + conn.getHeaderField(key)); + } + } + + // Create output file if specified + OutputStream os; + + if (writeToFile) { + if (outFileName == null) { + outFileName = reqURL.getFile(); + if (StringUtil.isEmpty(outFileName)) { + outFileName = conn.getHeaderField("Location"); + if (StringUtil.isEmpty(outFileName)) { + outFileName = "index"; + + // Find a suitable extension + // TODO: Replace with MIME-type util with MIME/file ext mapping + String ext = conn.getContentType(); + + if (!StringUtil.isEmpty(ext)) { + int idx = ext.lastIndexOf('/'); + + if (idx >= 0) { + ext = ext.substring(idx + 1); + } + idx = ext.indexOf(';'); + if (idx >= 0) { + ext = ext.substring(0, idx); + } + outFileName += "." + ext; + } + } + } + int idx = outFileName.lastIndexOf('/'); + + if (idx >= 0) { + outFileName = outFileName.substring(idx + 1); + } + idx = outFileName.indexOf('?'); + if (idx >= 0) { + outFileName = outFileName.substring(0, idx); + } + } + File outFile = new File(outFileName); + + if (!outFile.createNewFile()) { + if (outFile.exists()) { + System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", file allready exists."); + } + else { + System.err.println("Cannot write to file " + outFile.getAbsolutePath() + ", check write permissions."); + } + System.exit(5); + } + os = new FileOutputStream(outFile); + } + else if (writeToStdOut) { + os = System.out; + } + else { + os = null; + } + + // Get data. + if ((writeToFile || writeToStdOut) && is != null) { + FileUtil.copy(is, os); + } + + /* + Hashtable postData = new Hashtable(); + postData.put("SearchText", "condition"); + + try { + InputStream in = getInputStreamHttpPost(pArgs[argIdx], postData, + props, true, 0); + out = new FileOutputStream(file); + FileUtil.copy(in, out); + } + catch (Exception e) { + System.err.println("Error: " + e); + e.printStackTrace(System.err); + continue; + } + */ + } + + /* + public static class Cookie { + String mName = null; + String mValue = null; + + public Cookie(String pName, String pValue) { + mName = pName; + mValue = pValue; + } + + public String toString() { + return mName + "=" + mValue; + } + */ + + /* + // Just a way to set cookies.. + if (pCookies != null) { + String cookieStr = ""; + for (int i = 0; i < pCookies.length; i++) + cookieStr += ((i == pCookies.length) ? pCookies[i].toString() + : pCookies[i].toString() + ";"); + + // System.out.println("Cookie: " + cookieStr); + + conn.setRequestProperty("Cookie", cookieStr); + } + */ + + /* + } + */ + + /** + * Test if the given URL is using HTTP protocol. + * + * @param pURL the url to condition + * @return true if the protocol is HTTP. + */ + public static boolean isHttpURL(String pURL) { + return ((pURL != null) && pURL.startsWith(HTTP)); + } + + /** + * Test if the given URL is using HTTP protocol. + * + * @param pURL the url to condition + * @return true if the protocol is HTTP. + */ + public static boolean isHttpURL(URL pURL) { + return ((pURL != null) && pURL.getProtocol().equals("http")); + } + + /** + * Gets the content from a given URL, and returns it as a byte array. + * Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * NOTE: If you supply a username and password for HTTP + * authentication, this method uses the java.net.Authenticator's static + * {@code setDefault()} method, that can only be set ONCE. This + * means that if the default Authenticator is allready set, this method + * will fail. + * It also means if any other piece of code tries to register a new default + * Authenticator within the current VM, it will fail. + * + * @param pURL A String containing the URL, on the form + * [http://][:@]servername[/file.ext] + * where everything in brackets are optional. + * @return a byte array with the URL contents. If an error occurs, the + * returned array may be zero-length, but not null. + * @throws MalformedURLException if the urlName parameter is not a valid + * URL. Note that the protocol cannot be anything but HTTP. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see java.net.Authenticator + * @see SimpleAuthenticator + */ + public static byte[] getBytesHttp(String pURL) throws IOException { + return getBytesHttp(pURL, 0); + } + + /** + * Gets the content from a given URL, and returns it as a byte array. + * + * @param pURL the URL to get. + * @return a byte array with the URL contents. If an error occurs, the + * returned array may be zero-length, but not null. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getBytesHttp(String) + */ + public static byte[] getBytesHttp(URL pURL) throws IOException { + return getBytesHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * NOTE: If you supply a username and password for HTTP + * authentication, this method uses the java.net.Authenticator's static + * {@code setDefault()} method, that can only be set ONCE. This + * means that if the default Authenticator is allready set, this method + * will fail. + * It also means if any other piece of code tries to register a new default + * Authenticator within the current VM, it will fail. + * + * @param pURL A String containing the URL, on the form + * [http://][:@]servername[/file.ext] + * where everything in brackets are optional. + * @return an input stream that reads from the connection created by the + * given URL. + * @throws MalformedURLException if the urlName parameter specifies an + * unknown protocol, or does not form a valid URL. + * Note that the protocol cannot be anything but HTTP. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see java.net.Authenticator + * @see SimpleAuthenticator + */ + public static InputStream getInputStreamHttp(String pURL) throws IOException { + return getInputStreamHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL. + * + * @param pURL the URL to get. + * @return an input stream that reads from the connection created by the + * given URL. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(String) + */ + public static InputStream getInputStreamHttp(URL pURL) throws IOException { + return getInputStreamHttp(pURL, 0); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(String pURL, int pTimeout) throws IOException { + return getInputStreamHttp(pURL, null, true, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(final String pURL, final Properties pProperties, final boolean pFollowRedirects, final int pTimeout) + throws IOException { + + // Make sure we have properties + Properties properties = pProperties != null ? pProperties : new Properties(); + + //URL url = getURLAndRegisterPassword(pURL); + URL url = getURLAndSetAuthorization(pURL, properties); + + //unregisterPassword(url); + return getInputStreamHttp(url, properties, pFollowRedirects, pTimeout); + } + + /** + * Registers the password from the URL string, and returns the URL object. + * + * @param pURL the string representation of the URL, possibly including authorization part + * @param pProperties the + * @return the URL created from {@code pURL}. + * @throws java.net.MalformedURLException if there's a syntax error in {@code pURL} + */ + private static URL getURLAndSetAuthorization(final String pURL, final Properties pProperties) throws MalformedURLException { + String url = pURL; + // Split user/password away from url + String userPass = null; + String protocolPrefix = HTTP; + int httpIdx = url.indexOf(HTTPS); + + if (httpIdx >= 0) { + protocolPrefix = HTTPS; + url = url.substring(httpIdx + HTTPS.length()); + } + else { + httpIdx = url.indexOf(HTTP); + if (httpIdx >= 0) { + url = url.substring(httpIdx + HTTP.length()); + } + } + + // Get authorization part + int atIdx = url.indexOf("@"); + + if (atIdx >= 0) { + userPass = url.substring(0, atIdx); + url = url.substring(atIdx + 1); + } + + // Set authorization if user/password is present + if (userPass != null) { + // System.out.println("Setting password ("+ userPass + ")!"); + pProperties.setProperty("Authorization", "Basic " + BASE64.encode(userPass.getBytes())); + } + + // Return URL + return new URL(protocolPrefix + url); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see com.twelvemonkeys.net.HttpURLConnection + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.net.HttpURLConnection + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(URL pURL, int pTimeout) throws IOException { + return getInputStreamHttp(pURL, null, true, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static InputStream getInputStreamHttp(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + // Open the connection, and get the stream + java.net.HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); + + // HTTP GET method + conn.setRequestMethod(HTTP_GET); + + // This is where the connect happens + InputStream is = conn.getInputStream(); + + // We only accept the 200 OK message + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); + } + return is; + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pPostData the post data. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + */ + public static InputStream getInputStreamHttpPost(String pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + pProperties = pProperties != null ? pProperties : new Properties(); + + //URL url = getURLAndRegisterPassword(pURL); + URL url = getURLAndSetAuthorization(pURL, pProperties); + + //unregisterPassword(url); + return getInputStreamHttpPost(url, pPostData, pProperties, pFollowRedirects, pTimeout); + } + + /** + * Gets the InputStream from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. This + * might happen BEFORE OR AFTER this method returns, as the HTTP headers + * will be read and parsed from the InputStream before this method returns, + * while further read operations on the returned InputStream might be + * performed at a later stage. + *
+ *
+ * + * @param pURL the URL to get. + * @param pPostData the post data. + * @param pProperties the request header properties. + * @param pFollowRedirects specifying wether redirects should be followed. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + */ + public static InputStream getInputStreamHttpPost(URL pURL, Map pPostData, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + // Open the connection, and get the stream + java.net.HttpURLConnection conn = createHttpURLConnection(pURL, pProperties, pFollowRedirects, pTimeout); + + // HTTP POST method + conn.setRequestMethod(HTTP_POST); + + // Iterate over and create post data string + StringBuilder postStr = new StringBuilder(); + + if (pPostData != null) { + Iterator data = pPostData.entrySet().iterator(); + + while (data.hasNext()) { + Map.Entry entry = (Map.Entry) data.next(); + + // Properties key/values can be safely cast to strings + // Encode the string + postStr.append(URLEncoder.encode((String) entry.getKey(), "UTF-8")); + postStr.append('='); + postStr.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); + + if (data.hasNext()) { + postStr.append('&'); + } + } + } + + // Set entity headers + String encoding = conn.getRequestProperty("Content-Encoding"); + if (StringUtil.isEmpty(encoding)) { + encoding = "UTF-8"; + } + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + conn.setRequestProperty("Content-Length", String.valueOf(postStr.length())); + conn.setRequestProperty("Content-Encoding", encoding); + + // Get outputstream (this is where the connect actually happens) + OutputStream os = conn.getOutputStream(); + OutputStreamWriter writer = new OutputStreamWriter(os, encoding); + + // Write post data to the stream + writer.write(postStr.toString()); + writer.write("\r\n"); + writer.close(); // Does this close the underlying stream? + + // Get the inputstream + InputStream is = conn.getInputStream(); + + // We only accept the 200 OK message + // TODO: Accept all 200 messages, like ACCEPTED, CREATED or NO_CONTENT? + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + throw new IOException("The request gave the response: " + conn.getResponseCode() + ": " + conn.getResponseMessage()); + } + return is; + } + + /** + * Creates a HTTP connection to the given URL. + * + * @param pURL the URL to get. + * @param pProperties connection properties. + * @param pFollowRedirects specifies whether we should follow redirects. + * @param pTimeout the specified timeout, in milliseconds. + * @return a HttpURLConnection + * @throws UnknownHostException if the hostname in the URL cannot be found. + * @throws IOException if an I/O exception occurs. + */ + public static java.net.HttpURLConnection createHttpURLConnection(URL pURL, Properties pProperties, boolean pFollowRedirects, int pTimeout) + throws IOException { + + // Open the connection, and get the stream + java.net.HttpURLConnection conn; + + if (pTimeout > 0) { + // Supports timeout + conn = new com.twelvemonkeys.net.HttpURLConnection(pURL, pTimeout); + } + else { + // Faster, more compatible + conn = (java.net.HttpURLConnection) pURL.openConnection(); + } + + // Set user agent + if ((pProperties == null) || !pProperties.containsKey("User-Agent")) { + conn.setRequestProperty("User-Agent", + VERSION_ID + + " (" + System.getProperty("os.name") + "/" + System.getProperty("os.version") + "; " + + System.getProperty("os.arch") + "; " + + System.getProperty("java.vm.name") + "/" + System.getProperty("java.vm.version") + ")"); + } + + // Set request properties + if (pProperties != null) { + for (Map.Entry entry : pProperties.entrySet()) { + // Properties key/values can be safely cast to strings + conn.setRequestProperty((String) entry.getKey(), entry.getValue().toString()); + } + } + + try { + // Breaks with JRE1.2? + conn.setInstanceFollowRedirects(pFollowRedirects); + } + catch (LinkageError le) { + // This is the best we can do... + java.net.HttpURLConnection.setFollowRedirects(pFollowRedirects); + System.err.println("You are using an old Java Spec, consider upgrading."); + System.err.println("java.net.HttpURLConnection.setInstanceFollowRedirects(" + pFollowRedirects + ") failed."); + + //le.printStackTrace(System.err); + } + + conn.setDoInput(true); + conn.setDoOutput(true); + + //conn.setUseCaches(true); + return conn; + } + + /** + * This is a hack to get around the protected constructors in + * HttpURLConnection, should maybe consider registering and do things + * properly... + */ + + /* + private static class TimedHttpURLConnection + extends com.twelvemonkeys.net.HttpURLConnection { + TimedHttpURLConnection(URL pURL, int pTimeout) { + super(pURL, pTimeout); + } + } + */ + + /** + * Gets the content from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. Supports basic HTTP + * authentication, using a URL string similar to most browsers. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return a byte array that is read from the socket connection, created + * from the given URL. + * @throws MalformedURLException if the url parameter specifies an + * unknown protocol, or does not form a valid URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getBytesHttp(URL,int) + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static byte[] getBytesHttp(String pURL, int pTimeout) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); + + // Get all the bytes in loop + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes.write(buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes.toByteArray(); + } + + /** + * Gets the content from a given URL, with the given timeout. + * The timeout must be > 0. A timeout of zero is interpreted as an + * infinite timeout. + *

+ * Implementation note: If the timeout parameter is greater than 0, + * this method uses my own implementation of + * java.net.HttpURLConnection, that uses plain sockets, to create an + * HTTP connection to the given URL. The {@code read} methods called + * on the returned InputStream, will block only for the specified timeout. + * If the timeout expires, a java.io.InterruptedIOException is raised. + *
+ *
+ * + * @param pURL the URL to get. + * @param pTimeout the specified timeout, in milliseconds. + * @return an input stream that reads from the socket connection, created + * from the given URL. + * @throws UnknownHostException if the IP address for the given URL cannot + * be resolved. + * @throws FileNotFoundException if there is no file at the given URL. + * @throws IOException if an error occurs during transfer. + * @see #getInputStreamHttp(URL,int) + * @see com.twelvemonkeys.net.HttpURLConnection + * @see java.net.Socket + * @see java.net.Socket#setSoTimeout(int) setSoTimeout + * @see java.net.HttpURLConnection + * @see java.io.InterruptedIOException + * @see RFC 2616 + */ + public static byte[] getBytesHttp(URL pURL, int pTimeout) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL, pTimeout), BUF_SIZE * 2); + + // Get all the bytes in loop + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes.write(buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes.toByteArray(); + } + + /** + * Unregisters the password asscociated with this URL + */ + + /* + private static void unregisterPassword(URL pURL) { + Authenticator auth = registerAuthenticator(); + if (auth != null && auth instanceof SimpleAuthenticator) + ((SimpleAuthenticator) auth) + .unregisterPasswordAuthentication(pURL); + } + */ + + /** + * Registers the password from the URL string, and returns the URL object. + */ + + /* + private static URL getURLAndRegisterPassword(String pURL) + throws MalformedURLException + { + // Split user/password away from url + String userPass = null; + String protocolPrefix = HTTP; + + int httpIdx = pURL.indexOf(HTTPS); + if (httpIdx >= 0) { + protocolPrefix = HTTPS; + pURL = pURL.substring(httpIdx + HTTPS.length()); + } + else { + httpIdx = pURL.indexOf(HTTP); + if (httpIdx >= 0) + pURL = pURL.substring(httpIdx + HTTP.length()); + } + + int atIdx = pURL.indexOf("@"); + if (atIdx >= 0) { + userPass = pURL.substring(0, atIdx); + pURL = pURL.substring(atIdx + 1); + } + + // Set URL + URL url = new URL(protocolPrefix + pURL); + + // Set Authenticator if user/password is present + if (userPass != null) { + // System.out.println("Setting password ("+ userPass + ")!"); + + int colIdx = userPass.indexOf(":"); + if (colIdx < 0) + throw new MalformedURLException("Error in username/password!"); + + String userName = userPass.substring(0, colIdx); + String passWord = userPass.substring(colIdx + 1); + + // Try to register the authenticator + // System.out.println("Trying to register authenticator!"); + Authenticator auth = registerAuthenticator(); + + // System.out.println("Got authenticator " + auth + "."); + + // Register our username/password with it + if (auth != null && auth instanceof SimpleAuthenticator) { + ((SimpleAuthenticator) auth) + .registerPasswordAuthentication(url, + new PasswordAuthentication(userName, + passWord.toCharArray())); + } + else { + // Not supported! + throw new RuntimeException("Could not register PasswordAuthentication"); + } + } + + return url; + } + */ + + /** + * Registers the Authenticator given in the system property + * {@code java.net.Authenticator}, or the default implementation + * ({@code com.twelvemonkeys.net.SimpleAuthenticator}). + *

+ * BUG: What if authenticator has allready been set outside this class? + * + * @return The Authenticator created and set as default, or null, if it + * was not set as the default. However, there is no (clean) way to + * be sure the authenticator was set (the SimpleAuthenticator uses + * a hack to get around this), so it might be possible that the + * returned authenticator was not set as default... + * @see Authenticator#setDefault(Authenticator) + * @see SimpleAuthenticator + */ + public synchronized static Authenticator registerAuthenticator() { + if (sAuthenticator != null) { + return sAuthenticator; + } + + // Get the system property + String authenticatorName = System.getProperty("java.net.Authenticator"); + + // Try to get the Authenticator from the system property + if (authenticatorName != null) { + try { + Class authenticatorClass = Class.forName(authenticatorName); + + sAuthenticator = (Authenticator) authenticatorClass.newInstance(); + } + catch (ClassNotFoundException cnfe) { + // We should maybe rethrow this? + } + catch (InstantiationException ie) { + // Ignore + } + catch (IllegalAccessException iae) { + // Ignore + } + } + + // Get the default authenticator + if (sAuthenticator == null) { + sAuthenticator = SimpleAuthenticator.getInstance(); + } + + // Register authenticator as default + Authenticator.setDefault(sAuthenticator); + return sAuthenticator; + } + + /** + * Creates the InetAddress object from the given URL. + * Equivalent to calling {@code InetAddress.getByName(URL.getHost())} + * except that it returns null, instead of throwing UnknownHostException. + * + * @param pURL the URL to look up. + * @return the createad InetAddress, or null if the host was unknown. + * @see java.net.InetAddress + * @see java.net.URL + */ + public static InetAddress createInetAddressFromURL(URL pURL) { + try { + return InetAddress.getByName(pURL.getHost()); + } + catch (UnknownHostException e) { + return null; + } + } + + /** + * Creates an URL from the given InetAddress object, using the given + * protocol. + * Equivalent to calling + * {@code new URL(protocol, InetAddress.getHostName(), "")} + * except that it returns null, instead of throwing MalformedURLException. + * + * @param pIP the IP address to look up + * @param pProtocol the protocol to use in the new URL + * @return the created URL or null, if the URL could not be created. + * @see java.net.URL + * @see java.net.InetAddress + */ + public static URL createURLFromInetAddress(InetAddress pIP, String pProtocol) { + try { + return new URL(pProtocol, pIP.getHostName(), ""); + } + catch (MalformedURLException e) { + return null; + } + } + + /** + * Creates an URL from the given InetAddress object, using HTTP protocol. + * Equivalent to calling + * {@code new URL("http", InetAddress.getHostName(), "")} + * except that it returns null, instead of throwing MalformedURLException. + * + * @param pIP the IP address to look up + * @return the created URL or null, if the URL could not be created. + * @see java.net.URL + * @see java.net.InetAddress + */ + public static URL createURLFromInetAddress(InetAddress pIP) { + return createURLFromInetAddress(pIP, HTTP); + } + + /* + * TODO: Benchmark! + */ + static byte[] getBytesHttpOld(String pURL) throws IOException { + // Get the input stream from the url + InputStream in = new BufferedInputStream(getInputStreamHttp(pURL), BUF_SIZE * 2); + + // Get all the bytes in loop + byte[] bytes = new byte[0]; + int count; + byte[] buffer = new byte[BUF_SIZE]; + + try { + while ((count = in.read(buffer)) != -1) { + + // NOTE: According to the J2SE API doc, read(byte[]) will read + // at least 1 byte, or return -1, if end-of-file is reached. + bytes = (byte[]) CollectionUtil.mergeArrays(bytes, 0, bytes.length, buffer, 0, count); + } + } + finally { + + // Close the buffer + in.close(); + } + return bytes; + } + + /** + * Formats the time to a HTTP date, using the RFC 1123 format, as described + * in RFC 2616 (HTTP/1.1), sec. 3.3. + * + * @param pTime the time + * @return a {@code String} representation of the time + */ + public static String formatHTTPDate(long pTime) { + return formatHTTPDate(new Date(pTime)); + } + + /** + * Formats the time to a HTTP date, using the RFC 1123 format, as described + * in RFC 2616 (HTTP/1.1), sec. 3.3. + * + * @param pTime the time + * @return a {@code String} representation of the time + */ + public static String formatHTTPDate(Date pTime) { + synchronized (HTTP_RFC1123_FORMAT) { + return HTTP_RFC1123_FORMAT.format(pTime); + } + } + + /** + * Parses a HTTP date string into a {@code long} representing milliseconds + * since January 1, 1970 GMT. + *

+ * Use this method with headers that contain dates, such as + * {@code If-Modified-Since} or {@code Last-Modified}. + *

+ * The date string may be in either RFC 1123, RFC 850 or ANSI C asctime() + * format, as described in + * RFC 2616 (HTTP/1.1), sec. 3.3 + * + * @param pDate the date to parse + * + * @return a {@code long} value representing the date, expressed as the + * number of milliseconds since January 1, 1970 GMT, + * @throws NumberFormatException if the date parameter is not parseable. + * @throws IllegalArgumentException if the date paramter is {@code null} + */ + public static long parseHTTPDate(String pDate) throws NumberFormatException { + return parseHTTPDateImpl(pDate).getTime(); + } + + /** + * ParseHTTPDate implementation + * + * @param pDate the date string to parse + * + * @return a {@code Date} + * @throws NumberFormatException if the date parameter is not parseable. + * @throws IllegalArgumentException if the date paramter is {@code null} + */ + private static Date parseHTTPDateImpl(final String pDate) throws NumberFormatException { + if (pDate == null) { + throw new IllegalArgumentException("date == null"); + } + + if (StringUtil.isEmpty(pDate)) { + throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); + } + + DateFormat format; + + if (pDate.indexOf('-') >= 0) { + format = HTTP_RFC850_FORMAT; + update50YearWindowIfNeeded(); + } + else if (pDate.indexOf(',') < 0) { + format = HTTP_ASCTIME_FORMAT; + update50YearWindowIfNeeded(); + } + else { + format = HTTP_RFC1123_FORMAT; + // NOTE: RFC1123 always uses 4-digit years + } + + Date date; + try { + //noinspection SynchronizationOnLocalVariableOrMethodParameter + synchronized (format) { + date = format.parse(pDate); + } + } + catch (ParseException e) { + NumberFormatException nfe = new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); + nfe.initCause(e); + throw nfe; + } + + if (date == null) { + throw new NumberFormatException("Invalid HTTP date: \"" + pDate + "\""); + } + + return date; + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java new file mode 100755 index 00000000..3bbebcbb --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/PasswordAuthenticator.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.net; + +import java.net.*; + +/** + * Interface fro PasswordAuthenticators used by SimpleAuthenticator. + * + * @see SimpleAuthenticator + * @see java.net.Authenticator + * + * @author Harald Kuhr (haraldk@iconmedialab.no) + * + * @version 1.0 + */ +public interface PasswordAuthenticator { + public PasswordAuthentication requestPasswordAuthentication(InetAddress addr, int port, String protocol, String prompt, String scheme); +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java new file mode 100755 index 00000000..2aeccc14 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/SimpleAuthenticator.java @@ -0,0 +1,326 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.net; + +import java.net.Authenticator; +import java.net.InetAddress; +import java.net.PasswordAuthentication; +import java.net.URL; +import java.util.Hashtable; + +/** + * A simple Authenticator implementation. + * Singleton class, obtain reference through the static + * getInstance method. + *

+ * After swearing, sweating, pulling my hair, banging my head repeatedly + * into the walls and reading the java.net.Authenticator API documentation + * once more, an idea came to my mind. This is the result. I hope you find it + * useful. -- Harald K. + * + * @see java.net.Authenticator + * + * @author Harald Kuhr (haraldk@iconmedialab.no) + * @version 1.0 + */ +public class SimpleAuthenticator extends Authenticator { + + /** The reference to the single instance of this class. */ + private static SimpleAuthenticator sInstance = null; + /** Keeps track of the state of this class. */ + private static boolean sInitialized = false; + + // These are used for the identification hack. + private final static String MAGIC = "magic"; + private final static int FOURTYTWO = 42; + + /** Basic authentication scheme. */ + public final static String BASIC = "Basic"; + + /** + * The hastable that keeps track of the PasswordAuthentications. + */ + + protected Hashtable mPasswordAuthentications = null; + + /** + * The hastable that keeps track of the Authenticators. + */ + + protected Hashtable mAuthenticators = null; + + /** + * Creates a SimpleAuthenticator. + */ + + private SimpleAuthenticator() { + mPasswordAuthentications = new Hashtable(); + mAuthenticators = new Hashtable(); + } + + /** + * Gets the SimpleAuthenticator instance and registers it through the + * Authenticator.setDefault(). If there is no current instance + * of the SimpleAuthenticator in the VM, one is created. This method will + * try to figure out if the setDefault() succeeded (a hack), and will + * return null if it was not able to register the instance as default. + * + * @return The single instance of this class, or null, if another + * Authenticator is allready registered as default. + */ + + public static synchronized SimpleAuthenticator getInstance() { + if (!sInitialized) { + // Create an instance + sInstance = new SimpleAuthenticator(); + + // Try to set default (this may quietly fail...) + Authenticator.setDefault(sInstance); + + // A hack to figure out if we really did set the authenticator + PasswordAuthentication pa = + Authenticator.requestPasswordAuthentication(null, FOURTYTWO, + null, null, MAGIC); + + // If this test returns false, we didn't succeed, so we set the + // instance back to null. + if (pa == null || !MAGIC.equals(pa.getUserName()) || + !("" + FOURTYTWO).equals(new String(pa.getPassword()))) + sInstance = null; + + // Done + sInitialized = true; + } + + return sInstance; + } + + /** + * Gets the PasswordAuthentication for the request. Called when password + * authorization is needed. + * + * @return The PasswordAuthentication collected from the user, or null if + * none is provided. + */ + + protected PasswordAuthentication getPasswordAuthentication() { + // Don't worry, this is just a hack to figure out if we were able + // to set this Authenticator through the setDefault method. + if (!sInitialized && MAGIC.equals(getRequestingScheme()) + && getRequestingPort() == FOURTYTWO) + return new PasswordAuthentication(MAGIC, ("" + FOURTYTWO) + .toCharArray()); + /* + System.err.println("getPasswordAuthentication"); + System.err.println(getRequestingSite()); + System.err.println(getRequestingPort()); + System.err.println(getRequestingProtocol()); + System.err.println(getRequestingPrompt()); + System.err.println(getRequestingScheme()); + */ + + // TODO: + // Look for a more specific PasswordAuthenticatior before using + // Default: + // + // if (...) + // return pa.requestPasswordAuthentication(getRequestingSite(), + // getRequestingPort(), + // getRequestingProtocol(), + // getRequestingPrompt(), + // getRequestingScheme()); + + return (PasswordAuthentication) + mPasswordAuthentications.get(new AuthKey(getRequestingSite(), + getRequestingPort(), + getRequestingProtocol(), + getRequestingPrompt(), + getRequestingScheme())); + } + + /** + * Registers a PasswordAuthentication with a given URL address. + * + */ + + public PasswordAuthentication registerPasswordAuthentication(URL pURL, + PasswordAuthentication pPA) { + return registerPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), + pURL.getPort(), + pURL.getProtocol(), + null, // Prompt/Realm + BASIC, + pPA); + } + + /** + * Registers a PasswordAuthentication with a given net address. + * + */ + + public PasswordAuthentication registerPasswordAuthentication( + InetAddress pAddress, int pPort, String pProtocol, + String pPrompt, String pScheme, PasswordAuthentication pPA) + { + /* + System.err.println("registerPasswordAuthentication"); + System.err.println(pAddress); + System.err.println(pPort); + System.err.println(pProtocol); + System.err.println(pPrompt); + System.err.println(pScheme); + */ + + return (PasswordAuthentication) + mPasswordAuthentications.put(new AuthKey(pAddress, pPort, + pProtocol, pPrompt, + pScheme), + pPA); + } + + /** + * Unregisters a PasswordAuthentication with a given URL address. + * + */ + + public PasswordAuthentication unregisterPasswordAuthentication(URL pURL) { + return unregisterPasswordAuthentication(NetUtil.createInetAddressFromURL(pURL), + pURL.getPort(), + pURL.getProtocol(), + null, + BASIC); + } + + /** + * Unregisters a PasswordAuthentication with a given net address. + * + */ + + public PasswordAuthentication unregisterPasswordAuthentication( + InetAddress pAddress, int pPort, String pProtocol, + String pPrompt, String pScheme) + { + return (PasswordAuthentication) + mPasswordAuthentications.remove(new AuthKey(pAddress, pPort, + pProtocol, pPrompt, + pScheme)); + } + + /** + * TODO: Registers a PasswordAuthenticator that can answer authentication + * requests. + * + * @see PasswordAuthenticator + */ + + public void registerPasswordAuthenticator(PasswordAuthenticator pPA, + AuthenticatorFilter pFilter) { + mAuthenticators.put(pPA, pFilter); + } + + /** + * TODO: Unregisters a PasswordAuthenticator that can answer authentication + * requests. + * + * @see PasswordAuthenticator + */ + + public void unregisterPasswordAuthenticator(PasswordAuthenticator pPA) { + mAuthenticators.remove(pPA); + } + +} + +/** + * Utility class, used for caching the PasswordAuthentication objects. + * Everything but address may be null + */ + +class AuthKey { + + InetAddress mAddress = null; + int mPort = -1; + String mProtocol = null; + String mPrompt = null; + String mScheme = null; + + AuthKey(InetAddress pAddress, int pPort, String pProtocol, + String pPrompt, String pScheme) { + if (pAddress == null) + throw new IllegalArgumentException("Address argument can't be null!"); + + mAddress = pAddress; + mPort = pPort; + mProtocol = pProtocol; + mPrompt = pPrompt; + mScheme = pScheme; + + // System.out.println("Created: " + this); + } + + /** + * Creates a string representation of this object. + */ + + public String toString() { + return "AuthKey[" + mAddress + ":" + mPort + "/" + mProtocol + " \"" + mPrompt + "\" (" + mScheme + ")]"; + } + + public boolean equals(Object pObj) { + return (pObj instanceof AuthKey ? equals((AuthKey) pObj) : false); + } + + // Ahem.. Breaks the rule from Object.equals(Object): + // It is transitive: for any reference values x, y, and z, if x.equals(y) + // returns true and y.equals(z) returns true, then x.equals(z) + // should return true. + + public boolean equals(AuthKey pKey) { + // Maybe allow nulls, and still be equal? + return (mAddress.equals(pKey.mAddress) + && (mPort == -1 + || pKey.mPort == -1 + || mPort == pKey.mPort) + && (mProtocol == null + || pKey.mProtocol == null + || mProtocol.equals(pKey.mProtocol)) + && (mPrompt == null + || pKey.mPrompt == null + || mPrompt.equals(pKey.mPrompt)) + && (mScheme == null + || pKey.mScheme == null + || mScheme.equalsIgnoreCase(pKey.mScheme))); + } + + public int hashCode() { + // There won't be too many pr address, will it? ;-) + return mAddress.hashCode(); + } +} + diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/package.html b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/package.html new file mode 100755 index 00000000..b420b977 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/net/package.html @@ -0,0 +1,7 @@ + + + +Provides classes for net access. + + + diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java new file mode 100755 index 00000000..32dd2822 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractDecoratedMap.java @@ -0,0 +1,400 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.*; +import java.io.Serializable; + +/** + * 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> mEntries; + protected transient volatile int mModCount; + + private transient volatile Set> mEntrySet = null; + private transient volatile Set mKeySet = null; + private transient volatile Collection mValues = 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"); + } + + mEntries = 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 mEntries.size(); + } + + public void clear() { + mEntries.clear(); + mModCount++; + init(); + } + + public boolean isEmpty() { + return mEntries.isEmpty(); + } + + public boolean containsKey(Object pKey) { + return mEntries.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 = mValues; + return values != null ? values : (mValues = new Values()); + } + + public Set> entrySet() { + Set> es = mEntrySet; + return es != null ? es : (mEntrySet = new EntrySet()); + } + + public Set keySet() { + Set ks = mKeySet; + return ks != null ? ks : (mKeySet = 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.mValues = null; + map.mEntrySet = null; + map.mKeySet = 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 mEntries.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 = mEntries.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) mEntries.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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractResource.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractResource.java new file mode 100755 index 00000000..6c7ba54a --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractResource.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +/** + * AbstractResource class description. + * + * @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/AbstractResource.java#1 $ + */ +abstract class AbstractResource implements Resource { + protected final Object mResourceId; + protected final Object mWrappedResource; + + /** + * Creates a {@code Resource}. + * + * @param pResourceId + * @param pWrappedResource + */ + protected AbstractResource(Object pResourceId, Object pWrappedResource) { + if (pResourceId == null) { + throw new IllegalArgumentException("id == null"); + } + if (pWrappedResource == null) { + throw new IllegalArgumentException("resource == null"); + } + + mResourceId = pResourceId; + mWrappedResource = pWrappedResource; + } + + public final Object getId() { + return mResourceId; + } + + /** + * Default implementation simply returns {@code asURL().toExternalForm()}. + * + * @return a string representation of this resource + */ + public String toString() { + return asURL().toExternalForm(); + } + + /** + * Defautl implementation returns {@code mWrapped.hashCode()}. + * + * @return {@code mWrapped.hashCode()} + */ + public int hashCode() { + return mWrappedResource.hashCode(); + } + + /** + * Default implementation + * + * @param pObject + * @return + */ + public boolean equals(Object pObject) { + return pObject instanceof AbstractResource + && mWrappedResource.equals(((AbstractResource) pObject).mWrappedResource); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java new file mode 100755 index 00000000..edfa51bf --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/AbstractTokenIterator.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +/** + * 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, mNext.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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java new file mode 100755 index 00000000..9b056e10 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import com.twelvemonkeys.io.*; +import com.twelvemonkeys.io.enc.Base64Decoder; +import com.twelvemonkeys.io.enc.DecoderStream; + +import java.io.*; + + +/** + * This class does BASE64 encoding (and decoding). + * + * @author unascribed + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BASE64.java#1 $ + */ +public class BASE64 { + + /** + * This array maps the characters to their 6 bit values + */ + private final static char[] 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 + }; + + /** + * Encodes the input data using the standard base64 encoding scheme. + * + * @param pData the bytes to encode to base64 + * @return a string with base64 encoded data + */ + public static String encode(byte[] pData) { + int offset = 0; + int len; + StringBuilder buf = new StringBuilder(); + + while ((pData.length - offset) > 0) { + byte a, b, c; + if ((pData.length - offset) > 2) { + len = 3; + } + else { + len = pData.length - offset; + } + + switch (len) { + case 1: + a = pData[offset]; + b = 0; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append('='); + buf.append('='); + offset++; + break; + case 2: + a = pData[offset]; + b = pData[offset + 1]; + c = 0; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + buf.append('='); + offset += offset + 2; // ??? + break; + default: + a = pData[offset]; + b = pData[offset + 1]; + c = pData[offset + 2]; + buf.append(PEM_ARRAY[(a >>> 2) & 0x3F]); + buf.append(PEM_ARRAY[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + buf.append(PEM_ARRAY[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + buf.append(PEM_ARRAY[c & 0x3F]); + offset = offset + 3; + break; + } + + } + return buf.toString(); + } + + /** + * Quick implementation, using the undocumented + * {@code sun.misc.BASE64Decoder.decodeBuffer(String)}. + */ + public static byte[] decode(String pData) throws java.io.IOException { + //return DECODER.decodeBuffer(pData); + + InputStream in = new DecoderStream(new ByteArrayInputStream(pData.getBytes()), new Base64Decoder()); + ByteArrayOutputStream bytes = new FastByteArrayOutputStream(pData.length() * 3); + FileUtil.copy(in, bytes); + + return bytes.toByteArray(); + } + + //private final static sun.misc.BASE64Decoder DECODER = new sun.misc.BASE64Decoder(); + + public static void main(String[] pArgs) throws java.io.IOException { + if (pArgs.length == 1) { + System.out.println(encode(pArgs[0].getBytes())); + } + else + if (pArgs.length == 2 && ("-d".equals(pArgs[0]) || "--decode".equals(pArgs[0]))) + { + System.out.println(new String(decode(pArgs[1]))); + } + else { + System.err.println("BASE64 [ -d | --decode ] arg"); + System.err.println("Encodes or decodes a given string"); + System.exit(5); + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BeanMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BeanMap.java new file mode 100755 index 00000000..75d40df2 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BeanMap.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.beans.IndexedPropertyDescriptor; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.io.Serializable; + +/** + * A {@code Map} adapter for a Java Bean. + *

+ * Ruhtlessly 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 mDescriptors.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 + mDescriptors = initDescriptors(mBean); + return this; + } + + private class BeanSet extends AbstractSet> { + public Iterator> iterator() { + return new BeanIterator(mDescriptors.iterator()); + } + + public int size() { + return mDescriptors.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(mBean); + } + }); + } + + 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(mBean, 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BooleanKey.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BooleanKey.java new file mode 100755 index 00000000..b2eb177a --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/BooleanKey.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.io.Serializable; + +/** + * BooleanKey class description. + * + * @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/BooleanKey.java#1 $ + */ +public class BooleanKey extends TypedMap.AbstractKey implements Serializable { + public BooleanKey() { + super(); + } + + public BooleanKey(String pName) { + super(pName); + } + + public boolean isCompatibleValue(Object pValue) { + return pValue instanceof Boolean; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/CollectionUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/CollectionUtil.java new file mode 100755 index 00000000..0b15be9f --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/CollectionUtil.java @@ -0,0 +1,642 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.lang.reflect.Array; +import java.util.*; + +/** + * A utility class with some useful collection-related functions. + * + * @author Harald Kuhr + * @author Eirik Torske + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/CollectionUtil.java#3 $ + * @todo move makeList and makeSet to StringUtil? + * @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) { + test(); + + 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 origianl 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 {@code pLength} parameter is negative, it will be ignored. + * If there are not {@code pLength} elements in the original array + * after {@code pStart}, the {@code pLength} paramter will be + * ignored. + * If the sub array is same length as the original, the original array will + * be returned. + * + * @param pArray the origianl 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) { + if (pArray == null) { + throw new IllegalArgumentException("array == null"); + } + + // 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 subarray & 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; + } + + public static Iterator iterator(final Enumeration pEnum) { + 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 null and this + * collection does not support 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 usecase 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, 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 duplaicates. + * + * @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 dupliate 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(Comparator pOriginal) { + return new ReverseComparator(pOriginal); + } + + private static class ReverseComparator implements Comparator { + private Comparator mComparator; + + public ReverseComparator(Comparator pComparator) { + mComparator = pComparator; + } + + + public int compare(T pLeft, T pRight) { + int result = mComparator.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; + } + + @SuppressWarnings({"UnusedDeclaration"}) + static void test() { + List list = Collections.singletonList("foo"); + @SuppressWarnings({"unchecked"}) + Set set = new HashSet(list); + + List strs0 = CollectionUtil.generify(list, String.class); + List objs0 = CollectionUtil.generify(list, String.class); +// List strs01 = CollectionUtil.generify(list, Object.class); // Not okay + try { + List strs1 = CollectionUtil.generify(set, String.class); // Not ok, runtime CCE unless set is null + } + catch (RuntimeException e) { + e.printStackTrace(); + } + + try { + ArrayList strs01 = CollectionUtil.generify(list, String.class); // Not ok, runtime CCE unless list is null + } + catch (RuntimeException e) { + e.printStackTrace(); + } + + Set setstr1 = CollectionUtil.generify(set, String.class); + Set setobj1 = CollectionUtil.generify(set, String.class); + try { + Set setobj44 = CollectionUtil.generify(list, String.class); // Not ok, runtime CCE unless list is null + } + catch (RuntimeException e) { + e.printStackTrace(); + } + + List strs2 = CollectionUtil., String>generify2(list); + List objs2 = CollectionUtil., String>generify2(list); +// List morestrs = CollectionUtil., String>generify2(list); // Not ok + try { + List strs3 = CollectionUtil., String>generify2(set); // Not ok, runtime CCE unless set is null + } + catch (RuntimeException e) { + e.printStackTrace(); + } + } + + private static class ArrayIterator implements ListIterator { + private int mIndex; + private final int mStart; + private final int mLength; + private final E[] mArray; + + public ArrayIterator(E[] pArray, int pStart, int pLength) { + if (pArray == null) { + throw new IllegalArgumentException("array == null"); + } + if (pStart < 0) { + throw new IllegalArgumentException("start < 0"); + } + if (pLength > pArray.length - pStart) { + throw new IllegalArgumentException("length > array.length - start"); + } + mArray = pArray; + mStart = pStart; + mLength = pLength; + mIndex = mStart; + } + + public boolean hasNext() { + return mIndex < mLength + mStart; + } + + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + try { + return mArray[mIndex++]; + } + 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 mIndex > mStart; + } + + public int nextIndex() { + return mIndex + 1; + } + + public E previous() { + if (!hasPrevious()) { + throw new NoSuchElementException(); + } + + try { + return mArray[mIndex--]; + } + catch (ArrayIndexOutOfBoundsException e) { + NoSuchElementException nse = new NoSuchElementException(e.getMessage()); + nse.initCause(e); + throw nse; + } + } + + public int previousIndex() { + return mIndex - 1; + } + + public void set(E pElement) { + mArray[mIndex] = pElement; + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/DebugUtil.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/DebugUtil.java new file mode 100755 index 00000000..3d45d4e9 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/DebugUtil.java @@ -0,0 +1,1757 @@ +/**************************************************** + * * + * (c) 2000-2003 TwelveMonkeys * + * All rights reserved * + * http://www.twelvemonkeys.no * + * * + * $RCSfile: DebugUtil.java,v $ + * @version $Revision: #2 $ + * $Date: 2009/06/19 $ + * * + * @author Last modified by: $Author: haku $ + * * + ****************************************************/ + + + +/* + * Produced (p) 2002 TwelveMonkeys + * Address : Svovelstikka 1, Box 6432 Etterstad, 0605 Oslo, Norway. + * Phone : +47 22 57 70 00 + * Fax : +47 22 57 70 70 + */ +package com.twelvemonkeys.util; + + +import com.twelvemonkeys.lang.StringUtil; + +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.text.DateFormat; +import java.util.*; + + +/** + * A utility class to simplify debugging. + * This includes viewing generic data structures, printing timestamps, printing object info and more... + * NB! Only use this class for instrumentation purposes + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * usage: + *

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

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

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

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

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

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

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

+ * usage: + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

+ * + * @param pMilliseconds + * @return prettyprinted time duration. + */ + public static String getTimeInterval(final long pMilliseconds) { + + long timeIntervalMilliseconds = pMilliseconds; + long timeIntervalSeconds = 0; + long timeIntervalMinutes = 0; + long timeIntervalHours = 0; + long timeIntervalDays = 0; + boolean printMilliseconds = true; + boolean printSeconds = false; + boolean printMinutes = false; + boolean printHours = false; + boolean printDays = false; + final long MILLISECONDS_IN_SECOND = 1000; + final long MILLISECONDS_IN_MINUTE = 60 * MILLISECONDS_IN_SECOND; // 60000 + final long MILLISECONDS_IN_HOUR = 60 * MILLISECONDS_IN_MINUTE; // 3600000 + final long MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR; // 86400000 + StringBuilder timeIntervalBuffer = new StringBuilder(); + + // Days + if (timeIntervalMilliseconds >= MILLISECONDS_IN_DAY) { + timeIntervalDays = timeIntervalMilliseconds / MILLISECONDS_IN_DAY; + timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_DAY; + printDays = true; + printHours = true; + printMinutes = true; + printSeconds = true; + } + + // Hours + if (timeIntervalMilliseconds >= MILLISECONDS_IN_HOUR) { + timeIntervalHours = timeIntervalMilliseconds / MILLISECONDS_IN_HOUR; + timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_HOUR; + printHours = true; + printMinutes = true; + printSeconds = true; + } + + // Minutes + if (timeIntervalMilliseconds >= MILLISECONDS_IN_MINUTE) { + timeIntervalMinutes = timeIntervalMilliseconds / MILLISECONDS_IN_MINUTE; + timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_MINUTE; + printMinutes = true; + printSeconds = true; + } + + // Seconds + if (timeIntervalMilliseconds >= MILLISECONDS_IN_SECOND) { + timeIntervalSeconds = timeIntervalMilliseconds / MILLISECONDS_IN_SECOND; + timeIntervalMilliseconds = timeIntervalMilliseconds % MILLISECONDS_IN_SECOND; + printSeconds = true; + } + + // Prettyprint + if (printDays) { + timeIntervalBuffer.append(timeIntervalDays); + if (timeIntervalDays > 1) { + timeIntervalBuffer.append("days "); + } else { + timeIntervalBuffer.append("day "); + } + } + if (printHours) { + timeIntervalBuffer.append(timeIntervalHours); + timeIntervalBuffer.append("h "); + } + if (printMinutes) { + timeIntervalBuffer.append(timeIntervalMinutes); + timeIntervalBuffer.append("m "); + } + if (printSeconds) { + timeIntervalBuffer.append(timeIntervalSeconds); + timeIntervalBuffer.append("s "); + } + if (printMilliseconds) { + timeIntervalBuffer.append(timeIntervalMilliseconds); + timeIntervalBuffer.append("ms"); + } + return timeIntervalBuffer.toString(); + } +} + + +/*--- Formatted in Sun Java Convention Style on ma, des 1, '03 ---*/ + + +/*------ Formatted by Jindent 3.23 Basic 1.0 --- http://www.jindent.de ------*/ diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/DuplicateHandler.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/DuplicateHandler.java new file mode 100755 index 00000000..626eefb4 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/DuplicateHandler.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +/** + * 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ExpiringMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ExpiringMap.java new file mode 100755 index 00000000..5e2f7490 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ExpiringMap.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.Map; + +/** + * Call-back inteface for {@code Map} implementations that have mappings that + * may expire. + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ExpiringMap.java#1 $ + */ +public interface ExpiringMap extends Map { + + /** + * May be used by clients as a call-back to notify when mappings expire from + * the map. + * + * @param pRemoved the removed mapping + */ + void processRemoved(Entry pRemoved); +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FileResource.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FileResource.java new file mode 100755 index 00000000..bc2c93c8 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FileResource.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.io.InputStream; +import java.io.IOException; +import java.io.File; +import java.io.FileInputStream; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * FileResource class description. + * + * @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/FileResource.java#1 $ + */ +final class FileResource extends AbstractResource { + + /** + * Creates a {@code FileResource}. + * + * @param pResourceId the resource id + * @param pFile the file resource + */ + public FileResource(Object pResourceId, File pFile) { + super(pResourceId, pFile); + } + + private File getFile() { + return (File) mWrappedResource; + } + + public URL asURL() { + try { + return getFile().toURL(); + } + catch (MalformedURLException e) { + throw new IllegalStateException("The file \"" + getFile().getAbsolutePath() + + "\" is not parseable as an URL: " + e.getMessage()); + } + } + + public InputStream asStream() throws IOException { + return new FileInputStream(getFile()); + } + + public long lastModified() { + return getFile().lastModified(); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FilterIterator.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FilterIterator.java new file mode 100755 index 00000000..b0420b80 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FilterIterator.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import 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 mFilter; + protected final Iterator mIterator; + + private E mNext = null; + private E mCurrent = 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(Iterator pIterator, Filter pFilter) { + if (pIterator == null) { + throw new IllegalArgumentException("iterator == null"); + } + if (pFilter == null) { + throw new IllegalArgumentException("filter == null"); + } + + mIterator = pIterator; + mFilter = 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 (mNext == null && mIterator.hasNext()) { + E element = mIterator.next(); + + if (mFilter.accept(element)) { + mNext = element; + break; + } + } + + return mNext != null; + } + + /** + * Returns the next element in the interation. + * + * @return the next element in the iteration. + * @see FilterIterator.Filter#accept + */ + public E next() { + if (hasNext()) { + mCurrent = mNext; + + // Make sure we advance next time + mNext = null; + return mCurrent; + } + 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 (mCurrent != null) { + mIterator.remove(); + } + else { + throw new IllegalStateException("Iteration has no current element."); + } + } + + /** + * Used to tests wether or not an element fullfils certain criteria, and + * hence should be accepted by the FilterIterator instance. + */ + public static interface Filter { + + /** + * Tests wether or not the element fullfils 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FloatKey.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FloatKey.java new file mode 100755 index 00000000..2ad7d928 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/FloatKey.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.io.Serializable; + +/** + * FloatKey class description. + * + * @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/FloatKey.java#1 $ + */ +public class FloatKey extends TypedMap.AbstractKey implements Serializable { + public FloatKey() { + super(); + } + + public FloatKey(String pName) { + super(pName); + } + + public boolean isCompatibleValue(Object pValue) { + return pValue instanceof Float; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/IgnoreCaseMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/IgnoreCaseMap.java new file mode 100755 index 00000000..df606ff4 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/IgnoreCaseMap.java @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.Iterator; +import java.util.Map; +import java.io.Serializable; + +/** + * 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(mEntries.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(mEntries.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(mEntries.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 mEntries.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) mEntries.entrySet().iterator(); + } + + protected Iterator newKeyIterator() { + return mEntries.keySet().iterator(); + } + + protected Iterator newValueIterator() { + return (Iterator) mEntries.values().iterator(); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/IntegerKey.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/IntegerKey.java new file mode 100755 index 00000000..f35658b5 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/IntegerKey.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.io.Serializable; + +/** + * IntegerKey class description. + * + * @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/IntegerKey.java#1 $ + */ +public class IntegerKey extends TypedMap.AbstractKey implements Serializable { + public IntegerKey(String pName) { + super(pName); + } + + public IntegerKey() { + } + + public boolean isCompatibleValue(Object pValue) { + return pValue instanceof Integer; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUHashMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUHashMap.java new file mode 100755 index 00000000..39779eb6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUHashMap.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.Map; +import java.util.LinkedHashMap; +import java.util.Iterator; + +/** + * Map implementation with size limit, that keeps its entries in LRU + * (least recently used) order, also known as access-order. + * When the size limit is reached, the least recently accessed mappings are + * removed. The number of mappings to be removed from the map, is + * controlled by the trim factor. + *

+ *

    + *
  • Default size limit is 1000 elements. + * See {@link #setMaxSize(int)}/{@link #getMaxSize()}.
  • + *
  • Default trim factor is 1% ({@code 0.01f}). + * See {@link #setTrimFactor(float)}/{@link #getTrimFactor()}.
  • + *
+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUHashMap.java#1 $ + */ +public class LRUHashMap extends LinkedHashMap implements ExpiringMap { + + private int mMaxSize = 1000; + private float mTrimFactor = 0.01f; + + /** + * Creates an LRUHashMap with default max size (1000 entries). + * + * This is constructor is here to comply with the reccomendations for + * "standard" constructors in the {@code Map} interface. + * + * @see #LRUHashMap(int) + */ + public LRUHashMap() { + super(16, .75f, true); + } + + /** + * Creates an LRUHashMap with the given max size. + * + * @param pMaxSize size limit + */ + public LRUHashMap(int pMaxSize) { + super(16, .75f, true); + setMaxSize(pMaxSize); + } + + /** + * Creates an LRUHashMap with initial mappings from the given map, + * and default max size (1000 entries). + * + * 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 #LRUHashMap(java.util.Map, int) + */ + public LRUHashMap(Map pContents) { + super(16, .75f, true); + putAll(pContents); + } + + /** + * Creates an LRUHashMap with initial mappings from the given map, + * and the given max size. + * + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null}. + * @param pMaxSize size limit + */ + public LRUHashMap(Map pContents, int pMaxSize) { + super(16, .75f, true); + setMaxSize(pMaxSize); + putAll(pContents); + } + + /** + * Returns the maximum number of mappings in this map. + * + * @return the size limit + */ + public int getMaxSize() { + return mMaxSize; + } + + /** + * Sets the maximum number of elements in this map. + * + * If the current size is greater than the new max size, the map will be + * trimmed to fit the new max size constraint. + * + * @see #removeLRU() + * + * @param pMaxSize new size limit + */ + public void setMaxSize(int pMaxSize) { + if (pMaxSize < 0) { + throw new IllegalArgumentException("max size must be positive"); + } + + mMaxSize = pMaxSize; + + while(size() > mMaxSize) { + removeLRU(); + } + } + + /** + * Returns the current trim factor. + *

+ * The trim factor controls how many percent of the maps current size is + * reclaimed, when performing an {@code removeLRU} operation. + * Defaults to 1% ({@code 0.01f}). + * + * @return the current trim factor + */ + public float getTrimFactor() { + return mTrimFactor; + } + + /** + * Sets the trim factor. + *

+ * The trim factor controls how many percent of the maps current size is + * reclaimed, when performing an {@code removeLRU} operation. + * Defaults to 1% ({@code 0.01f}). + * + * @param pTrimFactor the new trim factor. Acceptable values are between + * 0 (inclusive) and 1 (exclusive). + * + * @see #removeLRU() + */ + public void setTrimFactor(float pTrimFactor) { + if (pTrimFactor < 0f || pTrimFactor >= 1f) { + throw new IllegalArgumentException("trim factor must be between 0 and 1"); + } + + mTrimFactor = pTrimFactor; + } + + /** + * always returns {@code false}, and instead invokes {@code removeLRU()} + * if {@code size >= maxSize}. + */ + protected boolean removeEldestEntry(Map.Entry pEldest) { + // NOTE: As removeLRU() may remove more than one entry, this is better + // than simply removing the eldest entry. + if (size() >= mMaxSize) { + removeLRU(); + } + return false; + } + + /** + * Default implementation does nothing. + * May be used by clients as a call-back to notify when mappings expire from + * the map. + * + * @param pRemoved the removed mapping + */ + public void processRemoved(Map.Entry pRemoved) { + } + + /** + * Removes the least recently used mapping(s) from this map. + *

+ * How many mappings are removed from the map, is controlled by the + * trim factor. + * In any case, at least one mapping will be removed. + * + * @see #getTrimFactor() + */ + public void removeLRU() { + int removeCount = (int) Math.max((size() * mTrimFactor), 1); + Iterator> entries = entrySet().iterator(); + while ((removeCount--) > 0 && entries.hasNext()) { + entries.next(); + entries.remove(); + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUMap.java new file mode 100755 index 00000000..bad6b61e --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUMap.java @@ -0,0 +1,229 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.Map; + +/** + * Map implementation with size limit, that keeps its entries in LRU + * (least recently used) order, also known as access-order. + * When the size limit is reached, the least recently accessed mappings are + * removed. The number of mappings to be removed from the map, is + * controlled by the trim factor. + *

+ *

    + *
  • Default size limit is 1000 elements. + * See {@link #setMaxSize(int)}/{@link #getMaxSize()}.
  • + *
  • Default trim factor is 1% ({@code 0.01f}). + * See {@link #setTrimFactor(float)}/{@link #getTrimFactor()}.
  • + *
+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LRUMap.java#1 $ + */ +public class LRUMap extends LinkedMap implements ExpiringMap { + + private int mMaxSize = 1000; + private float mTrimFactor = 0.01f; + + /** + * Creates an LRUMap with default max size (1000 entries). + * + * This is constructor is here to comply with the reccomendations for + * "standard" constructors in the {@code Map} interface. + * + * @see #LRUMap(int) + */ + public LRUMap() { + super(null, true); + } + + /** + * Creates an LRUMap with the given max size. + * + * @param pMaxSize size limit + */ + public LRUMap(int pMaxSize) { + super(null, true); + setMaxSize(pMaxSize); + } + + /** + * Creates an LRUMap with initial mappings from the given map, + * and default max size (1000 entries). + * + * 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 #LRUMap(Map, int) + */ + public LRUMap(Map pContents) { + super(pContents, true); + } + + /** + * Creates an LRUMap with initial mappings from the given map, + * and the given max size. + * + * @param pContents the map whose mappings are to be placed in this map. + * May be {@code null}. + * @param pMaxSize size limit + */ + public LRUMap(Map pContents, int pMaxSize) { + super(pContents, true); + setMaxSize(pMaxSize); + } + + /** + * Creates an LRUMap with initial mappings from the given map, + * and the given max size. + * + * @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 pMaxSize max size + */ + public LRUMap(Map> pBacking, Map pContents, int pMaxSize) { + super(pBacking, pContents, true); + setMaxSize(pMaxSize); + } + + /** + * Returns the maximum number of mappings in this map. + * + * @return the size limit + */ + public int getMaxSize() { + return mMaxSize; + } + + /** + * Sets the maximum number of elements in this map. + * + * If the current size is greater than the new max size, the map will be + * trimmed to fit the new max size constraint. + * + * @see #removeLRU() + * + * @param pMaxSize new size limit + */ + public void setMaxSize(int pMaxSize) { + if (pMaxSize < 0) { + throw new IllegalArgumentException("max size must be positive"); + } + + mMaxSize = pMaxSize; + + while(size() > mMaxSize) { + removeLRU(); + } + } + + /** + * Returns the current trim factor. + *

+ * The trim factor controls how many percent of the maps current size is + * reclaimed, when performing an {@code removeLRU} operation. + * Defaults to 1% ({@code 0.01f}). + * + * @return the current trim factor + */ + public float getTrimFactor() { + return mTrimFactor; + } + + /** + * Sets the trim factor. + *

+ * The trim factor controls how many percent of the maps current size is + * reclaimed, when performing an {@code removeLRU} operation. + * Defaults to 1% ({@code 0.01f}). + * + * @param pTrimFactor the new trim factor. Acceptable values are between + * 0 (inclusive) and 1 (exclusive). + * + * @see #removeLRU() + */ + public void setTrimFactor(float pTrimFactor) { + if (pTrimFactor < 0f || pTrimFactor >= 1f) { + throw new IllegalArgumentException("trim factor must be between 0 and 1"); + } + + mTrimFactor = pTrimFactor; + } + + /** + * always returns {@code false}, and instead invokes {@code removeLRU()} + * if {@code size >= maxSize}. + */ + protected boolean removeEldestEntry(Entry pEldest) { + // NOTE: As removeLRU() may remove more than one entry, this is better + // than simply removing the eldest entry. + if (size() >= mMaxSize) { + removeLRU(); + } + return false; + } + + protected Entry removeEntry(Entry pEntry) { + Entry entry = super.removeEntry(pEntry); + processRemoved(pEntry); + return entry; + } + + /** + * Default implementation does nothing. + * May be used by clients as a call-back to notify when mappings expire from + * the map. + * + * @param pRemoved the removed mapping + */ + public void processRemoved(Entry pRemoved) { + } + + /** + * Removes the least recently used mapping(s) from this map. + *

+ * How many mappings are removed from the map, is controlled by the + * trim factor. + * In any case, at least one mapping will be removed. + * + * @see #getTrimFactor() + */ + public void removeLRU() { + int removeCount = (int) Math.max((size() * mTrimFactor), 1); + while ((removeCount--) > 0) { + removeEntry(mHead.mNext); + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedMap.java new file mode 100755 index 00000000..fef6b51e --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedMap.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.*; +import java.io.Serializable; + +/** + * 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 mHead; + protected final boolean mAccessOrder; + + /** + * 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); + mAccessOrder = 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); + mAccessOrder = pAccessOrder; + } + + protected void init() { + mHead = 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"; + } + }; + mHead.mPrevious = mHead.mNext = mHead; + } + + public boolean containsValue(Object pValue) { + // Overridden to take advantage of faster iterator + if (pValue == null) { + for (LinkedEntry e = mHead.mNext; e != mHead; e = e.mNext) { + if (e.mValue == null) { + return true; + } + } + } else { + for (LinkedEntry e = mHead.mNext; e != mHead; e = e.mNext) { + 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 = mHead.mNext; + 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 = mModCount; + + public boolean hasNext() { + return mNextEntry != mHead; + } + + public void remove() { + if (mLastReturned == null) { + throw new IllegalStateException(); + } + + if (mModCount != mExpectedModCount) { + throw new ConcurrentModificationException(); + } + + LinkedMap.this.remove(mLastReturned.mKey); + mLastReturned = null; + + mExpectedModCount = mModCount; + } + + LinkedEntry nextEntry() { + if (mModCount != mExpectedModCount) { + throw new ConcurrentModificationException(); + } + + if (mNextEntry == mHead) { + throw new NoSuchElementException(); + } + + LinkedEntry e = mLastReturned = mNextEntry; + mNextEntry = e.mNext; + + 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) mEntries.get(pKey); + + if (entry != null) { + entry.recordAccess(this); + return entry.mValue; + } + + return null; + } + + public V remove(Object pKey) { + LinkedEntry entry = (LinkedEntry) mEntries.remove(pKey); + + if (entry != null) { + entry.remove(); + mModCount++; + + return entry.mValue; + } + return null; + } + + public V put(K pKey, V pValue) { + LinkedEntry entry = (LinkedEntry) mEntries.get(pKey); + V oldValue; + + if (entry == null) { + oldValue = null; + + // Remove eldest entry if instructed, else grow capacity if appropriate + LinkedEntry eldest = mHead.mNext; + if (removeEldestEntry(eldest)) { + removeEntry(eldest); + } + + entry = createEntry(pKey, pValue); + entry.addBefore(mHead); + + mEntries.put(pKey, entry); + } + else { + oldValue = entry.mValue; + + entry.mValue = pValue; + entry.recordAccess(this); + } + + mModCount++; + + 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 mPrevious; + LinkedEntry mNext; + + LinkedEntry(K pKey, V pValue, LinkedEntry pNext) { + super(pKey, pValue); + + mNext = 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) { + mNext = pExisting; + mPrevious = pExisting.mPrevious; + + mPrevious.mNext = this; + mNext.mPrevious = this; + } + + /** + * Removes this entry from the linked list. + */ + void remove() { + mPrevious.mNext = mNext; + mNext.mPrevious = mPrevious; + } + + /** + * 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.mAccessOrder) { + linkedMap.mModCount++; + remove(); + addBefore(linkedMap.mHead); + } + } + + /** + * Removes this entry from the linked list. + * + * @param pMap the map to record remoal from + */ + protected void recordRemoval(Map pMap) { + // TODO: Is this REALLY correct? + remove(); + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedSet.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedSet.java new file mode 100755 index 00000000..511acbb7 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/LinkedSet.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.*; +import java.io.Serializable; + +/** + * 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 mMap; + + public LinkedSet() { + mMap = 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 mMap.put(pValue, DUMMY) == null; + } + + public int size() { + return mMap.size(); + } + + public Iterator iterator() { + return mMap.keySet().iterator(); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/NullMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/NullMap.java new file mode 100755 index 00000000..9a3a1968 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/NullMap.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.*; +import java.io.Serializable; + +/** + * An (immutable) empty {@link Map}, that supports all {@code Map} operations + * without throwing expcetions (in contrast to {@link Collections#EMPTY_MAP} + * 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: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/PaintKey.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/PaintKey.java new file mode 100755 index 00000000..1005b053 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/PaintKey.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.awt.*; +import java.io.Serializable; + +/** + * PaintKey class description. + * + * @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/PaintKey.java#1 $ + */ +public class PaintKey extends TypedMap.AbstractKey implements Serializable { + public PaintKey() { + super(); + } + + public PaintKey(String pName) { + super(pName); + } + + public boolean isCompatibleValue(Object pValue) { + return pValue instanceof Paint; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Rectangle2DKey.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Rectangle2DKey.java new file mode 100755 index 00000000..ee4d0b7f --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Rectangle2DKey.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.awt.geom.Rectangle2D; +import java.io.Serializable; + +/** + * Rectangle2DKey class description. + * + * @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/Rectangle2DKey.java#1 $ + */ +public class Rectangle2DKey extends TypedMap.AbstractKey implements Serializable { + public Rectangle2DKey() { + super(); + } + + public Rectangle2DKey(String pName) { + super(pName); + } + + public boolean isCompatibleValue(Object pValue) { + return pValue instanceof Rectangle2D; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Resource.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Resource.java new file mode 100755 index 00000000..5fec4a5b --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Resource.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.io.InputStream; +import java.io.IOException; +import java.net.URL; + +/** + * Resource class description. + * + * @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/Resource.java#1 $ + */ +public interface Resource { + /** + * Returns the id of this resource. + * + * The id might be a {@code URL}, a {@code File} or a string + * representation of some system resource. + * It will always be equal to the {@code reourceId} parameter + * sent to the {@link ResourceMonitor#addResourceChangeListener} method + * for a given resource. + * + * @return the id of this resource + */ + public Object getId(); + + /** + * Returns the {@code URL} for the resource. + * + * @return the URL for the resource + */ + public URL asURL(); + + /** + * Opens an input stream, that reads from this reource. + * + * @return an intput stream, that reads frmo this resource. + * + * @throws IOException if an I/O exception occurs + */ + public InputStream asStream() throws IOException; + + /** + * Returns the last modified time. + * Should only be used for comparison. + * + * @return the time of the last modification of this resource, or + * {@code -1} if the last modification time cannot be determined. + */ + public long lastModified(); +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ResourceChangeListener.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ResourceChangeListener.java new file mode 100755 index 00000000..f510ca61 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ResourceChangeListener.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.EventListener; + +/** + * An {@code EventListener} that listens for updates in file or system + * resources. + * + * @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/ResourceChangeListener.java#1 $ + */ +public interface ResourceChangeListener extends EventListener { + /** + * Invoked when a resource is changed. + * Implementations that listens for multiple eventes, should check that + * {@code Resource.getId()} is equal to the {@code reourceId} parameter + * sent to the {@link ResourceMonitor#addResourceChangeListener} method. + * + * @param pResource changed file/url/etc. + */ + void resourceChanged(Resource pResource); +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ResourceMonitor.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ResourceMonitor.java new file mode 100755 index 00000000..dd37cef4 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/ResourceMonitor.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URL; +import java.util.Timer; +import java.util.TimerTask; +import java.util.Map; +import java.util.HashMap; + +// TODO: Could this be used for change-aware classloader? Woo.. +/** + * Monitors changes is files and system resources. + * + * Based on example code and ideas from + * Java + * Tip 125: Set your timer for dynamic properties. + * + * @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/ResourceMonitor.java#1 $ + */ +public abstract class ResourceMonitor { + + private static final ResourceMonitor INSTANCE = new ResourceMonitor() {}; + + private Timer mTimer; + + private Map mTimerEntries; + + public static ResourceMonitor getInstance() { + return INSTANCE; + } + + /** + * Creates a {@code ResourceMonitor}. + */ + protected ResourceMonitor() { + // Create timer, run timer thread as daemon... + mTimer = new Timer(true); + mTimerEntries = new HashMap(); + } + + /** + * Add a monitored {@code Resource} with a {@code ResourceChangeListener}. + * + * The {@code reourceId} might be a {@code File} a {@code URL} or a + * {@code String} containing a file path, or a path to a resource in the + * class path. Note that class path resources are resolved using the + * given {@code ResourceChangeListener}'s {@code ClassLoader}, then + * the current {@code Thread}'s context class loader, if not found. + * + * @param pListener pListener to notify when the file changed. + * @param pResourceId id of the resource to monitor (a {@code File} + * a {@code URL} or a {@code String} containing a file path). + * @param pPeriod polling pPeriod in milliseconds. + * + * @see ClassLoader#getResource(String) + */ + public void addResourceChangeListener(ResourceChangeListener pListener, + Object pResourceId, long pPeriod) throws IOException { + // Create the task + ResourceMonitorTask task = new ResourceMonitorTask(pListener, pResourceId); + + // Get unique Id + Object resourceId = getResourceId(pResourceId, pListener); + + // Remove the old task for this Id, if any, and register the new one + synchronized (mTimerEntries) { + removeListenerInternal(resourceId); + mTimerEntries.put(resourceId, task); + } + + mTimer.schedule(task, pPeriod, pPeriod); + } + + /** + * Remove the {@code ResourceChangeListener} from the notification list. + * + * @param pListener the pListener to be removed. + * @param pResourceId name of the resource to monitor. + */ + public void removeResourceChangeListener(ResourceChangeListener pListener, Object pResourceId) { + synchronized (mTimerEntries) { + removeListenerInternal(getResourceId(pResourceId, pListener)); + } + } + + private void removeListenerInternal(Object pResourceId) { + ResourceMonitorTask task = (ResourceMonitorTask) mTimerEntries.remove(pResourceId); + + if (task != null) { + task.cancel(); + } + } + + private Object getResourceId(Object pResourceName, ResourceChangeListener pListener) { + return pResourceName.toString() + System.identityHashCode(pListener); + } + + private void fireResourceChangeEvent(ResourceChangeListener pListener, Resource pResource) { + pListener.resourceChanged(pResource); + } + + /** + * + */ + private class ResourceMonitorTask extends TimerTask { + ResourceChangeListener mListener; + Resource mMonitoredResource; + long mLastModified; + + public ResourceMonitorTask(ResourceChangeListener pListener, Object pResourceId) throws IOException { + mListener = pListener; + mLastModified = 0; + + String resourceId = null; + File file = null; + URL url = null; + if (pResourceId instanceof File) { + file = (File) pResourceId; + resourceId = file.getAbsolutePath(); // For use by exception only + } + else if (pResourceId instanceof URL) { + url = (URL) pResourceId; + if ("file".equals(url.getProtocol())) { + file = new File(url.getFile()); + } + resourceId = url.toExternalForm(); // For use by exception only + } + else if (pResourceId instanceof String) { + resourceId = (String) pResourceId; // This one might be looked up + file = new File((String) resourceId); + } + + if (file != null && file.exists()) { + // Easy, this is a file + mMonitoredResource = new FileResource(pResourceId, file); + //System.out.println("File: " + mMonitoredResource); + } + else { + // No file there, but is it on CLASSPATH? + if (url == null) { + url = pListener.getClass().getClassLoader().getResource(resourceId); + } + if (url == null) { + url = Thread.currentThread().getContextClassLoader().getResource(resourceId); + } + + if (url != null && "file".equals(url.getProtocol()) + && (file = new File(url.getFile())).exists()) { + // It's a file in classpath, so try this as an optimization + mMonitoredResource = new FileResource(pResourceId, file); + //System.out.println("File: " + mMonitoredResource); + } + else if (url != null) { + // No, not a file, might even be an external resource + mMonitoredResource = new URLResource(pResourceId, url); + //System.out.println("URL: " + mMonitoredResource); + } + else { + throw new FileNotFoundException(resourceId); + } + } + + mLastModified = mMonitoredResource.lastModified(); + } + + public void run() { + long lastModified = mMonitoredResource.lastModified(); + + if (lastModified != mLastModified) { + mLastModified = lastModified; + fireResourceChangeEvent(mListener, mMonitoredResource); + } + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/StringKey.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/StringKey.java new file mode 100755 index 00000000..67b5228e --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/StringKey.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.io.Serializable; + +/** + * StringKey class description. + * + * @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/StringKey.java#1 $ + */ +public class StringKey extends TypedMap.AbstractKey implements Serializable { + public StringKey() { + super(); + } + + public StringKey(String pName) { + super(pName); + } + + public boolean isCompatibleValue(Object pValue) { + return pValue instanceof String; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/StringTokenIterator.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/StringTokenIterator.java new file mode 100755 index 00000000..433f2961 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/StringTokenIterator.java @@ -0,0 +1,329 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.NoSuchElementException; + +/** + * StringTokenIterator, a drop-in replacement for {@code StringTokenizer}. + * StringTokenIterator has the following features: + *

    + *
  • Iterates over a strings, 20-50% faster than {@code StringTokenizer} + * (and magnitudes faster than {@code String.split(..)} or + * {@code Pattern.split(..)})
  • + *
  • Implements the {@code Iterator} interface
  • + *
  • Optionally returns delimiters
  • + *
  • Optionally returns empty elements
  • + *
  • Optionally iterates in reverse
  • + *
  • Resettable
  • + *
+ * + * @see java.util.StringTokenizer + * @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/StringTokenIterator.java#1 $ + */ +public class StringTokenIterator extends AbstractTokenIterator { + + private final String mString; + private final char[] mDelimiters; + private int mPosition; + private final int mMaxPosition; + private String mNext; + private String mNextDelimiter; + private final boolean mIncludeDelimiters; + private final boolean mIncludeEmpty; + private final boolean mReverse; + + public final static int FORWARD = 1; + public final static int REVERSE = -1; + + /** + * Stores the value of the delimiter character with the highest value. + * It is used to optimize the detection of delimiter characters. + */ + private final char mMaxDelimiter; + + /** + * Creates a StringTokenIterator + * + * @param pString the string to be parsed. + */ + public StringTokenIterator(String pString) { + this(pString, " \t\n\r\f".toCharArray(), FORWARD, false, false); + } + + /** + * Creates a StringTokenIterator + * + * @param pString the string to be parsed. + * @param pDelimiters the delimiters. + */ + public StringTokenIterator(String pString, String pDelimiters) { + this(pString, toCharArray(pDelimiters), FORWARD, false, false); + } + + /** + * Creates a StringTokenIterator + * + * @param pString the string to be parsed. + * @param pDelimiters the delimiters. + * @param pDirection iteration direction. + */ + public StringTokenIterator(String pString, String pDelimiters, int pDirection) { + this(pString, toCharArray(pDelimiters), pDirection, false, false); + } + + /** + * Creates a StringTokenIterator + * + * @param pString the string to be parsed. + * @param pDelimiters the delimiters. + * @param pIncludeDelimiters flag indicating whether to return delimiters as tokens. + */ + public StringTokenIterator(String pString, String pDelimiters, boolean pIncludeDelimiters) { + this(pString, toCharArray(pDelimiters), FORWARD, pIncludeDelimiters, false); + } + + /** + * Creates a StringTokenIterator + * + * @param pString the string to be parsed. + * @param pDelimiters the delimiters. + * @param pDirection iteration direction. + * @param pIncludeDelimiters flag indicating whether to return delimiters as tokens. + * @param pIncludeEmpty flag indicating whether to return empty tokens + * + */ + public StringTokenIterator(String pString, String pDelimiters, int pDirection, + boolean pIncludeDelimiters, boolean pIncludeEmpty) { + this(pString, toCharArray(pDelimiters), pDirection, pIncludeDelimiters, pIncludeEmpty); + } + + /** + * Implementation. + * + * @param pString the string to be parsed. + * @param pDelimiters the delimiters. + * @param pDirection iteration direction. + * @param pIncludeDelimiters flag indicating whether to return delimiters as tokens. + * @param pIncludeEmpty flag indicating whether to return empty tokens + */ + private StringTokenIterator(String pString, char[] pDelimiters, + int pDirection, boolean pIncludeDelimiters, boolean pIncludeEmpty) { + if (pString == null) { + throw new IllegalArgumentException("string == null"); + } + + mString = pString; + mMaxPosition = pString.length(); + mDelimiters = pDelimiters; + mIncludeDelimiters = pIncludeDelimiters; + mReverse = (pDirection == REVERSE); + mIncludeEmpty = pIncludeEmpty; + mMaxDelimiter = initMaxDelimiter(pDelimiters); + + reset(); + } + + private static char[] toCharArray(String pDelimiters) { + if (pDelimiters == null) { + throw new IllegalArgumentException("delimiters == null"); + } + return pDelimiters.toCharArray(); + } + + /** + * Returns the highest char in the delimiter set. + * @param pDelimiters the delimiter set + * @return the highest char + */ + private static char initMaxDelimiter(char[] pDelimiters) { + if (pDelimiters == null) { + return 0; + } + + char max = 0; + for (char c : pDelimiters) { + if (max < c) { + max = c; + } + } + + return max; + } + + /** + * Resets this iterator. + * + */ + public void reset() { + mPosition = 0; + mNext = null; + mNextDelimiter = null; + } + + /** + * 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. + */ + public boolean hasNext() { + return (mNext != null || fetchNext() != null); + } + + private String fetchNext() { + // If next is delimiter, return fast + if (mNextDelimiter != null) { + mNext = mNextDelimiter; + mNextDelimiter = null; + return mNext; + } + + // If no more chars, return null + if (mPosition >= mMaxPosition) { + return null; + } + + return mReverse ? fetchReverse() : fetchForward(); + + } + + private String fetchReverse() { + // Get previous position + int prevPos = scanForPrev(); + + // Store next string + mNext = mString.substring(prevPos + 1, mMaxPosition - mPosition); + + if (mIncludeDelimiters && prevPos >= 0 && prevPos < mMaxPosition) { + mNextDelimiter = mString.substring(prevPos, prevPos + 1); + } + + mPosition = mMaxPosition - prevPos; + + // Skip empty + if (mNext.length() == 0 && !mIncludeEmpty) { + return fetchNext(); + } + + return mNext; + } + + private String fetchForward() { + // Get next position + int nextPos = scanForNext(); + + // Store next string + mNext = mString.substring(mPosition, nextPos); + + if (mIncludeDelimiters && nextPos >= 0 && nextPos < mMaxPosition) { + mNextDelimiter = mString.substring(nextPos, nextPos + 1); + } + + mPosition = ++nextPos; + + // Skip empty + if (mNext.length() == 0 && !mIncludeEmpty) { + return fetchNext(); + } + + return mNext; + } + + private int scanForNext() { + int position = mPosition; + + while (position < mMaxPosition) { + // Find next match, using all delimiters + char c = mString.charAt(position); + + if (c <= mMaxDelimiter) { + + // Find first delimiter match + for (char delimiter : mDelimiters) { + if (c == delimiter) { + return position;// Return if match + } + } + } + + // Next... + position++; + } + + // Return last position, if no match + return position; + } + + private int scanForPrev() { + int position = (mMaxPosition - 1) - mPosition; + + while (position >= 0) { + // Find next match, using all delimiters + char c = mString.charAt(position); + + if (c <= mMaxDelimiter) { + + // Find first delimiter match + for (char delimiter : mDelimiters) { + if (c == delimiter) { + return position;// Return if match + } + } + } + + // Next... + position--; + } + + // Return first position, if no match + return position; + } + + /** + * Returns the next element in the iteration. + * + * @return the next element in the iteration. + * @exception java.util.NoSuchElementException iteration has no more elements. + */ + public String next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + String next = mNext; + mNext = fetchNext(); + + return next; + } + +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Time.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Time.java new file mode 100755 index 00000000..49368127 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Time.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +/** + * 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 mTime = -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!"); + } + mTime = pTime; + } + + /** + * Gets the full time in seconds. + */ + public int getTime() { + return mTime; + } + + /** + * Gets the full time in milliseconds, for use in creating dates or + * similar. + * + * @see java.util.Date#setTime(long) + */ + public long getTimeInMillis() { + return (long) mTime * 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) { + mTime = getMinutes() * SECONDS_IN_MINUTE + pSeconds; + } + + /** + * Gets the seconds part of the time. + * + * @return an integer between 0 and 59 + */ + public int getSeconds() { + return mTime % SECONDS_IN_MINUTE; + } + + /** + * Sets the minutes part of the time. + * + * @param pMinutes an integer + */ + public void setMinutes(int pMinutes) { + mTime = pMinutes * SECONDS_IN_MINUTE + getSeconds(); + } + + /** + * Gets the minutes part of the time. + * + * @return an integer + */ + public int getMinutes() { + return mTime / 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeFormat.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeFormat.java new file mode 100755 index 00000000..d2e9405b --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeFormat.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +package com.twelvemonkeys.util; + +import java.text.FieldPosition; +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 java.text.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 mFormatString = 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.mFormatString + "\""); + time = in.parse(argv[0]); + } + else + time = new Time(); + + System.out.println("Time is \"" + out.format(time) + + "\" according to format \"" + out.mFormatString + "\""); + } + + + /** + * The formatter array. + */ + + protected TimeFormatter[] mFormatter; + + /** + * Creates a new TimeFormat with the given formatString, + */ + + public TimeFormat(String pStr) { + mFormatString = 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)).mDigits); + } + */ + mFormatter = (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 mFormatString; + } + + /** 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 < mFormatter.length; i++) { + buf.append(mFormatter[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 < mFormatter.length) + && (pos + skip < pStr.length()) ; i++) { + // Go to next offset + pos += skip; + + if (mFormatter[i] instanceof MinutesFormatter) { + // Parse MINUTES + if ((i + 1) < mFormatter.length + && mFormatter[i + 1] instanceof TextFormatter) { + // Skip until next format element + skip = pStr.indexOf(((TextFormatter) mFormatter[i + 1]).mText, pos); + // Error in format, try parsing to end + if (skip < 0) + skip = pStr.length(); + } + else if ((i + 1) >= mFormatter.length) { + // Skip until end of string + skip = pStr.length(); + } + else { + // Hope this is correct... + skip = mFormatter[i].mDigits; + } + + // May be first char + if (skip > pos) + min = Integer.parseInt(pStr.substring(pos, skip)); + } + else if (mFormatter[i] instanceof SecondsFormatter) { + // Parse SECONDS + if (mFormatter[i].mDigits == -1) { + // Only seconds (or full TIME) + if ((i + 1) < mFormatter.length + && mFormatter[i + 1] instanceof TextFormatter) { + // Skip until next format element + skip = pStr.indexOf(((TextFormatter) mFormatter[i + 1]).mText, pos); + + } + else if ((i + 1) >= mFormatter.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) < mFormatter.length + && mFormatter[i + 1] instanceof TextFormatter) { + // Skip until next format element + skip = pStr.indexOf(((TextFormatter) mFormatter[i + 1]).mText, pos); + + } + else if ((i + 1) >= mFormatter.length) { + // Skip until end of string + skip = pStr.length(); + } + else { + skip = mFormatter[i].mDigits; + } + // Get seconds + sec = Integer.parseInt(pStr.substring(pos, skip)); + } + } + else if (mFormatter[i] instanceof TextFormatter) { + skip = mFormatter[i].mDigits; + } + + } + + // 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 mDigits = 0; + + abstract String format(Time t); +} + +/** + * Formats the seconds part of the Time + */ +class SecondsFormatter extends TimeFormatter { + + SecondsFormatter(int pDigits) { + mDigits = pDigits; + } + + String format(Time t) { + // Negative number of digits, means all seconds, no padding + if (mDigits < 0) { + return Integer.toString(t.getTime()); + } + + // If seconds is more than mDigits long, simply return it + if (t.getSeconds() >= Math.pow(10, mDigits)) { + return Integer.toString(t.getSeconds()); + } + + // Else return it with leading 0's + //return StringUtil.formatNumber(t.getSeconds(), mDigits); + return com.twelvemonkeys.lang.StringUtil.pad("" + t.getSeconds(), mDigits, "0", true); + } +} + +/** + * Formats the minutes part of the Time + */ +class MinutesFormatter extends TimeFormatter { + + MinutesFormatter(int pDigits) { + mDigits = pDigits; + } + + String format(Time t) { + // If minutes is more than mDigits long, simply return it + if (t.getMinutes() >= Math.pow(10, mDigits)) { + return Integer.toString(t.getMinutes()); + } + + // Else return it with leading 0's + //return StringUtil.formatNumber(t.getMinutes(), mDigits); + return com.twelvemonkeys.lang.StringUtil.pad("" + t.getMinutes(), mDigits, "0", true); + } +} + +/** + * Formats text constant part of the Time + */ +class TextFormatter extends TimeFormatter { + String mText = null; + + TextFormatter(String pText) { + mText = pText; + + // Just to be able to skip over + if (pText != null) { + mDigits = pText.length(); + } + } + + String format(Time t) { + // Simply return the text + return mText; + } + +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeoutMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeoutMap.java new file mode 100755 index 00000000..dff22ded --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TimeoutMap.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.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 mExpiryTime = 60000L; // 1 minute + + ////////////////////// + private volatile long mNextExpiryTime; + ////////////////////// + + /** + * 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(); + mExpiryTime = 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); + mExpiryTime = pExpiryTime; + } + + /** + * Gets the maximum time any value will be kept in the map, before it expires. + * + * @return the expiry time + */ + public long getExpiryTime() { + return mExpiryTime; + } + + /** + * 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 = mExpiryTime; + + mExpiryTime = pExpiryTime; + + if (mExpiryTime < oldEexpiryTime) { + // Expire now + mNextExpiryTime = 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 mEntries.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 mEntries.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) mEntries.get(pKey); + + if (entry == null) { + return null; + } + else if (entry.isExpired()) { + //noinspection SuspiciousMethodCalls + mEntries.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) mEntries.get(pKey); + V oldValue; + + if (entry == null) { + oldValue = null; + + entry = createEntry(pKey, pValue); + + mEntries.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(); + mModCount++; + + 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) mEntries.remove(pKey); + return (entry != null) ? entry.getValue() : null; + } + + /** + * Removes all mappings from this map. + */ + public void clear() { + mEntries.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 > mNextExpiryTime) { + 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 > mNextExpiryTime) { + //// + long next = Long.MAX_VALUE; + mNextExpiryTime = 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; + } + //// + } + //// + mNextExpiryTime = 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 = mEntries.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 + mExpiryTime; + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TokenIterator.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TokenIterator.java new file mode 100755 index 00000000..7fa70b7a --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TokenIterator.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.Iterator; +import java.util.Enumeration; + +/** + * 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TypedMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TypedMap.java new file mode 100755 index 00000000..6caf52e6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/TypedMap.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.Map; +import java.util.HashMap; +import java.util.Collection; +import java.util.Set; +import java.io.Serializable; + +/** + * This {@code Map} implementation guarantees that the values have a type that + * are compatible with it's key. Different keys may have different types. + * + * @see TypedMap.Key + * + * @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/TypedMap.java#1 $ + */ +public class TypedMap implements Map, Serializable { + + /** + * The wrapped map + */ + protected Map mEntries; + + /** + * Creates a {@code TypedMap}. + * This {@code TypedMap} will be backed by a new {@code HashMap} instance. + */ + public TypedMap() { + mEntries = new HashMap(); + } + + /** + * Creates a {@code TypedMap} containing the same elements as the given + * map. + * This {@code TypedMap} 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 pMap the map used to populate this map + * @throws ClassCastException if all keys in the map are not instances of + * {@code TypedMap.Key}. + * @see java.util.Map + * @see #TypedMap(java.util.Map, boolean) + */ + public TypedMap(Map pMap) { + this(); + + if (pMap != null) { + putAll(pMap); + } + } + + /** + * Creates a {@code TypedMap}. + * This {@code TypedMap} 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 pUseElements if {@code true}, the elements in the map are + * retained. Otherwise, the map is cleared. For an empty {@code Map} the + * parameter has no effect. + * @throws ClassCastException if {@code pUseElements} is {@code true} + * all keys in the map are not instances of {@code TypedMap.Key}. + */ + public TypedMap(Map pBacking, boolean pUseElements) { + if (pBacking == null) { + throw new IllegalArgumentException("backing == null"); + } + + // This is safe, as we re-insert all values later + //noinspection unchecked + mEntries = (Map) pBacking; + + // Re-insert all elements to avoid undeterministic ClassCastExceptions + if (pUseElements) { + putAll(pBacking); + } + else if (mEntries.size() > 0) { + mEntries.clear(); + } + } + + /** + * 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() { + return mEntries.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 mEntries.isEmpty(); + } + + /** + * Returns {@code true} if this map contains a mapping for the specified + * key. + * + * @param pKey key whose presence in this map is to be tested. + * @return {@code true} if this map contains a mapping for the specified + * key. + */ + public boolean containsKey(Object pKey) { + return mEntries.containsKey(pKey); + } + + /** + * Returns {@code true} if this map maps one or more keys to the + * specified value. More formally, returns {@code true} if and only if + * this map contains at least one mapping to a value {@code v} such that + * {@code (pValue==null ? v==null : pValue.equals(v))}. This operation + * will probably require time linear in the map size for most + * implementations of the {@code Map} interface. + * + * @param pValue value whose presence in this map is to be tested. + * @return {@code true} if this map maps one or more keys to the + * specified value. + */ + public boolean containsValue(Object pValue) { + return mEntries.containsValue(pValue); + } + + /** + * Returns the value to which this map maps the specified key. Returns + * {@code null} if the map contains no mapping for this key. A return + * value of {@code null} does not necessarily indicate that the + * map contains no mapping for the key; it's also possible that the map + * explicitly maps the key to {@code null}. The {@code containsKey} + * operation may be used to distinguish these two cases. + * + * @param pKey key whose associated value is to be returned. + * @return the value to which this map maps the specified key, or + * {@code null} if the map contains no mapping for this key. + * @see #containsKey(java.lang.Object) + */ + public V get(Object pKey) { + return mEntries.get(pKey); + } + + /** + * Associates the specified value with the specified key in this map. + * If the map previously contained a mapping for + * the key, the old value is replaced. + * + * @param pKey key with which the specified value is to be associated. + * @param pValue value to be associated with the specified key. + * + * @return previous value associated with specified key, or {@code null} + * if there was no mapping for key. 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. + * + * @throws IllegalArgumentException if the value is not compatible with the + * key. + * + * @see TypedMap.Key + */ + public V put(K pKey, V pValue) { + if (!pKey.isCompatibleValue(pValue)) { + throw new IllegalArgumentException("incompatible value for key"); + } + return mEntries.put(pKey, pValue); + } + + /** + * Removes the mapping for this key from this map if present (optional + * operation). + * + * @param pKey key whose mapping is to be removed from the map. + * @return previous value associated with specified key, or {@code null} + * if there was no mapping for key. A {@code null} return can + * also indicate that the map previously associated {@code null} + * with the specified key, if the implementation supports + * {@code null} values. + */ + public V remove(Object pKey) { + return mEntries.remove(pKey); + } + + /** + * Copies all of the mappings from the specified map to this map + * (optional operation). These mappings will replace any mappings that + * this map had for any of the keys currently in the specified map. + *

+ * Note: If you override this method, make sure you add each element through + * the put method, to avoid resource leaks and undeterministic class casts. + * + * @param pMap Mappings to be stored in this map. + */ + public void putAll(Map pMap) { + for (final Entry e : pMap.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + + /** + * Removes all mappings from this map (optional operation). + */ + public void clear() { + mEntries.clear(); + } + + public Collection values() { + return mEntries.values(); + } + + public Set> entrySet() { + return mEntries.entrySet(); + } + + public Set keySet() { + return mEntries.keySet(); + } + + /** + * Keys for use with {@code TypedMap} must implement this interface. + * + * @see #isCompatibleValue(Object) + */ + public static interface Key { + + /** + * Tests if the given value is compatible with this {@code Key}. + * Only compatible values may be passed to the + * {@code TypedMap.put} method. + * + * @param pValue the value to test for compatibility + * @return {@code true} if compatible, otherwise {@code false} + */ + boolean isCompatibleValue(Object pValue); + } + + /** + * An abstract {@code Key} implementation that allows keys to have + * meaningful names. + */ + public static abstract class AbstractKey implements Key, Serializable { + private final String mStringRep; + + /** + * Creates a {@code Key} with the given name. + * + * @param pName name of this key + */ + public AbstractKey(String pName) { + if (pName == null) { + throw new IllegalArgumentException("name == null"); + } + mStringRep = getClass().getName() + '[' + pName + ']'; + } + + /** + * Creates a {@code Key} with no name. + */ + public AbstractKey() { + this("null"); + } + + @Override + public String toString() { + return mStringRep; + } + + @Override + public boolean equals(Object obj) { + return obj == this || + (obj != null && obj.getClass() == getClass() && + mStringRep.equals(((AbstractKey) obj).mStringRep)); + } + + @Override + public int hashCode() { + return mStringRep.hashCode(); + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/URLResource.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/URLResource.java new file mode 100755 index 00000000..890df112 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/URLResource.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +/** + * URLResource class description. + * + * @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/URLResource.java#1 $ + */ +final class URLResource extends AbstractResource { + + // NOTE: For the time being, we rely on the URL class (and helpers) to do + // some smart caching and reuse of connections... + // TODO: Change the implementation if this is a problem + private long mLastModified = -1; + + /** + * Creates a {@code URLResource}. + * + * @param pResourceId the resource id + * @param pURL the URL resource + */ + public URLResource(Object pResourceId, URL pURL) { + super(pResourceId, pURL); + } + + private URL getURL() { + return (URL) mWrappedResource; + } + + public URL asURL() { + return getURL(); + } + + public InputStream asStream() throws IOException { + URLConnection connection = getURL().openConnection(); + connection.setAllowUserInteraction(false); + connection.setUseCaches(true); + return connection.getInputStream(); + } + + public long lastModified() { + try { + URLConnection connection = getURL().openConnection(); + connection.setAllowUserInteraction(false); + connection.setUseCaches(true); + connection.setIfModifiedSince(mLastModified); + + mLastModified = connection.getLastModified(); + } + catch (IOException ignore) { + } + + return mLastModified; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Visitor.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Visitor.java new file mode 100755 index 00000000..e62026c6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/Visitor.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.util; + +/** + * A generic visitor. + * + * @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/Visitor.java#1 $ + * + * @see Visitor Patter + */ +public interface Visitor { + /** + * Visits an element. + * + * @param pElement the element to visit + */ + void visit(T pElement); +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/WeakWeakMap.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/WeakWeakMap.java new file mode 100755 index 00000000..c861d8b9 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/WeakWeakMap.java @@ -0,0 +1,216 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import java.util.*; +import java.lang.ref.WeakReference; + +/** + * Special-purpose map implementation with weak keys and weak values. This is + * useful for mapping between keys and values that refer to (for example by + * wrapping) their keys. + * For more info, see {@link WeakHashMap} on why the + * values in a {@code WeakHashMap} must never refer strongly to their keys. + * + * @see WeakHashMap + * @see WeakReference + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/WeakWeakMap.java#1 $ + */ +@SuppressWarnings({"unchecked"}) +public class WeakWeakMap extends WeakHashMap { + // TODO: Consider using a backing map and delegate, instead of extending... + + /** + * Creates a {@code WeakWeakMap} with default initial capacity and load + * factor. + * + * @see java.util.WeakHashMap#WeakHashMap() + */ + public WeakWeakMap() { + super(); + } + + /** + * Creates a {@code WeakWeakMap} with the given initial capacity and + * default load factor. + * + * @param pInitialCapacity the initial capacity + * + * @see java.util.WeakHashMap#WeakHashMap(int) + */ + public WeakWeakMap(int pInitialCapacity) { + super(pInitialCapacity); + } + + /** + * Creates a {@code WeakWeakMap} with the given initial capacity and + * load factor. + * + * @param pInitialCapacity the initial capacity + * @param pLoadFactor the load factor + * + * @see WeakHashMap#WeakHashMap(int, float) + */ + public WeakWeakMap(int pInitialCapacity, float pLoadFactor) { + super(pInitialCapacity, pLoadFactor); + } + + /** + * Creates a {@code WeakWeakMap} containing the mappings in the given map. + * + * @param pMap the map whose mappings are to be placed in this map. + * + * @see WeakHashMap#WeakHashMap(java.util.Map) + */ + public WeakWeakMap(Map pMap) { + super(pMap); + } + + @Override + public V put(K pKey, V pValue) { + // NOTE: This is wrong, but we don't really care.. + return super.put(pKey, (V) new WeakReference(pValue)); + } + + @Override + public V get(Object pKey) { + WeakReference ref = (WeakReference) super.get(pKey); + return ref != null ? ref.get() : null; + } + + @Override + public V remove(Object pKey) { + WeakReference ref = (WeakReference) super.remove(pKey); + return ref != null ? ref.get() : null; + } + + @Override + public boolean containsValue(Object pValue) { + for (final V value : values()) { + if (pValue == value || (value != null && value.equals(pValue))) { + return true; + } + } + return false; + } + + @Override + public void putAll(Map pMap) { + for (final Map.Entry entry : pMap.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public Set> entrySet() { + return new AbstractSet>() { + public Iterator> iterator() { + return new Iterator>() { + @SuppressWarnings({"unchecked"}) + final Iterator>> mIterator = (Iterator) WeakWeakMap.super.entrySet().iterator(); + + public boolean hasNext() { + return mIterator.hasNext(); + } + + public Map.Entry next() { + return new Map.Entry() { + final Map.Entry> mEntry = mIterator.next(); + + public K getKey() { + return mEntry.getKey(); + } + + public V getValue() { + WeakReference ref = mEntry.getValue(); + return ref.get(); + } + + public V setValue(V pValue) { + WeakReference ref = mEntry.setValue(new WeakReference(pValue)); + return ref != null ? ref.get() : null; + } + + public boolean equals(Object obj) { + return mEntry.equals(obj); + } + + public int hashCode() { + return mEntry.hashCode(); + } + + public String toString() { + return mEntry.toString(); + } + }; + } + + public void remove() { + mIterator.remove(); + } + }; + } + + public int size() { + return WeakWeakMap.this.size(); + } + }; + } + + @Override + public Collection values() { + return new AbstractCollection() { + public Iterator iterator() { + return new Iterator() { + @SuppressWarnings({"unchecked"}) + Iterator> mIterator = (Iterator>) WeakWeakMap.super.values().iterator(); + + public boolean hasNext() { + return mIterator.hasNext(); + } + + public V next() { + WeakReference ref = mIterator.next(); + return ref.get(); + } + + public void remove() { + mIterator.remove(); + } + }; + } + + public int size() { + return WeakWeakMap.this.size(); + } + }; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/XMLProperties.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/XMLProperties.java new file mode 100755 index 00000000..40b464cb --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/XMLProperties.java @@ -0,0 +1,1287 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.xml.XMLSerializer; +import org.w3c.dom.*; +import org.xml.sax.*; +import org.xml.sax.helpers.DefaultHandler; +import org.xml.sax.helpers.XMLReaderFactory; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * Properties subclass, that reads and writes files in a simple XML format. + * Can be used in-place where ever {@link java.util.Properties} + * is used. The major differences are that it reads + * and writes XML files, instead of ".properties" format files, and has + * support for typed values (where normal Properties only supports Strings). + *

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

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

+ * XML Format (DTD):
+ *

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

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

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

+ * If you need the old functionality back, + * getPropertyValue returns super.get directly. + * A cleaner approach would be to override the list and store + * methods, but it's too much work for nothing... + * + * @param pKey a key in this hashtable + * + * @return the value to which the key is mapped in this hashtable, + * converted to a string; null if the key is not mapped to any value in + * this hashtable. + * + * @see #getPropertyValue(String) + * @see Properties#getProperty(String) + * @see Hashtable#get(Object) + * @see StringUtil#valueOf(Object) + */ + public Object get(Object pKey) { + + //System.out.println("get(" + pKey + "): " + super.get(pKey)); + Object value = super.get(pKey); + + // --- + if ((value != null) && (value instanceof Date)) { // Hmm.. This is true for subclasses too... + + // Special threatment of Date + String format = getPropertyFormat(StringUtil.valueOf(pKey)); + + if (format != null) { + value = new SimpleDateFormat(format).format(value); + } else { + value = sDefaultFormat.format(value); + } + return value; + } + + // --- + // Simply return value + return StringUtil.valueOf(value); + } + + /** + * Searches for the property with the specified key in this property list. + * If the key is not found in this property list, the default property list, + * and its defaults, recursively, are then checked. The method returns the + * default value argument if the property is not found. + * + * @param pKey the hashtable key. + * @param pDefaultValue a default value. + * + * @return the value in this property list with the specified key value. + * @see #setProperty + * @see #defaults + */ + public String getProperty(String pKey, String pDefaultValue) { + + // Had to override this because Properties uses super.get()... + String value = (String) get(pKey); // Safe cast, see get(Object) + + if (value != null) { + return value; + } + return ((defaults != null) + ? defaults.getProperty(pKey) + : pDefaultValue); + } + + /** + * Searches for the property with the specified key in this property list. + * If the key is not found in this property list, the default property list, + * and its defaults, recursively, are then checked. The method returns + * {@code null} if the property is not found. + * + * @param pKey the property key. + * @return the value in this property list with the specified key value. + * @see #setProperty + * @see #defaults + */ + public String getProperty(String pKey) { + + // Had to override this because Properties uses super.get()... + return getProperty(pKey, null); + } + + /** + * Writes this property list (key and element pairs) in this + * {@code Properties} + * table to the output stream in a format suitable for loading into a + * Properties table using the load method. This implementation writes + * the list in XML format. The stream is written using the UTF-8 character + * encoding. + * + * @param pOutput the output stream to write to. + * @param pHeader a description of the property list. + * + * @exception IOException if writing this property list to the specified + * output stream throws an IOException. + * + * @see java.util.Properties#store(OutputStream,String) + */ + public synchronized void store(OutputStream pOutput, String pHeader) throws IOException { + storeXML(this, pOutput, pHeader); + } + + /** + * Utility method that stores the property list in normal properties + * format. This method writes the list of Properties (key and element + * pairs) in the given {@code Properties} + * table to the output stream in a format suitable for loading into a + * Properties table using the load method. The stream is written using the + * ISO 8859-1 character encoding. + * + * @param pProperties the Properties table to store + * @param pOutput the output stream to write to. + * @param pHeader a description of the property list. + * + * @exception IOException if writing this property list to the specified + * output stream throws an IOException. + * + * @see java.util.Properties#store(OutputStream,String) + */ + public static void storeProperties(Map pProperties, OutputStream pOutput, String pHeader) throws IOException { + // Create new properties + Properties props = new Properties(); + + // Copy all elements from the pProperties (shallow) + Iterator iterator = pProperties.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + + props.setProperty((String) entry.getKey(), StringUtil.valueOf(entry.getValue())); + } + + // Store normal properties + props.store(pOutput, pHeader); + } + + /** + * Utility method that stores the property list in XML format. This method + * writes the list of Properties (key and element pairs) in the given + * {@code Properties} + * table to the output stream in a format suitable for loading into a + * XMLProperties table using the load method. Useful for converting + * Properties into XMLProperties. + * The stream is written using the UTF-8 character + * encoding. + * + * @param pProperties the Properties table to store. + * @param pOutput the output stream to write to. + * @param pHeader a description of the property list. + * + * @exception IOException if writing this property list to the specified + * output stream throws an IOException. + * + * @see #store(OutputStream,String) + * @see java.util.Properties#store(OutputStream,String) + * + * @todo Find correct way of setting namespace URI's + * @todo Store type and format information + */ + public static void storeXML(Map pProperties, OutputStream pOutput, String pHeader) throws IOException { + // Build XML tree (Document) and write + // Find the implementation + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder; + try { + builder = factory.newDocumentBuilder(); + } + catch (ParserConfigurationException e) { + throw (IOException) new IOException(e.getMessage()).initCause(e); + } + DOMImplementation dom = builder.getDOMImplementation(); + + Document document = dom.createDocument(null, PROPERTIES, dom.createDocumentType(PROPERTIES, null, SYSTEM_DTD_URI)); + Element root = document.getDocumentElement(); + + // This is probably not the correct way of setting a default namespace + root.setAttribute(XMLNS, NAMESPACE_URI); + + // Create and insert the normal Properties headers as XML comments + if (pHeader != null) { + document.insertBefore(document.createComment(" " + pHeader + " "), root); + } + document.insertBefore(document.createComment(" " + new Date() + " "), root); + + // Insert elements from the Properties + Iterator iterator = pProperties.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + String key = (String) entry.getKey(); + Object value = entry.getValue(); + String format = null; + + if (pProperties instanceof XMLProperties) { + format = ((XMLProperties) pProperties).getPropertyFormat(key); + } + insertElement(document, key, value, format); + } + + // Create serializer and output document + //XMLSerializer serializer = new XMLSerializer(pOutput, new OutputFormat(document, UTF_8_ENCODING, true)); + XMLSerializer serializer = new XMLSerializer(pOutput, UTF_8_ENCODING); + + serializer.serialize(document); + } + + /** + * Inserts elements to the given document one by one, and creates all its + * parents if needed. + * + * @param pDocument the document to insert to. + * @param pName the name of the property element. + * @param pValue the value of the property element. + * @param pFormat + * + * @todo I guess this implementation could use some optimisaztion, as + * we do a lot of unneccessary looping. + */ + private static void insertElement(Document pDocument, String pName, Object pValue, String pFormat) { + + // Get names of all elements we need + String[] names = StringUtil.toStringArray(pName, "."); + + // Get value formatted as string + String value = null; + + if (pValue != null) { + // --- + if (pValue instanceof Date) { + + // Special threatment of Date + if (pFormat != null) { + value = new SimpleDateFormat(pFormat).format(pValue); + } + else { + value = sDefaultFormat.format(pValue); + } + } + else { + value = String.valueOf(pValue); + } + + // --- + } + + // Loop through document from root, and insert parents as needed + Element element = pDocument.getDocumentElement(); + + for (int i = 0; i < names.length; i++) { + boolean found = false; + + // Get children + NodeList children = element.getElementsByTagName(PROPERTY); + Element child = null; + + // Search for correct name + for (int j = 0; j < children.getLength(); j++) { + child = (Element) children.item(j); + if (names[i].equals(child.getAttribute(PROPERTY_NAME))) { + // Found + found = true; + element = child; + break; // Next name + } + } + + // Test if the node was not found, otherwise we need to insert + if (!found) { + // Not found + child = pDocument.createElement(PROPERTY); + child.setAttribute(PROPERTY_NAME, names[i]); + + // Insert it + element.appendChild(child); + element = child; + } + + // If it's the destination node, set the value + if ((i + 1) == names.length) { + + // If the value string contains special data, + // use a CDATA block instead of the "value" attribute + if (StringUtil.contains(value, "\n") || StringUtil.contains(value, "\t") || StringUtil.contains(value, "\"") + || StringUtil.contains(value, "&") || StringUtil.contains(value, "<") || StringUtil.contains(value, ">")) { + + // Create value element + Element valueElement = pDocument.createElement(PROPERTY_VALUE); + + // Set type attribute + String className = pValue.getClass().getName(); + + className = StringUtil.replace(className, "java.lang.", ""); + if (!DEFAULT_TYPE.equals(className)) { + valueElement.setAttribute(PROPERTY_TYPE, className); + } + + // Set format attribute + if (pFormat != null) { + valueElement.setAttribute(PROPERTY_FORMAT, pFormat); + } + + // Crate cdata section + CDATASection cdata = pDocument.createCDATASection(value); + + // Append to document tree + valueElement.appendChild(cdata); + child.appendChild(valueElement); + } + else { + // Just set normal attribute value + child.setAttribute(PROPERTY_VALUE, value); + + // Set type attribute + String className = pValue.getClass().getName(); + + className = StringUtil.replace(className, "java.lang.", ""); + if (!DEFAULT_TYPE.equals(className)) { + child.setAttribute(PROPERTY_TYPE, className); + } + + // If format is set, store in attribute + if (pFormat != null) { + child.setAttribute(PROPERTY_FORMAT, pFormat); + } + } + } + } + } + + /** + * Gets all properties in a properties group. + * If no properties exists in the specified group, {@code null} is + * returned. + * + * @param pGroupKey the group key + * + * @return a new Properties continaing all properties in the group. Keys in + * the new Properties wil not contain the group key. + * If no properties exists in the specified group, {@code null} is + * returned. + */ + public Properties getProperties(String pGroupKey) { + // Stupid impl... + XMLProperties props = new XMLProperties(); + String groupKey = pGroupKey; + + if (groupKey.charAt(groupKey.length()) != '.') { + groupKey += "."; + } + + Iterator iterator = entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + String key = (String) entry.getKey(); + + if (key.startsWith(groupKey)) { + String subKey = key.substring(key.indexOf(groupKey)); + + props.setPropertyValue(subKey, entry.getValue()); + } + } + + return ((props.size() > 0) ? props : null); + } + + /** + * Sets the properties in the given properties group. + * Existing properties in the same group, will not be removed, unless they + * are replaced by new values. + * Any existing properties in the same group that was replaced, are + * returned. If no properties are replaced, null is + * returned. + * + * @param pGroupKey the group key + * @param pProperties the properties to set into this group + * + * @return Any existing properties in the same group that was replaced. + * If no properties are replaced, null is + * returned. + */ + public Properties setProperties(String pGroupKey, Properties pProperties) { + XMLProperties old = new XMLProperties(); + String groupKey = pGroupKey; + + if (groupKey.charAt(groupKey.length()) != '.') { + groupKey += "."; + } + Iterator iterator = pProperties.entrySet().iterator(); + + while (iterator.hasNext()) { + Map.Entry entry = (Map.Entry) iterator.next(); + String key = (String) entry.getKey(); + Object obj = setPropertyValue(groupKey + key, entry.getValue()); + + // Return removed entries + if (obj != null) { + old.setPropertyValue(groupKey + key, entry.getValue()); + } + } + return ((old.size() > 0) ? old : null); + } + + /** + * For testing only. + */ + public static void main(String[] pArgs) throws Exception { + // -- Print DTD + System.out.println("DTD: \n" + DTD); + System.out.println("--"); + + // -- Test load + System.out.println("Reading properties from \"" + pArgs[0] + "\"..."); + XMLProperties props = new XMLProperties(); + + props.load(new FileInputStream(new File(pArgs[0]))); + props.list(System.out); + System.out.println("--"); + + // -- Test recursion + String key = "key"; + Object old = props.setProperty(key, "AAA"); + Properties p1 = new XMLProperties(new XMLProperties(props)); + Properties p2 = new Properties(new Properties(props)); + + System.out.println("XMLProperties: " + p1.getProperty(key) + " ==" + " Properties: " + p2.getProperty(key)); + if (old == null) { + props.remove("key"); + } else { + props.put("key", old); // Put old value back, to avoid confusion... + } + System.out.println("--"); + + // -- Test store + //props.store(System.out, "XML Properties file written by XMLProperties."); + File out = new File("copy_of_" + pArgs[0]); + + System.out.println("Writing properties to \"" + out.getName() + "\""); + if (!out.exists()) { + props.store(new FileOutputStream(out), "XML Properties file written by XMLProperties."); + } else { + System.err.println("File \"" + out.getName() + "\" allready exists, cannot write!"); + } + + // -- Test utility methods + // Write normal properties from XMLProperties + out = new File("copy_of_" + pArgs[0].substring(0, pArgs[0].lastIndexOf(".")) + ".properties"); + System.out.println("Writing properties to \"" + out.getName() + "\""); + if (!out.exists()) { + storeProperties(props, new FileOutputStream(out), "Properties file written by XMLProperties."); + } else { + System.err.println("File \"" + out.getName() + "\" allready exists, cannot write!"); + } + System.out.println("--"); + + // -- Test type attribute + System.out.println("getPropertyValue(\"one\"): " + props.getPropertyValue("one") + " class: " + + props.getPropertyValue("one").getClass()); + System.out.println("setPropertyValue(\"now\", " + new Date() + "): " + props.setPropertyValue("now", new Date()) + " class: " + + props.getPropertyValue("now").getClass()); + System.out.println("getPropertyValue(\"date\"): " + props.getPropertyValue("date") + " class: " + + props.getPropertyValue("date").getClass()); + System.out.println("getPropertyValue(\"time\"): " + props.getPropertyValue("time") + " class: " + + props.getPropertyValue("time").getClass()); + } + + /** + * ContentHandler, ErrorHandler and EntityResolver implementation for the + * SAX Parser. + */ + protected class PropertiesHandler extends DefaultHandler { + protected Stack mStack = null; + + /** Stores the characters read so far, from the characters callback */ + protected char[] mReadSoFar = null; + protected boolean mIsValue = false; + protected String mType = null; + protected String mFormat = null; + protected XMLProperties mProperties = null; + protected Locator mLocator = null; + + /** + * Creates a PropertiesHandler for the given XMLProperties. + */ + PropertiesHandler(XMLProperties pProperties) { + mProperties = pProperties; + mStack = new Stack(); + } + + /** + * setDocumentLocator implementation. + */ + public void setDocumentLocator(Locator pLocator) { + + // System.out.println("Setting locator: " + pLocator); + mLocator = pLocator; + } + + /** + * Calls XMLProperties.addXMLError with the given SAXParseException + * as the argument. + */ + public void error(SAXParseException pException) throws SAXParseException { + + //throw pException; + mProperties.addXMLError(pException); + + /* + System.err.println("error: " + pException.getMessage()); + System.err.println("line: " + mLocator.getLineNumber()); + System.err.println("column: " + mLocator.getColumnNumber()); + */ + } + + /** + * Throws the given SAXParseException (and stops the parsing). + */ + public void fatalError(SAXParseException pException) throws SAXParseException { + + throw pException; + + /* + System.err.println("fatal error: " + pException.getMessage()); + System.err.println("line: " + mLocator.getLineNumber()); + System.err.println("column: " + mLocator.getColumnNumber()); + */ + } + + /** + * Calls XMLProperties.addXMLWarning with the given SAXParseException + * as the argument. + */ + public void warning(SAXParseException pException) throws SAXParseException { + + // throw pException; + mProperties.addXMLWarning(pException); + + /* + System.err.println("warning: " + pException.getMessage()); + System.err.println("line: " + mLocator.getLineNumber()); + System.err.println("column: " + mLocator.getColumnNumber()); + */ + } + + /** + * startElement implementation. + */ + public void startElement(String pNamespaceURI, String pLocalName, String pQualifiedName, Attributes pAttributes) throws SAXException { + + /* + + String attributes = ""; + for (int i = 0; i < pAttributes.getLength(); i++) { + attributes += pAttributes.getQName(i) + "=" + pAttributes.getValue(i) + (i < pAttributes.getLength() ? ", " : ""); + } + + System.out.println("startElement: " + pNamespaceURI + + "." + pLocalName + + " (" + pQualifiedName + ") " + + attributes); + */ + if (XMLProperties.PROPERTY.equals(pLocalName)) { + + // Get attibute values + String name = pAttributes.getValue(XMLProperties.PROPERTY_NAME); + String value = pAttributes.getValue(XMLProperties.PROPERTY_VALUE); + String type = pAttributes.getValue(XMLProperties.PROPERTY_TYPE); + String format = pAttributes.getValue(XMLProperties.PROPERTY_FORMAT); + + // Get the full name of the property + if (!mStack.isEmpty()) { + name = (String) mStack.peek() + "." + name; + } + + // Set the property + if (value != null) { + mProperties.setProperty(name, value); + + // Store type & format + if (!XMLProperties.DEFAULT_TYPE.equals(type)) { + mType = type; + mFormat = format; // Might be null (no format) + } + } + + // Push the last name on the stack + mStack.push(name); + } // /PROPERTY + else if (XMLProperties.PROPERTY_VALUE.equals(pLocalName)) { + + // Get attibute values + String name = (String) mStack.peek(); + String type = pAttributes.getValue(XMLProperties.PROPERTY_TYPE); + String format = pAttributes.getValue(XMLProperties.PROPERTY_FORMAT); + + // Store type & format + if (!XMLProperties.DEFAULT_TYPE.equals(type)) { + mType = type; + mFormat = format; + } + mIsValue = true; + } + } + + /** + * endElement implementation. + */ + public void endElement(String pNamespaceURI, String pLocalName, String pQualifiedName) throws SAXException { + + /* + System.out.println("endElement: " + pNamespaceURI + + "." + pLocalName + " (" + pQualifiedName + ")"); + */ + if (XMLProperties.PROPERTY.equals(pLocalName)) { + + // Just remove the last name + String name = (String) mStack.pop(); + + // Init typed values + try { + String prop = mProperties.getProperty(name); + + // Value may be null, if so just skip + if (prop != null) { + Object value = mProperties.initPropertyValue(prop, mType, mFormat); + + // Store format + if ((mFormat != null) &&!XMLProperties.DEFAULT_DATE_FORMAT.equals(mFormat)) { + mProperties.setPropertyFormat(name, mFormat); + } + + //System.out.println("-->" + prop + "-->" + value); + mProperties.setPropertyValue(name, value); + } + + // Clear type & format + mType = null; + mFormat = null; + } catch (Exception e) { + e.printStackTrace(System.err); + throw new SAXException(e); + } + } else if (XMLProperties.PROPERTY_VALUE.equals(pLocalName)) { + if (mStack.isEmpty()) { + + // There can't be any characters here, really + return; + } + + // Get the full name of the property + String name = (String) mStack.peek(); + + // Set the property + String value = new String(mReadSoFar); + + //System.err.println("characters: >" + value+ "<"); + if (!StringUtil.isEmpty(value)) { + + // If there is allready a value, both the value attribute + // and element have been specified, this is an error + if (mProperties.containsKey(name)) { + throw new SAXParseException( + "Value can only be specified either using the \"value\" attribute, OR the \"value\" element, not both.", mLocator); + } + + // Finally, set the property + mProperties.setProperty(name, value); + } + + // Done value processing + mIsValue = false; + } + } + + /** + * characters implementation + */ + public void characters(char[] pChars, int pStart, int pLength) throws SAXException { + // TODO: Use StringBuilder instead? + if (mIsValue) { + // If nothing read so far + if (mReadSoFar == null) { + // Create new array and copy into + mReadSoFar = new char[pLength]; + System.arraycopy(pChars, pStart, mReadSoFar, 0, pLength); + } + else { + // Merge arrays + mReadSoFar = (char[]) CollectionUtil.mergeArrays(mReadSoFar, 0, mReadSoFar.length, pChars, pStart, pLength); + } + } + } + + + /** + * Intercepts the entity + * "http://www.twelvemonkeys.com/xml/XMLProperties.dtd", and return + * an InputSource based on the internal DTD of XMLProperties instead. + * + * @todo Maybe intercept a PUBLIC DTD and be able to have SYSTEM DTD + * override? + */ + public InputSource resolveEntity(String pPublicId, String pSystemId) { + // If we are looking for the standard SYSTEM DTD, then + // Return an InputSource based on the internal DTD. + if (XMLProperties.SYSTEM_DTD_URI.equals(pSystemId)) { + return new InputSource(new StringReader(XMLProperties.DTD)); + } + + // use the default behaviour + return null; + } + } +} + diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/ConversionException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/ConversionException.java new file mode 100755 index 00000000..0e139eb5 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/ConversionException.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.convert; + +/** + * This exception may be thrown by PropertyConverters, when an attempted + * conversion fails. + * + * @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/ConversionException.java#1 $ + */ +public class ConversionException extends IllegalArgumentException { + protected Throwable mCause = this; + + /** + * Creates a {@code ConversionException} with the given error message. + * + * @param pMessage the error message + */ + public ConversionException(String pMessage) { + super(pMessage); + } + + /** + * Creates a {@code ConversionException} with the given cause. + * + * @param pCause The Throwable that caused this exception + */ + public ConversionException(Throwable pCause) { + super(pCause == null ? null : pCause.getMessage()); + initCause(pCause); + } + + /** + * Returns the cause of this {@code Throwable} or {@code null} if the + * cause is nonexistent or unknown. + * + * @return the cause of this {@code Throwable} or {@code null} if the + * cause is nonexistent or unknown (the cause is the throwable that caused + * this throwable to get thrown). + */ + public Throwable getCause() { + if (mCause == this) { + return null; + } + return mCause; + } + + /** + * Initializes this ConversionException with the given cause. + * + * @param pCause The Throwable that caused this exception + * + * @throws IllegalStateException if cause is allready set + * @throws IllegalArgumentException if {@code pCause == this} + */ + public Throwable initCause(Throwable pCause) { + if (mCause != this) { + throw new IllegalStateException("Can't overwrite cause"); + } + if (pCause == this) { + throw new IllegalArgumentException("Can't be caused by self"); + } + + mCause = pCause; + + return this; + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/Converter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/Converter.java new file mode 100755 index 00000000..b95d82d3 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/Converter.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.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 entrypoint 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 alltogheter? +// TODO: ServiceRegistry for registering 3rd party converters +// TODO: URI scheme, for implicit typing? Is that a good idea? +public abstract class Converter implements PropertyConverter { + + /** Our singleton instance */ + protected static Converter sInstance = new ConverterImpl(); // Thread safe & EASY + + /** The conveters Map */ + protected Map mConverters = new Hashtable(); + + // 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(Class pType, PropertyConverter pConverter) { + getInstance().mConverters.put(pType, pConverter); + } + + /** + * Unregisters a converter for a given type. That is, making it unavailable + * for the converter framework, and making it (potentially) available for + * garbabe collection. + * + * @param pType the (super) type to remove converter for + * + * @see #registerConverter(Class,PropertyConverter) + */ + public static void unregisterConverter(Class pType) { + getInstance().mConverters.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(String pString, 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(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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/ConverterImpl.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/ConverterImpl.java new file mode 100755 index 00000000..dba48dc5 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/ConverterImpl.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.convert; + +/** + * The converter (singleton). Converts strings to objects and back. + * This is the entrypoint to the converter framework. + * + * @see #registerConverter(Class, PropertyConverter) + * + * @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/ConverterImpl.java#1 $ + */ +class ConverterImpl extends Converter { + + /** + * Creates a Converter. + */ + ConverterImpl() { + } + + /** + * Gets the registered converter for the given type. + * + * @param pType the type to convert to + * @return an instance of a {@code PropertyConverter} or {@code null} + */ + private PropertyConverter getConverterForType(Class pType) { + Object converter; + Class cl = pType; + + // Loop until we find a suitable converter + do { + // Have a match, return converter + if ((converter = getInstance().mConverters.get(cl)) != null) { + return (PropertyConverter) converter; + } + + } + while ((cl = cl.getSuperclass()) != null); + + // No converter found, return null + return 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 vonversion format + * + * @return the object created from the given string. + * + * @throws ConversionException if the string cannot be converted for any + * reason. + */ + public Object toObject(String pString, Class pType, String pFormat) + throws ConversionException { + + if (pString == null) { + return null; + } + + if (pType == null) { + throw new MissingTypeException(); + } + + // Get converter + PropertyConverter converter = getConverterForType(pType); + + if (converter == null) { + throw new NoAvailableConverterException("Cannot convert to object, no converter available for type \"" + pType.getName() + "\""); + } + + // Convert and return + return converter.toObject(pString, pType, pFormat); + } + + /** + * Converts the object to a string, using {@code object.toString()} + * + * @param pBean the object to convert + * @param pFormat the 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 String toString(Object pBean, String pFormat) + throws ConversionException { + if (pBean == null) { + return null; + } + + // Get converter + PropertyConverter converter = getConverterForType(pBean.getClass()); + + if (converter == null) { + throw new NoAvailableConverterException("Cannot object to string, no converter available for type \"" + pBean.getClass().getName() + "\""); + } + + // Convert and return string + return converter.toString(pBean, pFormat); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/DateConverter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/DateConverter.java new file mode 100755 index 00000000..ca94503e --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/DateConverter.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.convert; + +import com.twelvemonkeys.lang.*; + +import java.util.*; +import java.text.*; +import java.lang.reflect.InvocationTargetException; + +/** + * Converts strings to dates and back. + *

+ * This class has a static cache of {@code DateFormats}, to avoid + * creation and parsing of dateformats every time one is used. + * + * @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); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java new file mode 100755 index 00000000..c80f6cdb --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/DefaultConverter.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.convert; + +import com.twelvemonkeys.lang.*; + +import java.lang.reflect.*; + +/** + * 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(String pString, final Class pType, String pFormat) + throws ConversionException { + + if (pString == null) { + return null; + } + + if (pType == null) { + throw new MissingTypeException(); + } + + // Primitive -> wrapper + Class type; + if (pType == Boolean.TYPE) { + type = Boolean.class; + } + else { + type = 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("Could not convert String to " + pType.getName() + ": No constructor " + type.getName() + "(String) or static " + type.getName() + ".valueof(String) method found!"); + } + } + + return value; + } + catch (InvocationTargetException ite) { + throw new ConversionException(ite.getTargetException()); + } + catch (RuntimeException rte) { + throw new ConversionException(rte); + } + } + + /** + * 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(Object pObject, String pFormat) + throws ConversionException { + + try { + return (pObject != null ? pObject.toString() : null); + } + catch (RuntimeException rte) { + throw new ConversionException(rte); + } + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/MissingTypeException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/MissingTypeException.java new file mode 100755 index 00000000..e9b2da3f --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/MissingTypeException.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.convert; + +/** + * This exception may be thrown by {@code PropertyConverter}s, if a conversion is + * attempted without type (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/MissingTypeException.java#2 $ + */ +public class MissingTypeException extends ConversionException { + + /** + * Creates a {@code MissingTypeException} with no error message. + */ + public MissingTypeException() { + super("Cannot convert, missing type"); + } + + /** + * Creates a {@code MissingTypeException} with the given error message. + * + * @param pMessage the exception message + */ + public MissingTypeException(String pMessage) { + super(pMessage); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/NoAvailableConverterException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/NoAvailableConverterException.java new file mode 100755 index 00000000..42631321 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/NoAvailableConverterException.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.convert; + +/** + * This exception may be thrown by {@code PropertyConverter}s, if a conversion + * is attempted for a type (class), that has no registered {@code PropertyConverter}. + * + * @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/NoAvailableConverterException.java#2 $ + */ + +public class NoAvailableConverterException extends ConversionException { + + /** + * Creates a {@code NoAvailableConverterException} with no error message. + */ + public NoAvailableConverterException() { + super("Cannot convert, no converter available for given type"); + } + + /** + * Creates a {@code NoAvailableConverterException} with the given error message. + * + * @param pMessage the exception message + */ + public NoAvailableConverterException(String pMessage) { + super(pMessage); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/NumberConverter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/NumberConverter.java new file mode 100755 index 00000000..8a3b586b --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/NumberConverter.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.convert; + +import com.twelvemonkeys.lang.*; +import com.twelvemonkeys.util.LRUHashMap; + +import java.util.*; +import java.math.*; +import java.text.*; + +/** + * Converts strings to numbers and back. + *

+ * This class has a static cache of {@code NumberFormats}, to avoid + * creation and parsing of numberformats every time one is used. + * + * @author Harald Kuhr + * @author last modified by $Author: haku $ + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/NumberConverter.java#2 $ + */ +public class NumberConverter implements PropertyConverter { + // TODO: Need to either make this non-local aware, or document that it is... + + 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 = NumberFormat.getNumberInstance(); + 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 NumberFormat.getNumberInstance().format(pObject); + 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/PropertyConverter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/PropertyConverter.java new file mode 100755 index 00000000..ae1bf51b --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/PropertyConverter.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.convert; + +/** + * Converts strings to objects and back. + * + * @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/PropertyConverter.java#1 $ + */ +public interface PropertyConverter { + + /** + * Converts the string to an object, using the given format for parsing. + * + * @param pString the string to convert + * @param pType the type to convert to. {@code PropertyConverter} + * implementations may choose to ignore this parameter. + * @param pFormat the format used for parsing. {@code 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. + * + * @throws ConversionException if the string could not be converted to the + * specified type and format. + */ + public Object toObject(String pString, Class pType, String pFormat) + throws ConversionException; + + /** + * Converts the object to a string, using the given format + * + * @param pObject the object to convert + * @param pFormat the format used for parsing. {@code 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 string could not be converted to the + * specified type and format. + */ + public String toString(Object pObject, String pFormat) + throws ConversionException; +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/TimeConverter.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/TimeConverter.java new file mode 100755 index 00000000..0a6fd5a6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/TimeConverter.java @@ -0,0 +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 "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.convert; + +import com.twelvemonkeys.lang.*; +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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/TypeMismathException.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/TypeMismathException.java new file mode 100755 index 00000000..acc8c0f6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/TypeMismathException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.convert; + +/** + * This exception may be thrown by {@code PropertyConverter}s, if a conversion + * is attempted on the wrong type (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/TypeMismathException.java#1 $ + */ +public class TypeMismathException extends ConversionException { + + /** + * Creates a {@code TypeMismathException}. + * + * @param pWrongType the type we couldn't convert + */ + public TypeMismathException(Class pWrongType) { + super("Wrong type for conversion: " + pWrongType.getName()); + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/pacakge.html b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/pacakge.html new file mode 100755 index 00000000..7afb85e6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/convert/pacakge.html @@ -0,0 +1,8 @@ + + + +Provides a general purpose conversion framework, for conversion of values +between string representations and objects. + + + diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/package.html b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/package.html new file mode 100755 index 00000000..ca1379e6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/package.html @@ -0,0 +1,7 @@ + + + +Provides miscellaneous utility classes. + + + diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/REWildcardStringParser.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/REWildcardStringParser.java new file mode 100755 index 00000000..9a1c1b9d --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/REWildcardStringParser.java @@ -0,0 +1,398 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.regex; + +import com.twelvemonkeys.util.DebugUtil; + +import java.io.PrintStream; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * 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 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? + * + *

+ * + * This class uses the Regexp package from Apache's Jakarta Project, links below. + * + *


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

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


+ * + * @author Eirik Torske + * @see Jakarta Regexp + * @see {@code org.apache.regexp.RE} + * @see com.twelvemonkeys.util.regex.WildcardStringParser + * + * @todo Rewrite to use this regex package, and not Jakarta directly! + */ +public class REWildcardStringParser /*extends EntityObject*/ { + + // Constants + + /** Field ALPHABET */ + public static final char[] ALPHABET = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'æ', + 'ø', 'å', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'N', 'M', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', 'Æ', 'Ø', 'Å', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '_', '-' + }; + + /** Field FREE_RANGE_CHARACTER */ + public static final char FREE_RANGE_CHARACTER = '*'; + + /** Field FREE_PASS_CHARACTER */ + public static final char FREE_PASS_CHARACTER = '?'; + + // Members + Pattern mRegexpParser; + String mStringMask; + boolean mInitialized = false; + int mTotalNumberOfStringsParsed; + boolean mDebugging; + PrintStream out; + + // Properties + // Constructors + + /** + * Creates a wildcard string parser. + *

+ * @param pStringMask the wildcard string mask. + */ + public REWildcardStringParser(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 REWildcardStringParser(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 REWildcardStringParser(final String pStringMask, final boolean pDebugging, final PrintStream pDebuggingPrintStream) { + + this.mStringMask = pStringMask; + this.mDebugging = pDebugging; + this.out = pDebuggingPrintStream; + mInitialized = buildRegexpParser(); + } + + // Methods + + /** + * Converts wildcard string mask to regular expression. + * This method should reside in som utility class, but I don't know how proprietary the regular expression format is... + * @return the corresponding regular expression or {@code null} if an error occurred. + */ + private String convertWildcardExpressionToRegularExpression(final String pWildcardExpression) { + + if (pWildcardExpression == null) { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "wildcard expression is null - also returning null as regexp!"); + } + return null; + } + StringBuilder regexpBuffer = new StringBuilder(); + boolean convertingError = false; + + for (int i = 0; i < pWildcardExpression.length(); i++) { + if (convertingError) { + return null; + } + + // Free-range character '*' + char stringMaskChar = pWildcardExpression.charAt(i); + + if (isFreeRangeCharacter(stringMaskChar)) { + regexpBuffer.append("(([a-åA-Å0-9]|.|_|-)*)"); + } + + // Free-pass character '?' + else if (isFreePassCharacter(stringMaskChar)) { + regexpBuffer.append("([a-åA_Å0-9]|.|_|-)"); + } + + // Valid characters + else if (isInAlphabet(stringMaskChar)) { + regexpBuffer.append(stringMaskChar); + } + + // Invalid character - aborting + else { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + + "one or more characters in string mask are not legal characters - returning null as regexp!"); + } + convertingError = true; + } + } + return regexpBuffer.toString(); + } + + /** + * Builds the regexp parser. + */ + private boolean buildRegexpParser() { + + // Convert wildcard string mask to regular expression + String regexp = convertWildcardExpressionToRegularExpression(mStringMask); + + if (regexp == null) { + out.println(DebugUtil.getPrefixErrorMessage(this) + + "irregularity in regexp conversion - now not able to parse any strings, all strings will be rejected!"); + return false; + } + + // Instantiate a regular expression parser + try { + mRegexpParser = Pattern.compile(regexp); + } + catch (PatternSyntaxException e) { + if (mDebugging) { + out.println(DebugUtil.getPrefixErrorMessage(this) + "RESyntaxException \"" + e.getMessage() + + "\" caught - now not able to parse any strings, all strings will be rejected!"); + } + if (mDebugging) { + e.printStackTrace(System.err); + } + return false; + } + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "regular expression parser from regular expression " + regexp + + " extracted from wildcard string mask " + mStringMask + "."); + } + return true; + } + + /** + * Simple check of the string to be parsed. + */ + private boolean checkStringToBeParsed(final String pStringToBeParsed) { + + // Check for nullness + if (pStringToBeParsed == null) { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "string to be parsed is null - rejection!"); + } + return false; + } + + // Check if valid character (element in alphabet) + for (int i = 0; i < pStringToBeParsed.length(); i++) { + if (!isInAlphabet(pStringToBeParsed.charAt(i))) { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + + "one or more characters in string to be parsed are not legal characters - rejection!"); + } + return false; + } + } + 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 mStringMask; + } + + /** + * Parses a string. + *

+ * + * @param pStringToBeParsed + * @return {@code true} if and only if the string are accepted by the parser. + */ + public boolean parseString(final String pStringToBeParsed) { + + if (mDebugging) { + out.println(); + } + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "parsing \"" + pStringToBeParsed + "\"..."); + } + + // Update statistics + mTotalNumberOfStringsParsed++; + + // Check string to be parsed + if (!checkStringToBeParsed(pStringToBeParsed)) { + return false; + } + + // Perform parsing and return accetance/rejection flag + if (mInitialized) { + return mRegexpParser.matcher(pStringToBeParsed).matches(); + } else { + out.println(DebugUtil.getPrefixErrorMessage(this) + "trying to use non-initialized parser - string rejected!"); + } + return false; + } + + /* + * Overriding mandatory methods from EntityObject's. + */ + + /** + * Method toString + * + * + * @return + * + */ + public String toString() { + + StringBuilder buffer = new StringBuilder(); + + buffer.append(DebugUtil.getClassName(this)); + buffer.append(": String mask "); + buffer.append(mStringMask); + buffer.append("\n"); + return buffer.toString(); + } + + // Just taking the lazy, easy and dangerous way out + + /** + * Method equals + * + * + * @param pObject + * + * @return + * + */ + public boolean equals(Object pObject) { + + if (pObject instanceof REWildcardStringParser) { + REWildcardStringParser externalParser = (REWildcardStringParser) pObject; + + return (externalParser.mStringMask == this.mStringMask); + } + return ((Object) this).equals(pObject); + } + + // Just taking the lazy, easy and dangerous way out + + /** + * Method hashCode + * + * + * @return + * + */ + public int hashCode() { + return ((Object) this).hashCode(); + } + + protected Object clone() throws CloneNotSupportedException { + return new REWildcardStringParser(mStringMask); + } + + // Just taking the lazy, easy and dangerous way out + protected void finalize() throws Throwable {} +} + + +/*--- 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/RegExTokenIterator.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/RegExTokenIterator.java new file mode 100755 index 00000000..0dbfa61b --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/RegExTokenIterator.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.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 mMatcher; + private boolean mNext = 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"); + } + + mMatcher = Pattern.compile(pPattern).matcher(pString); + } + + /** + * Resets this iterator. + * + */ + public void reset() { + mMatcher.reset(); + } + + public boolean hasNext() { + return mNext || (mNext = mMatcher.find()); + } + + public String next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + mNext = false; + return mMatcher.group(); + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/WildcardStringParser.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/WildcardStringParser.java new file mode 100755 index 00000000..a58d47b6 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/WildcardStringParser.java @@ -0,0 +1,803 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.regex; + +import com.twelvemonkeys.util.DebugUtil; + +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 + * @see com.twelvemonkeys.util.regex.REWildcardStringParser + */ +public class WildcardStringParser { + + // Constants + + /** Field ALPHABET */ + public static final char[] ALPHABET = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'æ', + 'ø', 'å', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'N', 'M', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', + 'Z', 'Æ', 'Ø', 'Å', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', '_', '-' + }; + + /** Field FREE_RANGE_CHARACTER */ + public static final char FREE_RANGE_CHARACTER = '*'; + + /** Field FREE_PASS_CHARACTER */ + public static final char FREE_PASS_CHARACTER = '?'; + + // Members + boolean mInitialized; + String mStringMask; + WildcardStringParserState mInitialState; + int mTotalNumberOfStringsParsed; + boolean mDebugging; + 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.mStringMask = pStringMask; + this.mDebugging = pDebugging; + this.out = pDebuggingPrintStream; + mInitialized = buildAutomaton(); + } + + // Methods + private boolean checkIfStateInWildcardRange(WildcardStringParserState pState) { + + WildcardStringParserState runnerState = pState; + + while (runnerState.mPreviousState != null) { + runnerState = runnerState.mPreviousState; + if (isFreeRangeCharacter(runnerState.mChar)) { + return true; + } + if (!isFreePassCharacter(runnerState.mChar)) { + return false; + } // If free-pass char '?' - move on + } + return false; + } + + private boolean checkIfLastFreeRangeState(WildcardStringParserState pState) { + + if (isFreeRangeCharacter(pState.mChar)) { + return true; + } + if (isFreePassCharacter(pState.mChar)) { + 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 < mStringMask.length(); i++) { + if (!isFreeRangeCharacter(mStringMask.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 ((mStringMask != null) && (mStringMask.length() > 0)) { + newState = new WildcardStringParserState(mStringMask.charAt(0)); + newState.mAutomatonStateNumber = 0; + newState.mPreviousState = null; + if (checkIfLastFreeRangeState(newState)) { + lastFreeRangeState = newState; + } + runnerState = newState; + mInitialState = runnerState; + mInitialState.mAutomatonStateNumber = 0; + } else { + System.err.println(DebugUtil.getPrefixErrorMessage(this) + "string mask provided are null or empty - aborting!"); + return false; + } + + // Create the rest of the automaton + for (int i = 1; i < mStringMask.length(); i++) { + activeChar = mStringMask.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(DebugUtil.getPrefixErrorMessage(this) + + "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.mLastFreeRangeState = lastFreeRangeState; + + // Create next state, check if free-range state, set the state number and preceeding state + newState = new WildcardStringParserState(activeChar); + newState.mAutomatonStateNumber = i; + newState.mPreviousState = runnerState; + + // 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.mNextState = newState; + + // Move to the next state + runnerState = newState; + + // Special setting of the last free-range state for the last element + if (runnerState.mAutomatonStateNumber == mStringMask.length() - 1) { + runnerState.mLastFreeRangeState = lastFreeRangeState; + } + } + + // Initiate some statistics + mTotalNumberOfStringsParsed = 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 mStringMask; + } + + /** + * 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 (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "parsing \"" + pStringToParse + "\"..."); + } + + // Update statistics + mTotalNumberOfStringsParsed++; + + // Check string to be parsed for nullness + if (pStringToParse == null) { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "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 (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + + "one or more characters in string to be parsed are not legal characters - rejection!"); + } + return false; + } + + // Check if automaton is correctly initialized + if (!mInitialized) { + System.err.println(DebugUtil.getPrefixErrorMessage(this) + "automaton is not initialized - rejection!"); + return false; + } + + // Check if automaton is trivial (accepts all strings) + if (isTrivialAutomaton()) { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + + "automaton represents a trivial string mask (accepts all strings) - acceptance!"); + } + return true; + } + + // Check if string to be parsed is empty + if (parsableString.isEmpty()) { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "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.mCharArray[0] == mInitialState.mChar) || isWildcardCharacter(mInitialState.mChar)) { + runnerState = mInitialState; + parsableString.mIndex = 0; + } else { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "cannot enter first automaton state - rejection!"); + } + return false; + } + + // Initialize the free-pass character state visited count + if (isFreePassCharacter(runnerState.mChar)) { + numberOfFreePassCharactersRead_SinceLastFreePassState++; + } + + // Perform parsing according to the rules above + for (int i = 0; i < parsableString.length(); i++) { + if (mDebugging) { + out.println(); + } + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "parsing - index number " + i + ", active char: '" + + parsableString.getActiveChar() + "' char string index: " + parsableString.mIndex + + " number of chars since last free-range state: " + numberOfParsedCharactersRead_SinceLastFreePassState); + } + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "parsing - state: " + runnerState.mAutomatonStateNumber + " '" + + runnerState.mChar + "' - no of free-pass chars read: " + numberOfFreePassCharactersRead_SinceLastFreePassState); + } + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "parsing - hasPerformedFreeRangeMovement: " + hasPerformedFreeRangeMovement); + } + if (runnerState.mNextState == null) { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "parsing - runnerState.mNextState == null"); + } + + // If there are no subsequent state (final state) and the state represents '*' - acceptance! + if (isFreeRangeCharacter(runnerState.mChar)) { + + // Special free-range skipping check + if (hasPerformedFreeRangeMovement) { + if (parsableString.reachedEndOfString()) { + if (numberOfFreePassCharactersRead_SinceLastFreePassState > numberOfParsedCharactersRead_SinceLastFreePassState) { + if (mDebugging) { + out.println( + DebugUtil.getPrefixDebugMessage(this) + + "no subsequent state (final state) and the state represents '*' - end of parsing string, but not enough characters read - rejection!"); + } + return false; + } else { + if (mDebugging) { + out.println( + DebugUtil.getPrefixDebugMessage(this) + + "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 (mDebugging) { + out.println( + DebugUtil.getPrefixDebugMessage(this) + + "no subsequent state (final state) and the state represents '*' - not the end of parsing string and not enough characters read - read next character"); + } + parsableString.mIndex++; + numberOfParsedCharactersRead_SinceLastFreePassState++; + } else { + if (mDebugging) { + out.println( + DebugUtil.getPrefixDebugMessage(this) + + "no subsequent state (final state) and the state represents '*' - not the end of parsing string, but enough characters read - acceptance!"); + } + return true; + } + } + } else { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + + "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 (mDebugging) { + out.println( + DebugUtil.getPrefixDebugMessage(this) + + "no subsequent state (final state) and skipping has been performed and end of parsing string, but not enough characters read - rejection!"); + } + return false; + } + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + + "no subsequent state (final state) and the end of the string to test is reached - acceptance!"); + } + return true; + } else { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "parsing - escaping process..."); + } + } + } else { + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "parsing - runnerState.mNextState != null"); + } + + // Special Case: + // If this state represents '*' - go to the rightmost state representing '?'. + // This state will act as an '*' - except that you only can go to the next state or accept the string, if and only if the number of '?' read are equal or less than the number of character read from the parsing string. + if (isFreeRangeCharacter(runnerState.mChar)) { + numberOfFreePassCharactersRead_SinceLastFreePassState = 0; + numberOfParsedCharactersRead_SinceLastFreePassState = 0; + WildcardStringParserState freeRangeRunnerState = runnerState.mNextState; + + while ((freeRangeRunnerState != null) && (isFreePassCharacter(freeRangeRunnerState.mChar))) { + runnerState = freeRangeRunnerState; + hasPerformedFreeRangeMovement = true; + numberOfFreePassCharactersRead_SinceLastFreePassState++; + freeRangeRunnerState = freeRangeRunnerState.mNextState; + } + + // Special Case: if the mask is at the end + if (runnerState.mNextState == null) { + if (mDebugging) { + out.println(); + } + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "parsing - index number " + i + ", active char: '" + + parsableString.getActiveChar() + "' char string index: " + parsableString.mIndex + + " number of chars since last free-range state: " + numberOfParsedCharactersRead_SinceLastFreePassState); + } + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "parsing - state: " + runnerState.mAutomatonStateNumber + " '" + + runnerState.mChar + "' - no of free-pass chars read: " + numberOfFreePassCharactersRead_SinceLastFreePassState); + } + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "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.mNextState.mChar)) { + runnerState = runnerState.mNextState; + parsableString.mIndex++; + numberOfParsedCharactersRead_SinceLastFreePassState++; + } + + // If the next state represents '?' - go to this next state + else if (isFreePassCharacter(runnerState.mNextState.mChar)) { + runnerState = runnerState.mNextState; + parsableString.mIndex++; + numberOfFreePassCharactersRead_SinceLastFreePassState++; + numberOfParsedCharactersRead_SinceLastFreePassState++; + } + + // If the next state represents the same character as the next character in the string to test - go to this next state + else if ((!parsableString.reachedEndOfString()) && (runnerState.mNextState.mChar == parsableString.getSubsequentChar())) { + runnerState = runnerState.mNextState; + parsableString.mIndex++; + numberOfParsedCharactersRead_SinceLastFreePassState++; + } + + // If the next character in the string to test does not coincide with the next state - go to the last state representing '*'. If there are none - rejection! + else if (runnerState.mLastFreeRangeState != null) { + runnerState = runnerState.mLastFreeRangeState; + parsableString.mIndex++; + numberOfParsedCharactersRead_SinceLastFreePassState++; + } else { + if (mDebugging) { + out.println( + DebugUtil.getPrefixDebugMessage(this) + + "the next state does not represent the same character as the next character in the string to test, and there are no last-free-range-state - rejection!"); + } + return false; + } + } + } + if (mDebugging) { + out.println(DebugUtil.getPrefixDebugMessage(this) + "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 (!mInitialized) { + buffer.append(DebugUtil.getClassName(this)); + buffer.append(": Not initialized properly!"); + buffer.append("\n"); + buffer.append("\n"); + } else { + WildcardStringParserState runnerState = mInitialState; + + buffer.append(DebugUtil.getClassName(this)); + buffer.append(": String mask "); + buffer.append(mStringMask); + buffer.append("\n"); + buffer.append("\n"); + buffer.append(" Automaton: "); + while (runnerState != null) { + buffer.append(runnerState.mAutomatonStateNumber); + buffer.append(": "); + buffer.append(runnerState.mChar); + buffer.append(" ("); + if (runnerState.mLastFreeRangeState != null) { + buffer.append(runnerState.mLastFreeRangeState.mAutomatonStateNumber); + } else { + buffer.append("-"); + } + buffer.append(")"); + if (runnerState.mNextState != null) { + buffer.append(" --> "); + } + runnerState = runnerState.mNextState; + } + buffer.append("\n"); + buffer.append(" Format: : ()"); + buffer.append("\n"); + buffer.append(" Number of strings parsed: " + mTotalNumberOfStringsParsed); + 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.mInitialized == this.mInitialized) && (externalParser.mStringMask == this.mStringMask)); + } + 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 (mInitialized) { + return new WildcardStringParser(mStringMask); + } + 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 mAutomatonStateNumber; + char mChar; + WildcardStringParserState mPreviousState; + WildcardStringParserState mNextState; + WildcardStringParserState mLastFreeRangeState; + + // Constructors + + /** + * Constructor WildcardStringParserState + * + * + * @param pChar + * + */ + public WildcardStringParserState(final char pChar) { + this.mChar = pChar; + } + + // Methods + // Debug + } + + /** + * A simple holder class for a string to be parsed. + */ + class ParsableString { + + // Constants + // Members + char[] mCharArray; + int mIndex; + + // Constructors + ParsableString(final String pStringToParse) { + + if (pStringToParse != null) { + mCharArray = pStringToParse.toCharArray(); + } + mIndex = -1; + } + + // Methods + boolean reachedEndOfString() { + + //System.out.println(DebugUtil.DEBUG + DebugUtil.getClassName(this) + ": mIndex :" + mIndex); + //System.out.println(DebugUtil.DEBUG + DebugUtil.getClassName(this) + ": mCharArray.length :" + mCharArray.length); + return mIndex == mCharArray.length - 1; + } + + int length() { + return mCharArray.length; + } + + char getActiveChar() { + + if ((mIndex > -1) && (mIndex < mCharArray.length)) { + return mCharArray[mIndex]; + } + System.err.println(DebugUtil.ERROR + DebugUtil.getClassName(this) + ": trying to access character outside character array!"); + return ' '; + } + + char getSubsequentChar() { + + if ((mIndex > -1) && (mIndex + 1 < mCharArray.length)) { + return mCharArray[mIndex + 1]; + } + System.err.println(DebugUtil.ERROR + DebugUtil.getClassName(this) + ": 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 < mCharArray.length; i++) { + if (!WildcardStringParser.isInAlphabet(mCharArray[i])) { + return false; + } + } + } + return true; + } + + boolean isEmpty() { + return ((mCharArray == null) || (mCharArray.length == 0)); + } + + /** + * Method toString + * + * + * @return + * + */ + public String toString() { + return new String(mCharArray); + } + } +} + + +/*--- 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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/package.html b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/package.html new file mode 100755 index 00000000..7b46fe76 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/regex/package.html @@ -0,0 +1,10 @@ + + + +Provides functionality for regular expressions. This package contains a drop-in +replacement for java.util.regex that will work on JDK1.2+. +Plese see {@link Pattern} for more information on runtime dependencies and +configuration. + + + \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/RegisterableService.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/RegisterableService.java new file mode 100755 index 00000000..1c666ef2 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/RegisterableService.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceConfigurationError.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceConfigurationError.java new file mode 100755 index 00000000..0aa281f0 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceConfigurationError.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceRegistry.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceRegistry.java new file mode 100755 index 00000000..e6ce626d --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceRegistry.java @@ -0,0 +1,546 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.util.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: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/ServiceRegistry.java#2 $ + * @see RegisterableService + * @see JAR File Specification + */ +public class ServiceRegistry { + // TODO: Security issues? + // TODO: Application contexts? + + /** + * "META-INF/services/" + */ + public static final String SERVICES = "META-INF/services/"; + + // Class to CategoryRegistry mapping + private final Map, CategoryRegistry> mCategoryMap; + + /** + * 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 + mCategoryMap = 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 occured 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 classloader 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 = mCategoryMap.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 mCategoryMap.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, deregisters + * {@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).contatins(pProvider); + } + }) { + Class mCurrent; + + public Class next() { + return (mCurrent = super.next()); + } + + public void remove() { + if (mCurrent == null) { + throw new IllegalStateException("No current element"); + } + getRegistry(mCurrent).deregister(pProvider); + mCurrent = 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 = mCategoryMap.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 + * @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); + } + + /** + * Deregisters 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 + * @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 mCategory; + private final Map mProviders = new LinkedHashMap(); + + CategoryRegistry(Class pCategory) { + Validate.notNull(pCategory, "category"); + mCategory = pCategory; + } + + private void checkCategory(final Object pProvider) { + if (!mCategory.isInstance(pProvider)) { + throw new IllegalArgumentException(pProvider + " not instance of category " + mCategory.getName()); + } + } + + public boolean register(final T pProvider) { + checkCategory(pProvider); + + // NOTE: We only register the new instance, if we don't allready + // have an instance of pProvider's class. + if (!contatins(pProvider)) { + mProviders.put(pProvider.getClass(), pProvider); + processRegistration(pProvider); + return true; + } + + return false; + } + + void processRegistration(final T pProvider) { + if (pProvider instanceof RegisterableService) { + RegisterableService service = (RegisterableService) pProvider; + service.onRegistration(ServiceRegistry.this, mCategory); + } + } + + 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 = mProviders.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, mCategory); + } + } + + public boolean contatins(final Object pProvider) { + return mProviders.containsKey(pProvider.getClass()); + } + + 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 = mProviders.values().iterator(); + return new Iterator() { + T mCurrent; + + public boolean hasNext() { + return iterator.hasNext(); + + } + + public T next() { + return (mCurrent = iterator.next()); + } + + public void remove() { + iterator.remove(); + processDeregistration(mCurrent); + } + }; + } + } + + @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/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/package.html b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/package.html new file mode 100755 index 00000000..704ccbda --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/util/service/package.html @@ -0,0 +1,10 @@ + + + +Provides a service provider registry. +

+This package contains a service provider registry, as specified in the +JAR File Specification. + + + diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/DOMSerializer.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/DOMSerializer.java new file mode 100755 index 00000000..25e27cc0 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/DOMSerializer.java @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.xml; + +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.DOMConfiguration; +import org.w3c.dom.DOMImplementationList; +import org.w3c.dom.bootstrap.DOMImplementationRegistry; +import org.w3c.dom.ls.DOMImplementationLS; +import org.w3c.dom.ls.LSOutput; +import org.w3c.dom.ls.LSSerializer; + +import java.io.OutputStream; +import java.io.Writer; + +/** + * {@code DOMImplementationLS} backed implementation. + * + * @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/xml/DOMSerializer.java#2 $ + */ +public final class DOMSerializer { + + private static final String PARAM_PRETTY_PRINT = "format-pretty-print"; + private static final String PARAM_XML_DECLARATION = "xml-declaration"; + + private final LSSerializer mSerializer; + private final LSOutput mOutput; + + private DOMSerializer() { + DOMImplementationLS domImpl = Support.getImplementation(); + mSerializer = domImpl.createLSSerializer(); + mOutput = domImpl.createLSOutput(); + } + + /** + * Creates a serializer using the given byte stream and encoding. + * + * @param pStream the byte stream. + * @param pEncoding the encoding. + * @throws IllegalStateException if no {@code DOMImplementation} with the right features can be instantiated. + */ + public DOMSerializer(final OutputStream pStream, final String pEncoding) { + this(); + + mOutput.setByteStream(pStream); + mOutput.setEncoding(pEncoding); + } + + /** + * Creates a serializer using the given character stream and encoding. + * + * @param pStream the characted stream. + * @throws IllegalStateException if no {@code DOMImplementation} with the right features can be instantiated. + */ + public DOMSerializer(final Writer pStream) { + this(); + + mOutput.setCharacterStream(pStream); + } + + /* + // TODO: Is it useful? + public void setNewLine(final String pNewLine) { + mSerializer.setNewLine(pNewLine); + } + + public String getNewLine() { + return mSerializer.getNewLine(); + } + */ + + /** + * Specifies wether the serializer should use indentation and optimize for + * readability. + *

+ * Note: This is a hint, and may be ignored by DOM implemenations. + * + * @param pPrettyPrint {@code true} to enable pretty printing + */ + public void setPrettyPrint(final boolean pPrettyPrint) { + DOMConfiguration configuration = mSerializer.getDomConfig(); + if (configuration.canSetParameter(PARAM_PRETTY_PRINT, pPrettyPrint)) { + configuration.setParameter(PARAM_PRETTY_PRINT, pPrettyPrint); + } + } + + public boolean getPrettyPrint() { + return Boolean.TRUE.equals(mSerializer.getDomConfig().getParameter(PARAM_PRETTY_PRINT)); + } + + private void setXMLDeclaration(boolean pXMLDeclaration) { + mSerializer.getDomConfig().setParameter(PARAM_XML_DECLARATION, pXMLDeclaration); + } + + /** + * Serializes the entire document. + * + * @param pDocument the document. + */ + public void serialize(final Document pDocument) { + serializeImpl(pDocument, true); + } + + /** + * Serializes the given node, along with any subnodes. + * Will not emit XML declaration. + * + * @param pNode the top node. + */ + public void serialize(final Node pNode) { + serializeImpl(pNode, false); + } + + private void serializeImpl(final Node pNode, final boolean pOmitDecl) { + setXMLDeclaration(pOmitDecl); + mSerializer.write(pNode, mOutput); + } + + private static class Support { + private final static DOMImplementationRegistry DOM_REGISTRY = createDOMRegistry(); + + static DOMImplementationLS getImplementation() { + DOMImplementationLS implementation = (DOMImplementationLS) DOM_REGISTRY.getDOMImplementation("LS 3.0"); + if (implementation == null) { + + DOMImplementationList list = DOM_REGISTRY.getDOMImplementationList(""); + System.err.println("DOM implementations (" + list.getLength() + "):"); + for (int i = 0; i < list.getLength(); i++) { + System.err.println(" " + list.item(i)); + } + + throw new IllegalStateException("Could not create DOM Implementation (no LS support found)"); + } + return implementation; + } + + private static DOMImplementationRegistry createDOMRegistry() { + try { + return DOMImplementationRegistry.newInstance(); + } + catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } + catch (InstantiationException e) { + throw new IllegalStateException(e); + } + catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + } + +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/XMLReader.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/XMLReader.java new file mode 100755 index 00000000..a10affe9 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/XMLReader.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.xml; + +import java.io.Reader; +import java.io.IOException; + +/** + * XMLReader + * + * @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/xml/XMLReader.java#1 $ + */ +public class XMLReader extends Reader { + // TODO: + // Create a reader backed by a pushback(?) inputstream + // Check for Unicode byte order marks + // Otherwise, use pi + // Or.. Just snatch the code form ROME.. ;-) + + public void close() throws IOException { + throw new UnsupportedOperationException("Method close not implemented");// TODO: Implement + } + + public int read(char cbuf[], int off, int len) throws IOException { + throw new UnsupportedOperationException("Method read not implemented");// TODO: Implement + } +} diff --git a/twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java new file mode 100755 index 00000000..e7b56244 --- /dev/null +++ b/twelvemonkeys-core/src/main/java/com/twelvemonkeys/xml/XMLSerializer.java @@ -0,0 +1,601 @@ +/* + * Copyright (c) 2008, Harald Kuhr + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name "TwelveMonkeys" nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package com.twelvemonkeys.xml; + +import com.twelvemonkeys.lang.StringUtil; +import org.w3c.dom.*; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.*; +import java.nio.charset.Charset; +import java.util.Date; + +/** + * XMLSerializer + * + * @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/xml/XMLSerializer.java#1 $ + */ +public class XMLSerializer { + // TODO: Replace with DOMSerializer? Test performance, pretty printing etc... + // Main problem: Sun's Java 5 does not have LS 3.0 support + // This class has no dependencies, which probably makes it more useful + + // TODO: Support line breaking (at configurable width) + // TODO: Support skipping XML declaration? + // TODO: Support standalone? + // TODO: Support more than version 1.0? + // TODO: Consider using IOException to communicate trouble, rather than RTE, + // to be more compatible... + // TODO: Support not inserting line-breaks, to preserve space + + // TODO: Idea: Create a SerializationContext that stores attributes on + // serialization, to keep the serialization thread-safe + // Store preserveSpace attribute in this context, to avoid costly traversals + // Store user options here too + // TODO: Push/pop? + + private final OutputStream mOutput; + private final Charset mEncoding; + private final SerializationContext mContext; + + public XMLSerializer(final OutputStream pOutput, final String pEncoding) { + mOutput = pOutput; + mEncoding = Charset.forName(pEncoding); + mContext = new SerializationContext(); + } + + public void setIndentation(String pIndent) { + mContext.indent = pIndent != null ? pIndent : " "; + } + + public void setStripComments(boolean pStrip) { + mContext.stripComments = pStrip; + } + + public void serialize(final Document pDocument) { + PrintWriter out = new PrintWriter(new OutputStreamWriter(mOutput, mEncoding)); + try { + writeXMLDeclararion(out); + writeXML(out, pDocument, mContext.copy()); + } + finally { + out.flush(); + } + } + + private void writeXMLDeclararion(final PrintWriter pOut) { + pOut.print(""); + } + + private void writeXML(final PrintWriter pOut, final Document pDocument, final SerializationContext pContext) { + writeNodeRecursive(pOut, pDocument, pContext); + } + + private void writeNodeRecursive(final PrintWriter pOut, final Node pNode, final SerializationContext pContext) { + if (pNode.getNodeType() != Node.TEXT_NODE) { + indentToLevel(pOut, pContext); + } + + switch (pNode.getNodeType()) { + case Node.DOCUMENT_NODE: + case Node.DOCUMENT_FRAGMENT_NODE: + writeDocument(pOut, pNode, pContext); + break; + case Node.DOCUMENT_TYPE_NODE: + writeDoctype(pOut, (DocumentType) pNode); + break; + case Node.ELEMENT_NODE: + boolean preserveSpace = pContext.preserveSpace; + updatePreserveSpace(pNode, pContext); + writeElement(pOut, (Element) pNode, pContext); + pContext.preserveSpace = preserveSpace; + break; + case Node.CDATA_SECTION_NODE: + writeCData(pOut, pNode); + break; + case Node.TEXT_NODE: + writeText(pOut, pNode, pContext); + break; + case Node.COMMENT_NODE: + writeComment(pOut, pNode, pContext); + break; + case Node.PROCESSING_INSTRUCTION_NODE: + writeProcessingInstruction(pOut, pNode); + break; + case Node.ATTRIBUTE_NODE: + throw new IllegalArgumentException("Malformed input Document: Attribute nodes should only occur inside Element nodes"); + case Node.ENTITY_NODE: + // '' + case Node.ENTITY_REFERENCE_NODE: + // ( '&' | '%' ) + getNodeName + ';' + case Node.NOTATION_NODE: + // '' + default: + throw new InternalError("Lazy programmer never implemented serialization of " + pNode.getClass()); + } + } + + private void writeProcessingInstruction(final PrintWriter pOut, final Node pNode) { + pOut.print("\n"); + } + + private void writeText(final PrintWriter pOut, final Node pNode, final SerializationContext pContext) { + // TODO: Is this really as specified? + String value = pNode.getNodeValue(); + if (pContext.preserveSpace) { + pOut.print(maybeEscapeElementValue(value)); + } + else if (!StringUtil.isEmpty(value)) { + indentToLevel(pOut, pContext); + pOut.println(maybeEscapeElementValue(value.trim())); + } + } + + private void writeCData(final PrintWriter pOut, final Node pNode) { + pOut.print(""); + } + + private static void updatePreserveSpace(final Node pNode, final SerializationContext pContext) { + NamedNodeMap attributes = pNode.getAttributes(); + if (attributes != null) { + Node space = attributes.getNamedItem("xml:space"); + if (space != null) { + if ("preserve".equals(space.getNodeValue())) { + pContext.preserveSpace = true; + } + else if ("default".equals(space.getNodeValue())) { + pContext.preserveSpace = false; + } + // No other values are allowed per spec, ingore + } + } + } + + private static void indentToLevel(final PrintWriter pOut, final SerializationContext pContext) { + for (int i = 0; i < pContext.level; i++) { + pOut.print(pContext.indent); + } + } + + private void writeComment(final PrintWriter pOut, final Node pNode, final SerializationContext pContext) { + if (pContext.stripComments) { + return; + } + + String value = pNode.getNodeValue(); + validateCommenValue(value); + + if (value.startsWith(" ")) { + pOut.print(""); + } + else { + pOut.println(" -->"); + } + } + + /** + * Returns an escaped version of the input string. The string is guaranteed + * to not contain illegal XML characters ({@code &<>}). + * If no escaping is needed, the input string is returned as is. + * + * @param pValue the input string that might need escaping. + * @return an escaped version of the input string. + */ + static String maybeEscapeElementValue(final String pValue) { + int startEscape = needsEscapeElement(pValue); + + if (startEscape < 0) { + // If no escpaing is needed, simply return original + return pValue; + } + else { + // Otherwise, start replacing + StringBuilder builder = new StringBuilder(pValue.substring(0, startEscape)); + builder.ensureCapacity(pValue.length() + 30); + + int pos = startEscape; + for (int i = pos; i < pValue.length(); i++) { + switch (pValue.charAt(i)) { + case '&': + pos = appendAndEscape(pValue, pos, i, builder, "&"); + break; + case '<': + pos = appendAndEscape(pValue, pos, i, builder, "<"); + break; + case '>': + pos = appendAndEscape(pValue, pos, i, builder, ">"); + break; + //case '\'': + // pos = appendAndEscape(pString, pos, i, builder, "'"); + // break; + //case '"': + // pos = appendAndEscape(pString, pos, i, builder, """); + // break; + default: + break; + } + } + + builder.append(pValue.substring(pos)); + return builder.toString(); + } + } + + private static int appendAndEscape(final String pString, int pStart, final int pEnd, final StringBuilder pBuilder, final String pEntity) { + pBuilder.append(pString.substring(pStart, pEnd)); + pBuilder.append(pEntity); + return pEnd + 1; + } + + /** + * Returns an the first index from the input string that should be escaped + * if escaping is needed, otherwise {@code -1}. + * + * @param pString the input string that might need escaping. + * @return the first index from the input string that should be escaped, + * or {@code -1}. + */ + private static int needsEscapeElement(final String pString) { + for (int i = 0; i < pString.length(); i++) { + switch (pString.charAt(i)) { + case '&': + case '<': + case '>': + //case '\'': + //case '"': + return i; + default: + } + } + return -1; + } + + private static String maybeEscapeAttributeValue(final String pValue) { + int startEscape = needsEscapeAttribute(pValue); + + if (startEscape < 0) { + return pValue; + } + else { + StringBuilder builder = new StringBuilder(pValue.substring(0, startEscape)); + builder.ensureCapacity(pValue.length() + 16); + + int pos = startEscape; + for (int i = pos; i < pValue.length(); i++) { + switch (pValue.charAt(i)) { + case '&': + pos = appendAndEscape(pValue, pos, i, builder, "&"); + break; + case '"': + pos = appendAndEscape(pValue, pos, i, builder, """); + break; + default: + break; + } + } + + //StringBuilder builder = new StringBuilder(pValue.length() + 30); + // + //int start = 0; + //while (end >= 0) { + // builder.append(pValue.substring(start, end)); + // builder.append("""); + // start = end + 1; + // end = pValue.indexOf('"', start); + //} + //builder.append(pValue.substring(start)); + + builder.append(pValue.substring(pos)); + + return builder.toString(); + } + } + + /** + * Returns an the first index from the input string that should be escaped + * if escaping is needed, otherwise {@code -1}. + * + * @param pString the input string that might need escaping. + * @return the first index from the input string that should be escaped, + * or {@code -1}. + */ + private static int needsEscapeAttribute(final String pString) { + for (int i = 0; i < pString.length(); i++) { + switch (pString.charAt(i)) { + case '&': + //case '<': + //case '>': + //case '\'': + case '"': + return i; + default: + } + } + return -1; + } + + private static String validateCDataValue(final String pValue) { + if (pValue.indexOf("]]>") >= 0) { + throw new IllegalArgumentException("Malformed input document: CDATA block may not contain the string ']]>'"); + } + return pValue; + } + + private static String validateCommenValue(final String pValue) { + if (pValue.indexOf("--") >= 0) { + throw new IllegalArgumentException("Malformed input document: Comment may not contain the string '--'"); + } + return pValue; + } + + private void writeDocument(final PrintWriter pOut, final Node pNode, final SerializationContext pContext) { + // Document fragments might not have child nodes... + if (pNode.hasChildNodes()) { + NodeList nodes = pNode.getChildNodes(); + for (int i = 0; i < nodes.getLength(); i++) { + writeNodeRecursive(pOut, nodes.item(i), pContext); + } + } + } + + private void writeElement(final PrintWriter pOut, final Element pNode, final SerializationContext pContext) { + pOut.print("<"); + pOut.print(pNode.getTagName()); + + // TODO: Attributes should probably include namespaces, so that it works + // even if the document was created using attributes instead of namespaces... + + // Handle namespace + String namespace = pNode.getNamespaceURI(); + if (namespace != null && !namespace.equals(pContext.defaultNamespace)) { + String prefix = pNode.getPrefix(); + if (prefix == null) { + pContext.defaultNamespace = namespace; + pOut.print(" xmlns"); + } + else { + pOut.print(" xmlns:"); + pOut.print(prefix); + } + pOut.print("=\""); + pOut.print(namespace); + pOut.print("\""); + } + + // Iterate attributes if any + if (pNode.hasAttributes()) { + NamedNodeMap attributes = pNode.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Attr attribute = (Attr) attributes.item(i); + String name = attribute.getName(); + if (!(name.startsWith("xmlns") && (name.length() == 5 || name.charAt(5) == ':'))) { + pOut.print(" "); + pOut.print(name); + pOut.print("=\""); + pOut.print(maybeEscapeAttributeValue(attribute.getValue())); + pOut.print("\""); + } + //else { + // System.err.println("attribute.getName(): " + name); + //} + } + } + + // Iterate children if any + if (pNode.hasChildNodes()) { + pOut.print(">"); + if (!pContext.preserveSpace) { + pOut.println(); + } + + NodeList children = pNode.getChildNodes(); + //pContext.level++; + for (int i = 0; i < children.getLength(); i++) { + writeNodeRecursive(pOut, children.item(i), pContext.push()); + } + //pContext.level--; + + if (!pContext.preserveSpace) { + indentToLevel(pOut, pContext); + } + + pOut.print(""); + } + else { + pOut.println("/>"); + } + + } + + private void writeDoctype(final PrintWriter pOut, final DocumentType pDoctype) { + // NOTE: The DOMImplementationLS LSSerializer actually inserts SYSTEM or + // PUBLIC identifiers even if they are empty strings. The result is, it + // will create invalid documents. + // Testing for empty strings seems to be more compatible. + if (pDoctype != null) { + pOut.print(""); + } + } + + public static void main(String[] pArgs) throws IOException, SAXException { + // Build XML tree (Document) and write + // Find the implementation + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + DocumentBuilder builder; + try { + builder = factory.newDocumentBuilder(); + } + catch (ParserConfigurationException e) { + throw (IOException) new IOException(e.getMessage()).initCause(e); + } + DOMImplementation dom = builder.getDOMImplementation(); + + Document document = dom.createDocument("http://www.twelvemonkeys.com/xml/test", "test", dom.createDocumentType("test", null, null)); + + Element root = document.getDocumentElement(); + + // This is probably not the correct way of setting a default namespace + //root.setAttribute("xmlns", "http://www.twelvemonkeys.com/xml/test"); + + // Create and insert the normal Properties headers as XML comments + document.insertBefore(document.createComment(new Date().toString()), root); + + Element test = document.createElement("sub"); + root.appendChild(test); + Element more = document.createElementNS("http://more.com/1999/namespace", "more:more"); + more.setAttribute("foo", "test"); + more.setAttribute("bar", "'really' \"legal\" & ok"); + test.appendChild(more); + more.appendChild(document.createTextNode("Simply some text.")); + more.appendChild(document.createCDATASection("&something escaped;")); + more.appendChild(document.createTextNode("More & !")); + more.appendChild(document.createTextNode("\"<<'&'>>\"")); + Element another = document.createElement("another"); + test.appendChild(another); + Element yet = document.createElement("yet-another"); + yet.setAttribute("this-one", "with-params"); + test.appendChild(yet); + + Element pre = document.createElementNS("http://www.twelvemonkeys.com/xml/test", "pre"); + pre.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve"); + pre.appendChild(document.createTextNode(" \t \n\r some text & white ' ' \n ")); + test.appendChild(pre); + + // Create serializer and output document + //XMLSerializer serializer = new XMLSerializer(pOutput, new OutputFormat(document, UTF_8_ENCODING, true)); + System.out.println("XMLSerializer:"); + XMLSerializer serializer = new XMLSerializer(System.out, "UTF-8"); + serializer.serialize(document); + System.out.println(); + + System.out.println("DOMSerializer:"); + DOMSerializer serializerD = new DOMSerializer(System.out, "UTF-8"); + serializerD.setPrettyPrint(true); + serializerD.serialize(document); + System.out.println(); + + System.out.println("\n"); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + XMLSerializer serializer2 = new XMLSerializer(out, "UTF-8"); + serializer2.serialize(document); + + ByteArrayOutputStream outD = new ByteArrayOutputStream(); + DOMSerializer serializer2D = new DOMSerializer(outD, "UTF-8"); + serializer2D.serialize(document); + + Document document2 = builder.parse(new ByteArrayInputStream(out.toByteArray())); + System.out.println("XMLSerializer reparsed XMLSerializer:"); + serializer.serialize(document2); + System.out.println(); + System.out.println("DOMSerializer reparsed XMLSerializer:"); + serializerD.serialize(document2); + System.out.println(); + + + Document documentD = builder.parse(new ByteArrayInputStream(outD.toByteArray())); + System.out.println("XMLSerializer reparsed DOMSerializer:"); + serializer.serialize(documentD); + System.out.println(); + System.out.println("DOMSerializer reparsed DOMSerializer:"); + serializerD.serialize(documentD); + System.out.println(); + } + + static class SerializationContext implements Cloneable { + String indent = " "; + int level = 0; + boolean preserveSpace = false; + boolean stripComments = false; + String defaultNamespace; + + public SerializationContext copy() { + try { + return (SerializationContext) clone(); + } + catch (CloneNotSupportedException e) { + throw new Error(e); + } + } + + public SerializationContext push() { + SerializationContext context = copy(); + context.level++; + return context; + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/main/resources/com/twelvemonkeys/net/MIMEUtil.properties b/twelvemonkeys-core/src/main/resources/com/twelvemonkeys/net/MIMEUtil.properties new file mode 100755 index 00000000..9598cc47 --- /dev/null +++ b/twelvemonkeys-core/src/main/resources/com/twelvemonkeys/net/MIMEUtil.properties @@ -0,0 +1,111 @@ +############################################################################## +# +# MIME type mappings for MIMEUtil. +# $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/main/resources/com/twelvemonkeys/net/MIMEUtil.properties#2 $ +# +############################################################################## +# +# Format: +# [','','..]=[';'';'..] +# +# If possible, the "default ext" and "default mime" must be the official file +# extension and MIME type respectively. +# +# See http://www.iana.org/assignments/media-types/ for the complete list. +# +# TODO: Change to this format: http://java.sun.com/javase/6/docs/api/javax/activation/MimetypesFileTypeMap.html + +# Application types +doc,dot=application/msword +bin,exe,arc,lha,lhx,lzx,zoo=application/octet-stream +arj=application/arj +pdf=application/pdf +ai,eps,ps=application/postscript +ppt=application/vnd.ms-powerpoint;application/x-powerpoint;application/powerpoint +csh=application/x-csh +gtar=application/x-gtar +gz,gzip,z=application/x-gzip;application/x-compressed +class=application/x-java-vm +ser=application/x-java-serialized-object +jar=application/x-java-archive +tex=application/x-tex;application/x-latex +texinfo,texi=application/x-texinfo +t,tr,roff=application/x-troff +sh=application/x-sh +tar=application/x-tar +zip=application/zip +hqx=application/binhex +swf=application/x-shockwave-flash +xls=application/vnd.ms-excel +prc,pdb,pqa,oprc=application/vnd.palm +ogg=application/ogg + +# Audio types +au,snd=audio/basic +wav=audio/x-wav;audio/wav +aiff,aif,aifc=audio/x-aiff +midi,mid=audio/x-midi +gsm,gsd=audio/x-gsm +mp3=audio/x-mpeg-3;audio/mpeg3 +ram,ra=audio/x-pn-realaudio;audio/x-realaudio + +# Image types +bmp=image/bmp;image/x-bmp;image/x-win-bmp +cgm=image/cgm +ras=image/cmu-raster;image/x-cmu-raster +gif=image/gif +ico=image/ico;image/x-icon +ief=image/ief +iff,ilbm=image/x-iff;image/iff +jpeg,jpg,jpe,jfif=image/jpeg;image/x-jpeg +jpm=image/jpm +png=image/png;image/x-png +# NOTE: image/svg-xml is an old reccomendation, should not be used +svg,svgz=image/svg+xml;image/svg-xml;image/x-svg +tga=image/targa;image/x-targa +tif,tiff=image/tiff;image/x-tiff +ras,rast=image/x-cmu-raster;image/cmu-raster +pict,pct,pic=image/x-pict;image/pict +pcd=image/x-pcd;image/pcd +pcx=image/x-pcx +#pdb=image/x-palm-db;image/palm-db +pnm=image/x-portable-anymap +pbm=image/x-portable-bitmap +pgm=image/x-portable-graymap +ppm=image/x-portable-pixmap +psd=image/x-psd;image/psd +wbmp=image/vnd.wap.wbmp;image/x-wbmp +wmf,emf=image/x-wmf;image/wmf;windows/metafile +xbm=image/x-xbitmap;image/x-xbm;image/xbm +xpm=image/x-xpixmap +xwd=image/x-windowdump + +# Message types +# No known file extensions + +# Model types +vrml;wrl;wrz=model/vrml;x-world/x-vrml + +# Multipart types +# No known file extensions + +# Text types +txt=text/plain +html,htm=text/html +xml=text/xml;application/xml +css=text/css +js=text/javascript +rtf,rtx=text/richtext;application/rtf;application/x-rtf +xhtml,xht=application/xhtml+xml +sgml,sgm=text/sgml;text/x-sgml +wml=vnd.wap.wml +wmls=vnd.wap.wmlscript +ics=text/calendar + +# Video types +mpg,mpeg,mpe=video/mpeg +qt,mov=video/quicktime +avi=video/x-msvideo;video/msvideo;video/avi +movie=video/x-sgi-movie +scm=video/x-scm +rv=video/vnd.rn-realvideo diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java new file mode 100755 index 00000000..57820f2b --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/image/ImageUtilTestCase.java @@ -0,0 +1,574 @@ + +package com.twelvemonkeys.image; + +import junit.framework.TestCase; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.awt.image.IndexColorModel; +import java.awt.image.RenderedImage; +import java.io.InputStream; +import java.lang.reflect.InvocationTargetException; + +/** + * Created by IntelliJ IDEA. + * + * @author $author wmhakur$ + * @version $id: $ + * To change this template use Options | File Templates. + */ +public class ImageUtilTestCase extends TestCase { + + private final static String IMAGE_NAME = "/sunflower.jpg"; + private BufferedImage mOriginal; + private BufferedImage mImage; + private Image mScaled; + + public ImageUtilTestCase() throws Exception { + mImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); + mScaled = mImage.getScaledInstance(5, 5, Image.SCALE_FAST); + + // Read image from class path + InputStream is = getClass().getResourceAsStream(IMAGE_NAME); + mOriginal = ImageIO.read(is); + + assertNotNull(mOriginal); + } + + /* + public void setUp() throws Exception { + mImage = new BufferedImage(10, 10, BufferedImage.TYPE_INT_ARGB); + mScaled = mImage.getScaledInstance(5, 5, Image.SCALE_FAST); + + // Read image from class path + InputStream is = ClassLoader.getSystemResourceAsStream(IMAGE_NAME); + mOriginal = ImageIO.read(is); + + assertNotNull(mOriginal); + } + + protected void tearDown() throws Exception { + mOriginal = null; + } + */ + + public void testToBufferedImageNull() { + BufferedImage img = null; + boolean threwRuntimeException = false; + + try { + img = ImageUtil.toBuffered((Image) null); + } + catch (RuntimeException ne) { + threwRuntimeException = true; + } + // No input should return null + assertNull(img); + + // Should have thrown an exception + assertTrue(threwRuntimeException); + } + + public void testToBufferedImageTypeNull() { + BufferedImage img = null; + boolean threwRuntimeException = false; + + try { + img = ImageUtil.toBuffered(null, BufferedImage.TYPE_INT_ARGB); + } + catch (RuntimeException ne) { + threwRuntimeException = true; + } + // No input should return null + assertNull(img); + + // Should have thrown an exception + assertTrue(threwRuntimeException); + } + + public void testImageIsNotBufferedImage() { + // Should not be a buffered image + assertFalse( + "FOR SOME IMPLEMENTATIONS THIS MIGHT FAIL!\nIn that case, testToBufferedImage() will fail too.", + mScaled instanceof BufferedImage + ); + } + + public void testToBufferedImage() { + BufferedImage sameAsImage = ImageUtil.toBuffered((RenderedImage) mImage); + BufferedImage bufferedScaled = ImageUtil.toBuffered(mScaled); + + // Should be no need to convert + assertSame(mImage, sameAsImage); + + // Should have same dimensions + assertEquals(mScaled.getWidth(null), bufferedScaled.getWidth()); + assertEquals(mScaled.getHeight(null), bufferedScaled.getHeight()); + + // Hmmm... + assertTrue(new Integer(42).equals(bufferedScaled.getProperty("lucky-number")) + || bufferedScaled.getPropertyNames() == null + || bufferedScaled.getPropertyNames().length == 0); + } + + public void testToBufferedImageType() { + // Assumes mImage is TYPE_INT_ARGB + BufferedImage converted = ImageUtil.toBuffered(mImage, BufferedImage.TYPE_BYTE_INDEXED); + BufferedImage convertedToo = ImageUtil.toBuffered(mImage, BufferedImage.TYPE_BYTE_BINARY); + + // Should not be the same + assertNotSame(mImage, converted); + assertNotSame(mImage, convertedToo); + + // Correct type + assertTrue(converted.getType() == BufferedImage.TYPE_BYTE_INDEXED); + assertTrue(convertedToo.getType() == BufferedImage.TYPE_BYTE_BINARY); + + // Should have same dimensions + assertEquals(mImage.getWidth(), converted.getWidth()); + assertEquals(mImage.getHeight(), converted.getHeight()); + + assertEquals(mImage.getWidth(), convertedToo.getWidth()); + assertEquals(mImage.getHeight(), convertedToo.getHeight()); + } + + public void testBrightness() { + final BufferedImage original = mOriginal; + assertNotNull(original); + + final BufferedImage notBrightened = ImageUtil.toBuffered(ImageUtil.brightness(original, 0f)); + // Assumed: Images should be equal + if (original != notBrightened) { // Don't care to test if images are same + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 0; x < original.getWidth(); x++) { + assertEquals(original.getRGB(x, y), notBrightened.getRGB(x, y)); + } + } + } + + // Assumed: All pixels should be brighter or equal to original + final BufferedImage brightened = ImageUtil.toBuffered(ImageUtil.brightness(original, 0.4f)); + final BufferedImage brightenedMore = ImageUtil.toBuffered(ImageUtil.brightness(original, 0.9f)); + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 0; x < original.getWidth(); x++) { + assertTrue(original.getRGB(x, y) <= brightened.getRGB(x, y)); + assertTrue(brightened.getRGB(x, y) <= brightenedMore.getRGB(x, y)); + } + } + + // Assumed: Image should be all white + final BufferedImage brightenedMax = ImageUtil.toBuffered(ImageUtil.brightness(original, 2f)); + for (int y = 0; y < brightenedMax.getHeight(); y++) { + for (int x = 0; x < brightenedMax.getWidth(); x++) { + assertEquals(0x00FFFFFF, brightenedMax.getRGB(x, y) & 0x00FFFFFF); + } + } + + // Assumed: All pixels should be darker or equal to originial + final BufferedImage brightenedNegative = ImageUtil.toBuffered(ImageUtil.brightness(original, -0.4f)); + final BufferedImage brightenedNegativeMore = ImageUtil.toBuffered(ImageUtil.brightness(original, -0.9f)); + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 0; x < original.getWidth(); x++) { + assertTrue(original.getRGB(x, y) >= brightenedNegative.getRGB(x, y)); + assertTrue(brightenedNegative.getRGB(x, y) >= brightenedNegativeMore.getRGB(x, y)); + } + } + // Assumed: Image should be all black + final BufferedImage brightenedMaxNegative = ImageUtil.toBuffered(ImageUtil.brightness(original, -2f)); + for (int y = 0; y < brightenedMaxNegative.getHeight(); y++) { + for (int x = 0; x < brightenedMaxNegative.getWidth(); x++) { + assertEquals(0x0, brightenedMaxNegative.getRGB(x, y) & 0x00FFFFFF); + } + } + + /* + JFrame frame = new JFrame("Sunflower - brightness"); + frame.setSize(sunflower.getWidth() * 4, sunflower.getHeight() * 2); + + Canvas canvas = new Canvas() { + public void paint(Graphics g) { + // Draw original for comparison + g.drawImage(original, 0, 0, null); + + // This should look like original + g.drawImage(notBrightened, 0, original.getHeight(), null); + + // Different versions + g.drawImage(brightened, original.getWidth(), 0, null); + g.drawImage(brightenedMore, original.getWidth() * 2, 0, null); + g.drawImage(brightenedMax, original.getWidth() * 3, 0, null); + + g.drawImage(brightenedNegative, original.getWidth(), original.getHeight(), null); + g.drawImage(brightenedNegativeMore, original.getWidth() * 2, original.getHeight(), null); + g.drawImage(brightenedMaxNegative, original.getWidth() * 3, original.getHeight(), null); + } + }; + + frame.getContentPane().add(canvas); + frame.setVisible(true); + + assertTrue(true); + */ + } + + + public void testContrast() { + final BufferedImage original = mOriginal; + + assertNotNull(original); + + final BufferedImage notContrasted = ImageUtil.toBuffered(ImageUtil.contrast(original, 0f)); + // Assumed: Images should be equal + if (original != notContrasted) { // Don't care to test if images are same + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 0; x < original.getWidth(); x++) { + assertEquals("0 constrast should not change image", original.getRGB(x, y), notContrasted.getRGB(x, y)); + } + } + } + + // Assumed: Contrast should be greater or equal to original + final BufferedImage contrasted = ImageUtil.toBuffered(ImageUtil.contrast(original)); + final BufferedImage contrastedDefault = ImageUtil.toBuffered(ImageUtil.contrast(original, 0.5f)); + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 0; x < original.getWidth(); x++) { + int oRGB = original.getRGB(x, y); + int cRGB = contrasted.getRGB(x, y); + int dRGB = contrastedDefault.getRGB(x, y); + + int oR = oRGB >> 16 & 0xFF; + int oG = oRGB >> 8 & 0xFF; + int oB = oRGB & 0xFF; + + int cR = cRGB >> 16 & 0xFF; + int cG = cRGB >> 8 & 0xFF; + int cB = cRGB & 0xFF; + + int dR = dRGB >> 16 & 0xFF; + int dG = dRGB >> 8 & 0xFF; + int dB = dRGB & 0xFF; + + // RED + if (oR < 127) { + assertTrue("Contrast should be decreased or same", oR >= cR && cR >= dR); + } + else { + assertTrue("Contrast should be increased or same", oR <= cR && cR <= dR); + } + // GREEN + if (oG < 127) { + assertTrue("Contrast should be decreased or same", oG >= cG && cG >= dG); + } + else { + assertTrue("Contrast should be increased or same", oG <= cG && cG <= dG); + } + // BLUE + if (oB < 127) { + assertTrue("Contrast should be decreased or same", oB >= cB && cB >= dB); + } + else { + assertTrue("Contrast should be increased or same", oB <= cB && cB <= dB); + } + + } + } + // Assumed: Only primary colors (w/b/r/g/b/c/y/m) + final BufferedImage contrastedMax = ImageUtil.toBuffered(ImageUtil.contrast(original, 1f)); + for (int y = 0; y < contrastedMax.getHeight(); y++) { + for (int x = 0; x < contrastedMax.getWidth(); x++) { + int rgb = contrastedMax.getRGB(x, y); + int r = rgb >> 16 & 0xFF; + int g = rgb >> 8 & 0xFF; + int b = rgb & 0xFF; + assertTrue("Max contrast should only produce primary colors", r == 0 || r == 255); + assertTrue("Max contrast should only produce primary colors", g == 0 || g == 255); + assertTrue("Max contrast should only produce primary colors", b == 0 || b == 255); + } + } + + // Assumed: Contrasts should be less than or equal to original + final BufferedImage contrastedNegative = ImageUtil.toBuffered(ImageUtil.contrast(original, -0.5f)); + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 0; x < original.getWidth(); x++) { + int oRGB = original.getRGB(x, y); + int cRGB = contrastedNegative.getRGB(x, y); + + int oR = oRGB >> 16 & 0xFF; + int oG = oRGB >> 8 & 0xFF; + int oB = oRGB & 0xFF; + + int cR = cRGB >> 16 & 0xFF; + int cG = cRGB >> 8 & 0xFF; + int cB = cRGB & 0xFF; + + // RED + if (oR >= 127) { + assertTrue("Contrast should be decreased or same", oR >= cR); + } + else { + assertTrue("Contrast should be increased or same", oR <= cR); + } + // GREEN + if (oG >= 127) { + assertTrue("Contrast should be decreased or same", oG >= cG); + } + else { + assertTrue("Contrast should be increased or same", oG <= cG); + } + // BLUE + if (oB >= 127) { + assertTrue("Contrast should be decreased or same", oB >= cB); + } + else { + assertTrue("Contrast should be increased or same", oB <= cB); + } + } + } + + // Assumed: All gray (127)! + final BufferedImage contrastedMoreNegative = ImageUtil.toBuffered(ImageUtil.contrast(original, -1.0f)); + for (int y = 0; y < contrastedMoreNegative.getHeight(); y++) { + for (int x = 0; x < contrastedMoreNegative.getWidth(); x++) { + int rgb = contrastedMoreNegative.getRGB(x, y); + int r = rgb >> 16 & 0xFF; + int g = rgb >> 8 & 0xFF; + int b = rgb & 0xFF; + assertTrue("Minimum contrast should be all gray", r == 127 && g == 127 &&b == 127); + } + } + + /* + JFrame frame = new JFrame("Sunflower - contrast"); + frame.setSize(sunflower.getWidth() * 4, sunflower.getHeight() * 2); + + Canvas canvas = new Canvas() { + public void paint(Graphics g) { + // Draw original for comparison + g.drawImage(original, 0, 0, null); + + // This should look like original + g.drawImage(notContrasted, 0, original.getHeight(), null); + + // Different versions + g.drawImage(contrasted, original.getWidth(), 0, null); + g.drawImage(contrastedDefault, original.getWidth() * 2, 0, null); + g.drawImage(contrastedMax, original.getWidth() * 3, 0, null); + g.drawImage(contrastedNegative, original.getWidth() * 2, original.getHeight(), null); + g.drawImage(contrastedMoreNegative, original.getWidth() * 3, original.getHeight(), null); + } + }; + + frame.getContentPane().add(canvas); + frame.setVisible(true); + + assertTrue(true); + */ + } + + public void testSharpen() { + final BufferedImage original = mOriginal; + + assertNotNull(original); + + final BufferedImage notSharpened = ImageUtil.sharpen(original, 0f); + // Assumed: Images should be equal + if (original != notSharpened) { // Don't care to test if images are same + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 0; x < original.getWidth(); x++) { + assertEquals("0 sharpen should not change image", original.getRGB(x, y), notSharpened.getRGB(x, y)); + } + } + } + + // Assumed: Difference between neighbouring pixels should increase for higher sharpen values + // Assumed: Dynamics of entire image should not change + final BufferedImage sharpened = ImageUtil.sharpen(original); + final BufferedImage sharpenedDefault = ImageUtil.sharpen(original, 0.3f); + final BufferedImage sharpenedMore = ImageUtil.sharpen(original, 1.3f); + + long diffOriginal = 0; + long diffSharpened = 0; + long diffDefault = 0; + long diffMore = 0; + + long absDiffOriginal = 0; + long absDiffSharpened = 0; + long absDiffDefault = 0; + long absDiffMore = 0; + + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 1; x < original.getWidth(); x++) { + int oRGB = 0x00FFFFFF & original.getRGB(x, y); + int sRGB = 0x00FFFFFF & sharpened.getRGB(x, y); + int dRGB = 0x00FFFFFF & sharpenedDefault.getRGB(x, y); + int mRGB = 0x00FFFFFF & sharpenedMore.getRGB(x, y); + + int poRGB = 0x00FFFFFF & original.getRGB(x - 1, y); + int psRGB = 0x00FFFFFF & sharpened.getRGB(x - 1, y); + int pdRGB = 0x00FFFFFF & sharpenedDefault.getRGB(x - 1, y); + int pmRGB = 0x00FFFFFF & sharpenedMore.getRGB(x - 1, y); + + diffOriginal += poRGB - oRGB; + diffSharpened += psRGB - sRGB; + diffDefault += pdRGB - dRGB; + diffMore += pmRGB - mRGB; + + absDiffOriginal += Math.abs(poRGB - oRGB); + absDiffSharpened += Math.abs(psRGB - sRGB); + absDiffDefault += Math.abs(pdRGB - dRGB); + absDiffMore += Math.abs(pmRGB - mRGB); + } + } + + //* + showEm(original, notSharpened, sharpened, sharpenedDefault, sharpenedMore, "sharpen"); + //*/ + +// assertEquals("Difference should not change", diffOriginal, diffSharpened); + assertTrue("Abs difference should increase", absDiffOriginal < absDiffSharpened); +// assertEquals("Difference should not change", diffOriginal, diffDefault); + assertTrue("Abs difference should increase", absDiffOriginal < absDiffDefault); +// assertEquals("Difference should not change", diffOriginal, diffMore); + assertTrue("Abs difference should increase", absDiffOriginal < absDiffMore); +// assertEquals("Difference should not change", diffSharpened, diffMore); + assertTrue("Abs difference should increase", absDiffSharpened < absDiffMore); + } + + private void showEm(final BufferedImage pOriginal, final BufferedImage pNotSharpened, final BufferedImage pSharpened, final BufferedImage pSharpenedDefault, final BufferedImage pSharpenedMore, final String pTitle) { + if (pOriginal != null) { + return; + } + + try { + SwingUtilities.invokeAndWait(new Runnable() { + public void run() { + JFrame frame = new JFrame("Sunflower - " + pTitle); + frame.setSize(pOriginal.getWidth() * 4, pOriginal.getHeight() * 2); + + Canvas canvas = new Canvas() { + public void paint(Graphics g) { + // Draw original for comparison + g.drawImage(pOriginal, 0, 0, null); + + // This should look like original + g.drawImage(pNotSharpened, 0, pOriginal.getHeight(), null); + + // Different versions + g.drawImage(pSharpened, pOriginal.getWidth(), 0, null); + g.drawImage(pSharpenedDefault, pOriginal.getWidth() * 2, 0, null); + g.drawImage(pSharpenedMore, pOriginal.getWidth() * 3, 0, null); + } + }; + + frame.getContentPane().add(canvas); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + synchronized (ImageUtilTestCase.this) { + ImageUtilTestCase.this.notify(); + } + } + }); + frame.setVisible(true); + } + }); + } + catch (InterruptedException e) { + throw new RuntimeException(e); + } + catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + + synchronized (ImageUtilTestCase.this) { + try { + ImageUtilTestCase.this.wait(); + } + catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + public void testBlur() { + final BufferedImage original = mOriginal; + + assertNotNull(original); + + final BufferedImage notBlurred = ImageUtil.blur(original, 0f); + // Assumed: Images should be equal + if (original != notBlurred) { // Don't care to test if images are same + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 0; x < original.getWidth(); x++) { + assertEquals("0 blur should not change image", original.getRGB(x, y), notBlurred.getRGB(x, y)); + } + } + } + + // Assumed: Difference between neighbouring pixels should decrease for higher blur values + // Assumed: Dynamics of entire image should not change + final BufferedImage blurred = ImageUtil.blur(original); + final BufferedImage blurredDefault = ImageUtil.blur(original, 1.5f); + final BufferedImage blurredMore = ImageUtil.blur(original, 3f); + + long diffOriginal = 0; + long diffBlurred = 0; + long diffDefault = 0; + long diffMore = 0; + + long absDiffOriginal = 0; + long absDiffBlurred = 0; + long absDiffDefault = 0; + long absDiffMore = 0; + + + for (int y = 0; y < original.getHeight(); y++) { + for (int x = 1; x < original.getWidth(); x++) { + int oRGB = 0x00FFFFFF & original.getRGB(x, y); + int bRGB = 0x00FFFFFF & blurred.getRGB(x, y); + int dRGB = 0x00FFFFFF & blurredDefault.getRGB(x, y); + int mRGB = 0x00FFFFFF & blurredMore.getRGB(x, y); + + int poRGB = 0x00FFFFFF & original.getRGB(x - 1, y); + int pbRGB = 0x00FFFFFF & blurred.getRGB(x - 1, y); + int pdRGB = 0x00FFFFFF & blurredDefault.getRGB(x - 1, y); + int pmRGB = 0x00FFFFFF & blurredMore.getRGB(x - 1, y); + + diffOriginal += poRGB - oRGB; + diffBlurred += pbRGB - bRGB; + diffDefault += pdRGB - dRGB; + diffMore += pmRGB - mRGB; + + absDiffOriginal += Math.abs(poRGB - oRGB); + absDiffBlurred += Math.abs(pbRGB - bRGB); + absDiffDefault += Math.abs(pdRGB - dRGB); + absDiffMore += Math.abs(pmRGB - mRGB); + } + } + + showEm(original, notBlurred, blurred, blurredDefault, blurredMore, "blur"); + +// assertEquals("Difference should not change", diffOriginal, diffBlurred); + assertTrue(String.format("Abs difference should decrease: %s <= %s", absDiffOriginal, absDiffBlurred), absDiffOriginal > absDiffBlurred); +// assertEquals("Difference should not change", diffOriginal, diffDefault); + assertTrue("Abs difference should decrease", absDiffOriginal > absDiffDefault); +// assertEquals("Difference should not change", diffOriginal, diffMore); + assertTrue("Abs difference should decrease", absDiffOriginal > absDiffMore); +// assertEquals("Difference should not change", diffBlurred, diffMore); + assertTrue("Abs difference should decrease", absDiffBlurred > absDiffMore); + } + + public void testIndexImage() { + BufferedImage sunflower = mOriginal; + + assertNotNull(sunflower); + + BufferedImage image = ImageUtil.createIndexed(sunflower); + assertNotNull("Image was null", image); + assertTrue(image.getColorModel() instanceof IndexColorModel); + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/image/ResampleOpTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/image/ResampleOpTestCase.java new file mode 100755 index 00000000..9adc8221 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/image/ResampleOpTestCase.java @@ -0,0 +1,263 @@ +package com.twelvemonkeys.image; + +import junit.framework.TestCase; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ImagingOpException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * ResampleOpTestCase + * + * @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/image/ResampleOpTestCase.java#1 $ + */ +public class ResampleOpTestCase extends TestCase { + + protected BufferedImage createImage(final int pWidth, final int pHeigth) { + return createImage(pWidth, pHeigth, BufferedImage.TYPE_INT_ARGB); + } + + protected BufferedImage createImage(final int pWidth, final int pHeigth, final int pType) { + BufferedImage image = new BufferedImage(pWidth, pHeigth, pType); + Graphics2D g = image.createGraphics(); + try { + g.setPaint(new GradientPaint(0, 0, Color.RED, pWidth, pHeigth, new Color(0x00000000, true))); + g.fillRect(0, 0, pWidth, pHeigth); + } + finally { + g.dispose(); + } + + return image; + } + + public void testCreateImage() { + // Sanity test the create method + BufferedImage image = createImage(79, 84); + assertNotNull(image); + assertEquals(79, image.getWidth()); + assertEquals(84, image.getHeight()); + } + + private void assertResample(final BufferedImage pImage, final int pWidth, final int pHeight, final int pFilterType) { + BufferedImage result = new ResampleOp(pWidth, pHeight, pFilterType).filter(pImage, null); + assertNotNull(result); + assertEquals(pWidth, result.getWidth()); + assertEquals(pHeight, result.getHeight()); + + result = new ResampleOp(pImage.getWidth(), pImage.getHeight(), pFilterType).filter(createImage(pWidth, pHeight), pImage); + assertNotNull(result); + assertEquals(pImage.getType(), result.getType()); + assertSame(pImage, result); + assertEquals(pImage.getWidth(), result.getWidth()); + assertEquals(pImage.getHeight(), result.getHeight()); + + result = new ResampleOp(pImage.getWidth(), pImage.getHeight(), pFilterType).filter(createImage(pWidth, pHeight), createImage(pWidth, pHeight, pImage.getType())); + assertNotNull(result); + assertEquals(pImage.getType(), result.getType()); + assertEquals(pWidth, result.getWidth()); + assertEquals(pHeight, result.getHeight()); + } + + private void assertResampleBufferedImageTypes(final int pFilterType) { + List exceptions = new ArrayList(); + + // Test all image types in BufferedImage + for (int type = BufferedImage.TYPE_INT_ARGB; type <= BufferedImage.TYPE_BYTE_INDEXED; type++) { + // TODO: Does not currently work with TYPE_BYTE_GRAY or TYPE_USHORT_GRAY + // TODO: FixMe! + if ((pFilterType == ResampleOp.FILTER_POINT || pFilterType == ResampleOp.FILTER_TRIANGLE) && + (type == BufferedImage.TYPE_BYTE_GRAY || type == BufferedImage.TYPE_USHORT_GRAY)) { + continue; + } + + BufferedImage image = createImage(10, 10, type); + try { + assertResample(image, 15, 5, pFilterType); + } + catch (ImagingOpException e) { + // NOTE: It is currently allowed for filters to throw this exception + System.err.println(e.getMessage() + ", image: " + image); + e.printStackTrace(); + } + catch (Throwable t) { + exceptions.add(t.toString() + ": " + image.toString()); + } + } + + assertEquals("Filter threw exceptions: ", Collections.EMPTY_LIST, exceptions); + } + + // 1x1 + public void testResample1x1Point() { + assertResample(createImage(1, 1), 10, 11, ResampleOp.FILTER_POINT); + } + + public void testResample1x1Box() { + assertResample(createImage(1, 1), 10, 11, ResampleOp.FILTER_BOX); + } + + public void testResample1x1Triangle() { + assertResample(createImage(1, 1), 19, 13, ResampleOp.FILTER_TRIANGLE); + } + + public void testResample1x1Lanczos() { + assertResample(createImage(1, 1), 7, 49, ResampleOp.FILTER_LANCZOS); + } + + public void testResample1x1Gaussian() { + assertResample(createImage(1, 1), 11, 34, ResampleOp.FILTER_GAUSSIAN); + } + + public void testResample1x1Sinc() { + assertResample(createImage(1, 1), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC); + } + + // 2x2 + public void testResample2x2Point() { + assertResample(createImage(2, 2), 10, 11, ResampleOp.FILTER_POINT); + } + + public void testResample2x2Box() { + assertResample(createImage(2, 2), 10, 11, ResampleOp.FILTER_BOX); + } + + public void testResample2x2Triangle() { + assertResample(createImage(2, 2), 19, 13, ResampleOp.FILTER_TRIANGLE); + } + + public void testResample2x2Lanczos() { + assertResample(createImage(2, 2), 7, 49, ResampleOp.FILTER_LANCZOS); + } + + public void testResample2x2Gaussian() { + assertResample(createImage(2, 2), 11, 34, ResampleOp.FILTER_GAUSSIAN); + } + + public void testResample2x2Sinc() { + assertResample(createImage(2, 2), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC); + } + + // 3x3 + public void testResample3x3Point() { + assertResample(createImage(3, 3), 10, 11, ResampleOp.FILTER_POINT); + } + + public void testResample3x3Box() { + assertResample(createImage(3, 3), 10, 11, ResampleOp.FILTER_BOX); + } + + public void testResample3x3Triangle() { + assertResample(createImage(3, 3), 19, 13, ResampleOp.FILTER_TRIANGLE); + } + + public void testResample3x3Lanczos() { + assertResample(createImage(3, 3), 7, 49, ResampleOp.FILTER_LANCZOS); + } + + public void testResample3x3Gaussian() { + assertResample(createImage(3, 3), 11, 34, ResampleOp.FILTER_GAUSSIAN); + } + + public void testResample3x3Sinc() { + assertResample(createImage(3, 3), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC); + } + + // 4x4 + public void testResample4x4Point() { + assertResample(createImage(4, 4), 10, 11, ResampleOp.FILTER_POINT); + } + + public void testResample4x4Box() { + assertResample(createImage(4, 4), 10, 11, ResampleOp.FILTER_BOX); + } + + public void testResample4x4Triangle() { + assertResample(createImage(4, 4), 19, 13, ResampleOp.FILTER_TRIANGLE); + } + + public void testResample4x4Lanczos() { + assertResample(createImage(4, 4), 7, 49, ResampleOp.FILTER_LANCZOS); + } + + public void testResample4x4Gaussian() { + assertResample(createImage(4, 4), 11, 34, ResampleOp.FILTER_GAUSSIAN); + } + + public void testResample4x4Sinc() { + assertResample(createImage(4, 4), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC); + } + + // 20x20 + public void testResample20x20Point() { + assertResample(createImage(20, 20), 10, 11, ResampleOp.FILTER_POINT); + } + + public void testResample20x20Box() { + assertResample(createImage(20, 20), 10, 11, ResampleOp.FILTER_BOX); + } + + public void testResample20x20Triangle() { + assertResample(createImage(20, 20), 19, 13, ResampleOp.FILTER_TRIANGLE); + } + + public void testResample20x20Lanczos() { + assertResample(createImage(20, 20), 7, 49, ResampleOp.FILTER_LANCZOS); + } + + public void testResample20x20Gaussian() { + assertResample(createImage(20, 20), 11, 34, ResampleOp.FILTER_GAUSSIAN); + } + + public void testResample20x20Sinc() { + assertResample(createImage(20, 20), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC); + } + + // 200x160 + public void testResample200x160Point() { + assertResample(createImage(200, 160), 10, 11, ResampleOp.FILTER_POINT); + } + + public void testResample200x160Box() { + assertResample(createImage(200, 160), 10, 11, ResampleOp.FILTER_BOX); + } + + public void testResample200x160Triangle() { + assertResample(createImage(200, 160), 19, 13, ResampleOp.FILTER_TRIANGLE); + } + + public void testResample200x160Lanczos() { + assertResample(createImage(200, 160), 7, 49, ResampleOp.FILTER_LANCZOS); + } + + public void testResample200x160Gaussian() { + assertResample(createImage(200, 160), 11, 34, ResampleOp.FILTER_GAUSSIAN); + } + + public void testResample200x160Sinc() { + assertResample(createImage(200, 160), 2, 8, ResampleOp.FILTER_BLACKMAN_SINC); + } + + // Test 10x10 -> 15x5 with different algorithms and types + public void testResamplePoint() { + assertResampleBufferedImageTypes(ResampleOp.FILTER_POINT); + } + + public void testResampleBox() { + assertResampleBufferedImageTypes(ResampleOp.FILTER_BOX); + } + + public void testResampleTriangle() { + assertResampleBufferedImageTypes(ResampleOp.FILTER_TRIANGLE); + } + + public void testResampleLanczos() { + assertResampleBufferedImageTypes(ResampleOp.FILTER_LANCZOS); + } +} + diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/CompoundReaderTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/CompoundReaderTestCase.java new file mode 100755 index 00000000..5419741a --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/CompoundReaderTestCase.java @@ -0,0 +1,63 @@ +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.StringUtil; +import com.twelvemonkeys.util.CollectionUtil; + +import java.io.Reader; +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.ArrayList; + +/** + * CompoundReaderTestCase + *

+ * + * @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/io/CompoundReaderTestCase.java#2 $ + */ +public class CompoundReaderTestCase extends ReaderAbstractTestCase { + + protected Reader makeReader(String pInput) { + // Split + String[] input = StringUtil.toStringArray(pInput, " "); + List readers = new ArrayList(input.length); + + // Reappend spaces... + // TODO: Add other readers + for (int i = 0; i < input.length; i++) { + if (i != 0) { + input[i] = " " + input[i]; + } + readers.add(new StringReader(input[i])); + } + + return new CompoundReader(readers.iterator()); + } + + public void testNullConstructor() { + try { + new CompoundReader(null); + fail("Should not allow null argument"); + } + catch (RuntimeException e) { + assertNotNull(e.getMessage()); + } + } + + public void testEmptyIteratorConstructor() throws IOException { + Reader reader = new CompoundReader(CollectionUtil.iterator(new Reader[0])); + assertEquals(-1, reader.read()); + } + + public void testIteratorWithNullConstructor() throws IOException { + try { + new CompoundReader(CollectionUtil.iterator(new Reader[] {null})); + fail("Should not allow null in iterator argument"); + } + catch (RuntimeException e) { + assertNotNull(e.getMessage()); + } + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FastByteArrayOutputStreamTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FastByteArrayOutputStreamTestCase.java new file mode 100755 index 00000000..389184bf --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FastByteArrayOutputStreamTestCase.java @@ -0,0 +1,31 @@ +package com.twelvemonkeys.io; + +import java.io.IOException; +import java.io.InputStream; + +/** + * FastByteArrayOutputStreamTestCase + *

+ * + * @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/io/FastByteArrayOutputStreamTestCase.java#1 $ + */ +public class FastByteArrayOutputStreamTestCase extends OutputStreamAbstractTestCase { + protected FastByteArrayOutputStream makeObject() { + return new FastByteArrayOutputStream(256); + } + + public void testCreateInputStream() throws IOException { + FastByteArrayOutputStream out = makeObject(); + + String hello = "Hello World"; + out.write(hello.getBytes("UTF-8")); + + InputStream in = out.createInputStream(); + + byte[] read = FileUtil.read(in); + + assertEquals(hello, new String(read, "UTF-8")); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileCacheSeekableStreamTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileCacheSeekableStreamTestCase.java new file mode 100755 index 00000000..3f66aa38 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileCacheSeekableStreamTestCase.java @@ -0,0 +1,26 @@ +package com.twelvemonkeys.io; + +import java.io.IOException; +import java.io.InputStream; + +/** + * FileCacheSeekableStreamTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileCacheSeekableStreamTestCase.java#3 $ + */ +public class FileCacheSeekableStreamTestCase extends SeekableInputStreamAbstractTestCase { + public FileCacheSeekableStreamTestCase(String name) { + super(name); + } + + protected SeekableInputStream makeInputStream(final InputStream pStream) { + try { + return new FileCacheSeekableStream(pStream); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileSeekableStreamTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileSeekableStreamTestCase.java new file mode 100755 index 00000000..6dbb401e --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileSeekableStreamTestCase.java @@ -0,0 +1,72 @@ +package com.twelvemonkeys.io; + +import java.io.*; + +/** + * MemoryCacheSeekableStreamTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/FileSeekableStreamTestCase.java#3 $ + */ +public class FileSeekableStreamTestCase extends SeekableInputStreamAbstractTestCase { + public FileSeekableStreamTestCase(String name) { + super(name); + } + + protected SeekableInputStream makeInputStream(final InputStream pStream) { + try { + return new FileSeekableStream(createFileWithContent(pStream)); + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + private File createFileWithContent(final InputStream pStream) throws IOException { + File temp = File.createTempFile("tm-io-junit", null); + temp.deleteOnExit(); + OutputStream os = new FileOutputStream(temp); + try { + FileUtil.copy(pStream, os); + } + finally { + os.close(); + pStream.close(); + } + return temp; + } + + @Override + public void testCloseUnderlyingStream() throws IOException { + // There is no underlying stream here... + } + + public void testCloseUnderlyingFile() throws IOException { + final boolean[] closed = new boolean[1]; + + File file = createFileWithContent(new ByteArrayInputStream(makeRandomArray(256))); + + RandomAccessFile raf = new RandomAccessFile(file, "r") { + @Override + public void close() throws IOException { + closed[0] = true; + super.close(); + } + }; + + FileSeekableStream stream = new FileSeekableStream(raf); + + try { + FileUtil.read(stream); // Read until EOF + + assertEquals("EOF not reached (test case broken)", -1, stream.read()); + assertFalse("Underlying stream closed before close", closed[0]); + } + finally { + stream.close(); + } + + assertTrue("Underlying stream not closed", closed[0]); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java new file mode 100755 index 00000000..70e862cb --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java @@ -0,0 +1,399 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Random; + +/** + * InputStreamAbstractTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/InputStreamAbstractTestCase.java#1 $ + */ +public abstract class InputStreamAbstractTestCase extends ObjectAbstractTestCase { + // TODO: FixMe! THIS TEST IS (WAS) COMPLETELY BROKEN... + // It relies on the contents of the stream being a certain order byte0 == 0, byte1 == 1 etc.. + // But the subclasses don't implement this.. Need to fix. + + final static private long SEED = 29487982745l; + final static Random sRandom = new Random(SEED); + + public InputStreamAbstractTestCase(String name) { + super(name); + } + + protected final Object makeObject() { + return makeInputStream(); + } + + protected InputStream makeInputStream() { + return makeInputStream(16); + } + + protected InputStream makeInputStream(int pSize) { + byte[] bytes = makeRandomArray(pSize); + return makeInputStream(bytes); + } + + protected abstract InputStream makeInputStream(byte[] pBytes); + + protected final byte[] makeRandomArray(final int pSize) { + byte[] bytes = new byte[pSize]; + sRandom.nextBytes(bytes); + return bytes; + } + + protected final byte[] makeOrderedArray(final int pSize) { + byte[] bytes = new byte[pSize]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) i; + } + return bytes; + } + + public void testRead() throws Exception { + int size = 5; + InputStream input = makeInputStream(makeOrderedArray(size)); + for (int i = 0; i < size; i++) { + assertEquals("Check Size [" + i + "]", (size - i), input.available()); + assertEquals("Check Value [" + i + "]", i, input.read()); + } + assertEquals("Available after contents all read", 0, input.available()); + + // Test reading after the end of file + try { + int result = input.read(); + assertEquals("Wrong value read after end of file", -1, result); + } + catch (IOException e) { + fail("Should not have thrown an IOException: " + e.getMessage()); + } + } + + public void testAvailable() throws Exception { + InputStream input = makeInputStream(1); + assertFalse("Unexpected EOF", input.read() < 0); + assertEquals("Available after contents all read", 0, input.available()); + + // Check availbale is zero after End of file + assertEquals("End of File", -1, input.read()); + assertEquals("Available after End of File", 0, input.available()); + } + + public void testReadByteArray() throws Exception { + byte[] bytes = new byte[10]; + byte[] data = makeOrderedArray(15); + InputStream input = makeInputStream(data); + + // Read into array + int count1 = input.read(bytes); + assertEquals("Read 1", bytes.length, count1); + for (int i = 0; i < count1; i++) { + assertEquals("Check Bytes 1", i, bytes[i]); + } + + // Read into array + int count2 = input.read(bytes); + assertEquals("Read 2", 5, count2); + for (int i = 0; i < count2; i++) { + assertEquals("Check Bytes 2", count1 + i, bytes[i]); + } + + // End of File + int count3 = input.read(bytes); + assertEquals("Read 3 (EOF)", -1, count3); + + // Test reading after the end of file + try { + int result = input.read(bytes); + assertEquals("Wrong value read after end of file", -1, result); + } + catch (IOException e) { + fail("Should not have thrown an IOException: " + e.getMessage()); + } + + // Reset + input = makeInputStream(data); + + // Read into array using offset & length + int offset = 2; + int lth = 4; + int count5 = input.read(bytes, offset, lth); + assertEquals("Read 5", lth, count5); + for (int i = offset; i < lth; i++) { + assertEquals("Check Bytes 2", i - offset, bytes[i]); + } + } + + public void testEOF() throws Exception { + InputStream input = makeInputStream(makeOrderedArray(2)); + assertEquals("Read 1", 0, input.read()); + assertEquals("Read 2", 1, input.read()); + assertEquals("Read 3", -1, input.read()); + assertEquals("Read 4", -1, input.read()); + assertEquals("Read 5", -1, input.read()); + } + + public void testMarkResetUnsupported() throws IOException { + InputStream input = makeInputStream(10); + if (input.markSupported()) { + return; + } + + input.mark(100); // Should be a no-op + + int read = input.read(); + assertEquals(0, read); + + // TODO: According to InputStream#reset, it is allowed to do some + // implementation specific reset, and still be correct... + try { + input.reset(); + fail("Should throw IOException"); + } + catch (IOException e) { + assertTrue("Wrong messge: " + e.getMessage(), e.getMessage().contains("reset")); + } + } + + public void testResetNoMark() throws Exception { + InputStream input = makeInputStream(makeOrderedArray(10)); + + if (!input.markSupported()) { + return; // Not supported, skip test + } + + int read = input.read(); + assertEquals(0, read); + + // No mark may either throw exception, or reset to beginning of stream. + try { + input.reset(); + assertEquals("Re-read of reset data should be same", 0, input.read()); + } + catch (Exception e) { + assertTrue("Wrong no mark IOException message", e.getMessage().contains("mark")); + } + } + + public void testMarkReset() throws Exception { + InputStream input = makeInputStream(makeOrderedArray(25)); + + if (!input.markSupported()) { + return; // Not supported, skip test + } + + int read = input.read(); + assertEquals(0, read); + + int position = 1; + int readlimit = 10; + + // Mark + input.mark(readlimit); + + // Read further + for (int i = 0; i < 3; i++) { + assertEquals("Read After Mark [" + i + "]", (position + i), input.read()); + } + + // Reset + input.reset(); + + // Read from marked position + for (int i = 0; i < readlimit + 1; i++) { + assertEquals("Read After Reset [" + i + "]", (position + i), input.read()); + } + } + + public void testResetAfterReadLimit() throws Exception { + InputStream input = makeInputStream(makeOrderedArray(25)); + + if (!input.markSupported()) { + return; // Not supported, skip test + } + + int read = input.read(); + assertEquals(0, read); + + int position = 1; + int readlimit = 5; + + // Mark + input.mark(readlimit); + + // Read past marked position + for (int i = 0; i < readlimit + 1; i++) { + assertEquals("Read After Reset [" + i + "]", (position + i), input.read()); + } + + // Reset after read limit passed, may either throw exception, or reset to last mark + try { + input.reset(); + assertEquals("Re-read of reset data should be same", 1, input.read()); + } + catch (Exception e) { + assertTrue("Wrong read-limit IOException message", e.getMessage().contains("mark")); + } + } + + public void testResetAfterReset() throws Exception { + InputStream input = makeInputStream(makeOrderedArray(25)); + + if (!input.markSupported()) { + return; // Not supported, skip test + } + + assertTrue("Expected to read positive value", input.read() >= 0); + + int readlimit = 5; + + // Mark + input.mark(readlimit); + int read = input.read(); + assertTrue("Expected to read positive value", read >= 0); + + input.reset(); + assertEquals("Expected value read differes from actual", read, input.read()); + + // Reset after read limit passed, may either throw exception, or reset to last mark + try { + input.reset(); + assertEquals("Re-read of reset data should be same", read, input.read()); + } + catch (Exception e) { + assertTrue("Wrong read-limit IOException message", e.getMessage().contains("mark")); + } + } + + public void testSkip() throws Exception { + InputStream input = makeInputStream(makeOrderedArray(10)); + + assertEquals("Unexpected value read", 0, input.read()); + assertEquals("Unexpected value read", 1, input.read()); + assertEquals("Unexpected number of bytes skipped", 5, input.skip(5)); + assertEquals("Unexpected value read", 7, input.read()); + + assertEquals("Unexpected number of bytes skipped", 2, input.skip(5)); // only 2 left to skip + assertEquals("Unexpected value read after EOF", -1, input.read()); + + // Spec says skip might return 0 or negative after EOF... + assertTrue("Positive value skipped after EOF", input.skip(5) <= 0); // End of file + assertEquals("Unexpected value read after EOF", -1, input.read()); + } + + public void testSanityOrdered() throws IOException { + // This is to sanity check that the test itself is correct... + byte[] bytes = makeOrderedArray(25); + InputStream expected = new ByteArrayInputStream(bytes); + InputStream actual = makeInputStream(bytes); + + for (byte b : bytes) { + assertEquals((int) b, expected.read()); + assertEquals((int) b, actual.read()); + } + } + + public void testSanityOrdered2() throws IOException { + // This is to sanity check that the test itself is correct... + byte[] bytes = makeOrderedArray(25); + InputStream expected = new ByteArrayInputStream(bytes); + InputStream actual = makeInputStream(bytes); + + byte[] e = new byte[bytes.length]; + byte[] a = new byte[bytes.length]; + + assertEquals(e.length, expected.read(e, 0, e.length)); + assertEquals(a.length, actual.read(a, 0, a.length)); + + for (int i = 0; i < bytes.length; i++) { + assertEquals(bytes[i], e[i]); + assertEquals(bytes[i], a[i]); + } + } + + public void testSanityNegative() throws IOException { + // This is to sanity check that the test itself is correct... + byte[] bytes = new byte[25]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) (255 - i); + } + InputStream expected = new ByteArrayInputStream(bytes); + InputStream actual = makeInputStream(bytes); + + for (byte b : bytes) { + assertEquals(b & 0xff, expected.read()); + assertEquals(b & 0xff, actual.read()); + } + } + + public void testSanityNegative2() throws IOException { + // This is to sanity check that the test itself is correct... + byte[] bytes = new byte[25]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) (255 - i); + } + InputStream expected = new ByteArrayInputStream(bytes); + InputStream actual = makeInputStream(bytes); + + byte[] e = new byte[bytes.length]; + byte[] a = new byte[bytes.length]; + + assertEquals(e.length, expected.read(e, 0, e.length)); + assertEquals(a.length, actual.read(a, 0, a.length)); + + for (int i = 0; i < bytes.length; i++) { + assertEquals(bytes[i], e[i]); + assertEquals(bytes[i], a[i]); + } + } + + public void testSanityRandom() throws IOException { + // This is to sanity check that the test itself is correct... + byte[] bytes = makeRandomArray(25); + InputStream expected = new ByteArrayInputStream(bytes); + InputStream actual = makeInputStream(bytes); + + for (byte b : bytes) { + assertEquals(b & 0xff, expected.read()); + assertEquals(b & 0xff, actual.read()); + } + } + + public void testSanityRandom2() throws IOException { + // This is to sanity check that the test itself is correct... + byte[] bytes = makeRandomArray(25); + InputStream expected = new ByteArrayInputStream(bytes); + InputStream actual = makeInputStream(bytes); + + byte[] e = new byte[bytes.length]; + byte[] a = new byte[bytes.length]; + + assertEquals(e.length, expected.read(e, 0, e.length)); + assertEquals(a.length, actual.read(a, 0, a.length)); + + for (int i = 0; i < bytes.length; i++) { + assertEquals(bytes[i], e[i]); + assertEquals(bytes[i], a[i]); + } + }} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/MemoryCacheSeekableStreamTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/MemoryCacheSeekableStreamTestCase.java new file mode 100755 index 00000000..e0ee5289 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/MemoryCacheSeekableStreamTestCase.java @@ -0,0 +1,20 @@ +package com.twelvemonkeys.io; + +import java.io.InputStream; + +/** + * MemoryCacheSeekableStreamTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/MemoryCacheSeekableStreamTestCase.java#2 $ + */ +public class MemoryCacheSeekableStreamTestCase extends SeekableInputStreamAbstractTestCase { + public MemoryCacheSeekableStreamTestCase(String name) { + super(name); + } + + protected SeekableInputStream makeInputStream(final InputStream pStream) { + return new MemoryCacheSeekableStream(pStream); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/OutputStreamAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/OutputStreamAbstractTestCase.java new file mode 100755 index 00000000..4ed2568b --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/OutputStreamAbstractTestCase.java @@ -0,0 +1,236 @@ +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; + +import java.io.OutputStream; +import java.io.IOException; + +/** + * InputStreamAbstractTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/OutputStreamAbstractTestCase.java#1 $ + */ +public abstract class OutputStreamAbstractTestCase extends ObjectAbstractTestCase { + protected abstract OutputStream makeObject(); + + public void testWrite() throws IOException { + OutputStream os = makeObject(); + + for (int i = 0; i < 256; i++) { + os.write((byte) i); + } + } + + public void testWriteByteArray() throws IOException { + OutputStream os = makeObject(); + + os.write(new byte[256]); + } + + public void testWriteByteArrayNull() { + OutputStream os = makeObject(); + try { + os.write(null); + fail("Should not accept null-argument"); + } + catch (IOException e) { + fail("Should not throw IOException of null-arguemnt: " + e.getMessage()); + } + catch (NullPointerException e) { + assertNotNull(e); + } + catch (RuntimeException e) { + fail("Should only throw NullPointerException: " + e.getClass() + ": " + e.getMessage()); + } + } + + public void testWriteByteArrayOffsetLenght() throws IOException { + byte[] input = new byte[256]; + + OutputStream os = makeObject(); + + // TODO: How to test that data is actually written!? + for (int i = 0; i < 256; i++) { + input[i] = (byte) i; + } + + for (int i = 0; i < 256; i++) { + os.write(input, i, 256 - i); + } + + for (int i = 0; i < 4; i++) { + os.write(input, i * 64, 64); + } + } + + public void testWriteByteArrayZeroLenght() { + OutputStream os = makeObject(); + try { + os.write(new byte[1], 0, 0); + } + catch (Exception e) { + fail("Should not throw Exception: " + e.getMessage()); + } + } + + public void testWriteByteArrayOffsetLenghtNull() { + OutputStream os = makeObject(); + try { + os.write(null, 5, 10); + fail("Should not accept null-argument"); + } + catch (IOException e) { + fail("Should not throw IOException of null-arguemnt: " + e.getMessage()); + } + catch (NullPointerException e) { + assertNotNull(e); + } + catch (RuntimeException e) { + fail("Should only throw NullPointerException: " + e.getClass() + ": " + e.getMessage()); + } + } + + public void testWriteByteArrayNegativeOffset() { + OutputStream os = makeObject(); + try { + os.write(new byte[5], -3, 5); + fail("Should not accept negative offset"); + } + catch (IOException e) { + fail("Should not throw IOException negative offset: " + e.getMessage()); + } + catch (IndexOutOfBoundsException e) { + assertNotNull(e); + } + catch (RuntimeException e) { + fail("Should only throw IndexOutOfBoundsException: " + e.getClass() + ": " + e.getMessage()); + } + } + + public void testWriteByteArrayNegativeLength() { + OutputStream os = makeObject(); + try { + os.write(new byte[5], 2, -5); + fail("Should not accept negative length"); + } + catch (IOException e) { + fail("Should not throw IOException negative length: " + e.getMessage()); + } + catch (IndexOutOfBoundsException e) { + assertNotNull(e); + } + catch (RuntimeException e) { + fail("Should only throw IndexOutOfBoundsException: " + e.getClass() + ": " + e.getMessage()); + } + } + + public void testWriteByteArrayOffsetOutOfBounds() { + OutputStream os = makeObject(); + try { + os.write(new byte[5], 5, 1); + fail("Should not accept offset out of bounds"); + } + catch (IOException e) { + fail("Should not throw IOException offset out of bounds: " + e.getMessage()); + } + catch (IndexOutOfBoundsException e) { + assertNotNull(e); + } + catch (RuntimeException e) { + fail("Should only throw IndexOutOfBoundsException: " + e.getClass() + ": " + e.getMessage()); + } + } + + public void testWriteByteArrayLengthOutOfBounds() { + OutputStream os = makeObject(); + try { + os.write(new byte[5], 1, 5); + fail("Should not accept length out of bounds"); + } + catch (IOException e) { + fail("Should not throw IOException length out of bounds: " + e.getMessage()); + } + catch (IndexOutOfBoundsException e) { + assertNotNull(e); + } + catch (RuntimeException e) { + fail("Should only throw IndexOutOfBoundsException: " + e.getClass() + ": " + e.getMessage()); + } + } + + public void testFlush() { + // TODO: Implement + } + + public void testClose() { + // TODO: Implement + } + + public void testWriteAfterClose() throws IOException { + OutputStream os = makeObject(); + + os.close(); + + boolean success = false; + try { + os.write(0); + success = true; + // TODO: Not all streams throw exception! (ByteArrayOutputStream) + //fail("Write after close"); + } + catch (IOException e) { + assertNotNull(e.getMessage()); + } + + try { + os.write(new byte[16]); + // TODO: Not all streams throw exception! (ByteArrayOutputStream) + //fail("Write after close"); + if (!success) { + fail("Inconsistent write(int)/write(byte[]) after close"); + } + } + catch (IOException e) { + assertNotNull(e.getMessage()); + if (success) { + fail("Inconsistent write(int)/write(byte[]) after close"); + } + } + } + + public void testFlushAfterClose() throws IOException { + OutputStream os = makeObject(); + + os.close(); + + try { + os.flush(); + // TODO: Not all streams throw exception! (ByteArrayOutputStream) + //fail("Flush after close"); + try { + os.write(0); + } + catch (IOException e) { + fail("Inconsistent write/flush after close"); + } + } + catch (IOException e) { + assertNotNull(e.getMessage()); + } + } + + public void testCloseAfterClose() throws IOException { + OutputStream os = makeObject(); + + os.close(); + + try { + os.close(); + } + catch (IOException e) { + fail("Close after close, failed: " + e.getMessage()); + } + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/ReaderAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/ReaderAbstractTestCase.java new file mode 100755 index 00000000..cee9bc6f --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/ReaderAbstractTestCase.java @@ -0,0 +1,215 @@ +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; + +import java.io.Reader; +import java.io.IOException; + +/** + * ReaderAbstractTestCase + *

+ * + * @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/io/ReaderAbstractTestCase.java#1 $ + */ +public abstract class ReaderAbstractTestCase extends ObjectAbstractTestCase { + + // Kindly provided by lipsum.org :-) + protected final String mInput = + "Cras tincidunt euismod tellus. Aenean a odio. " + + "Aenean metus. Sed tristique est non purus. Class aptent " + + "taciti sociosqu ad litora torquent per conubia nostra, per " + + "inceptos hymenaeos. Fusce vulputate dolor non mauris. " + + "Nullam nunc massa, pretium quis, ultricies a, varius quis, " + + "neque. Nam id nulla eu ante malesuada fermentum. Sed " + + "vulputate purus eget magna. Sed mollis. Curabitur enim " + + "diam, faucibus ac, hendrerit eu, consequat nec, augue."; + + protected final Object makeObject() { + return makeReader(); + } + + protected Reader makeReader() { + return makeReader(mInput); + } + + protected abstract Reader makeReader(String pInput); + + public void testRead() throws IOException { + Reader reader = makeReader(); + + int count = 0; + int ch; + StringBuilder buffer = new StringBuilder(mInput.length()); + while ((ch = reader.read()) > 0) { + count++; + buffer.append((char) ch); + } + + assertEquals(mInput.length(), count); + assertEquals(mInput, buffer.toString()); + } + + public void testReadBuffer() throws IOException { + Reader reader = makeReader(); + + char[] chars = new char[mInput.length()]; + StringBuilder buffer = new StringBuilder(mInput.length()); + + int count; + int offset = 0; + int lenght = chars.length; + while ((count = reader.read(chars, offset, lenght)) > 0) { + buffer.append(chars, offset, count); + offset += count; + lenght -= count; + } + + assertEquals(mInput, buffer.toString()); + assertEquals(mInput, new String(chars)); + } + + public void testSkipToEnd() throws IOException { + Reader reader = makeReader(); + + int toSkip = mInput.length(); + while (toSkip > 0) { + long skipped = reader.skip(toSkip); + assertFalse("Skipped < 0", skipped < 0); + toSkip -= skipped; + } + + assertEquals(0, toSkip); + } + + public void testSkipToEndAndRead() throws IOException { + Reader reader = makeReader(); + + int toSkip = mInput.length(); + while (toSkip > 0) { + toSkip -= reader.skip(toSkip); + } + + assertEquals(reader.read(), -1); + } + + // TODO: It's possible to support reset and not mark (resets to beginning of stream, for example) + public void testResetMarkSupported() throws IOException { + Reader reader = makeReader(); + + if (reader.markSupported()) { + // Mark at 0 + reader.mark(mInput.length() / 4); + + // Read one char + char ch = (char) reader.read(); + reader.reset(); + assertEquals(ch, (char) reader.read()); + reader.reset(); + + // Read from start + StringBuilder first = new StringBuilder(mInput.length() / 4); + for (int i = 0; i < mInput.length() / 4; i++) { + first.append((char) reader.read()); + } + + reader.reset(); // 0 + + StringBuilder second = new StringBuilder(mInput.length() / 4); + for (int i = 0; i < mInput.length() / 4; i++) { + second.append((char) reader.read()); + } + + assertEquals(first.toString(), second.toString()); + + // Mark at 1/4 + reader.mark(mInput.length() / 4); + + // Read from 1/4 + first = new StringBuilder(mInput.length() / 4); + for (int i = 0; i < mInput.length() / 4; i++) { + first.append((char) reader.read()); + } + + reader.reset(); // 1/4 + + second = new StringBuilder(mInput.length() / 4); + for (int i = 0; i < mInput.length() / 4; i++) { + second.append((char) reader.read()); + } + + assertEquals(first.toString(), second.toString()); + + // Read past limit + reader.read(); + + // This may or may not fail, depending on the stream + try { + reader.reset(); + } + catch (IOException ioe) { + assertNotNull(ioe.getMessage()); + } + } + } + + public void testResetMarkNotSupported() throws IOException { + Reader reader = makeReader(); + + if (!reader.markSupported()) { + try { + reader.mark(mInput.length()); + fail("Mark set, while markSupprted is false"); + } + catch (IOException e) { + assertNotNull(e.getMessage()); + } + + // Read one char + char ch = (char) reader.read(); + try { + reader.reset(); + assertEquals(ch, (char) reader.read()); + } + catch (IOException ioe) { + assertNotNull(ioe.getMessage()); + } + + // Read from start + StringBuilder first = new StringBuilder(mInput.length() / 4); + for (int i = 0; i < mInput.length() / 4; i++) { + first.append((char) reader.read()); + } + + try { + reader.reset(); // 0 + + StringBuilder second = new StringBuilder(mInput.length() / 4); + for (int i = 0; i < mInput.length() / 4; i++) { + second.append((char) reader.read()); + } + + assertEquals(first.toString(), second.toString()); + } + catch (IOException ioe) { + assertNotNull(ioe.getMessage()); + } + } + } + + + public void testReadAfterClose() throws IOException { + Reader reader = makeReader("foo bar"); + + reader.close(); + + try { + reader.read(); + fail("Should not allow read after close"); + } + catch (IOException ioe) { + assertNotNull(ioe.getMessage()); + } + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableAbstractTestCase.java new file mode 100755 index 00000000..61b471ab --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableAbstractTestCase.java @@ -0,0 +1,23 @@ +package com.twelvemonkeys.io; + +import junit.framework.TestCase; + +/** + * SeekableAbstractTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableAbstractTestCase.java#1 $ + */ +public abstract class SeekableAbstractTestCase extends TestCase implements SeekableInterfaceTest { + + protected abstract Seekable createSeekable(); + + public void testFail() { + fail(); + } + + public void testSeekable() { + assertTrue(createSeekable() instanceof Seekable); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java new file mode 100755 index 00000000..f0c0b844 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java @@ -0,0 +1,480 @@ +package com.twelvemonkeys.io; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; + +/** + * SeekableInputStreamAbstractTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInputStreamAbstractTestCase.java#4 $ + */ +public abstract class SeekableInputStreamAbstractTestCase extends InputStreamAbstractTestCase implements SeekableInterfaceTest { + + public SeekableInputStreamAbstractTestCase(String name) { + super(name); + } + + //// TODO: Figure out a better way of creating interface tests without duplicating code + final SeekableAbstractTestCase mSeekableTestCase = new SeekableAbstractTestCase() { + protected Seekable createSeekable() { + return makeInputStream(); + } + }; + + @Override + protected SeekableInputStream makeInputStream() { + return (SeekableInputStream) super.makeInputStream(); + } + + @Override + protected SeekableInputStream makeInputStream(final int pSize) { + return (SeekableInputStream) super.makeInputStream(pSize); + } + + protected SeekableInputStream makeInputStream(byte[] pBytes) { + return makeInputStream(new ByteArrayInputStream(pBytes)); + } + + protected abstract SeekableInputStream makeInputStream(InputStream pStream); + + @Override + public void testResetAfterReset() throws Exception { + InputStream input = makeInputStream(makeOrderedArray(25)); + + if (!input.markSupported()) { + return; // Not supported, skip test + } + + assertTrue("Expected to read positive value", input.read() >= 0); + + int readlimit = 5; + + // Mark + input.mark(readlimit); + int read = input.read(); + assertTrue("Expected to read positive value", read >= 0); + + input.reset(); + assertEquals("Expected value read differes from actual", read, input.read()); + + // Reset after read limit passed, may either throw exception, or reset to last mark + try { + input.reset(); + assertEquals("Re-read of reset data should be first", 0, input.read()); + } + catch (Exception e) { + assertTrue("Wrong read-limit IOException message", e.getMessage().contains("mark")); + } + } + + public void testSeekable() { + mSeekableTestCase.testSeekable(); + } + + public void testFlushBeyondCurrentPos() throws Exception { + SeekableInputStream seekable = makeInputStream(20); + + int pos = 10; + try { + seekable.flushBefore(pos); + fail("Flush beyond current position should throw IndexOutOfBoundsException"); + } + catch (IndexOutOfBoundsException e) { + // Ignore + } + } + + public void testSeek() throws Exception { + SeekableInputStream seekable = makeInputStream(55); + int pos = 37; + + seekable.seek(pos); + long streamPos = seekable.getStreamPosition(); + assertEquals("Stream positon should match seeked position", pos, streamPos); + } + + public void testSeekFlush() throws Exception { + SeekableInputStream seekable = makeInputStream(133); + int pos = 45; + seekable.seek(pos); + seekable.flushBefore(pos); + long flushedPos = seekable.getFlushedPosition(); + assertEquals("Flushed positon should match position", pos, flushedPos); + + try { + seekable.seek(pos - 1); + fail("Read before flushed position succeeded"); + } + catch (IndexOutOfBoundsException e) { + // Ignore + } + } + + public void testMarkFlushReset() throws Exception { + SeekableInputStream seekable = makeInputStream(77); + + seekable.mark(); + + int position = 55; + seekable.seek(position); + seekable.flushBefore(position); + + try { + seekable.reset(); + fail("Reset before flushed position succeeded"); + } + catch (IOException e) { + // Ignore + } + + assertEquals(position, seekable.getStreamPosition()); + } + + public void testSeekSkipRead() throws Exception { + SeekableInputStream seekable = makeInputStream(133); + int pos = 45; + for (int i = 0; i < 10; i++) { + seekable.seek(pos); + //noinspection ResultOfMethodCallIgnored + seekable.skip(i); + byte[] bytes = FileUtil.read(seekable); + assertEquals(133, seekable.getStreamPosition()); + assertEquals(133 - 45- i, bytes.length); + } + } + + public void testSeekSkip(SeekableInputStream pSeekable, String pStr) throws IOException { + System.out.println(); + pSeekable.seek(pStr.length()); + FileUtil.read(pSeekable); + for (int i = 0; i < 10; i++) { + byte[] bytes = FileUtil.read(pSeekable); + int len = bytes.length; + if (len != 0) { + System.err.println("Error in buffer length after full read..."); + System.err.println("len: " + len); + System.err.println("bytes: \"" + new String(bytes) + "\""); + break; + } + } + + System.out.println(); + + for (int i = 0; i < 10; i++) { + pSeekable.seek(0); + int skip = i * 3; + //noinspection ResultOfMethodCallIgnored + pSeekable.skip(skip); + String str = new String(FileUtil.read(pSeekable)); + System.out.println(str); + if (str.length() != pStr.length() - skip) { + throw new Error("Error in buffer length after skip"); + } + } + + System.out.println(); + System.out.println("seek/skip ok!"); + System.out.println(); + } + + protected static void markReset(SeekableInputStream pSeekable) throws IOException { + for (int i = 0; i < 10; i++) { + pSeekable.mark(); + System.out.println(new String(FileUtil.read(pSeekable))); + pSeekable.reset(); + } + + System.out.println(); + System.out.println("mark/reset ok!"); + } + + protected static void timeRead(SeekableInputStream pSeekable) throws IOException { + for (int i = 0; i < 5000; i++) { + pSeekable.mark(); + FileUtil.read(pSeekable); + pSeekable.reset(); + } + + long start = System.currentTimeMillis(); + final int times = 200000; + for (int i = 0; i < times; i++) { + pSeekable.mark(); + FileUtil.read(pSeekable); + pSeekable.reset(); + } + long time = System.currentTimeMillis() - start; + + System.out.println("Time; " + time + "ms (" + (time / (float) times) + "ms/inv)"); + } + + /* + + // Test code below... + protected final static String STR = "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Fusce massa orci, adipiscing vel, dapibus et, vulputate tristique, tortor. Quisque sodales. Mauris varius turpis et pede. Nam ac dolor vel diam condimentum elementum. Pellentesque eget tellus. Praesent magna. Sed fringilla. Proin ullamcorper tincidunt ante. Fusce dapibus nibh nec dolor. Etiam erat. Nullam dignissim laoreet nibh. Maecenas scelerisque. Pellentesque in quam. Maecenas sollicitudin, magna nec imperdiet facilisis, metus quam tristique ipsum, vitae consequat massa purus eget leo. Nulla ipsum. Proin non purus eget tellus lobortis iaculis. In lorem justo, posuere id, vulputate at, adipiscing ut, nisl. Nunc dui erat, tincidunt ac, interdum quis, rutrum et, libero. Etiam lectus dui, viverra sit amet, elementum ut, malesuada sed, massa. Vestibulum mi nulla, sodales vel, vestibulum sed, congue blandit, velit."; + + protected static void flushSeek(SeekableInputStream pSeekable, String pStr) throws IOException { + pSeekable.seek(0); + pSeekable.mark(); + int pos = pStr.length() / 2; + try { + pSeekable.flushBefore(pos); + System.err.println("Error in flush/seek"); + } + catch (IndexOutOfBoundsException e) { + // Ignore + } + pSeekable.seek(pos); + long streamPos = pSeekable.getStreamPosition(); + if (streamPos != pos) { + System.err.println("Streampos not equal seeked pos"); + } + + pSeekable.flushBefore(pos); + long flushedPos = pSeekable.getFlushedPosition(); + if (flushedPos != pos) { + System.err.println("flushedpos not equal set flushed pos"); + } + + for (int i = 0; i < 10; i++) { + pSeekable.seek(pos); + //noinspection ResultOfMethodCallIgnored + pSeekable.skip(i); + System.out.println(new String(FileUtil.read(pSeekable))); + } + + try { + pSeekable.seek(pos - 1); + System.err.println("Error in flush/seek"); + } + catch (IndexOutOfBoundsException e) { + // Ignore + } + try { + pSeekable.reset(); + System.err.println("Error in flush/seek"); + } + catch (IOException e) { + // Ignore + } + + System.out.println(); + System.out.println("flush/seek ok!"); + } + + protected static void seekSkip(SeekableInputStream pSeekable, String pStr) throws IOException { + System.out.println(); + pSeekable.seek(pStr.length()); + FileUtil.read(pSeekable); + for (int i = 0; i < 10; i++) { + byte[] bytes = FileUtil.read(pSeekable); + int len = bytes.length; + if (len != 0) { + System.err.println("Error in buffer length after full read..."); + System.err.println("len: " + len); + System.err.println("bytes: \"" + new String(bytes) + "\""); + break; + } + } + + System.out.println(); + + for (int i = 0; i < 10; i++) { + pSeekable.seek(0); + int skip = i * 3; + //noinspection ResultOfMethodCallIgnored + pSeekable.skip(skip); + String str = new String(FileUtil.read(pSeekable)); + System.out.println(str); + if (str.length() != pStr.length() - skip) { + throw new Error("Error in buffer length after skip"); + } + } + + System.out.println(); + System.out.println("seek/skip ok!"); + System.out.println(); + } + + protected static void markReset(SeekableInputStream pSeekable) throws IOException { + for (int i = 0; i < 10; i++) { + pSeekable.mark(); + System.out.println(new String(FileUtil.read(pSeekable))); + pSeekable.reset(); + } + + System.out.println(); + System.out.println("mark/reset ok!"); + } + + protected static void timeRead(SeekableInputStream pSeekable) throws IOException { + for (int i = 0; i < 5000; i++) { + pSeekable.mark(); + FileUtil.read(pSeekable); + pSeekable.reset(); + } + + long start = System.currentTimeMillis(); + final int times = 200000; + for (int i = 0; i < times; i++) { + pSeekable.mark(); + FileUtil.read(pSeekable); + pSeekable.reset(); + } + long time = System.currentTimeMillis() - start; + + System.out.println("Time; " + time + "ms (" + (time / (float) times) + "ms/inv)"); + } + */ + + public void testReadResetReadDirectBufferBug() throws IOException { + // Make sure we use the exact size of the buffer + final int size = 1024; + + // Fill bytes + byte[] bytes = new byte[size * 2]; + sRandom.nextBytes(bytes); + + // Create wrapper stream + SeekableInputStream stream = makeInputStream(bytes); + + // Read to fill the buffer, then reset + int val; + + val = stream.read(); + assertFalse("Unexepected EOF", val == -1); + val = stream.read(); + assertFalse("Unexepected EOF", val == -1); + val = stream.read(); + assertFalse("Unexepected EOF", val == -1); + val = stream.read(); + assertFalse("Unexepected EOF", val == -1); + + stream.seek(0); + + // Read fully and compare + byte[] result = new byte[size]; + + readFully(stream, result); + assertTrue(rangeEquals(bytes, 0, result, 0, size)); + + readFully(stream, result); + assertTrue(rangeEquals(bytes, size, result, 0, size)); + } + + public void testReadAllByteValuesRegression() throws IOException { + final int size = 128; + + // Fill bytes + byte[] bytes = new byte[256]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) i; + } + + // Create wrapper stream + SeekableInputStream stream = makeInputStream(bytes); + + // Fill buffer + byte[] buffer = new byte[size]; + while (stream.read(buffer) >= 0) { + } + + stream.seek(0); + for (int i = 0; i < bytes.length; i += 2) { + assertEquals("Wrong stream position", i, stream.getStreamPosition()); + int count = stream.read(buffer, 0, 2); + assertEquals(2, count); + assertEquals(String.format("Wrong value read at pos %d", stream.getStreamPosition()), bytes[i], buffer[0]); + assertEquals(String.format("Wrong value read at pos %d", stream.getStreamPosition()), bytes[i + 1], buffer[1]); + } + + stream.seek(0); + for (int i = 0; i < bytes.length; i++) { + assertEquals("Wrong stream position", i, stream.getStreamPosition()); + int actual = stream.read(); + assertEquals(String.format("Wrong value read at pos %d", stream.getStreamPosition()), bytes[i] & 0xff, actual); + assertEquals(String.format("Wrong value read at pos %d", stream.getStreamPosition()), bytes[i], (byte) actual); + } + + } + + public void testCloseUnderlyingStream() throws IOException { + final boolean[] closed = new boolean[1]; + + ByteArrayInputStream input = new ByteArrayInputStream(makeRandomArray(256)) { + @Override + public void close() throws IOException { + closed[0] = true; + super.close(); + } + }; + + SeekableInputStream stream = makeInputStream(input); + + try { + FileUtil.read(stream); // Read until EOF + + assertEquals("EOF not reached (test case broken)", -1, stream.read()); + assertFalse("Underlying stream closed before close", closed[0]); + } + finally { + stream.close(); + } + + assertTrue("Underlying stream not closed", closed[0]); + + } + + private void readFully(InputStream pStream, byte[] pResult) throws IOException { + int pos = 0; + while (pos < pResult.length) { + int read = pStream.read(pResult, pos, pResult.length - pos); + if (read == -1) { + throw new EOFException(); + } + pos += read; + } + } + + /** + * Test two arrays for range equality. That is, they contain the same elements for some specified range. + * + * @param pFirst one array to test for equality + * @param pFirstOffset the offset into the first array to start testing for equality + * @param pSecond the other array to test for equality + * @param pSecondOffset the offset into the second array to start testing for equality + * @param pLength the length of the range to check for equality + * + * @return {@code true} if both arrays are non-{@code null} + * and have at least {@code offset + pLength} elements + * and all elements in the range from the first array is equal to the elements from the second array, + * or if {@code pFirst == pSecond} (including both arrays being {@code null}) + * and {@code pFirstOffset == pSecondOffset}. + * Otherwise {@code false}. + */ + static boolean rangeEquals(byte[] pFirst, int pFirstOffset, byte[] pSecond, int pSecondOffset, int pLength) { + if (pFirst == pSecond && pFirstOffset == pSecondOffset) { + return true; + } + + if (pFirst == null || pSecond == null) { + return false; + } + + if (pFirst.length < pFirstOffset + pLength || pSecond.length < pSecondOffset + pLength) { + return false; + } + + for (int i = 0; i < pLength; i++) { + if (pFirst[pFirstOffset + i] != pSecond[pSecondOffset + i]) { + return false; + } + } + + return true; + } + +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInterfaceTest.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInterfaceTest.java new file mode 100755 index 00000000..1c59b52c --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInterfaceTest.java @@ -0,0 +1,12 @@ +package com.twelvemonkeys.io; + +/** + * SeekableInterfaceTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/SeekableInterfaceTest.java#1 $ + */ +public interface SeekableInterfaceTest { + void testSeekable(); +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/StringArrayReaderTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/StringArrayReaderTestCase.java new file mode 100755 index 00000000..42af1ddf --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/StringArrayReaderTestCase.java @@ -0,0 +1,52 @@ +package com.twelvemonkeys.io; + +import com.twelvemonkeys.lang.StringUtil; + +import java.io.Reader; +import java.io.IOException; + +/** + * StringArrayReaderTestCase + *

+ * + * @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/io/StringArrayReaderTestCase.java#1 $ + */ +public class StringArrayReaderTestCase extends ReaderAbstractTestCase { + + protected Reader makeReader(String pInput) { + // Split + String[] input = StringUtil.toStringArray(pInput, " "); + // Reappend spaces... + for (int i = 0; i < input.length; i++) { + if (i != 0) { + input[i] = " " + input[i]; + } + } + + return new StringArrayReader(input); + } + + public void testNullConstructor() { + try { + new StringArrayReader(null); + fail("Should not allow null argument"); + } + catch (RuntimeException e) { + assertNotNull(e.getMessage()); + } + } + + public void testEmptyArrayConstructor() throws IOException { + Reader reader = new StringArrayReader(new String[0]); + assertEquals(-1, reader.read()); + } + + public void testEmptyStringConstructor() throws IOException { + Reader reader = new StringArrayReader(new String[] {""}); + assertEquals(-1, reader.read()); + } + + +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/Base64DecoderTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/Base64DecoderTestCase.java new file mode 100755 index 00000000..3a304437 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/Base64DecoderTestCase.java @@ -0,0 +1,65 @@ +package com.twelvemonkeys.io.enc; + + +import com.twelvemonkeys.io.FileUtil; + +import java.io.*; + +/** + * Base64DecoderTest + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/Base64DecoderTestCase.java#1 $ + */ +public class Base64DecoderTestCase extends DecoderAbstractTestCase { + + public Decoder createDecoder() { + return new Base64Decoder(); + } + + public Encoder createCompatibleEncoder() { + return new Base64Encoder(); + } + + public void testEmptyDecode2() throws IOException { + String data = ""; + + InputStream in = new DecoderStream(new ByteArrayInputStream(data.getBytes()), createDecoder()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + FileUtil.copy(in, bytes); + + assertEquals("Strings does not match", "", new String(bytes.toByteArray())); + } + + public void testShortDecode() throws IOException { + String data = "dGVzdA=="; + + InputStream in = new DecoderStream(new ByteArrayInputStream(data.getBytes()), createDecoder()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + FileUtil.copy(in, bytes); + + assertEquals("Strings does not match", "test", new String(bytes.toByteArray())); + } + + public void testLongDecode() throws IOException { + String data = "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVlciBhZGlwaXNjaW5nIGVsaXQuIEZ1" + + "c2NlIGVzdC4gTW9yYmkgbHVjdHVzIGNvbnNlY3RldHVlciBqdXN0by4gVml2YW11cyBkYXBpYnVzIGxh" + + "b3JlZXQgcHVydXMuIE51bmMgdml2ZXJyYSBkaWN0dW0gbmlzbC4gSW50ZWdlciB1bGxhbWNvcnBlciwg" + + "bmlzaSBpbiBkaWN0dW0gYW1ldC4="; + + InputStream in = new DecoderStream(new ByteArrayInputStream(data.getBytes()), createDecoder()); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + FileUtil.copy(in, bytes); + + assertEquals("Strings does not match", + "Lorem ipsum dolor sit amet, consectetuer adipiscing " + + "elit. Fusce est. Morbi luctus consectetuer justo. Vivamus " + + "dapibus laoreet purus. Nunc viverra dictum nisl. Integer " + + "ullamcorper, nisi in dictum amet.", + new String(bytes.toByteArray())); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java new file mode 100755 index 00000000..f9b3731d --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java @@ -0,0 +1,71 @@ +package com.twelvemonkeys.io.enc; + +import java.io.*; + +/** + * Base64EncoderTest + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/Base64EncoderTestCase.java#1 $ + */ +public class Base64EncoderTestCase extends EncoderAbstractTestCase { + + protected Encoder createEncoder() { + return new Base64Encoder(); + } + + protected Decoder createCompatibleDecoder() { + return new Base64Decoder(); + } + + public void testNegativeEncode() throws IOException { + Encoder base64 = createEncoder(); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + base64.encode(bytes, new byte[1], 2, 1); + fail("wrong index should throw IndexOutOfBoundsException"); + } + catch (IndexOutOfBoundsException e) { + } + } + + public void testEmptyEncode() throws IOException { + String data = ""; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + OutputStream out = new EncoderStream(bytes, createEncoder(), true); + out.write(data.getBytes()); + + assertEquals("Strings does not match", "", new String(bytes.toByteArray())); + } + + public void testShortEncode() throws IOException { + String data = "test"; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + OutputStream out = new EncoderStream(bytes, createEncoder(), true); + out.write(data.getBytes()); + + assertEquals("Strings does not match", "dGVzdA==", new String(bytes.toByteArray())); + } + + public void testLongEncode() throws IOException { + String data = "Lorem ipsum dolor sit amet, consectetuer adipiscing " + + "elit. Fusce est. Morbi luctus consectetuer justo. Vivamus " + + "dapibus laoreet purus. Nunc viverra dictum nisl. Integer " + + "ullamcorper, nisi in dictum amet."; + + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + OutputStream out = new EncoderStream(bytes, createEncoder(), true); + out.write(data.getBytes()); + + assertEquals("Strings does not match", + "TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVlciBhZGlwaXNjaW5nIGVsaXQuIEZ1" + + "c2NlIGVzdC4gTW9yYmkgbHVjdHVzIGNvbnNlY3RldHVlciBqdXN0by4gVml2YW11cyBkYXBpYnVzIGxh" + + "b3JlZXQgcHVydXMuIE51bmMgdml2ZXJyYSBkaWN0dW0gbmlzbC4gSW50ZWdlciB1bGxhbWNvcnBlciwg" + + "bmlzaSBpbiBkaWN0dW0gYW1ldC4=", + new String(bytes.toByteArray())); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java new file mode 100755 index 00000000..1f4e8e27 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java @@ -0,0 +1,99 @@ +package com.twelvemonkeys.io.enc; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import com.twelvemonkeys.io.enc.Decoder; +import com.twelvemonkeys.io.enc.DecoderStream; +import com.twelvemonkeys.io.enc.Encoder; +import com.twelvemonkeys.io.enc.EncoderStream; +import com.twelvemonkeys.io.FileUtil; + +import java.io.*; +import java.util.Arrays; + +/** + * AbstractDecoderTest + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DecoderAbstractTestCase.java#1 $ + */ +public abstract class DecoderAbstractTestCase extends ObjectAbstractTestCase { + + public abstract Decoder createDecoder(); + public abstract Encoder createCompatibleEncoder(); + + protected Object makeObject() { + return createDecoder(); + } + + public final void testNullDecode() throws IOException { + Decoder decoder = createDecoder(); + ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[20]); + + try { + decoder.decode(bytes, null); + fail("null should throw NullPointerException"); + } + catch (NullPointerException e) { + } + } + + public final void testEmptyDecode() throws IOException { + Decoder decoder = createDecoder(); + ByteArrayInputStream bytes = new ByteArrayInputStream(new byte[0]); + + try { + int count = decoder.decode(bytes, new byte[2]); + assertEquals("Should not be able to read any bytes", 0, count); + } + catch (EOFException allowed) { + // Okay + } + } + + private byte[] createData(int pLength) throws Exception { + byte[] bytes = new byte[pLength]; + EncoderAbstractTestCase.RANDOM.nextBytes(bytes); + return bytes; + } + + private void runStreamTest(int pLength) throws Exception { + byte[] data = createData(pLength); + + ByteArrayOutputStream outBytes = new ByteArrayOutputStream(); + OutputStream out = new EncoderStream(outBytes, createCompatibleEncoder(), true); + out.write(data); + out.close(); + byte[] encoded = outBytes.toByteArray(); + + byte[] decoded = FileUtil.read(new DecoderStream(new ByteArrayInputStream(encoded), createDecoder())); + assertTrue(Arrays.equals(data, decoded)); + + InputStream in = new DecoderStream(new ByteArrayInputStream(encoded), createDecoder()); + outBytes = new ByteArrayOutputStream(); + /* + byte[] buffer = new byte[3]; + for (int n = in.read(buffer); n > 0; n = in.read(buffer)) { + outBytes.write(buffer, 0, n); + } + */ + FileUtil.copy(in, outBytes); + + outBytes.close(); + in.close(); + decoded = outBytes.toByteArray(); + assertTrue(Arrays.equals(data, decoded)); + } + + public final void testStreams() throws Exception { + for (int i = 0; i < 100; ++i) { + runStreamTest(i); + } + for (int i = 100; i < 2000; i += 250) { + runStreamTest(i); + } + for (int i = 2000; i < 80000; i += 1000) { + runStreamTest(i); + } + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DeflateDecoderTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DeflateDecoderTestCase.java new file mode 100755 index 00000000..3c079a17 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DeflateDecoderTestCase.java @@ -0,0 +1,23 @@ +package com.twelvemonkeys.io.enc; + +import com.twelvemonkeys.io.enc.Decoder; +import com.twelvemonkeys.io.enc.DeflateEncoder; +import com.twelvemonkeys.io.enc.Encoder; +import com.twelvemonkeys.io.enc.InflateDecoder; + +/** + * DeflateDecoderTest + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/DeflateDecoderTestCase.java#1 $ + */ +public class DeflateDecoderTestCase extends EncoderAbstractTestCase { + protected Encoder createEncoder() { + return new DeflateEncoder(); + } + + protected Decoder createCompatibleDecoder() { + return new InflateDecoder(); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java new file mode 100755 index 00000000..d0317ae8 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java @@ -0,0 +1,88 @@ +package com.twelvemonkeys.io.enc; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; +import com.twelvemonkeys.io.enc.Decoder; +import com.twelvemonkeys.io.enc.DecoderStream; +import com.twelvemonkeys.io.enc.Encoder; +import com.twelvemonkeys.io.enc.EncoderStream; +import com.twelvemonkeys.io.FileUtil; + +import java.io.*; +import java.util.Arrays; +import java.util.Random; + +/** + * AbstractEncoderTest + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/EncoderAbstractTestCase.java#1 $ + */ +public abstract class EncoderAbstractTestCase extends ObjectAbstractTestCase { + // Use seed to make sure we create same number all the time + static final long SEED = 12345678; + static final Random RANDOM = new Random(SEED); + + protected abstract Encoder createEncoder(); + protected abstract Decoder createCompatibleDecoder(); + + protected Object makeObject() { + return createEncoder(); + } + + public final void testNullEncode() throws IOException { + Encoder base64 = createEncoder(); + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + try { + base64.encode(bytes, null, 0, 1); + fail("null should throw NullPointerException"); + } + catch (NullPointerException e) { + } + } + + private byte[] createData(int pLength) throws Exception { + byte[] bytes = new byte[pLength]; + RANDOM.nextBytes(bytes); + return bytes; + } + + private void runStreamTest(int pLength) throws Exception { + byte[] data = createData(pLength); + ByteArrayOutputStream out_bytes = new ByteArrayOutputStream(); + OutputStream out = new EncoderStream(out_bytes, createEncoder(), true); + out.write(data); + out.close(); + byte[] encoded = out_bytes.toByteArray(); + byte[] decoded = FileUtil.read(new DecoderStream(new ByteArrayInputStream(encoded), createCompatibleDecoder())); + assertTrue(Arrays.equals(data, decoded)); + + InputStream in = new DecoderStream(new ByteArrayInputStream(encoded), createCompatibleDecoder()); + out_bytes = new ByteArrayOutputStream(); + /** + byte[] buffer = new byte[3]; + for (int n = in.read(buffer); n > 0; n = in.read(buffer)) { + out_bytes.write(buffer, 0, n); + } + //*/ + FileUtil.copy(in, out_bytes); + + out_bytes.close(); + in.close(); + decoded = out_bytes.toByteArray(); + assertTrue(Arrays.equals(data, decoded)); + } + + public final void testStreams() throws Exception { + for (int i = 0; i < 100; ++i) { + runStreamTest(i); + } + for (int i = 100; i < 2000; i += 250) { + runStreamTest(i); + } + for (int i = 2000; i < 80000; i += 1000) { + runStreamTest(i); + } + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java new file mode 100755 index 00000000..388d01aa --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java @@ -0,0 +1,23 @@ +package com.twelvemonkeys.io.enc; + +import com.twelvemonkeys.io.enc.Decoder; +import com.twelvemonkeys.io.enc.DeflateEncoder; +import com.twelvemonkeys.io.enc.Encoder; +import com.twelvemonkeys.io.enc.InflateDecoder; + +/** + * InflateEncoderTest + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/InflateDecoderTestCase.java#1 $ + */ +public class InflateDecoderTestCase extends DecoderAbstractTestCase { + public Decoder createDecoder() { + return new InflateDecoder(); + } + + public Encoder createCompatibleEncoder() { + return new DeflateEncoder(); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/PackBitsDecoderTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/PackBitsDecoderTestCase.java new file mode 100755 index 00000000..b69b57f0 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/PackBitsDecoderTestCase.java @@ -0,0 +1,23 @@ +package com.twelvemonkeys.io.enc; + +import com.twelvemonkeys.io.enc.Decoder; +import com.twelvemonkeys.io.enc.Encoder; +import com.twelvemonkeys.io.enc.PackBitsDecoder; +import com.twelvemonkeys.io.enc.PackBitsEncoder; + +/** + * PackBitsDecoderTest + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/PackBitsDecoderTestCase.java#1 $ + */ +public class PackBitsDecoderTestCase extends DecoderAbstractTestCase { + public Decoder createDecoder() { + return new PackBitsDecoder(); + } + + public Encoder createCompatibleEncoder() { + return new PackBitsEncoder(); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/PackBitsEncoderTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/PackBitsEncoderTestCase.java new file mode 100755 index 00000000..023e99e1 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/PackBitsEncoderTestCase.java @@ -0,0 +1,23 @@ +package com.twelvemonkeys.io.enc; + +import com.twelvemonkeys.io.enc.Decoder; +import com.twelvemonkeys.io.enc.Encoder; +import com.twelvemonkeys.io.enc.PackBitsDecoder; +import com.twelvemonkeys.io.enc.PackBitsEncoder; + +/** + * PackBitsEncoderTest + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/enc/PackBitsEncoderTestCase.java#1 $ + */ +public class PackBitsEncoderTestCase extends EncoderAbstractTestCase { + protected Encoder createEncoder() { + return new PackBitsEncoder(); + } + + protected Decoder createCompatibleDecoder() { + return new PackBitsDecoder(); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocumentTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocumentTestCase.java new file mode 100755 index 00000000..24d9b3da --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/io/ole2/CompoundDocumentTestCase.java @@ -0,0 +1,77 @@ +package com.twelvemonkeys.io.ole2; + +import junit.framework.TestCase; + +import javax.imageio.stream.MemoryCacheImageInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteOrder; + +/** + * CompoundDocumentTestCase + * + * @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/io/ole2/CompoundDocumentTestCase.java#1 $ + */ +public class CompoundDocumentTestCase extends TestCase { + public void testReadCatalogInputStream() throws IOException { + InputStream input = getClass().getResourceAsStream("/Thumbs-camera.db"); + + assertNotNull("Missing test resource!", input); + + CompoundDocument document = new CompoundDocument(input); + Entry root = document.getRootEntry(); + assertNotNull(root); + assertEquals(25, root.getChildEntries().size()); + + Entry catalog = root.getChildEntry("Catalog"); + assertNotNull(catalog); + assertNotNull("Input stream may not be null", catalog.getInputStream()); + } + + public void testReadCatalogImageInputStream() throws IOException { + InputStream input = getClass().getResourceAsStream("/Thumbs-camera.db"); + + assertNotNull("Missing test resource!", input); + + MemoryCacheImageInputStream stream = new MemoryCacheImageInputStream(input); + stream.setByteOrder(ByteOrder.LITTLE_ENDIAN); + + CompoundDocument document = new CompoundDocument(stream); + + Entry root = document.getRootEntry(); + + assertNotNull(root); + assertEquals(25, root.getChildEntries().size()); + + Entry catalog = root.getChildEntry("Catalog"); + + assertNotNull(catalog); + assertNotNull("Input stream may not be null", catalog.getInputStream()); + } + + public void testReadThumbsCatalogFile() throws IOException, URISyntaxException { + URL input = getClass().getResource("/Thumbs-camera.db"); + + assertNotNull("Missing test resource!", input); + assertEquals("Test resource not a file:// resource", "file", input.getProtocol()); + + File file = new File(input.toURI()); + + CompoundDocument document = new CompoundDocument(file); + + Entry root = document.getRootEntry(); + + assertNotNull(root); + assertEquals(25, root.getChildEntries().size()); + + Entry catalog = root.getChildEntry("Catalog"); + + assertNotNull(catalog); + assertNotNull("Input stream may not be null", catalog.getInputStream()); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java new file mode 100755 index 00000000..f009905a --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java @@ -0,0 +1,228 @@ +package com.twelvemonkeys.lang; + +import junit.framework.TestCase; + +import java.util.Map; +import java.util.HashMap; +import java.lang.reflect.InvocationTargetException; +import java.text.NumberFormat; + +/** + * BeanUtilTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/BeanUtilTestCase.java#1 $ + */ +public class BeanUtilTestCase extends TestCase { + + public void testConfigureNoMehtod() { + TestBean bean = new TestBean(); + + Map map = new HashMap(); + + map.put("noSuchMethod", "jaffa"); + + try { + BeanUtil.configure(bean, map); + } + catch (InvocationTargetException e) { + fail(e.getMessage()); + } + } + + public void testConfigureNoMethodArgs() { + TestBean bean = new TestBean(); + + Map map = new HashMap(); + + map.put("doubleValue", new Object()); // Should not be able to convert this + + try { + BeanUtil.configure(bean, map); + } + catch (InvocationTargetException e) { + fail(e.getMessage()); + } + + assertNull(bean.getDoubleValue()); + + } + + public void testConfigureNullValue() { + TestBean bean = new TestBean(); + + Map map = new HashMap(); + + map.put("stringValue", null); + + try { + BeanUtil.configure(bean, map); + } + catch (InvocationTargetException e) { + fail(e.getMessage()); + } + + assertNull(bean.getStringValue()); + } + + public void testConfigureSimple() { + TestBean bean = new TestBean(); + + Map map = new HashMap(); + + map.put("stringValue", "one"); + map.put("intValue", new Integer(2)); + map.put("doubleValue", new Double(.3)); + + try { + BeanUtil.configure(bean, map); + } + catch (InvocationTargetException e) { + fail(e.getMessage()); + } + + assertEquals("one", bean.getStringValue()); + assertEquals(2, bean.getIntValue()); + assertEquals(new Double(.3), bean.getDoubleValue()); + } + + public void testConfigureConvert() { + TestBean bean = new TestBean(); + + Map map = new HashMap(); + + map.put("stringValue", new Integer(1)); + map.put("intValue", "2"); + map.put("doubleValue", NumberFormat.getNumberInstance().format(0.3)); // Note, format is locale specific... + + try { + BeanUtil.configure(bean, map); + } + catch (InvocationTargetException e) { + fail(e.getMessage()); + } + + assertEquals("1", bean.getStringValue()); + assertEquals(2, bean.getIntValue()); + assertEquals(new Double(.3), bean.getDoubleValue()); + } + + public void testConfigureAmbigious1() { + TestBean bean = new TestBean(); + + Map map = new HashMap(); + + String value = "one"; + map.put("ambigious", value); + + try { + BeanUtil.configure(bean, map); + } + catch (InvocationTargetException e) { + fail(e.getMessage()); + } + + assertNotNull(bean.getAmbigious()); + assertEquals("String converted rather than invoking setAmbigiouos(String), ordering not predictable", + "one", bean.getAmbigious()); + assertSame("String converted rather than invoking setAmbigiouos(String), ordering not predictable", + value, bean.getAmbigious()); + } + + public void testConfigureAmbigious2() { + TestBean bean = new TestBean(); + + Map map = new HashMap(); + + Integer value = new Integer(2); + map.put("ambigious", value); + + try { + BeanUtil.configure(bean, map); + } + catch (InvocationTargetException e) { + fail(e.getMessage()); + } + + assertNotNull(bean.getAmbigious()); + assertEquals("Integer converted rather than invoking setAmbigiouos(Integer), ordering not predictable", + new Integer(2), bean.getAmbigious()); + assertSame("Integer converted rather than invoking setAmbigiouos(Integer), ordering not predictable", + value, bean.getAmbigious()); + } + + public void testConfigureAmbigious3() { + TestBean bean = new TestBean(); + + Map map = new HashMap(); + + Double value = new Double(.3); + map.put("ambigious", value); + + try { + BeanUtil.configure(bean, map); + } + catch (InvocationTargetException e) { + fail(e.getMessage()); + } + + assertNotNull(bean.getAmbigious()); + assertEquals("Object converted rather than invoking setAmbigious(Object), ordering not predictable", + value.getClass(), bean.getAmbigious().getClass()); + assertSame("Object converted rather than invoking setAmbigious(Object), ordering not predictable", + value, bean.getAmbigious()); + } + + static class TestBean { + private String mString; + private int mInt; + private Double mDouble; + + private Object mAmbigious; + + public Double getDoubleValue() { + return mDouble; + } + + public int getIntValue() { + return mInt; + } + + public String getStringValue() { + return mString; + } + + public void setStringValue(String pString) { + mString = pString; + } + + public void setIntValue(int pInt) { + mInt = pInt; + } + + public void setDoubleValue(Double pDouble) { + mDouble = pDouble; + } + + public void setAmbigious(String pString) { + mAmbigious = pString; + } + + public void setAmbigious(Object pObject) { + mAmbigious = pObject; + } + + public void setAmbigious(Integer pInteger) { + mAmbigious = pInteger; + } + + public void setAmbigious(int pInt) { + mAmbigious = new Long(pInt); // Just to differentiate... + } + + public Object getAmbigious() { + return mAmbigious; + } + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/ObjectAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/ObjectAbstractTestCase.java new file mode 100755 index 00000000..a8394c63 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/ObjectAbstractTestCase.java @@ -0,0 +1,321 @@ +package com.twelvemonkeys.lang; + +import junit.framework.TestCase; + +import java.lang.reflect.Method; +import java.io.*; + +/** + * AbstractObjectTestCase + *

+ * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/ObjectAbstractTestCase.java#1 $ + */ +public abstract class ObjectAbstractTestCase extends TestCase { + // TODO: See com.tm.util.ObjectAbstractTestCase + // TODO: The idea is that this should be some generic base-class that + // implements the basic object tests + + // TODO: Create Serializable test similar way + // TODO: Create Comparable test similar way + + /** + * Creates a TestCase. + * + * @param testName the test class name + */ + protected ObjectAbstractTestCase(String testName) { + super(testName); + } + + protected ObjectAbstractTestCase() { + super(); + } + + /** + * Returns an instance of the class we are testing. + * Implement this method to return the object to test. + * + * @return the object to test + */ + protected abstract Object makeObject(); + + // TODO: Can we really do serious testing with just one object? + // TODO: How can we make sure we create equal or different objects?! + //protected abstract Object makeDifferentObject(Object pObject); + //protected abstract Object makeEqualObject(Object pObject); + + + public void testToString() { + assertNotNull(makeObject().toString()); + // TODO: What more can we test? + } + + // TODO: assert that either BOTH or NONE of equals/hashcode is overridden + public void testEqualsHashCode(){ + Object obj = makeObject(); + + Class cl = obj.getClass(); + if (isEqualsOverriden(cl)) { + assertTrue("Class " + cl.getName() + + " implements equals but not hashCode", isHashCodeOverriden(cl)); + } + else if (isHashCodeOverriden(cl)) { + assertTrue("Class " + cl.getName() + + " implements hashCode but not equals", isEqualsOverriden(cl)); + } + + } + + protected static boolean isEqualsOverriden(Class pClass) { + return getDeclaredMethod(pClass, "equals", new Class[]{Object.class}) != null; + } + + protected static boolean isHashCodeOverriden(Class pClass) { + return getDeclaredMethod(pClass, "hashCode", null) != null; + } + + private static Method getDeclaredMethod(Class pClass, String pName, Class[] pArameters) { + try { + return pClass.getDeclaredMethod(pName, pArameters); + } + catch (NoSuchMethodException ignore) { + return null; + } + } + + public void testObjectEqualsSelf() { + Object obj = makeObject(); + assertEquals("An Object should equal itself", obj, obj); + } + + public void testEqualsNull() { + Object obj = makeObject(); + // NOTE: Makes sure this doesn't throw NPE either + //noinspection ObjectEqualsNull + assertFalse("An object should never equal null", obj.equals(null)); + } + + public void testObjectHashCodeEqualsSelfHashCode() { + Object obj = makeObject(); + assertEquals("hashCode should be repeatable", obj.hashCode(), obj.hashCode()); + } + + public void testObjectHashCodeEqualsContract() { + Object obj1 = makeObject(); + if (obj1.equals(obj1)) { + assertEquals( + "[1] When two objects are equal, their hashCodes should be also.", + obj1.hashCode(), obj1.hashCode()); + } + // TODO: Make sure we create at least one equal object, and one different object + Object obj2 = makeObject(); + if (obj1.equals(obj2)) { + assertEquals( + "[2] When two objects are equal, their hashCodes should be also.", + obj1.hashCode(), obj2.hashCode()); + assertTrue( + "When obj1.equals(obj2) is true, then obj2.equals(obj1) should also be true", + obj2.equals(obj1)); + } + } + + /* + public void testFinalize() { + // TODO: Implement + } + */ + + //////////////////////////////////////////////////////////////////////////// + // Cloneable interface + public void testClone() throws Exception { + Object obj = makeObject(); + if (obj instanceof Cloneable) { + Class cl = obj.getClass(); + + Method clone = findMethod(cl, "clone"); + + // Disregard protected modifier + // NOTE: This will throw a SecurityException if a SecurityManager + // disallows access, but should not happen in a test context + if (!clone.isAccessible()) { + clone.setAccessible(true); + } + + Object cloned = clone.invoke(obj, null); + + assertNotNull("Cloned object should never be null", cloned); + + // TODO: This can only be asserted if equals() test is based on + // value equality, not reference (identity) equality + // Maybe it's possible to do a reflective introspection of + // the objects fields? + if (isHashCodeOverriden(cl)) { + assertEquals("Cloned object not equal", obj, cloned); + } + } + } + + private static Method findMethod(Class pClass, String pName) throws NoSuchMethodException { + if (pClass == null) { + throw new IllegalArgumentException("class == null"); + } + if (pName == null) { + throw new IllegalArgumentException("name == null"); + } + + Class cl = pClass; + + while (cl != null) { + try { + return cl.getDeclaredMethod(pName, new Class[0]); + } + catch (NoSuchMethodException e) { + } + catch (SecurityException e) { + } + + cl = cl.getSuperclass(); + } + + throw new NoSuchMethodException(pName + " in class " + pClass.getName()); + } + + /////////////////////////////////////////////////////////////////////////// + // Serializable interface + public void testSerializeDeserializeThenCompare() throws Exception { + Object obj = makeObject(); + if (obj instanceof Serializable) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(buffer); + try { + out.writeObject(obj); + } + finally { + out.close(); + } + + Object dest; + ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())); + try { + dest = in.readObject(); + } + finally { + in.close(); + } + + // TODO: This can only be asserted if equals() test is based on + // value equality, not reference (identity) equality + // Maybe it's possible to do a reflective introspection of + // the objects fields? + if (isEqualsOverriden(obj.getClass())) { + assertEquals("obj != deserialize(serialize(obj))", obj, dest); + } + } + } + + /** + * Sanity check method, makes sure that any Serializable + * class can be serialized and de-serialized in memory, + * using the handy makeObject() method + * + * @throws java.io.IOException + * @throws ClassNotFoundException + */ + public void testSimpleSerialization() throws Exception { + Object o = makeObject(); + if (o instanceof Serializable) { + byte[] object = writeExternalFormToBytes((Serializable) o); + readExternalFormFromBytes(object); + } + } + + /** + * Write a Serializable or Externalizable object as + * a file at the given path. + * NOT USEFUL as part + * of a unit test; this is just a utility method + * for creating disk-based objects in CVS that can become + * the basis for compatibility tests using + * readExternalFormFromDisk(String path) + * + * @param o Object to serialize + * @param path path to write the serialized Object + * @exception java.io.IOException + */ + protected void writeExternalFormToDisk(Serializable o, String path) throws IOException { + FileOutputStream fileStream = new FileOutputStream(path); + writeExternalFormToStream(o, fileStream); + } + + /** + * Converts a Serializable or Externalizable object to + * bytes. Useful for in-memory tests of serialization + * + * @param o Object to convert to bytes + * @return serialized form of the Object + * @exception java.io.IOException + */ + protected byte[] writeExternalFormToBytes(Serializable o) throws IOException { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + writeExternalFormToStream(o, byteStream); + return byteStream.toByteArray(); + } + + /** + * Reads a Serialized or Externalized Object from disk. + * Useful for creating compatibility tests between + * different CVS versions of the same class + * + * @param path path to the serialized Object + * @return the Object at the given path + * @exception java.io.IOException + * @exception ClassNotFoundException + */ + protected Object readExternalFormFromDisk(String path) throws IOException, ClassNotFoundException { + FileInputStream stream = new FileInputStream(path); + return readExternalFormFromStream(stream); + } + + /** + * Read a Serialized or Externalized Object from bytes. + * Useful for verifying serialization in memory. + * + * @param b byte array containing a serialized Object + * @return Object contained in the bytes + * @exception java.io.IOException + * @exception ClassNotFoundException + */ + protected Object readExternalFormFromBytes(byte[] b) throws IOException, ClassNotFoundException { + ByteArrayInputStream stream = new ByteArrayInputStream(b); + return readExternalFormFromStream(stream); + } + + // private implementation + //----------------------------------------------------------------------- + private Object readExternalFormFromStream(InputStream stream) throws IOException, ClassNotFoundException { + ObjectInputStream oStream = new ObjectInputStream(stream); + return oStream.readObject(); + } + + private void writeExternalFormToStream(Serializable o, OutputStream stream) throws IOException { + ObjectOutputStream oStream = new ObjectOutputStream(stream); + oStream.writeObject(o); + } + + public static final class SanityTestTestCase extends ObjectAbstractTestCase { + /** + * Creates a TestCase. + * + */ + public SanityTestTestCase() { + super(SanityTestTestCase.class.getName()); + } + + protected Object makeObject() { + return new Cloneable() {}; + } + } + +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/StringUtilTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/StringUtilTestCase.java new file mode 100755 index 00000000..0ae6cf32 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/lang/StringUtilTestCase.java @@ -0,0 +1,861 @@ + +package com.twelvemonkeys.lang; + +import junit.framework.TestCase; + +import java.awt.*; +import java.util.Date; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.sql.Timestamp; + +/** + * StringUtilTestCase + * + * + * + * + * @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/lang/StringUtilTestCase.java#1 $ + * + */ +public class StringUtilTestCase extends TestCase { + final static Object TEST_OBJECT = new Object(); + final static Integer TEST_INTEGER = new Integer(42); + final static String TEST_STRING = "TheQuickBrownFox"; // No WS! + final static String TEST_SUB_STRING = TEST_STRING.substring(2, 5); + final static String TEST_DELIM_STRING = "one,two, three\n four\tfive six"; + final static String[] STRING_ARRAY = {"one", "two", "three", "four", "five", "six"}; + final static String TEST_INT_DELIM_STRING = "1,2, 3\n 4\t5 6"; + final static int[] INT_ARRAY = {1, 2, 3, 4, 5, 6}; + final static String TEST_DOUBLE_DELIM_STRING = "1.4,2.1, 3\n .4\t-5 6e5"; + final static double[] DOUBLE_ARRAY = {1.4, 2.1, 3, .4, -5, 6e5}; + final static String EMPTY_STRING = ""; + final static String WHITESPACE_STRING = " \t \r \n "; + + public void testValueOfObject() { + + assertNotNull(StringUtil.valueOf(TEST_OBJECT)); + assertEquals(StringUtil.valueOf(TEST_OBJECT), TEST_OBJECT.toString()); + assertEquals(StringUtil.valueOf(TEST_INTEGER), TEST_INTEGER.toString()); + assertEquals(StringUtil.valueOf(TEST_STRING), TEST_STRING); + assertSame(StringUtil.valueOf(TEST_STRING), TEST_STRING); + + assertNull(StringUtil.valueOf(null)); + } + + public void testToUpperCase() { + String str = StringUtil.toUpperCase(TEST_STRING); + assertNotNull(str); + assertEquals(TEST_STRING.toUpperCase(), str); + + str = StringUtil.toUpperCase(null); + assertNull(str); + } + + public void testToLowerCase() { + String str = StringUtil.toLowerCase(TEST_STRING); + assertNotNull(str); + assertEquals(TEST_STRING.toLowerCase(), str); + + str = StringUtil.toLowerCase(null); + assertNull(str); + } + + public void testIsEmpty() { + assertTrue(StringUtil.isEmpty((String) null)); + assertTrue(StringUtil.isEmpty(EMPTY_STRING)); + assertTrue(StringUtil.isEmpty(WHITESPACE_STRING)); + assertFalse(StringUtil.isEmpty(TEST_STRING)); + } + + public void testIsEmptyArray() { + assertTrue(StringUtil.isEmpty((String[]) null)); + assertTrue(StringUtil.isEmpty(new String[]{EMPTY_STRING})); + assertTrue(StringUtil.isEmpty(new String[]{EMPTY_STRING, WHITESPACE_STRING})); + assertFalse(StringUtil.isEmpty(new String[]{EMPTY_STRING, TEST_STRING})); + assertFalse(StringUtil.isEmpty(new String[]{WHITESPACE_STRING, TEST_STRING})); + } + + public void testContains() { + assertTrue(StringUtil.contains(TEST_STRING, TEST_STRING)); + assertTrue(StringUtil.contains(TEST_STRING, TEST_SUB_STRING)); + assertTrue(StringUtil.contains(TEST_STRING, EMPTY_STRING)); + assertFalse(StringUtil.contains(TEST_STRING, WHITESPACE_STRING)); + assertFalse(StringUtil.contains(TEST_SUB_STRING, TEST_STRING)); + assertFalse(StringUtil.contains(EMPTY_STRING, TEST_STRING)); + assertFalse(StringUtil.contains(WHITESPACE_STRING, TEST_STRING)); + assertFalse(StringUtil.contains(null, TEST_STRING)); + assertFalse(StringUtil.contains(null, null)); + } + + public void testContainsIgnoreCase() { + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING, TEST_STRING)); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING)); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING)); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING, TEST_STRING.toUpperCase())); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING, TEST_SUB_STRING)); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING.toUpperCase(), TEST_SUB_STRING)); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING.toLowerCase(), TEST_SUB_STRING)); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING, TEST_SUB_STRING.toUpperCase())); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING, EMPTY_STRING)); + assertFalse(StringUtil.containsIgnoreCase(TEST_STRING, WHITESPACE_STRING)); + assertFalse(StringUtil.containsIgnoreCase(TEST_SUB_STRING, TEST_STRING)); + assertFalse(StringUtil.containsIgnoreCase(EMPTY_STRING, TEST_STRING)); + assertFalse(StringUtil.containsIgnoreCase(WHITESPACE_STRING, TEST_STRING)); + assertFalse(StringUtil.containsIgnoreCase(null, TEST_STRING)); + assertFalse(StringUtil.containsIgnoreCase(null, null)); + } + + public void testContainsChar() { + for (int i = 0; i < TEST_STRING.length(); i++) { + assertTrue(StringUtil.contains(TEST_STRING, TEST_STRING.charAt(i))); + assertFalse(StringUtil.contains(EMPTY_STRING, TEST_STRING.charAt(i))); + assertFalse(StringUtil.contains(WHITESPACE_STRING, TEST_STRING.charAt(i))); + assertFalse(StringUtil.contains(null, TEST_STRING.charAt(i))); + } + for (int i = 0; i < TEST_SUB_STRING.length(); i++) { + assertTrue(StringUtil.contains(TEST_STRING, TEST_SUB_STRING.charAt(i))); + } + for (int i = 0; i < WHITESPACE_STRING.length(); i++) { + assertFalse(StringUtil.contains(TEST_STRING, WHITESPACE_STRING.charAt(i))); + } + + // Test all alpha-chars + for (int i = 'a'; i < 'z'; i++) { + if (TEST_STRING.indexOf(i) < 0) { + assertFalse(TEST_STRING + " seems to contain '" + (char) i + "', at index " + TEST_STRING.indexOf(i), StringUtil.contains(TEST_STRING, i)); + } + else { + assertTrue(TEST_STRING + " seems to not contain '" + (char) i + "', at index " + TEST_STRING.indexOf(i), StringUtil.contains(TEST_STRING, i)); + } + } + } + + public void testContainsIgnoreCaseChar() { + // Must contain all chars in string + for (int i = 0; i < TEST_STRING.length(); i++) { + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING, TEST_STRING.charAt(i))); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING.charAt(i))); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING.charAt(i))); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING, Character.toUpperCase(TEST_STRING.charAt(i)))); + assertFalse(StringUtil.containsIgnoreCase(EMPTY_STRING, TEST_STRING.charAt(i))); + assertFalse(StringUtil.containsIgnoreCase(WHITESPACE_STRING, TEST_STRING.charAt(i))); + assertFalse(StringUtil.containsIgnoreCase(null, TEST_STRING.charAt(i))); + } + for (int i = 0; i < TEST_SUB_STRING.length(); i++) { + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING, TEST_SUB_STRING.charAt(i))); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING.toUpperCase(), TEST_SUB_STRING.charAt(i))); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING.toLowerCase(), TEST_SUB_STRING.charAt(i))); + assertTrue(StringUtil.containsIgnoreCase(TEST_STRING, TEST_SUB_STRING.toUpperCase().charAt(i))); + } + + for (int i = 0; i < WHITESPACE_STRING.length(); i++) { + assertFalse(StringUtil.containsIgnoreCase(TEST_STRING, WHITESPACE_STRING.charAt(i))); + } + + // Test all alpha-chars + for (int i = 'a'; i < 'z'; i++) { + if ((TEST_STRING.indexOf(i) < 0) && (TEST_STRING.indexOf(Character.toUpperCase((char) i)) < 0)) { + assertFalse(TEST_STRING + " seems to contain '" + (char) i + "', at index " + Math.max(TEST_STRING.indexOf(i), TEST_STRING.indexOf(Character.toUpperCase((char) i))), StringUtil.containsIgnoreCase(TEST_STRING, i)); + } + else { + assertTrue(TEST_STRING + " seems to not contain '" + (char) i + "', at index " + TEST_STRING.indexOf(i), StringUtil.containsIgnoreCase(TEST_STRING, i)); + } + } + } + + public void testIndexOfIgnoreCase() { + assertEquals(0, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING)); + assertEquals(0, StringUtil.indexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING)); + assertEquals(0, StringUtil.indexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING)); + assertEquals(0, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.toUpperCase())); + assertEquals(0, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.toLowerCase())); + + for (int i = 1; i < TEST_STRING.length(); i++) { + assertEquals(i, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.substring(i))); + assertEquals(i, StringUtil.indexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING.substring(i))); + assertEquals(i, StringUtil.indexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING.substring(i))); + assertEquals(i, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.toUpperCase().substring(i))); + assertEquals(i, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.toLowerCase().substring(i))); + } + + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_SUB_STRING)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_SUB_STRING)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toUpperCase())); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toLowerCase())); + + for (int i = 1; i < TEST_STRING.length(); i++) { + assertEquals(-1, StringUtil.indexOfIgnoreCase(TEST_STRING.substring(i), TEST_STRING)); + assertEquals(-1, StringUtil.indexOfIgnoreCase(TEST_STRING.substring(i), TEST_STRING)); + } + + assertEquals(-1, StringUtil.indexOfIgnoreCase(null, TEST_STRING)); + assertEquals(-1, StringUtil.indexOfIgnoreCase(null, null)); + } + + public void testIndexOfIgnoreCasePos() { + assertEquals(-1, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING, 1)); + assertEquals(-1, StringUtil.indexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING, 2)); + assertEquals(-1, StringUtil.indexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING, 3)); + assertEquals(-1, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.toUpperCase(), 4)); + assertEquals(-1, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.toLowerCase(), 5)); + + for (int i = 1; i < TEST_STRING.length(); i++) { + assertEquals(i, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.substring(i), i - 1)); + assertEquals(i, StringUtil.indexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING.substring(i), i - 1)); + assertEquals(i, StringUtil.indexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING.substring(i), i - 1)); + assertEquals(i, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.toUpperCase().substring(i), i - 1)); + assertEquals(i, StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.toLowerCase().substring(i), i - 1)); + } + + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING, 1)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_SUB_STRING, 1)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_SUB_STRING, 2)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toUpperCase(), 1)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toLowerCase(), 2)); + + assertEquals(-1, StringUtil.indexOfIgnoreCase(null, TEST_STRING, 234)); + assertEquals(-1, StringUtil.indexOfIgnoreCase(null, null, -45)); + } + + public void testLastIndexOfIgnoreCase() { + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.toUpperCase())); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.toLowerCase())); + + for (int i = 1; i < TEST_STRING.length(); i++) { + assertEquals(i, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.substring(i))); + assertEquals(i, StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING.substring(i))); + assertEquals(i, StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING.substring(i))); + assertEquals(i, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.toUpperCase().substring(i))); + assertEquals(i, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.toLowerCase().substring(i))); + } + + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING)); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_SUB_STRING)); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_SUB_STRING)); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toUpperCase())); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toLowerCase())); + + for (int i = 1; i < TEST_STRING.length(); i++) { + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(TEST_STRING.substring(i), TEST_STRING)); + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(TEST_STRING.substring(i), TEST_STRING)); + } + + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(null, TEST_STRING)); + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(null, null)); + + } + + public void testLastIndexOfIgnoreCasePos() { + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING, 1)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING, 2)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING, 3)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.toUpperCase(), 4)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.toLowerCase(), 5)); + + for (int i = 1; i < TEST_STRING.length(); i++) { + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.substring(0, i), i - 1)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING.substring(0, i), i - 1)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING.substring(0, i), i - 1)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.toUpperCase().substring(0, i), i - 1)); + assertEquals(0, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.toLowerCase().substring(0, i), i - 1)); + } + + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING, TEST_SUB_STRING.length() + 3)); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_SUB_STRING, TEST_SUB_STRING.length() + 3)); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_SUB_STRING, TEST_SUB_STRING.length() + 4)); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toUpperCase(), TEST_SUB_STRING.length() + 3)); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toLowerCase(), TEST_SUB_STRING.length() + 4)); + + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(null, TEST_STRING, 234)); + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(null, null, -45)); + + } + + public void testIndexOfIgnoreCaseChar() { + // Must contain all chars in string + for (int i = 0; i < TEST_STRING.length(); i++) { + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.charAt(i))); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING.charAt(i))); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING.charAt(i))); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, Character.toUpperCase(TEST_STRING.charAt(i)))); + assertEquals(-1, StringUtil.indexOfIgnoreCase(EMPTY_STRING, TEST_STRING.charAt(i))); + assertEquals(-1, StringUtil.indexOfIgnoreCase(WHITESPACE_STRING, TEST_STRING.charAt(i))); + assertEquals(-1, StringUtil.indexOfIgnoreCase(null, TEST_STRING.charAt(i))); + } + + for (int i = 0; i < TEST_SUB_STRING.length(); i++) { + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.charAt(i))); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_SUB_STRING.charAt(i))); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_SUB_STRING.charAt(i))); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toUpperCase().charAt(i))); + } + + for (int i = 0; i < WHITESPACE_STRING.length(); i++) { + assertEquals(-1, StringUtil.indexOfIgnoreCase(TEST_STRING, WHITESPACE_STRING.charAt(i))); + } + + // Test all alpha-chars + for (int i = 'a'; i < 'z'; i++) { + if ((TEST_STRING.indexOf(i) < 0) && (TEST_STRING.indexOf(Character.toUpperCase((char) i)) < 0)) { + assertEquals(TEST_STRING + " seems to contain '" + (char) i + "', at index " + Math.max(TEST_STRING.indexOf(i), TEST_STRING.indexOf(Character.toUpperCase((char) i))), -1, StringUtil.indexOfIgnoreCase(TEST_STRING, i)); + } + else { + assertTrue(TEST_STRING + " seems to not contain '" + (char) i + "', at index " + TEST_STRING.indexOf(i), 0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, i)); + } + } + } + + public void testIndexOfIgnoreCaseCharPos() { + // Must contain all chars in string + for (int i = 0; i < TEST_STRING.length(); i++) { + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_STRING.charAt(i), i)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING.charAt(i), i)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING.charAt(i), i)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, Character.toUpperCase(TEST_STRING.charAt(i)), i)); + assertEquals(-1, StringUtil.indexOfIgnoreCase(EMPTY_STRING, TEST_STRING.charAt(i), i)); + assertEquals(-1, StringUtil.indexOfIgnoreCase(WHITESPACE_STRING, TEST_STRING.charAt(i), i)); + assertEquals(-1, StringUtil.indexOfIgnoreCase(null, TEST_STRING.charAt(i), i)); + } + + for (int i = 0; i < TEST_SUB_STRING.length(); i++) { + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.charAt(i), i)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_SUB_STRING.charAt(i), i)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_SUB_STRING.charAt(i), i)); + assertTrue(0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toUpperCase().charAt(i), i)); + } + + for (int i = 0; i < WHITESPACE_STRING.length(); i++) { + assertEquals(-1, StringUtil.indexOfIgnoreCase(TEST_STRING, WHITESPACE_STRING.charAt(i), i)); + } + + // Test all alpha-chars + for (int i = 'a'; i < 'z'; i++) { + if ((TEST_STRING.indexOf(i) < 0) && (TEST_STRING.indexOf(Character.toUpperCase((char) i)) < 0)) { + assertEquals(TEST_STRING + " seems to contain '" + (char) i + "', at index " + Math.max(TEST_STRING.indexOf(i), TEST_STRING.indexOf(Character.toUpperCase((char) i))), -1, StringUtil.indexOfIgnoreCase(TEST_STRING, i, 0)); + } + else { + assertTrue(TEST_STRING + " seems to not contain '" + (char) i + "', at index " + TEST_STRING.indexOf(i), 0 <= StringUtil.indexOfIgnoreCase(TEST_STRING, i, 0)); + } + } + } + + public void testLastIndexOfIgnoreCaseChar() { + // Must contain all chars in string + for (int i = 0; i < TEST_STRING.length(); i++) { + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.charAt(i))); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING.charAt(i))); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING.charAt(i))); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, Character.toUpperCase(TEST_STRING.charAt(i)))); + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(EMPTY_STRING, TEST_STRING.charAt(i))); + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(WHITESPACE_STRING, TEST_STRING.charAt(i))); + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(null, TEST_STRING.charAt(i))); + } + + for (int i = 0; i < TEST_SUB_STRING.length(); i++) { + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.charAt(i))); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_SUB_STRING.charAt(i))); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_SUB_STRING.charAt(i))); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toUpperCase().charAt(i))); + } + + for (int i = 0; i < WHITESPACE_STRING.length(); i++) { + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, WHITESPACE_STRING.charAt(i))); + } + + // Test all alpha-chars + for (int i = 'a'; i < 'z'; i++) { + if ((TEST_STRING.indexOf(i) < 0) && (TEST_STRING.indexOf(Character.toUpperCase((char) i)) < 0)) { + assertEquals(TEST_STRING + " seems to contain '" + (char) i + "', at index " + Math.max(TEST_STRING.indexOf(i), TEST_STRING.indexOf(Character.toUpperCase((char) i))), -1, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, i)); + } + else { + assertTrue(TEST_STRING + " seems to not contain '" + (char) i + "', at index " + TEST_STRING.indexOf(i), 0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, i)); + } + } + } + + public void testLastIndexOfIgnoreCaseCharPos() { + // Must contain all chars in string + for (int i = 0; i < TEST_STRING.length(); i++) { + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_STRING.charAt(i), i)); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_STRING.charAt(i), i)); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_STRING.charAt(i), i)); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, Character.toUpperCase(TEST_STRING.charAt(i)), i)); + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(EMPTY_STRING, TEST_STRING.charAt(i), i)); + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(WHITESPACE_STRING, TEST_STRING.charAt(i), i)); + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(null, TEST_STRING.charAt(i), i)); + } + + for (int i = 0; i < TEST_SUB_STRING.length(); i++) { + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.charAt(i), TEST_STRING.length())); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toUpperCase(), TEST_SUB_STRING.charAt(i), TEST_STRING.length())); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING.toLowerCase(), TEST_SUB_STRING.charAt(i), TEST_STRING.length())); + assertTrue(0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, TEST_SUB_STRING.toUpperCase().charAt(i), TEST_STRING.length())); + } + + for (int i = 0; i < WHITESPACE_STRING.length(); i++) { + assertEquals(-1, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, WHITESPACE_STRING.charAt(i), TEST_STRING.length())); + } + + // Test all alpha-chars + for (int i = 'a'; i < 'z'; i++) { + if ((TEST_STRING.indexOf(i) < 0) && (TEST_STRING.indexOf(Character.toUpperCase((char) i)) < 0)) { + assertEquals(TEST_STRING + " seems to contain '" + (char) i + "', at index " + Math.max(TEST_STRING.indexOf(i), TEST_STRING.indexOf(Character.toUpperCase((char) i))), -1, StringUtil.lastIndexOfIgnoreCase(TEST_STRING, i, TEST_STRING.length())); + } + else { + assertTrue(TEST_STRING + " seems to not contain '" + (char) i + "', at index " + TEST_STRING.indexOf(i), 0 <= StringUtil.lastIndexOfIgnoreCase(TEST_STRING, i, TEST_STRING.length())); + } + } + } + + public void testLtrim() { + assertEquals(TEST_STRING, StringUtil.ltrim(TEST_STRING)); + assertEquals(TEST_STRING, StringUtil.ltrim(" " + TEST_STRING)); + assertEquals(TEST_STRING, StringUtil.ltrim(WHITESPACE_STRING + TEST_STRING)); + assertFalse(TEST_STRING.equals(StringUtil.ltrim(TEST_STRING + WHITESPACE_STRING))); + // TODO: Test is not complete + } + + public void testRtrim() { + assertEquals(TEST_STRING, StringUtil.rtrim(TEST_STRING)); + assertEquals(TEST_STRING, StringUtil.rtrim(TEST_STRING + " ")); + assertEquals(TEST_STRING, StringUtil.rtrim(TEST_STRING + WHITESPACE_STRING)); + assertFalse(TEST_STRING.equals(StringUtil.rtrim(WHITESPACE_STRING + TEST_STRING))); + // TODO: Test is not complete + } + + public void testReplace() { + assertEquals("", StringUtil.replace(TEST_STRING, TEST_STRING, "")); + assertEquals("", StringUtil.replace("", "", "")); + assertEquals("", StringUtil.replace("", "xyzzy", "xyzzy")); + assertEquals(TEST_STRING, StringUtil.replace(TEST_STRING, "", "xyzzy")); + assertEquals("aabbdd", StringUtil.replace("aabbccdd", "c", "")); + assertEquals("aabbccdd", StringUtil.replace("aabbdd", "bd", "bccd")); + // TODO: Test is not complete + } + + public void testReplaceIgnoreCase() { + assertEquals("", StringUtil.replaceIgnoreCase(TEST_STRING, TEST_STRING.toUpperCase(), "")); + assertEquals("", StringUtil.replaceIgnoreCase("", "", "")); + assertEquals("", StringUtil.replaceIgnoreCase("", "xyzzy", "xyzzy")); + assertEquals(TEST_STRING, StringUtil.replaceIgnoreCase(TEST_STRING, "", "xyzzy")); + assertEquals("aabbdd", StringUtil.replaceIgnoreCase("aabbCCdd", "c", "")); + assertEquals("aabbdd", StringUtil.replaceIgnoreCase("aabbccdd", "C", "")); + assertEquals("aabbccdd", StringUtil.replaceIgnoreCase("aabbdd", "BD", "bccd")); + assertEquals("aabbccdd", StringUtil.replaceIgnoreCase("aabBDd", "bd", "bccd")); + // TODO: Test is not complete + } + + public void testCut() { + assertEquals(TEST_STRING, StringUtil.cut(TEST_STRING, TEST_STRING.length(), "..")); + assertEquals("This is a test..", StringUtil.cut("This is a test of how this works", 16, "..")); + assertEquals("This is a test", StringUtil.cut("This is a test of how this works", 16, null)); + assertEquals("This is a test", StringUtil.cut("This is a test of how this works", 16, "")); + // TODO: Test is not complete + } + + public void testCaptialize() { + assertNull(StringUtil.capitalize(null)); + assertEquals(TEST_STRING.toUpperCase(), StringUtil.capitalize(TEST_STRING.toUpperCase())); + assertTrue(StringUtil.capitalize("abc").charAt(0) == 'A'); + } + + public void testCaptializePos() { + assertNull(StringUtil.capitalize(null, 45)); + + // TODO: Should this throw IllegalArgument or StringIndexOutOfBonds? + assertEquals(TEST_STRING, StringUtil.capitalize(TEST_STRING, TEST_STRING.length() + 45)); + + for (int i = 0; i < TEST_STRING.length(); i++) { + assertTrue(Character.isUpperCase(StringUtil.capitalize(TEST_STRING, i).charAt(i))); + } + } + + public void testPad() { + assertEquals(TEST_STRING + "...", StringUtil.pad(TEST_STRING, TEST_STRING.length() + 3, "..", false)); + assertEquals(TEST_STRING, StringUtil.pad(TEST_STRING, 4, ".", false)); + assertEquals(TEST_STRING, StringUtil.pad(TEST_STRING, 4, ".", true)); + assertEquals("..." + TEST_STRING, StringUtil.pad(TEST_STRING, TEST_STRING.length() + 3, "..", true)); + } + + public void testToDate() { + long time = System.currentTimeMillis(); + Date now = new Date(time - time % 60000); // Default format seems to have no seconds.. + Date date = StringUtil.toDate(DateFormat.getInstance().format(now)); + assertNotNull(date); + assertEquals(now, date); + } + + public void testToDateWithFormatString() { + Calendar cal = new GregorianCalendar(); + cal.clear(); + cal.set(1976, 2, 16); // Month is 0-based + Date date = StringUtil.toDate("16.03.1976", "dd.MM.yyyy"); + assertNotNull(date); + assertEquals(cal.getTime(), date); + + cal.clear(); + cal.set(2004, 4, 13, 23, 51, 3); + date = StringUtil.toDate("2004-5-13 23:51 (03)", "yyyy-MM-dd hh:mm (ss)"); + assertNotNull(date); + assertEquals(cal.getTime(), date); + + cal.clear(); + cal.set(Calendar.HOUR, 1); + cal.set(Calendar.MINUTE, 2); + cal.set(Calendar.SECOND, 3); + date = StringUtil.toDate("123", "hms"); + assertNotNull(date); + assertEquals(cal.getTime(), date); + } + + public void testToDateWithFormat() { + Calendar cal = new GregorianCalendar(); + cal.clear(); + cal.set(1976, 2, 16); // Month is 0-based + Date date = StringUtil.toDate("16.03.1976", new SimpleDateFormat("dd.MM.yyyy")); + assertNotNull(date); + assertEquals(cal.getTime(), date); + + cal.clear(); + cal.set(2004, 4, 13, 23, 51); + date = StringUtil.toDate("13.5.04 23:51", + DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, new Locale("no", "NO"))); + assertNotNull(date); + assertEquals(cal.getTime(), date); + + cal.clear(); + cal.set(Calendar.HOUR, 1); + cal.set(Calendar.MINUTE, 2); + date = StringUtil.toDate("1:02 am", + DateFormat.getTimeInstance(DateFormat.SHORT, Locale.US)); + assertNotNull(date); + assertEquals(cal.getTime(), date); + } + + public void testToTimestamp() { + Calendar cal = new GregorianCalendar(); + cal.clear(); + cal.set(1976, 2, 16, 21, 28, 4); // Month is 0-based + Date date = StringUtil.toTimestamp("1976-03-16 21:28:04"); + assertNotNull(date); + assertTrue(date instanceof Timestamp); + assertEquals(cal.getTime(), date); + } + + public void testToStringArray() { + String[] arr = StringUtil.toStringArray(TEST_DELIM_STRING); + assertNotNull(arr); + assertEquals(STRING_ARRAY.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(STRING_ARRAY[i], arr[i]); + } + } + + public void testToStringArrayDelim() { + String[] arr = StringUtil.toStringArray("-1---2-3--4-5", "---"); + String[] arr2 = {"1", "2", "3", "4", "5"}; + assertNotNull(arr); + assertEquals(arr2.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(arr2[i], arr[i]); + } + + arr = StringUtil.toStringArray("1, 2, 3; 4 5", ",; "); + assertNotNull(arr); + assertEquals(arr2.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(arr2[i], arr[i]); + } + } + + public void testToIntArray() { + int[] arr = StringUtil.toIntArray(TEST_INT_DELIM_STRING); + assertNotNull(arr); + assertEquals(INT_ARRAY.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(INT_ARRAY[i], arr[i]); + } + } + + public void testToIntArrayDelim() { + int[] arr = StringUtil.toIntArray("-1---2-3--4-5", "---"); + int[] arr2 = {1, 2, 3, 4, 5}; + assertNotNull(arr); + assertEquals(arr2.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(arr2[i], arr[i]); + } + + arr = StringUtil.toIntArray("1, 2, 3; 4 5", ",; "); + assertNotNull(arr); + assertEquals(arr2.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(arr2[i], arr[i]); + } + } + + public void testToIntArrayDelimBase() { + int[] arr = StringUtil.toIntArray("-1___2_3__F_a", "___", 16); + int[] arr2 = {-1, 2, 3, 0xf, 0xa}; + assertNotNull(arr); + assertEquals(arr2.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(arr2[i], arr[i]); + } + + arr = StringUtil.toIntArray("-1, 2, 3; 17 12", ",; ", 8); + assertNotNull(arr); + assertEquals(arr2.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(arr2[i], arr[i]); + } + } + + public void testToLongArray() { + long[] arr = StringUtil.toLongArray(TEST_INT_DELIM_STRING); + assertNotNull(arr); + assertEquals(INT_ARRAY.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(INT_ARRAY[i], arr[i]); + } + } + + public void testToLongArrayDelim() { + long[] arr = StringUtil.toLongArray("-12854928752983___2_3__4_5", "___"); + long[] arr2 = {-12854928752983L, 2, 3, 4, 5}; + assertNotNull(arr); + assertEquals(arr2.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(arr2[i], arr[i]); + } + + arr = StringUtil.toLongArray("-12854928752983, 2, 3; 4 5", ",; "); + assertNotNull(arr); + assertEquals(arr2.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(arr2[i], arr[i]); + } + } + + public void testToDoubleArray() { + double[] arr = StringUtil.toDoubleArray(TEST_DOUBLE_DELIM_STRING); + assertNotNull(arr); + assertEquals(DOUBLE_ARRAY.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(DOUBLE_ARRAY[i], arr[i], 0d); + } + } + + public void testToDoubleArrayDelim() { + double[] arr = StringUtil.toDoubleArray("-12854928752983___.2_3__4_5e4", "___"); + double[] arr2 = {-12854928752983L, .2, 3, 4, 5e4}; + assertNotNull(arr); + assertEquals(arr2.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(arr2[i], arr[i], 0d); + } + + arr = StringUtil.toDoubleArray("-12854928752983, .2, 3; 4 5E4", ",; "); + assertNotNull(arr); + assertEquals(arr2.length, arr.length); + for (int i = 0; i < arr.length; i++) { + assertEquals(arr2[i], arr[i], 0d); + } + } + + public void testTestToColor() { + // Test all constants + assertEquals(Color.black, StringUtil.toColor("black")); + assertEquals(Color.black, StringUtil.toColor("BLACK")); + assertEquals(Color.blue, StringUtil.toColor("blue")); + assertEquals(Color.blue, StringUtil.toColor("BLUE")); + assertEquals(Color.cyan, StringUtil.toColor("cyan")); + assertEquals(Color.cyan, StringUtil.toColor("CYAN")); + assertEquals(Color.darkGray, StringUtil.toColor("darkGray")); + assertEquals(Color.darkGray, StringUtil.toColor("DARK_GRAY")); + assertEquals(Color.gray, StringUtil.toColor("gray")); + assertEquals(Color.gray, StringUtil.toColor("GRAY")); + assertEquals(Color.green, StringUtil.toColor("green")); + assertEquals(Color.green, StringUtil.toColor("GREEN")); + assertEquals(Color.lightGray, StringUtil.toColor("lightGray")); + assertEquals(Color.lightGray, StringUtil.toColor("LIGHT_GRAY")); + assertEquals(Color.magenta, StringUtil.toColor("magenta")); + assertEquals(Color.magenta, StringUtil.toColor("MAGENTA")); + assertEquals(Color.orange, StringUtil.toColor("orange")); + assertEquals(Color.orange, StringUtil.toColor("ORANGE")); + assertEquals(Color.pink, StringUtil.toColor("pink")); + assertEquals(Color.pink, StringUtil.toColor("PINK")); + assertEquals(Color.red, StringUtil.toColor("red")); + assertEquals(Color.red, StringUtil.toColor("RED")); + assertEquals(Color.white, StringUtil.toColor("white")); + assertEquals(Color.white, StringUtil.toColor("WHITE")); + assertEquals(Color.yellow, StringUtil.toColor("yellow")); + assertEquals(Color.yellow, StringUtil.toColor("YELLOW")); + + System.out.println(StringUtil.deepToString(Color.yellow)); + System.out.println(StringUtil.deepToString(Color.pink, true, -1)); + + // Test HTML/CSS style color + for (int i = 0; i < 256; i++) { + int c = i; + if (i < 0x10) { + c = i * 16; + } + String colorStr = "#" + Integer.toHexString(i) + Integer.toHexString(i) + Integer.toHexString(i); + String colorStrAlpha = "#" + Integer.toHexString(i) + Integer.toHexString(i) + Integer.toHexString(i) + Integer.toHexString(i); + assertEquals(new Color(c, c, c), StringUtil.toColor(colorStr)); + assertEquals(new Color(c, c, c, c), StringUtil.toColor(colorStrAlpha)); + + } + + // Test null + // TODO: Hmmm.. Maybe reconsider this.. + assertNull(StringUtil.toColor(null)); + + // Test + try { + StringUtil.toColor("illegal-color-value"); + fail("toColor with illegal color value should throw IllegalArgumentException."); + } + catch (IllegalArgumentException e) { + assertNotNull(e.getMessage()); + } + } + + public void testToColorString() { + assertEquals("#ff0000", StringUtil.toColorString(Color.red)); + assertEquals("#00ff00", StringUtil.toColorString(Color.green)); + assertEquals("#0000ff", StringUtil.toColorString(Color.blue)); + assertEquals("#101010", StringUtil.toColorString(new Color(0x10, 0x10, 0x10))); + + for (int i = 0; i < 256; i++) { + String str = (i < 0x10 ? "0" : "") + Integer.toHexString(i); + assertEquals("#" + str + str + str, StringUtil.toColorString(new Color(i, i, i))); + } + + // Test null + // TODO: Hmmm.. Maybe reconsider this.. + assertNull(StringUtil.toColorString(null)); + } + + public void testIsNumber() { + assertTrue(StringUtil.isNumber("0")); + assertTrue(StringUtil.isNumber("12345")); + assertTrue(StringUtil.isNumber(TEST_INTEGER.toString())); + assertTrue(StringUtil.isNumber("1234567890123456789012345678901234567890")); + assertTrue(StringUtil.isNumber(String.valueOf(Long.MAX_VALUE) + String.valueOf(Long.MAX_VALUE))); + assertFalse(StringUtil.isNumber("abc")); + assertFalse(StringUtil.isNumber(TEST_STRING)); + } + + public void testIsNumberNegative() { + assertTrue(StringUtil.isNumber("-12345")); + assertTrue(StringUtil.isNumber('-' + TEST_INTEGER.toString())); + assertTrue(StringUtil.isNumber("-1234567890123456789012345678901234567890")); + assertTrue(StringUtil.isNumber('-' + String.valueOf(Long.MAX_VALUE) + String.valueOf(Long.MAX_VALUE))); + assertFalse(StringUtil.isNumber("-abc")); + assertFalse(StringUtil.isNumber('-' + TEST_STRING)); + } + + public void testCamelToLispNull() { + try { + StringUtil.camelToLisp(null); + fail("should not accept null"); + } + catch (IllegalArgumentException iae) { + assertNotNull(iae.getMessage()); + } + } + public void testCamelToLispNoConversion() { + assertEquals("", StringUtil.camelToLisp("")); + assertEquals("equal", StringUtil.camelToLisp("equal")); + assertEquals("allready-lisp", StringUtil.camelToLisp("allready-lisp")); + } + + public void testCamelToLispSimple() { + // Simple tests + assertEquals("foo-bar", StringUtil.camelToLisp("fooBar")); + } + + public void testCamelToLispCase() { + // Casing + assertEquals("my-url", StringUtil.camelToLisp("myURL")); + assertEquals("another-url", StringUtil.camelToLisp("AnotherURL")); + } + + public void testCamelToLispMulti() { + // Several words + assertEquals("http-request-wrapper", StringUtil.camelToLisp("HttpRequestWrapper")); + String s = StringUtil.camelToLisp("HttpURLConnection"); + assertEquals("http-url-connection", s); + // Long and short abbre in upper case + assertEquals("welcome-to-my-world", StringUtil.camelToLisp("WELCOMEToMYWorld")); + } + + public void testCamelToLispLeaveUntouched() { + // Leave others untouched + assertEquals("a-slightly-longer-and-more-bumpy-string?.,[]()", StringUtil.camelToLisp("ASlightlyLongerANDMoreBumpyString?.,[]()")); + } + public void testCamelToLispNumbers() { + // Numbers + // TODO: FixMe + String s = StringUtil.camelToLisp("my45Caliber"); + assertEquals("my-45-caliber", s); + assertEquals("hello-12345-world-67890", StringUtil.camelToLisp("Hello12345world67890")); + assertEquals("hello-12345-my-world-67890-this-time", StringUtil.camelToLisp("HELLO12345MyWorld67890thisTime")); + assertEquals("hello-12345-world-67890-too", StringUtil.camelToLisp("Hello12345WORLD67890too")); + } + + public void testLispToCamelNull() { + try { + StringUtil.lispToCamel(null); + fail("should not accept null"); + } + catch (IllegalArgumentException iae) { + assertNotNull(iae.getMessage()); + } + } + public void testLispToCamelNoConversion() { + assertEquals("", StringUtil.lispToCamel("")); + assertEquals("equal", StringUtil.lispToCamel("equal")); + assertEquals("allreadyCamel", StringUtil.lispToCamel("allreadyCamel")); + } + + public void testLispToCamelSimple() { + // Simple tests + assertEquals("fooBar", StringUtil.lispToCamel("foo-bar")); + assertEquals("myUrl", StringUtil.lispToCamel("my-URL")); + assertEquals("anotherUrl", StringUtil.lispToCamel("ANOTHER-URL")); + } + + public void testLispToCamelCase() { + // Casing + assertEquals("Object", StringUtil.lispToCamel("object", true)); + assertEquals("object", StringUtil.lispToCamel("Object", false)); + } + + public void testLispToCamelMulti() { + // Several words + assertEquals("HttpRequestWrapper", StringUtil.lispToCamel("http-request-wrapper", true)); + } + + public void testLispToCamelLeaveUntouched() { + // Leave others untouched + assertEquals("ASlightlyLongerAndMoreBumpyString?.,[]()", StringUtil.lispToCamel("a-slightly-longer-and-more-bumpy-string?.,[]()", true)); + } + + public void testLispToCamelNumber() { + // Numbers + assertEquals("my45Caliber", StringUtil.lispToCamel("my-45-caliber")); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java new file mode 100755 index 00000000..f4875444 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java @@ -0,0 +1,59 @@ +package com.twelvemonkeys.net; + +import junit.framework.TestCase; + +/** + * NetUtilTestCase + *

+ * + * + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/net/NetUtilTestCase.java#1 $ + */ +public class NetUtilTestCase extends TestCase { + public void setUp() throws Exception { + super.setUp(); + } + + public void tearDown() throws Exception { + super.tearDown(); + } + + public void testParseHTTPDateRFC1123() { + long time = NetUtil.parseHTTPDate("Sun, 06 Nov 1994 08:49:37 GMT"); + assertEquals(784111777000l, time); + + time = NetUtil.parseHTTPDate("Sunday, 06 Nov 1994 08:49:37 GMT"); + assertEquals(784111777000l, time); + } + + public void testParseHTTPDateRFC850() { + long time = NetUtil.parseHTTPDate("Sunday, 06-Nov-1994 08:49:37 GMT"); + assertEquals(784111777000l, time); + + time = NetUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT"); + assertEquals(784111777000l, time); + + // NOTE: This test will fail some time, around 2044, + // as the 50 year window will slide... + time = NetUtil.parseHTTPDate("Sunday, 06-Nov-94 08:49:37 GMT"); + assertEquals(784111777000l, time); + + time = NetUtil.parseHTTPDate("Sun, 06-Nov-94 08:49:37 GMT"); + assertEquals(784111777000l, time); + } + + public void testParseHTTPDateAsctime() { + long time = NetUtil.parseHTTPDate("Sun Nov 6 08:49:37 1994"); + assertEquals(784111777000l, time); + + time = NetUtil.parseHTTPDate("Sun Nov 6 08:49:37 94"); + assertEquals(784111777000l, time); + } + + public void testFormatHTTPDateRFC1123() { + long time = 784111777000l; + assertEquals("Sun, 06 Nov 1994 08:49:37 GMT", NetUtil.formatHTTPDate(time)); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/BeanMapTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/BeanMapTestCase.java new file mode 100755 index 00000000..c33cde2e --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/BeanMapTestCase.java @@ -0,0 +1,146 @@ +package com.twelvemonkeys.util; + +import java.util.Map; +import java.beans.IntrospectionException; +import java.io.Serializable; + +/** + * 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 $ + */ +public class BeanMapTestCase extends MapAbstractTestCase { + + public boolean isPutAddSupported() { + return false; + } + + public boolean isRemoveSupported() { + return false; + } + + public boolean isSetValueSupported() { + return true; + } + + public boolean isAllowNullKey() { + return false; + } + + public Object[] getSampleKeys() { + return new Object[] { + "blah", "foo", "bar", "baz", "tmp", "gosh", "golly", "gee" + }; + } + + public Object[] getSampleValues() { + return new Object[] { + "blahv", "foov", "barv", "bazv", "tmpv", "goshv", "gollyv", "geev" + }; + } + + public Object[] getNewSampleValues() { + return new Object[] { + "newblahv", "newfoov", "newbarv", "newbazv", "newtmpv", "newgoshv", "newgollyv", "newgeev" + }; + } + + public Map makeEmptyMap() { + try { + return new BeanMap(new NullBean()); + } + catch (IntrospectionException e) { + throw new RuntimeException(e); + } + } + + public Map makeFullMap() { + try { + return new BeanMap(new MyBean()); + } + catch (IntrospectionException e) { + throw new RuntimeException(e); + } + } + + public static class MyBean implements Serializable { + Object blah = "blahv"; + Object foo = "foov"; + Object bar = "barv"; + Object baz = "bazv"; + Object tmp = "tmpv"; + Object gosh = "goshv"; + Object golly = "gollyv"; + Object gee = "geev"; + + public Object getBar() { + return bar; + } + + public void setBar(Object pBar) { + bar = pBar; + } + + public Object getBaz() { + return baz; + } + + public void setBaz(Object pBaz) { + baz = pBaz; + } + + public Object getBlah() { + return blah; + } + + public void setBlah(Object pBlah) { + blah = pBlah; + } + + public Object getFoo() { + return foo; + } + + public void setFoo(Object pFoo) { + foo = pFoo; + } + + public Object getGee() { + return gee; + } + + public void setGee(Object pGee) { + gee = pGee; + } + + public Object getGolly() { + return golly; + } + + public void setGolly(Object pGolly) { + golly = pGolly; + } + + public Object getGosh() { + return gosh; + } + + public void setGosh(Object pGosh) { + gosh = pGosh; + } + + public Object getTmp() { + return tmp; + } + + public void setTmp(Object pTmp) { + tmp = pTmp; + } + } + + static class NullBean implements Serializable { } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/CollectionAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/CollectionAbstractTestCase.java new file mode 100755 index 00000000..27ba846a --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/CollectionAbstractTestCase.java @@ -0,0 +1,1308 @@ +/* + * Copyright 2001-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twelvemonkeys.util; + +import java.lang.reflect.Array; +import java.util.*; + +/** + * Abstract test class for {@link java.util.Collection} methods and contracts. + *

+ * You should create a concrete subclass of this class to test any custom + * {@link Collection} implementation. At minimum, you'll have to + * implement the {@link #makeCollection()} method. You might want to + * override some of the additional public methods as well: + *

+ * Element Population Methods + *

+ * Override these if your collection restricts what kind of elements are + * allowed (for instance, if null is not permitted): + *

    + *
  • {@link #getFullElements()} + *
  • {@link #getOtherElements()} + *
+ *

+ * Supported Operation Methods + *

+ * Override these if your collection doesn't support certain operations: + *

    + *
  • {@link #isAddSupported()} + *
  • {@link #isRemoveSupported()} + *
  • {@link #areEqualElementsDistinguishable()} + *
  • {@link #isNullSupported()} + *
  • {@link #isFailFastSupported()} + *
+ *

+ * Fixture Methods + *

+ * Fixtures are used to verify that the the operation results in correct state + * for the collection. Basically, the operation is performed against your + * collection implementation, and an identical operation is performed against a + * confirmed collection implementation. A confirmed collection + * implementation is something like java.util.ArrayList, which is + * known to conform exactly to its collection interface's contract. After the + * operation takes place on both your collection implementation and the + * confirmed collection implementation, the two collections are compared to see + * if their state is identical. The comparison is usually much more involved + * than a simple equals test. This verification is used to ensure + * proper modifications are made along with ensuring that the collection does + * not change when read-only modifications are made. + *

+ * The {@link #collection} field holds an instance of your collection + * implementation; the {@link #confirmed} field holds an instance of the + * confirmed collection implementation. The {@link #resetEmpty()} and + * {@link #resetFull()} methods set these fields to empty or full collections, + * so that tests can proceed from a known state. + *

+ * After a modification operation to both {@link #collection} and + * {@link #confirmed}, the {@link #verifyAll()} method is invoked to compare + * the results. You may want to override {@link # verifyAll ()} to perform + * additional verifications. For instance, when testing the collection + * views of a map, {@link AbstractTestMap} would override {@link # verifyAll ()} to make + * sure the map is changed after the collection view is changed. + *

+ * If you're extending this class directly, you will have to provide + * implementations for the following: + *

    + *
  • {@link #makeConfirmedCollection()} + *
  • {@link #makeConfirmedFullCollection()} + *
+ *

+ * Those methods should provide a confirmed collection implementation + * that's compatible with your collection implementation. + *

+ * If you're extending {@link AbstractTestList}, {@link SetAbstractTestCase}, + * or {@link AbstractTestBag}, you probably don't have to worry about the + * above methods, because those three classes already override the methods + * to provide standard JDK confirmed collections.

+ *

+ * Other notes + *

+ * If your {@link Collection} fails one of these tests by design, + * you may still use this base set of cases. Simply override the + * test case (method) your {@link Collection} fails. + * + * @version $Revision: #2 $ $Date: 2008/07/15 $ + * + * @author Rodney Waldhoff + * @author Paul Jack + * @author Michael A. Smith + * @author Neil O'Toole + * @author Stephen Colebourne + */ +public abstract class CollectionAbstractTestCase extends ObjectAbstractTestCase { + + // + // NOTE: + // + // Collection doesn't define any semantics for equals, and recommends you + // use reference-based default behavior of Object.equals. (And a test for + // that already exists in ObjectAbstractTestCase). Tests for equality of lists, sets + // and bags will have to be written in test subclasses. Thus, there is no + // tests on Collection.equals nor any for Collection.hashCode. + // + + + // These fields are used by reset() and verifyAll(), and any test + // method that tests a modification. + + /** + * A collection instance that will be used for testing. + */ + public Collection collection; + + /** + * Confirmed collection. This is an instance of a collection that is + * confirmed to conform exactly to the java.util.Collection contract. + * Modification operations are tested by performing a mod on your + * collection, performing the exact same mod on an equivalent confirmed + * collection, and then calling verifyAll() to make sure your collection + * still matches the confirmed collection. + */ + public Collection confirmed; + + //----------------------------------------------------------------------- + /** + * Specifies whether equal elements in the collection are, in fact, + * distinguishable with information not readily available. That is, if a + * particular value is to be removed from the collection, then there is + * one and only one value that can be removed, even if there are other + * elements which are equal to it. + * + *

In most collection cases, elements are not distinguishable (equal is + * equal), thus this method defaults to return false. In some cases, + * however, they are. For example, the collection returned from the map's + * values() collection view are backed by the map, so while there may be + * two values that are equal, their associated keys are not. Since the + * keys are distinguishable, the values are. + * + *

This flag is used to skip some verifications for iterator.remove() + * where it is impossible to perform an equivalent modification on the + * confirmed collection because it is not possible to determine which + * value in the confirmed collection to actually remove. Tests that + * override the default (i.e. where equal elements are distinguishable), + * should provide additional tests on iterator.remove() to make sure the + * proper elements are removed when remove() is called on the iterator. + **/ + public boolean areEqualElementsDistinguishable() { + return false; + } + + /** + * Returns true if the collections produced by + * {@link #makeCollection()} and {@link #makeFullCollection()} + * support the add and addAll + * operations.

+ * Default implementation returns true. Override if your collection + * class does not support add or addAll. + */ + public boolean isAddSupported() { + return true; + } + + /** + * Returns true if the collections produced by + * {@link #makeCollection()} and {@link #makeFullCollection()} + * support the remove, removeAll, + * retainAll, clear and + * iterator().remove() methods. + * Default implementation returns true. Override if your collection + * class does not support removal operations. + */ + public boolean isRemoveSupported() { + return true; + } + + /** + * Returns true to indicate that the collection supports holding null. + * The default implementation returns true; + */ + public boolean isNullSupported() { + return true; + } + + /** + * Returns true to indicate that the collection supports fail fast iterators. + * The default implementation returns true; + */ + public boolean isFailFastSupported() { + return false; + } + + /** + * Returns true to indicate that the collection supports equals() comparisons. + * This implementation returns false; + */ + public boolean isEqualsCheckable() { + return false; + } + + //----------------------------------------------------------------------- + /** + * Verifies that {@link #collection} and {@link #confirmed} have + * identical state. + */ + public void verifyAll() { + int confirmedSize = confirmed.size(); + assertEquals("Collection size should match confirmed collection's", + confirmedSize, collection.size()); + assertEquals("Collection isEmpty() result should match confirmed " + + " collection's", + confirmed.isEmpty(), collection.isEmpty()); + + // verify the collections are the same by attempting to match each + // object in the collection and confirmed collection. To account for + // duplicates and differing orders, each confirmed element is copied + // into an array and a flag is maintained for each element to determine + // whether it has been matched once and only once. If all elements in + // the confirmed collection are matched once and only once and there + // aren't any elements left to be matched in the collection, + // verification is a success. + + // copy each collection value into an array + Object[] confirmedValues = new Object[confirmedSize]; + + Iterator iter; + + iter = confirmed.iterator(); + int pos = 0; + while(iter.hasNext()) { + confirmedValues[pos++] = iter.next(); + } + + // allocate an array of boolean flags for tracking values that have + // been matched once and only once. + boolean[] matched = new boolean[confirmedSize]; + + // now iterate through the values of the collection and try to match + // the value with one in the confirmed array. + iter = collection.iterator(); + while(iter.hasNext()) { + Object o = iter.next(); + boolean match = false; + for(int i = 0; i < confirmedSize; i++) { + if(matched[i]) { + // skip values already matched + continue; + } + if(o == confirmedValues[i] || + (o != null && o.equals(confirmedValues[i]))) { + // values matched + matched[i] = true; + match = true; + break; + } + } + // no match found! + if(!match) { + fail("Collection should not contain a value that the " + + "confirmed collection does not have: " + o + + "\nTest: " + collection + "\nReal: " + confirmed); + } + } + + // make sure there aren't any unmatched values + for(int i = 0; i < confirmedSize; i++) { + if(!matched[i]) { + // the collection didn't match all the confirmed values + fail("Collection should contain all values that are in the confirmed collection" + + "\nTest: " + collection + "\nReal: " + confirmed); + } + } + } + + //----------------------------------------------------------------------- + /** + * Resets the {@link #collection} and {@link #confirmed} fields to empty + * collections. Invoke this method before performing a modification + * test. + */ + public void resetEmpty() { + this.collection = makeCollection(); + this.confirmed = makeConfirmedCollection(); + } + + /** + * Resets the {@link #collection} and {@link #confirmed} fields to full + * collections. Invoke this method before performing a modification + * test. + */ + public void resetFull() { + this.collection = makeFullCollection(); + this.confirmed = makeConfirmedFullCollection(); + } + + //----------------------------------------------------------------------- + /** + * Returns a confirmed empty collection. + * For instance, an {@link java.util.ArrayList} for lists or a + * {@link java.util.HashSet} for sets. + * + * @return a confirmed empty collection + */ + public abstract Collection makeConfirmedCollection(); + + /** + * Returns a confirmed full collection. + * For instance, an {@link java.util.ArrayList} for lists or a + * {@link java.util.HashSet} for sets. The returned collection + * should contain the elements returned by {@link #getFullElements()}. + * + * @return a confirmed full collection + */ + public abstract Collection makeConfirmedFullCollection(); + + /** + * Return a new, empty {@link Collection} to be used for testing. + */ + public abstract Collection makeCollection(); + + /** + * Returns a full collection to be used for testing. The collection + * returned by this method should contain every element returned by + * {@link #getFullElements()}. The default implementation, in fact, + * simply invokes addAll on an empty collection with + * the results of {@link #getFullElements()}. Override this default + * if your collection doesn't support addAll. + */ + public Collection makeFullCollection() { + Collection c = makeCollection(); + c.addAll(Arrays.asList(getFullElements())); + return c; + } + + /** + * Returns an empty collection for Object tests. + */ + public Object makeObject() { + return makeCollection(); + } + + /** + * Creates a new Map Entry that is independent of the first and the map. + */ + public Map.Entry cloneMapEntry(Map.Entry entry) { + HashMap map = new HashMap(); + map.put(entry.getKey(), entry.getValue()); + return (Map.Entry) map.entrySet().iterator().next(); + } + + //----------------------------------------------------------------------- + /** + * Returns an array of objects that are contained in a collection + * produced by {@link #makeFullCollection()}. Every element in the + * returned array must be an element in a full collection.

+ * The default implementation returns a heterogenous array of + * objects with some duplicates. null is added if allowed. + * Override if you require specific testing elements. Note that if you + * override {@link #makeFullCollection()}, you must override + * this method to reflect the contents of a full collection. + */ + public Object[] getFullElements() { + if (isNullSupported()) { + ArrayList list = new ArrayList(); + list.addAll(Arrays.asList(getFullNonNullElements())); + list.add(4, null); + return list.toArray(); + } else { + return (Object[]) getFullNonNullElements().clone(); + } + } + + /** + * Returns an array of elements that are not contained in a + * full collection. Every element in the returned array must + * not exist in a collection returned by {@link #makeFullCollection()}. + * The default implementation returns a heterogenous array of elements + * without null. Note that some of the tests add these elements + * to an empty or full collection, so if your collection restricts + * certain kinds of elements, you should override this method. + */ + public Object[] getOtherElements() { + return getOtherNonNullElements(); + } + + //----------------------------------------------------------------------- + /** + * Returns a list of elements suitable for return by + * {@link #getFullElements()}. The array returned by this method + * does not include null, but does include a variety of objects + * of different types. Override getFullElements to return + * the results of this method if your collection does not support + * the null element. + */ + public Object[] getFullNonNullElements() { + return new Object[] { + new String(""), + new String("One"), + new Integer(2), + "Three", + new Integer(4), + "One", + new Double(5), + new Float(6), + "Seven", + "Eight", + new String("Nine"), + new Integer(10), + new Short((short)11), + new Long(12), + "Thirteen", + "14", + "15", + new Byte((byte)16) + }; + } + + /** + * Returns the default list of objects returned by + * {@link #getOtherElements()}. Includes many objects + * of different types. + */ + public Object[] getOtherNonNullElements() { + return new Object[] { + new Integer(0), + new Float(0), + new Double(0), + "Zero", + new Short((short)0), + new Byte((byte)0), + new Long(0), + new Character('\u0000'), + "0" + }; + } + + /** + * Returns a list of string elements suitable for return by + * {@link #getFullElements()}. Override getFullElements to return + * the results of this method if your collection does not support + * heterogenous elements or the null element. + */ + public Object[] getFullNonNullStringElements() { + return new Object[] { + "If","the","dull","substance","of","my","flesh","were","thought", + "Injurious","distance","could","not","stop","my","way", + }; + } + + /** + * Returns a list of string elements suitable for return by + * {@link #getOtherElements()}. Override getOtherElements to return + * the results of this method if your collection does not support + * heterogenous elements or the null element. + */ + public Object[] getOtherNonNullStringElements() { + return new Object[] { + "For","then","despite",/* of */"space","I","would","be","brought", + "From","limits","far","remote","where","thou","dost","stay" + }; + } + + // Tests + //----------------------------------------------------------------------- + /** + * Tests {@link Collection#add(Object)}. + */ + public void testCollectionAdd() { + if (!isAddSupported()) return; + + Object[] elements = getFullElements(); + for (int i = 0; i < elements.length; i++) { + resetEmpty(); + boolean r = collection.add(elements[i]); + confirmed.add(elements[i]); + verifyAll(); + assertTrue("Empty collection changed after add", r); + assertEquals("Collection size is 1 after first add", 1, collection.size()); + } + + resetEmpty(); + int size = 0; + for (int i = 0; i < elements.length; i++) { + boolean r = collection.add(elements[i]); + confirmed.add(elements[i]); + verifyAll(); + if (r) size++; + assertEquals("Collection size should grow after add", + size, collection.size()); + assertTrue("Collection should contain added element", + collection.contains(elements[i])); + } + } + + + /** + * Tests {@link Collection#addAll(Collection)}. + */ + public void testCollectionAddAll() { + if (!isAddSupported()) return; + + resetEmpty(); + Object[] elements = getFullElements(); + boolean r = collection.addAll(Arrays.asList(elements)); + confirmed.addAll(Arrays.asList(elements)); + verifyAll(); + assertTrue("Empty collection should change after addAll", r); + for (int i = 0; i < elements.length; i++) { + assertTrue("Collection should contain added element", + collection.contains(elements[i])); + } + + resetFull(); + int size = collection.size(); + elements = getOtherElements(); + r = collection.addAll(Arrays.asList(elements)); + confirmed.addAll(Arrays.asList(elements)); + verifyAll(); + assertTrue("Full collection should change after addAll", r); + for (int i = 0; i < elements.length; i++) { + assertTrue("Full collection should contain added element", + collection.contains(elements[i])); + } + assertEquals("Size should increase after addAll", + size + elements.length, collection.size()); + + resetFull(); + size = collection.size(); + r = collection.addAll(Arrays.asList(getFullElements())); + confirmed.addAll(Arrays.asList(getFullElements())); + verifyAll(); + if (r) { + assertTrue("Size should increase if addAll returns true", + size < collection.size()); + } else { + assertEquals("Size should not change if addAll returns false", + size, collection.size()); + } + } + + + /** + * If {@link #isAddSupported()} returns false, tests that add operations + * raise UnsupportedOperationException. + */ + public void testUnsupportedAdd() { + if (isAddSupported()) return; + + resetEmpty(); + try { + collection.add(new Object()); + fail("Emtpy collection should not support add."); + } catch (UnsupportedOperationException e) { + // expected + } + // make sure things didn't change even if the expected exception was + // thrown. + verifyAll(); + + try { + collection.addAll(Arrays.asList(getFullElements())); + fail("Emtpy collection should not support addAll."); + } catch (UnsupportedOperationException e) { + // expected + } + // make sure things didn't change even if the expected exception was + // thrown. + verifyAll(); + + resetFull(); + try { + collection.add(new Object()); + fail("Full collection should not support add."); + } catch (UnsupportedOperationException e) { + // expected + } + // make sure things didn't change even if the expected exception was + // thrown. + verifyAll(); + + try { + collection.addAll(Arrays.asList(getOtherElements())); + fail("Full collection should not support addAll."); + } catch (UnsupportedOperationException e) { + // expected + } + // make sure things didn't change even if the expected exception was + // thrown. + verifyAll(); + } + + + /** + * Test {@link Collection#clear()}. + */ + public void testCollectionClear() { + if (!isRemoveSupported()) return; + + resetEmpty(); + collection.clear(); // just to make sure it doesn't raise anything + verifyAll(); + + resetFull(); + collection.clear(); + confirmed.clear(); + verifyAll(); + } + + + /** + * Tests {@link Collection#contains(Object)}. + */ + public void testCollectionContains() { + Object[] elements; + + resetEmpty(); + elements = getFullElements(); + for(int i = 0; i < elements.length; i++) { + assertTrue("Empty collection shouldn't contain element[" + i + "]", + !collection.contains(elements[i])); + } + // make sure calls to "contains" don't change anything + verifyAll(); + + elements = getOtherElements(); + for(int i = 0; i < elements.length; i++) { + assertTrue("Empty collection shouldn't contain element[" + i + "]", + !collection.contains(elements[i])); + } + // make sure calls to "contains" don't change anything + verifyAll(); + + resetFull(); + elements = getFullElements(); + for(int i = 0; i < elements.length; i++) { + assertTrue("Full collection should contain element[" + i + "]", + collection.contains(elements[i])); + } + // make sure calls to "contains" don't change anything + verifyAll(); + + resetFull(); + elements = getOtherElements(); + for(int i = 0; i < elements.length; i++) { + assertTrue("Full collection shouldn't contain element", + !collection.contains(elements[i])); + } + } + + + /** + * Tests {@link Collection#containsAll(Collection)}. + */ + public void testCollectionContainsAll() { + resetEmpty(); + Collection col = new HashSet(); + assertTrue("Every Collection should contain all elements of an " + + "empty Collection.", collection.containsAll(col)); + col.addAll(Arrays.asList(getOtherElements())); + assertTrue("Empty Collection shouldn't contain all elements of " + + "a non-empty Collection.", !collection.containsAll(col)); + // make sure calls to "containsAll" don't change anything + verifyAll(); + + resetFull(); + assertTrue("Full collection shouldn't contain other elements", + !collection.containsAll(col)); + + col.clear(); + col.addAll(Arrays.asList(getFullElements())); + assertTrue("Full collection should containAll full elements", + collection.containsAll(col)); + // make sure calls to "containsAll" don't change anything + verifyAll(); + + int min = (getFullElements().length < 2 ? 0 : 2); + int max = (getFullElements().length == 1 ? 1 : + (getFullElements().length <= 5 ? getFullElements().length - 1 : 5)); + col = Arrays.asList(getFullElements()).subList(min, max); + assertTrue("Full collection should containAll partial full " + + "elements", collection.containsAll(col)); + assertTrue("Full collection should containAll itself", + collection.containsAll(collection)); + // make sure calls to "containsAll" don't change anything + verifyAll(); + + col = new ArrayList(); + col.addAll(Arrays.asList(getFullElements())); + col.addAll(Arrays.asList(getFullElements())); + assertTrue("Full collection should containAll duplicate full " + + "elements", collection.containsAll(col)); + + // make sure calls to "containsAll" don't change anything + verifyAll(); + } + + /** + * Tests {@link Collection#isEmpty()}. + */ + public void testCollectionIsEmpty() { + resetEmpty(); + assertEquals("New Collection should be empty.", + true, collection.isEmpty()); + // make sure calls to "isEmpty() don't change anything + verifyAll(); + + resetFull(); + assertEquals("Full collection shouldn't be empty", + false, collection.isEmpty()); + // make sure calls to "isEmpty() don't change anything + verifyAll(); + } + + + /** + * Tests the read-only functionality of {@link Collection#iterator()}. + */ + public void testCollectionIterator() { + resetEmpty(); + Iterator it1 = collection.iterator(); + assertEquals("Iterator for empty Collection shouldn't have next.", + false, it1.hasNext()); + try { + it1.next(); + fail("Iterator at end of Collection should throw " + + "NoSuchElementException when next is called."); + } catch(NoSuchElementException e) { + // expected + } + // make sure nothing has changed after non-modification + verifyAll(); + + resetFull(); + it1 = collection.iterator(); + for (int i = 0; i < collection.size(); i++) { + assertTrue("Iterator for full collection should haveNext", + it1.hasNext()); + it1.next(); + } + assertTrue("Iterator should be finished", !it1.hasNext()); + + ArrayList list = new ArrayList(); + it1 = collection.iterator(); + for (int i = 0; i < collection.size(); i++) { + Object next = it1.next(); + assertTrue("Collection should contain element returned by " + + "its iterator", collection.contains(next)); + list.add(next); + } + try { + it1.next(); + fail("iterator.next() should raise NoSuchElementException " + + "after it finishes"); + } catch (NoSuchElementException e) { + // expected + } + // make sure nothing has changed after non-modification + verifyAll(); + } + + + /** + * Tests removals from {@link Collection#iterator()}. + */ + public void testCollectionIteratorRemove() { + if (!isRemoveSupported()) return; + + resetEmpty(); + try { + collection.iterator().remove(); + fail("New iterator.remove should raise IllegalState"); + } catch (IllegalStateException e) { + // expected + } + verifyAll(); + + try { + Iterator iter = collection.iterator(); + iter.hasNext(); + iter.remove(); + fail("New iterator.remove should raise IllegalState " + + "even after hasNext"); + } catch (IllegalStateException e) { + // expected + } + verifyAll(); + + resetFull(); + int size = collection.size(); + Iterator iter = collection.iterator(); + while (iter.hasNext()) { + Object o = iter.next(); + // TreeMap reuses the Map Entry, so the verify below fails + // Clone it here if necessary + if (o instanceof Map.Entry) { + o = cloneMapEntry((Map.Entry) o); + } + iter.remove(); + + // if the elements aren't distinguishable, we can just remove a + // matching element from the confirmed collection and verify + // contents are still the same. Otherwise, we don't have the + // ability to distinguish the elements and determine which to + // remove from the confirmed collection (in which case, we don't + // verify because we don't know how). + // + // see areEqualElementsDistinguishable() + if(!areEqualElementsDistinguishable()) { + confirmed.remove(o); + verifyAll(); + } + + size--; + assertEquals("Collection should shrink by one after " + + "iterator.remove", size, collection.size()); + } + assertTrue("Collection should be empty after iterator purge", + collection.isEmpty()); + + resetFull(); + iter = collection.iterator(); + iter.next(); + iter.remove(); + try { + iter.remove(); + fail("Second iter.remove should raise IllegalState"); + } catch (IllegalStateException e) { + // expected + } + } + + + /** + * Tests {@link Collection#remove(Object)}. + */ + public void testCollectionRemove() { + if (!isRemoveSupported()) return; + + resetEmpty(); + Object[] elements = getFullElements(); + for (int i = 0; i < elements.length; i++) { + assertTrue("Shouldn't remove nonexistent element", + !collection.remove(elements[i])); + verifyAll(); + } + + Object[] other = getOtherElements(); + + resetFull(); + for (int i = 0; i < other.length; i++) { + assertTrue("Shouldn't remove nonexistent other element", + !collection.remove(other[i])); + verifyAll(); + } + + int size = collection.size(); + for (int i = 0; i < elements.length; i++) { + resetFull(); + assertTrue("Collection should remove extant element: " + elements[i], + collection.remove(elements[i])); + + // if the elements aren't distinguishable, we can just remove a + // matching element from the confirmed collection and verify + // contents are still the same. Otherwise, we don't have the + // ability to distinguish the elements and determine which to + // remove from the confirmed collection (in which case, we don't + // verify because we don't know how). + // + // see areEqualElementsDistinguishable() + if(!areEqualElementsDistinguishable()) { + confirmed.remove(elements[i]); + verifyAll(); + } + + assertEquals("Collection should shrink after remove", + size - 1, collection.size()); + } + } + + + /** + * Tests {@link Collection#removeAll(Collection)}. + */ + public void testCollectionRemoveAll() { + if (!isRemoveSupported()) return; + + resetEmpty(); + assertTrue("Emtpy collection removeAll should return false for " + + "empty input", + !collection.removeAll(Collections.EMPTY_SET)); + verifyAll(); + + assertTrue("Emtpy collection removeAll should return false for " + + "nonempty input", + !collection.removeAll(new ArrayList(collection))); + verifyAll(); + + resetFull(); + assertTrue("Full collection removeAll should return false for " + + "empty input", + !collection.removeAll(Collections.EMPTY_SET)); + verifyAll(); + + assertTrue("Full collection removeAll should return false for other elements", + !collection.removeAll(Arrays.asList(getOtherElements()))); + verifyAll(); + + assertTrue("Full collection removeAll should return true for full elements", + collection.removeAll(new HashSet(collection))); + confirmed.removeAll(new HashSet(confirmed)); + verifyAll(); + + resetFull(); + int size = collection.size(); + int min = (getFullElements().length < 2 ? 0 : 2); + int max = (getFullElements().length == 1 ? 1 : + (getFullElements().length <= 5 ? getFullElements().length - 1 : 5)); + Collection all = Arrays.asList(getFullElements()).subList(min, max); + assertTrue("Full collection removeAll should work", + collection.removeAll(all)); + confirmed.removeAll(all); + verifyAll(); + + assertTrue("Collection should shrink after removeAll", + collection.size() < size); + Iterator iter = all.iterator(); + while (iter.hasNext()) { + assertTrue("Collection shouldn't contain removed element", + !collection.contains(iter.next())); + } + } + + + /** + * Tests {@link Collection#retainAll(Collection)}. + */ + public void testCollectionRetainAll() { + if (!isRemoveSupported()) return; + + resetEmpty(); + List elements = Arrays.asList(getFullElements()); + List other = Arrays.asList(getOtherElements()); + + assertTrue("Empty retainAll() should return false", + !collection.retainAll(Collections.EMPTY_SET)); + verifyAll(); + + assertTrue("Empty retainAll() should return false", + !collection.retainAll(elements)); + verifyAll(); + + resetFull(); + assertTrue("Collection should change from retainAll empty", + collection.retainAll(Collections.EMPTY_SET)); + confirmed.retainAll(Collections.EMPTY_SET); + verifyAll(); + + resetFull(); + assertTrue("Collection changed from retainAll other", + collection.retainAll(other)); + confirmed.retainAll(other); + verifyAll(); + + resetFull(); + int size = collection.size(); + assertTrue("Collection shouldn't change from retainAll elements", + !collection.retainAll(elements)); + verifyAll(); + assertEquals("Collection size shouldn't change", size, + collection.size()); + + if (getFullElements().length > 1) { + resetFull(); + size = collection.size(); + int min = (getFullElements().length < 2 ? 0 : 2); + int max = (getFullElements().length <= 5 ? getFullElements().length - 1 : 5); + assertTrue("Collection should changed by partial retainAll", + collection.retainAll(elements.subList(min, max))); + confirmed.retainAll(elements.subList(min, max)); + verifyAll(); + + Iterator iter = collection.iterator(); + while (iter.hasNext()) { + assertTrue("Collection only contains retained element", + elements.subList(min, max).contains(iter.next())); + } + } + + resetFull(); + HashSet set = new HashSet(elements); + size = collection.size(); + assertTrue("Collection shouldn't change from retainAll without " + + "duplicate elements", !collection.retainAll(set)); + verifyAll(); + assertEquals("Collection size didn't change from nonduplicate " + + "retainAll", size, collection.size()); + } + + + /** + * Tests {@link Collection#size()}. + */ + public void testCollectionSize() { + resetEmpty(); + assertEquals("Size of new Collection is 0.", 0, collection.size()); + + resetFull(); + assertTrue("Size of full collection should be greater than zero", + collection.size() > 0); + } + + + /** + * Tests {@link Collection#toArray()}. + */ + public void testCollectionToArray() { + resetEmpty(); + assertEquals("Empty Collection should return empty array for toArray", + 0, collection.toArray().length); + + resetFull(); + Object[] array = collection.toArray(); + assertEquals("Full collection toArray should be same size as " + + "collection", array.length, collection.size()); + Object[] confirmedArray = confirmed.toArray(); + assertEquals("length of array from confirmed collection should " + + "match the length of the collection's array", + confirmedArray.length, array.length); + boolean[] matched = new boolean[array.length]; + + for (int i = 0; i < array.length; i++) { + assertTrue("Collection should contain element in toArray", + collection.contains(array[i])); + + boolean match = false; + // find a match in the confirmed array + for(int j = 0; j < array.length; j++) { + // skip already matched + if(matched[j]) continue; + if(array[i] == confirmedArray[j] || + (array[i] != null && array[i].equals(confirmedArray[j]))) { + matched[j] = true; + match = true; + break; + } + } + if(!match) { + fail("element " + i + " in returned array should be found " + + "in the confirmed collection's array"); + } + } + for(int i = 0; i < matched.length; i++) { + assertEquals("Collection should return all its elements in " + + "toArray", true, matched[i]); + } + } + + + /** + * Tests {@link Collection#toArray(Object[])}. + */ + public void testCollectionToArray2() { + resetEmpty(); + Object[] a = new Object[] { new Object(), null, null }; + Object[] array = collection.toArray(a); + assertEquals("Given array shouldn't shrink", array, a); + assertEquals("Last element should be set to null", a[0], null); + verifyAll(); + + resetFull(); + try { + array = collection.toArray(new Void[0]); + fail("toArray(new Void[0]) should raise ArrayStore"); + } catch (ArrayStoreException e) { + // expected + } + verifyAll(); + + try { + array = collection.toArray(null); + fail("toArray(null) should raise NPE"); + } catch (NullPointerException e) { + // expected + } + verifyAll(); + + array = collection.toArray(new Object[0]); + a = collection.toArray(); + assertEquals("toArrays should be equal", + Arrays.asList(array), Arrays.asList(a)); + + // Figure out if they're all the same class + // TODO: It'd be nicer to detect a common superclass + HashSet classes = new HashSet(); + for (int i = 0; i < array.length; i++) { + classes.add((array[i] == null) ? null : array[i].getClass()); + } + if (classes.size() > 1) return; + + Class cl = (Class)classes.iterator().next(); + if (Map.Entry.class.isAssignableFrom(cl)) { // check needed for protective cases like Predicated/Unmod map entrySet + cl = Map.Entry.class; + } + a = (Object[])Array.newInstance(cl, 0); + array = collection.toArray(a); + assertEquals("toArray(Object[]) should return correct array type", + a.getClass(), array.getClass()); + assertEquals("type-specific toArrays should be equal", + Arrays.asList(array), + Arrays.asList(collection.toArray())); + verifyAll(); + } + + + /** + * Tests toString on a collection. + */ + public void testCollectionToString() { + resetEmpty(); + assertTrue("toString shouldn't return null", + collection.toString() != null); + + resetFull(); + assertTrue("toString shouldn't return null", + collection.toString() != null); + } + + + /** + * If isRemoveSupported() returns false, tests to see that remove + * operations raise an UnsupportedOperationException. + */ + public void testUnsupportedRemove() { + if (isRemoveSupported()) return; + + resetEmpty(); + try { + collection.clear(); + fail("clear should raise UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // expected + } + verifyAll(); + + try { + collection.remove(null); + fail("remove should raise UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // expected + } + verifyAll(); + + try { + collection.removeAll(null); + fail("removeAll should raise UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // expected + } + verifyAll(); + + try { + collection.retainAll(null); + fail("removeAll should raise UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // expected + } + verifyAll(); + + resetFull(); + try { + Iterator iterator = collection.iterator(); + iterator.next(); + iterator.remove(); + fail("iterator.remove should raise UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + // expected + } + verifyAll(); + + } + + + /** + * Tests that the collection's iterator is fail-fast. + */ + public void testCollectionIteratorFailFast() { + if (!isFailFastSupported()) return; + + if (isAddSupported()) { + resetFull(); + try { + Iterator iter = collection.iterator(); + Object o = getOtherElements()[0]; + collection.add(o); + confirmed.add(o); + iter.next(); + fail("next after add should raise ConcurrentModification"); + } catch (ConcurrentModificationException e) { + // expected + } + verifyAll(); + + resetFull(); + try { + Iterator iter = collection.iterator(); + collection.addAll(Arrays.asList(getOtherElements())); + confirmed.addAll(Arrays.asList(getOtherElements())); + iter.next(); + fail("next after addAll should raise ConcurrentModification"); + } catch (ConcurrentModificationException e) { + // expected + } + verifyAll(); + } + + if (!isRemoveSupported()) return; + + resetFull(); + try { + Iterator iter = collection.iterator(); + collection.clear(); + iter.next(); + fail("next after clear should raise ConcurrentModification"); + } catch (ConcurrentModificationException e) { + // expected + } catch (NoSuchElementException e) { + // (also legal given spec) + } + + resetFull(); + try { + Iterator iter = collection.iterator(); + collection.remove(getFullElements()[0]); + iter.next(); + fail("next after remove should raise ConcurrentModification"); + } catch (ConcurrentModificationException e) { + // expected + } + + resetFull(); + try { + Iterator iter = collection.iterator(); + List sublist = Arrays.asList(getFullElements()).subList(2,5); + collection.removeAll(sublist); + iter.next(); + fail("next after removeAll should raise ConcurrentModification"); + } catch (ConcurrentModificationException e) { + // expected + } + + resetFull(); + try { + Iterator iter = collection.iterator(); + List sublist = Arrays.asList(getFullElements()).subList(2,5); + collection.retainAll(sublist); + iter.next(); + fail("next after retainAll should raise ConcurrentModification"); + } catch (ConcurrentModificationException e) { + // expected + } + } + + /* + public void testSerializeDeserializeThenCompare() throws Exception { + Object obj = makeCollection(); + if (obj instanceof Serializable && isTestSerialization()) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(buffer); + out.writeObject(obj); + out.close(); + + ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())); + Object dest = in.readObject(); + in.close(); + if (isEqualsCheckable()) { + assertEquals("obj != deserialize(serialize(obj)) - EMPTY Collection", obj, dest); + } + } + obj = makeFullCollection(); + if (obj instanceof Serializable && isTestSerialization()) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(buffer); + out.writeObject(obj); + out.close(); + + ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())); + Object dest = in.readObject(); + in.close(); + if (isEqualsCheckable()) { + assertEquals("obj != deserialize(serialize(obj)) - FULL Collection", obj, dest); + } + } + } + */ + +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/LRUMapTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/LRUMapTestCase.java new file mode 100755 index 00000000..8f13b97b --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/LRUMapTestCase.java @@ -0,0 +1,182 @@ +/* + * Copyright 2001-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twelvemonkeys.util; + +import java.util.*; + +/** + * Tests LRUMap. + * + * @version $Revision: #2 $ $Date: 2008/07/15 $ + * + * @author James Strachan + * @author Morgan Delagrange + * @author Stephen Colebourne + */ +public class LRUMapTestCase extends LinkedMapTestCase { + + public boolean isGetStructuralModify() { + return true; + } + + //----------------------------------------------------------------------- + public Map makeEmptyMap() { + LRUMap map = new LRUMap(); + return map; + } + + //----------------------------------------------------------------------- + public void testRemoveLRU() { + LRUMap map2 = new LRUMap(3); + map2.put(new Integer(1),"foo"); + map2.put(new Integer(2),"foo"); + map2.put(new Integer(3),"foo"); + map2.put(new Integer(4),"foo"); // removes 1 since max size exceeded + map2.removeLRU(); // should be Integer(2) + + assertTrue("Second to last value should exist",map2.get(new Integer(3)).equals("foo")); + assertTrue("First value inserted should not exist", map2.get(new Integer(1)) == null); + } + + public void testMultiplePuts() { + LRUMap map2 = new LRUMap(2); + map2.put(new Integer(1),"foo"); + map2.put(new Integer(2),"bar"); + map2.put(new Integer(3),"foo"); + map2.put(new Integer(4),"bar"); + + assertTrue("last value should exist",map2.get(new Integer(4)).equals("bar")); + assertTrue("LRU should not exist", map2.get(new Integer(1)) == null); + } + + /** + * Confirm that putAll(Map) does not cause the LRUMap + * to exceed its maxiumum size. + */ + public void testPutAll() { + LRUMap map2 = new LRUMap(3); + map2.put(new Integer(1),"foo"); + map2.put(new Integer(2),"foo"); + map2.put(new Integer(3),"foo"); + + HashMap hashMap = new HashMap(); + hashMap.put(new Integer(4),"foo"); + + map2.putAll(hashMap); + + assertTrue("max size is 3, but actual size is " + map2.size(), + map2.size() == 3); + assertTrue("map should contain the Integer(4) object", + map2.containsKey(new Integer(4))); + } + + /** + * Test that the size of the map is reduced immediately + * when setMaximumSize(int) is called + */ + public void testSetMaximumSize() { + LRUMap map = new LRUMap(6); + map.put("1","1"); + map.put("2","2"); + map.put("3","3"); + map.put("4","4"); + map.put("5","5"); + map.put("6","6"); + map.setMaxSize(3); + + assertTrue("map should have size = 3, but actually = " + map.size(), + map.size() == 3); + } + + public void testGetPromotion() { + LRUMap map = new LRUMap(3); + map.put("1","1"); + map.put("2","2"); + map.put("3","3"); + // LRU is now 1 (then 2 then 3) + + // promote 1 to top + // eviction order is now 2,3,1 + map.get("1"); + + // add another value, forcing a remove + // 2 should be evicted (then 3,1,4) + map.put("4","4"); + + Iterator keyIterator = map.keySet().iterator(); + Object[] keys = new Object[3]; + for (int i = 0; keyIterator.hasNext() ; ++i) { + keys[i] = keyIterator.next(); + } + + assertTrue("first evicted should be 3, was " + keys[0], keys[0].equals("3")); + assertTrue("second evicted should be 1, was " + keys[1], keys[1].equals("1")); + assertTrue("third evicted should be 4, was " + keys[2], keys[2].equals("4")); + + } + + /** + * You should be able to subclass LRUMap and perform a + * custom action when items are removed automatically + * by the LRU algorithm (the removeLRU() method). + */ + public void testLRUSubclass() { + LRUCounter counter = new LRUCounter(3); + // oldest <--> newest + // 1 + counter.put("1","foo"); + // 1 2 + counter.put("2","foo"); + // 1 2 3 + counter.put("3","foo"); + // 2 3 1 + counter.put("1","foo"); + // 3 1 4 (2 goes out) + counter.put("4","foo"); + // 1 4 5 (3 goes out) + counter.put("5","foo"); + // 4 5 2 (1 goes out) + counter.put("2","foo"); + // 4 2 + counter.remove("5"); + + assertTrue("size should be 2, but was " + counter.size(), counter.size() == 2); + assertTrue("removedCount should be 3 but was " + counter.removedCount, + counter.removedCount == 3); + + assertTrue("first removed was '2'",counter.list.get(0).equals("2")); + assertTrue("second removed was '3'",counter.list.get(1).equals("3")); + assertTrue("third removed was '1'",counter.list.get(2).equals("1")); + + //assertTrue("oldest key is '4'",counter.get(0).equals("4")); + //assertTrue("newest key is '2'",counter.get(1).equals("2")); + } + + private class LRUCounter extends LRUMap { + int removedCount = 0; + List list = new ArrayList(3); + + LRUCounter(int i) { + super(i); + } + + public void processRemoved(Entry pEntry) { + ++removedCount; + list.add(pEntry.getKey()); + } + } +} + diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/LinkedMapTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/LinkedMapTestCase.java new file mode 100755 index 00000000..7cdaefe1 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/LinkedMapTestCase.java @@ -0,0 +1,175 @@ + +/* + * Copyright 2001-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twelvemonkeys.util; + +import java.util.Iterator; +import java.util.Map; + +/** + * Unit tests + * {@link org.apache.commons.collections.SequencedHashMap}. + * Be sure to use the "labRat" instance whenever possible, + * so that subclasses will be tested correctly. + * + * @version $Revision: #2 $ $Date: 2008/07/15 $ + * + * @author Morgan Delagrange + * @author Daniel Rall + * @author Henning P. Schmiedehausen + * @author James Strachan + */ +public class LinkedMapTestCase extends MapAbstractTestCase { + /** + * The instance to experiment on. + */ + protected LinkedMap labRat; + + public void setUp() throws Exception { + super.setUp(); + // use makeMap and cast the result to a SeqHashMap + // so that subclasses of SeqHashMap can share these tests + labRat = (LinkedMap) makeEmptyMap(); + } + + public Map makeEmptyMap() { + return new LinkedMap(); + } + + protected Object[] getKeys() { + return new Object[] { "foo", "baz", "eek" }; + } + + protected Object[] getValues() { + return new Object[] { "bar", "frob", new Object() }; + } + + public void testSequenceMap() throws Throwable { + Object[] keys = getKeys(); + int expectedSize = keys.length; + Object[] values = getValues(); + for (int i = 0; i < expectedSize; i++) { + labRat.put(keys[i], values[i]); + } + + // Test size(). + assertEquals("size() does not match expected size", + expectedSize, labRat.size()); + + // Test clone(), iterator(), and get(Object). + LinkedMap clone = (LinkedMap) labRat.clone(); + assertEquals("Size of clone does not match original", + labRat.size(), clone.size()); + Iterator origEntries = labRat.entrySet().iterator(); + Iterator copiedEntries = clone.entrySet().iterator(); + while (origEntries.hasNext()) { + Map.Entry origEntry = (Map.Entry)origEntries.next(); + Map.Entry copiedEntry = (Map.Entry)copiedEntries.next(); + assertEquals("Cloned key does not match original", + origEntry.getKey(), copiedEntry.getKey()); + assertEquals("Cloned value does not match original", + origEntry.getValue(), copiedEntry.getValue()); + assertEquals("Cloned entry does not match original", + origEntry, copiedEntry); + } + assertTrue("iterator() returned different number of elements than keys()", + !copiedEntries.hasNext()); + + // Test sequence() + /* + List seq = labRat.sequence(); + assertEquals("sequence() returns more keys than in the Map", + expectedSize, seq.size()); + + for (int i = 0; i < seq.size(); i++) { + assertEquals("Key " + i + " is not the same as the key in the Map", + keys[i], seq.get(i)); + } + */ + } + + /* + public void testYoungest() { + labRat.put(new Integer(1),"foo"); + labRat.put(new Integer(2),"bar"); + assertTrue("first key is correct",labRat.get(0).equals(new Integer(1))); + labRat.put(new Integer(1),"boo"); + assertTrue("second key is reassigned to first",labRat.get(0).equals(new Integer(2))); + } + + public void testYoungestReplaceNullWithValue() { + labRat.put(new Integer(1),null); + labRat.put(new Integer(2),"foo"); + assertTrue("first key is correct",labRat.get(0).equals(new Integer(1))); + labRat.put(new Integer(1),"bar"); + assertTrue("second key is reassigned to first",labRat.get(0).equals(new Integer(2))); + } + + public void testYoungestReplaceValueWithNull() { + labRat.put(new Integer(1),"bar"); + labRat.put(new Integer(2),"foo"); + assertTrue("first key is correct",labRat.get(0).equals(new Integer(1))); + labRat.put(new Integer(1),null); + assertTrue("second key is reassigned to first",labRat.get(0).equals(new Integer(2))); + } + */ + + // override TestMap method with more specific tests + /* + public void testFullMapSerialization() + throws IOException, ClassNotFoundException { + LinkedMap map = (LinkedMap) makeFullMap(); + + if (!(map instanceof Serializable)) return; + + byte[] objekt = writeExternalFormToBytes((Serializable) map); + LinkedMap map2 = (LinkedMap) readExternalFormFromBytes(objekt); + + assertEquals("Both maps are same size",map.size(), getSampleKeys().length); + assertEquals("Both maps are same size",map2.size(),getSampleKeys().length); + + assertEquals("Both maps have the same first key", + map.getFirstKey(),getSampleKeys()[0]); + assertEquals("Both maps have the same first key", + map2.getFirstKey(),getSampleKeys()[0]); + assertEquals("Both maps have the same last key", + map.getLastKey(),getSampleKeys()[getSampleKeys().length - 1]); + assertEquals("Both maps have the same last key", + map2.getLastKey(),getSampleKeys()[getSampleKeys().length - 1]); + } + */ + + /* + public void testIndexOf() throws Exception { + Object[] keys = getKeys(); + int expectedSize = keys.length; + Object[] values = getValues(); + for (int i = 0; i < expectedSize; i++) { + labRat.put(keys[i], values[i]); + } + // test that the index returned are in the same order that they were + // placed in the map + for (int i = 0; i < keys.length; i++) { + assertEquals("indexOf with existing key failed", i, labRat.indexOf(keys[i])); + } + // test non existing key.. + assertEquals("test with non-existing key failed", -1, labRat.indexOf("NonExistingKey")); + } + */ + public void tearDown() throws Exception { + labRat = null; + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/MapAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/MapAbstractTestCase.java new file mode 100755 index 00000000..ff1c6ea9 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/MapAbstractTestCase.java @@ -0,0 +1,1660 @@ +/* + * Copyright 2001-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twelvemonkeys.util; + +import java.util.*; + +/** + * Abstract test class for {@link java.util.Map} methods and contracts. + *

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

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

+ * Entry Population Methods + *

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

    + *
  • {@link #getSampleKeys()} + *
  • {@link #getSampleValues()} + *
  • {@link #getNewSampleValues()} + *
  • {@link #getOtherKeys()} + *
  • {@link #getOtherValues()} + *
+ * + * Supported Operation Methods + *

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

    + *
  • {@link #isPutAddSupported()} + *
  • {@link #isPutChangeSupported()} + *
  • {@link #isSetValueSupported()} + *
  • {@link #isRemoveSupported()} + *
  • {@link #isGetStructuralModify()} + *
  • {@link #isAllowDuplicateValues()} + *
  • {@link #isAllowNullKey()} + *
  • {@link #isAllowNullValue()} + *
+ * + * Fixture Methods + *

+ * For tests on modification operations (puts and removes), fixtures are used + * to verify that that operation results in correct state for the map and its + * collection views. Basically, the modification is performed against your + * map implementation, and an identical modification is performed against + * a confirmed map implementation. A confirmed map implementation is + * something like java.util.HashMap, which is known to conform + * exactly to the {@link Map} contract. After the modification takes place + * on both your map implementation and the confirmed map implementation, the + * two maps are compared to see if their state is identical. The comparison + * also compares the collection views to make sure they're still the same.

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

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

+ * + * After a modification operation to both {@link #map} and {@link #confirmed}, + * the {@link #verifyAll()} method is invoked to compare the results. The + * {@link # verify0} method calls separate methods to verify the map and its three + * collection views ({@link #verifyMap}, {@link #verifyEntrySet}, + * {@link #verifyKeySet}, and {@link #verifyValues}). You may want to override + * one of the verification methodsto perform additional verifications. For + * instance, TestDoubleOrderedMap would want override its + * {@link #verifyValues()} method to verify that the values are unique and in + * ascending order.

+ * + * Other Notes + *

+ * If your {@link Map} fails one of these tests by design, you may still use + * this base set of cases. Simply override the test case (method) your map + * fails and/or the methods that define the assumptions used by the test + * cases. For example, if your map does not allow duplicate values, override + * {@link #isAllowDuplicateValues()} and have it return false + * + * @author Michael Smith + * @author Rodney Waldhoff + * @author Paul Jack + * @author Stephen Colebourne + * @version $Revision: #2 $ $Date: 2008/07/15 $ + */ +public abstract class MapAbstractTestCase extends ObjectAbstractTestCase { + + /** + * JDK1.2 has bugs in null handling of Maps, especially HashMap.Entry.toString + * This avoids nulls for JDK1.2 + */ + private static final boolean JDK12; + static { + String str = System.getProperty("java.version"); + JDK12 = str.startsWith("1.2"); + } + + // These instance variables are initialized with the reset method. + // Tests for map methods that alter the map (put, putAll, remove) + // first call reset() to create the map and its views; then perform + // the modification on the map; perform the same modification on the + // confirmed; and then call verifyAll() to ensure that the map is equal + // to the confirmed, that the already-constructed collection views + // are still equal to the confirmed's collection views. + + + /** Map created by reset(). */ + protected Map map; + + /** Entry set of map created by reset(). */ + protected Set entrySet; + + /** Key set of map created by reset(). */ + protected Set keySet; + + /** Values collection of map created by reset(). */ + protected Collection values; + + /** HashMap created by reset(). */ + protected Map confirmed; + + // TODO: Figure out if we need these tests... + protected boolean skipSerializedCanonicalTests() { + return true; + } + + /** + * Returns true if the maps produced by + * {@link #makeEmptyMap()} and {@link #makeFullMap()} + * support the put and putAll operations + * adding new mappings. + *

+ * Default implementation returns true. + * Override if your collection class does not support put adding. + */ + public boolean isPutAddSupported() { + return true; + } + + /** + * Returns true if the maps produced by + * {@link #makeEmptyMap()} and {@link #makeFullMap()} + * support the put and putAll operations + * changing existing mappings. + *

+ * Default implementation returns true. + * Override if your collection class does not support put changing. + */ + public boolean isPutChangeSupported() { + return true; + } + + /** + * Returns true if the maps produced by + * {@link #makeEmptyMap()} and {@link #makeFullMap()} + * support the setValue operation on entrySet entries. + *

+ * Default implementation returns isPutChangeSupported(). + * Override if your collection class does not support setValue but does + * support put changing. + */ + public boolean isSetValueSupported() { + return isPutChangeSupported(); + } + + /** + * Returns true if the maps produced by + * {@link #makeEmptyMap()} and {@link #makeFullMap()} + * support the remove and clear operations. + *

+ * Default implementation returns true. + * Override if your collection class does not support removal operations. + */ + public boolean isRemoveSupported() { + return true; + } + + /** + * Returns true if the maps produced by + * {@link #makeEmptyMap()} and {@link #makeFullMap()} + * can cause structural modification on a get(). The example is LRUMap. + *

+ * Default implementation returns false. + * Override if your map class structurally modifies on get. + */ + public boolean isGetStructuralModify() { + return false; + } + + /** + * Returns whether the sub map views of SortedMap are serializable. + * If the class being tested is based around a TreeMap then you should + * override and return false as TreeMap has a bug in deserialization. + * + * @return false + */ + public boolean isSubMapViewsSerializable() { + return true; + } + + /** + * Returns true if the maps produced by + * {@link #makeEmptyMap()} and {@link #makeFullMap()} + * supports null keys. + *

+ * Default implementation returns true. + * Override if your collection class does not support null keys. + */ + public boolean isAllowNullKey() { + return true; + } + + /** + * Returns true if the maps produced by + * {@link #makeEmptyMap()} and {@link #makeFullMap()} + * supports null values. + *

+ * Default implementation returns true. + * Override if your collection class does not support null values. + */ + public boolean isAllowNullValue() { + return true; + } + + /** + * Returns true if the maps produced by + * {@link #makeEmptyMap()} and {@link #makeFullMap()} + * supports duplicate values. + *

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

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

+ */ + public Object[] getOtherNonNullStringElements() { + return new Object[] { + "For","then","despite",/* of */"space","I","would","be","brought", + "From","limits","far","remote","where","thou","dost","stay" + }; + } + + /** + * Returns the set of values in the mappings used to test the map. This + * method must return an array with the same length as + * {@link #getSampleKeys()}. The default implementation constructs a set of + * String values and includes a single null value if + * {@link #isAllowNullValue()} returns true, and includes + * two values that are the same if {@link #isAllowDuplicateValues()} returns + * true. + */ + public Object[] getSampleValues() { + Object[] result = new Object[] { + "blahv", "foov", "barv", "bazv", "tmpv", "goshv", "gollyv", "geev", + "hellov", "goodbyev", "we'llv", "seev", "youv", "allv", "againv", + (isAllowNullValue() && !JDK12) ? null : "nonnullvalue", + "value", + (isAllowDuplicateValues()) ? "value" : "value2", + }; + return result; + } + + /** + * Returns a the set of values that can be used to replace the values + * returned from {@link #getSampleValues()}. This method must return an + * array with the same length as {@link #getSampleValues()}. The values + * returned from this method should not be the same as those returned from + * {@link #getSampleValues()}. The default implementation constructs a + * set of String values and includes a single null value if + * {@link #isAllowNullValue()} returns true, and includes two values + * that are the same if {@link #isAllowDuplicateValues()} returns + * true. + */ + public Object[] getNewSampleValues() { + Object[] result = new Object[] { + (isAllowNullValue() && !JDK12 && isAllowDuplicateValues()) ? null : "newnonnullvalue", + "newvalue", + (isAllowDuplicateValues()) ? "newvalue" : "newvalue2", + "newblahv", "newfoov", "newbarv", "newbazv", "newtmpv", "newgoshv", + "newgollyv", "newgeev", "newhellov", "newgoodbyev", "newwe'llv", + "newseev", "newyouv", "newallv", "newagainv", + }; + return result; + } + + /** + * Helper method to add all the mappings described by + * {@link #getSampleKeys()} and {@link #getSampleValues()}. + */ + public void addSampleMappings(Map m) { + + Object[] keys = getSampleKeys(); + Object[] values = getSampleValues(); + + for(int i = 0; i < keys.length; i++) { + try { + m.put(keys[i], values[i]); + } catch (NullPointerException exception) { + assertTrue("NullPointerException only allowed to be thrown " + + "if either the key or value is null.", + keys[i] == null || values[i] == null); + + assertTrue("NullPointerException on null key, but " + + "isAllowNullKey is not overridden to return false.", + keys[i] == null || !isAllowNullKey()); + + assertTrue("NullPointerException on null value, but " + + "isAllowNullValue is not overridden to return false.", + values[i] == null || !isAllowNullValue()); + + assertTrue("Unknown reason for NullPointer.", false); + } + } + assertEquals("size must reflect number of mappings added.", + keys.length, m.size()); + } + + //----------------------------------------------------------------------- + /** + * Return a new, empty {@link Map} to be used for testing. + * + * @return the map to be tested + */ + public abstract Map makeEmptyMap(); + + /** + * Return a new, populated map. The mappings in the map should match the + * keys and values returned from {@link #getSampleKeys()} and + * {@link #getSampleValues()}. The default implementation uses makeEmptyMap() + * and calls {@link #addSampleMappings} to add all the mappings to the + * map. + * + * @return the map to be tested + */ + public Map makeFullMap() { + Map m = makeEmptyMap(); + addSampleMappings(m); + return m; + } + + /** + * Implements the superclass method to return the map to be tested. + * + * @return the map to be tested + */ + public Object makeObject() { + return makeEmptyMap(); + } + + /** + * Override to return a map other than HashMap as the confirmed map. + * + * @return a map that is known to be valid + */ + public Map makeConfirmedMap() { + return new HashMap(); + } + + /** + * Creates a new Map Entry that is independent of the first and the map. + */ + public Map.Entry cloneMapEntry(Map.Entry entry) { + HashMap map = new HashMap(); + map.put(entry.getKey(), entry.getValue()); + return (Map.Entry) map.entrySet().iterator().next(); + } + + /** + * Gets the compatability version, needed for package access. + */ + public String getCompatibilityVersion() { + return super.getCompatibilityVersion(); + } + //----------------------------------------------------------------------- + /** + * Test to ensure the test setup is working properly. This method checks + * to ensure that the getSampleKeys and getSampleValues methods are + * returning results that look appropriate. That is, they both return a + * non-null array of equal length. The keys array must not have any + * duplicate values, and may only contain a (single) null key if + * isNullKeySupported() returns true. The values array must only have a null + * value if useNullValue() is true and may only have duplicate values if + * isAllowDuplicateValues() returns true. + */ + public void testSampleMappings() { + Object[] keys = getSampleKeys(); + Object[] values = getSampleValues(); + Object[] newValues = getNewSampleValues(); + + assertTrue("failure in test: Must have keys returned from " + + "getSampleKeys.", keys != null); + + assertTrue("failure in test: Must have values returned from " + + "getSampleValues.", values != null); + + // verify keys and values have equivalent lengths (in case getSampleX are + // overridden) + assertEquals("failure in test: not the same number of sample " + + "keys and values.", keys.length, values.length); + + + assertEquals("failure in test: not the same number of values and new values.", + values.length, newValues.length); + + // verify there aren't duplicate keys, and check values + for(int i = 0; i < keys.length - 1; i++) { + for(int j = i + 1; j < keys.length; j++) { + assertTrue("failure in test: duplicate null keys.", + (keys[i] != null || keys[j] != null)); + assertTrue("failure in test: duplicate non-null key.", + (keys[i] == null || keys[j] == null || + (!keys[i].equals(keys[j]) && + !keys[j].equals(keys[i])))); + } + assertTrue("failure in test: found null key, but isNullKeySupported " + + "is false.", keys[i] != null || isAllowNullKey()); + assertTrue("failure in test: found null value, but isNullValueSupported " + + "is false.", values[i] != null || isAllowNullValue()); + assertTrue("failure in test: found null new value, but isNullValueSupported " + + "is false.", newValues[i] != null || isAllowNullValue()); + assertTrue("failure in test: values should not be the same as new value", + values[i] != newValues[i] && + (values[i] == null || !values[i].equals(newValues[i]))); + } + } + + // tests begin here. Each test adds a little bit of tested functionality. + // Many methods assume previous methods passed. That is, they do not + // exhaustively recheck things that have already been checked in a previous + // test methods. + + /** + * Test to ensure that makeEmptyMap and makeFull returns a new non-null + * map with each invocation. + */ + public void testMakeMap() { + Map em = makeEmptyMap(); + assertTrue("failure in test: makeEmptyMap must return a non-null map.", + em != null); + + Map em2 = makeEmptyMap(); + assertTrue("failure in test: makeEmptyMap must return a non-null map.", + em != null); + + assertTrue("failure in test: makeEmptyMap must return a new map " + + "with each invocation.", em != em2); + + Map fm = makeFullMap(); + assertTrue("failure in test: makeFullMap must return a non-null map.", + fm != null); + + Map fm2 = makeFullMap(); + assertTrue("failure in test: makeFullMap must return a non-null map.", + fm != null); + + assertTrue("failure in test: makeFullMap must return a new map " + + "with each invocation.", fm != fm2); + } + + /** + * Tests Map.isEmpty() + */ + public void testMapIsEmpty() { + resetEmpty(); + assertEquals("Map.isEmpty() should return true with an empty map", + true, map.isEmpty()); + verifyAll(); + + resetFull(); + assertEquals("Map.isEmpty() should return false with a non-empty map", + false, map.isEmpty()); + verifyAll(); + } + + /** + * Tests Map.size() + */ + public void testMapSize() { + resetEmpty(); + assertEquals("Map.size() should be 0 with an empty map", + 0, map.size()); + verifyAll(); + + resetFull(); + assertEquals("Map.size() should equal the number of entries " + + "in the map", getSampleKeys().length, map.size()); + verifyAll(); + } + + /** + * Tests {@link Map#clear()}. If the map {@link #isRemoveSupported()} + * can add and remove elements}, then {@link Map#size()} and + * {@link Map#isEmpty()} are used to ensure that map has no elements after + * a call to clear. If the map does not support adding and removing + * elements, this method checks to ensure clear throws an + * UnsupportedOperationException. + */ + public void testMapClear() { + if (!isRemoveSupported()) { + try { + resetFull(); + map.clear(); + fail("Expected UnsupportedOperationException on clear"); + } catch (UnsupportedOperationException ex) {} + return; + } + + resetEmpty(); + map.clear(); + confirmed.clear(); + verifyAll(); + + resetFull(); + map.clear(); + confirmed.clear(); + verifyAll(); + } + + + /** + * Tests Map.containsKey(Object) by verifying it returns false for all + * sample keys on a map created using an empty map and returns true for + * all sample keys returned on a full map. + */ + public void testMapContainsKey() { + Object[] keys = getSampleKeys(); + + resetEmpty(); + for(int i = 0; i < keys.length; i++) { + assertTrue("Map must not contain key when map is empty", + !map.containsKey(keys[i])); + } + verifyAll(); + + resetFull(); + for(int i = 0; i < keys.length; i++) { + assertTrue("Map must contain key for a mapping in the map. " + + "Missing: " + keys[i], map.containsKey(keys[i])); + } + verifyAll(); + } + + /** + * Tests Map.containsValue(Object) by verifying it returns false for all + * sample values on an empty map and returns true for all sample values on + * a full map. + */ + public void testMapContainsValue() { + Object[] values = getSampleValues(); + + resetEmpty(); + for(int i = 0; i < values.length; i++) { + assertTrue("Empty map must not contain value", + !map.containsValue(values[i])); + } + verifyAll(); + + resetFull(); + for(int i = 0; i < values.length; i++) { + assertTrue("Map must contain value for a mapping in the map.", + map.containsValue(values[i])); + } + verifyAll(); + } + + + /** + * Tests Map.equals(Object) + */ + public void testMapEquals() { + resetEmpty(); + assertTrue("Empty maps unequal.", map.equals(confirmed)); + verifyAll(); + + resetFull(); + assertTrue("Full maps unequal.", map.equals(confirmed)); + verifyAll(); + + resetFull(); + // modify the HashMap created from the full map and make sure this + // change results in map.equals() to return false. + Iterator iter = confirmed.keySet().iterator(); + iter.next(); + iter.remove(); + assertTrue("Different maps equal.", !map.equals(confirmed)); + + resetFull(); + assertTrue("equals(null) returned true.", !map.equals(null)); + assertTrue("equals(new Object()) returned true.", + !map.equals(new Object())); + verifyAll(); + } + + + /** + * Tests Map.get(Object) + */ + public void testMapGet() { + resetEmpty(); + + Object[] keys = getSampleKeys(); + Object[] values = getSampleValues(); + + for (int i = 0; i < keys.length; i++) { + assertTrue("Empty map.get() should return null.", + map.get(keys[i]) == null); + } + verifyAll(); + + resetFull(); + for (int i = 0; i < keys.length; i++) { + assertEquals("Full map.get() should return value from mapping.", + values[i], map.get(keys[i])); + } + } + + /** + * Tests Map.hashCode() + */ + public void testMapHashCode() { + resetEmpty(); + assertTrue("Empty maps have different hashCodes.", + map.hashCode() == confirmed.hashCode()); + + resetFull(); + assertTrue("Equal maps have different hashCodes.", + map.hashCode() == confirmed.hashCode()); + } + + /** + * Tests Map.toString(). Since the format of the string returned by the + * toString() method is not defined in the Map interface, there is no + * common way to test the results of the toString() method. Thereforce, + * it is encouraged that Map implementations override this test with one + * that checks the format matches any format defined in its API. This + * default implementation just verifies that the toString() method does + * not return null. + */ + public void testMapToString() { + resetEmpty(); + assertTrue("Empty map toString() should not return null", + map.toString() != null); + verifyAll(); + + resetFull(); + assertTrue("Empty map toString() should not return null", + map.toString() != null); + verifyAll(); + } + + + /** + * Compare the current serialized form of the Map + * against the canonical version in CVS. + */ + /* + public void testEmptyMapCompatibility() throws Exception { + /* + * Create canonical objects with this code + Map map = makeEmptyMap(); + if (!(map instanceof Serializable)) return; + + writeExternalFormToDisk((Serializable) map, getCanonicalEmptyCollectionName(map)); + // + + // test to make sure the canonical form has been preserved + Map map = makeEmptyMap(); + if (map instanceof Serializable && !skipSerializedCanonicalTests() && isTestSerialization()) { + Map map2 = (Map) readExternalFormFromDisk(getCanonicalEmptyCollectionName(map)); + assertEquals("Map is empty", 0, map2.size()); + } + } + */ + + /** + * Compare the current serialized form of the Map + * against the canonical version in CVS. + */ + //public void testFullMapCompatibility() throws Exception { + /** + * Create canonical objects with this code + Map map = makeFullMap(); + if (!(map instanceof Serializable)) return; + + writeExternalFormToDisk((Serializable) map, getCanonicalFullCollectionName(map)); + */ +/* + // test to make sure the canonical form has been preserved + Map map = makeFullMap(); + if (map instanceof Serializable && !skipSerializedCanonicalTests() && isTestSerialization()) { + Map map2 = (Map) readExternalFormFromDisk(getCanonicalFullCollectionName(map)); + assertEquals("Map is the right size", getSampleKeys().length, map2.size()); + } + }*/ + + /** + * Tests Map.put(Object, Object) + */ + public void testMapPut() { + resetEmpty(); + Object[] keys = getSampleKeys(); + Object[] values = getSampleValues(); + Object[] newValues = getNewSampleValues(); + + if (isPutAddSupported()) { + for (int i = 0; i < keys.length; i++) { + Object o = map.put(keys[i], values[i]); + confirmed.put(keys[i], values[i]); + verifyAll(); + assertTrue("First map.put should return null", o == null); + assertTrue("Map should contain key after put", + map.containsKey(keys[i])); + assertTrue("Map should contain value after put", + map.containsValue(values[i])); + } + if (isPutChangeSupported()) { + for (int i = 0; i < keys.length; i++) { + Object o = map.put(keys[i], newValues[i]); + confirmed.put(keys[i], newValues[i]); + verifyAll(); + assertEquals("Map.put should return previous value when changed", + values[i], o); + assertTrue("Map should still contain key after put when changed", + map.containsKey(keys[i])); + assertTrue("Map should contain new value after put when changed", + map.containsValue(newValues[i])); + + // if duplicates are allowed, we're not guaranteed that the value + // no longer exists, so don't try checking that. + if (!isAllowDuplicateValues()) { + assertTrue("Map should not contain old value after put when changed", + !map.containsValue(values[i])); + } + } + } else { + try { + // two possible exception here, either valid + map.put(keys[0], newValues[0]); + fail("Expected IllegalArgumentException or UnsupportedOperationException on put (change)"); + } catch (IllegalArgumentException ex) { + } catch (UnsupportedOperationException ex) {} + } + + } else if (isPutChangeSupported()) { + resetEmpty(); + try { + map.put(keys[0], values[0]); + fail("Expected UnsupportedOperationException or IllegalArgumentException on put (add) when fixed size"); + } catch (IllegalArgumentException ex) { + } catch (UnsupportedOperationException ex) { + } + + resetFull(); + int i = 0; + for (Iterator it = map.keySet().iterator(); it.hasNext() && i < newValues.length; i++) { + Object key = it.next(); + Object o = map.put(key, newValues[i]); + Object value = confirmed.put(key, newValues[i]); + verifyAll(); + assertEquals("Map.put should return previous value when changed", + value, o); + assertTrue("Map should still contain key after put when changed", + map.containsKey(key)); + assertTrue("Map should contain new value after put when changed", + map.containsValue(newValues[i])); + + // if duplicates are allowed, we're not guaranteed that the value + // no longer exists, so don't try checking that. + if (!isAllowDuplicateValues()) { + assertTrue("Map should not contain old value after put when changed", + !map.containsValue(values[i])); + } + } + } else { + try { + map.put(keys[0], values[0]); + fail("Expected UnsupportedOperationException on put (add)"); + } catch (UnsupportedOperationException ex) {} + } + } + + /** + * Tests Map.put(null, value) + */ + public void testMapPutNullKey() { + resetFull(); + Object[] values = getSampleValues(); + + if (isPutAddSupported()) { + if (isAllowNullKey()) { + map.put(null, values[0]); + } else { + try { + map.put(null, values[0]); + fail("put(null, value) should throw NPE/IAE"); + } catch (NullPointerException ex) { + } catch (IllegalArgumentException ex) {} + } + } + } + + /** + * Tests Map.put(null, value) + */ + public void testMapPutNullValue() { + resetFull(); + Object[] keys = getSampleKeys(); + + if (isPutAddSupported()) { + if (isAllowNullValue()) { + map.put(keys[0], null); + } else { + try { + map.put(keys[0], null); + fail("put(key, null) should throw NPE/IAE"); + } catch (NullPointerException ex) { + } catch (IllegalArgumentException ex) {} + } + } + } + + /** + * Tests Map.putAll(map) + */ + public void testMapPutAll() { + if (!isPutAddSupported()) { + if (!isPutChangeSupported()) { + Map temp = makeFullMap(); + resetEmpty(); + try { + map.putAll(temp); + fail("Expected UnsupportedOperationException on putAll"); + } catch (UnsupportedOperationException ex) {} + } + return; + } + + resetEmpty(); + + Map m2 = makeFullMap(); + + map.putAll(m2); + confirmed.putAll(m2); + verifyAll(); + + resetEmpty(); + + m2 = makeConfirmedMap(); + Object[] keys = getSampleKeys(); + Object[] values = getSampleValues(); + for(int i = 0; i < keys.length; i++) { + m2.put(keys[i], values[i]); + } + + map.putAll(m2); + confirmed.putAll(m2); + verifyAll(); + } + + /** + * Tests Map.remove(Object) + */ + public void testMapRemove() { + if (!isRemoveSupported()) { + try { + resetFull(); + map.remove(map.keySet().iterator().next()); + fail("Expected UnsupportedOperationException on remove"); + } catch (UnsupportedOperationException ex) {} + return; + } + + resetEmpty(); + + Object[] keys = getSampleKeys(); + Object[] values = getSampleValues(); + for(int i = 0; i < keys.length; i++) { + Object o = map.remove(keys[i]); + assertTrue("First map.remove should return null", o == null); + } + verifyAll(); + + resetFull(); + + for(int i = 0; i < keys.length; i++) { + Object o = map.remove(keys[i]); + confirmed.remove(keys[i]); + verifyAll(); + + assertEquals("map.remove with valid key should return value", + values[i], o); + } + + Object[] other = getOtherKeys(); + + resetFull(); + int size = map.size(); + for (int i = 0; i < other.length; i++) { + Object o = map.remove(other[i]); + assertEquals("map.remove for nonexistent key should return null", + o, null); + assertEquals("map.remove for nonexistent key should not " + + "shrink map", size, map.size()); + } + verifyAll(); + } + + //----------------------------------------------------------------------- + /** + * Tests that the {@link Map#values} collection is backed by + * the underlying map for clear(). + */ + public void testValuesClearChangesMap() { + if (!isRemoveSupported()) return; + + // clear values, reflected in map + resetFull(); + Collection values = map.values(); + assertTrue(map.size() > 0); + assertTrue(values.size() > 0); + values.clear(); + assertTrue(map.size() == 0); + assertTrue(values.size() == 0); + + // clear map, reflected in values + resetFull(); + values = map.values(); + assertTrue(map.size() > 0); + assertTrue(values.size() > 0); + map.clear(); + assertTrue(map.size() == 0); + assertTrue(values.size() == 0); + } + + /** + * Tests that the {@link Map#keySet} collection is backed by + * the underlying map for clear(). + */ + public void testKeySetClearChangesMap() { + if (!isRemoveSupported()) return; + + // clear values, reflected in map + resetFull(); + Set keySet = map.keySet(); + assertTrue(map.size() > 0); + assertTrue(keySet.size() > 0); + keySet.clear(); + assertTrue(map.size() == 0); + assertTrue(keySet.size() == 0); + + // clear map, reflected in values + resetFull(); + keySet = map.keySet(); + assertTrue(map.size() > 0); + assertTrue(keySet.size() > 0); + map.clear(); + assertTrue(map.size() == 0); + assertTrue(keySet.size() == 0); + } + + /** + * Tests that the {@link Map#entrySet()} collection is backed by + * the underlying map for clear(). + */ + public void testEntrySetClearChangesMap() { + if (!isRemoveSupported()) return; + + // clear values, reflected in map + resetFull(); + Set entrySet = map.entrySet(); + assertTrue(map.size() > 0); + assertTrue(entrySet.size() > 0); + entrySet.clear(); + assertTrue(map.size() == 0); + assertTrue(entrySet.size() == 0); + + // clear map, reflected in values + resetFull(); + entrySet = map.entrySet(); + assertTrue(map.size() > 0); + assertTrue(entrySet.size() > 0); + map.clear(); + assertTrue(map.size() == 0); + assertTrue(entrySet.size() == 0); + } + + //----------------------------------------------------------------------- + public void testEntrySetContains1() { + resetFull(); + Set entrySet = map.entrySet(); + Map.Entry entry = (Map.Entry) entrySet.iterator().next(); + assertEquals(true, entrySet.contains(entry)); + } + public void testEntrySetContains2() { + resetFull(); + Set entrySet = map.entrySet(); + Map.Entry entry = (Map.Entry) entrySet.iterator().next(); + Map.Entry test = cloneMapEntry(entry); + assertEquals(true, entrySet.contains(test)); + } + public void testEntrySetContains3() { + resetFull(); + Set entrySet = map.entrySet(); + Map.Entry entry = (Map.Entry) entrySet.iterator().next(); + HashMap temp = new HashMap(); + temp.put(entry.getKey(), "A VERY DIFFERENT VALUE"); + Map.Entry test = (Map.Entry) temp.entrySet().iterator().next(); + assertEquals(false, entrySet.contains(test)); + } + + public void testEntrySetRemove1() { + if (!isRemoveSupported()) return; + resetFull(); + int size = map.size(); + Set entrySet = map.entrySet(); + Map.Entry entry = (Map.Entry) entrySet.iterator().next(); + Object key = entry.getKey(); + + assertEquals(true, entrySet.remove(entry)); + assertEquals(false, map.containsKey(key)); + assertEquals(size - 1, map.size()); + } + public void testEntrySetRemove2() { + if (!isRemoveSupported()) return; + resetFull(); + int size = map.size(); + Set entrySet = map.entrySet(); + Map.Entry entry = (Map.Entry) entrySet.iterator().next(); + Object key = entry.getKey(); + Map.Entry test = cloneMapEntry(entry); + + assertEquals(true, entrySet.remove(test)); + assertEquals(false, map.containsKey(key)); + assertEquals(size - 1, map.size()); + } + public void testEntrySetRemove3() { + if (!isRemoveSupported()) return; + resetFull(); + int size = map.size(); + Set entrySet = map.entrySet(); + Map.Entry entry = (Map.Entry) entrySet.iterator().next(); + Object key = entry.getKey(); + HashMap temp = new HashMap(); + temp.put(entry.getKey(), "A VERY DIFFERENT VALUE"); + Map.Entry test = (Map.Entry) temp.entrySet().iterator().next(); + + assertEquals(false, entrySet.remove(test)); + assertEquals(true, map.containsKey(key)); + assertEquals(size, map.size()); + } + + //----------------------------------------------------------------------- + /** + * Tests that the {@link Map#values} collection is backed by + * the underlying map by removing from the values collection + * and testing if the value was removed from the map. + *

+ * We should really test the "vice versa" case--that values removed + * from the map are removed from the values collection--also, + * but that's a more difficult test to construct (lacking a + * "removeValue" method.) + *

+ *

+ * See bug + * 9573. + *

+ */ + public void testValuesRemoveChangesMap() { + resetFull(); + Object[] sampleValues = getSampleValues(); + Collection values = map.values(); + for (int i = 0; i < sampleValues.length; i++) { + if (map.containsValue(sampleValues[i])) { + int j = 0; // loop counter prevents infinite loops when remove is broken + while (values.contains(sampleValues[i]) && j < 10000) { + try { + values.remove(sampleValues[i]); + } catch (UnsupportedOperationException e) { + // if values.remove is unsupported, just skip this test + return; + } + j++; + } + assertTrue("values().remove(obj) is broken", j < 10000); + assertTrue( + "Value should have been removed from the underlying map.", + !map.containsValue(sampleValues[i])); + } + } + } + + /** + * Tests that the {@link Map#keySet} set is backed by + * the underlying map by removing from the keySet set + * and testing if the key was removed from the map. + */ + public void testKeySetRemoveChangesMap() { + resetFull(); + Object[] sampleKeys = getSampleKeys(); + Set keys = map.keySet(); + for (int i = 0; i < sampleKeys.length; i++) { + try { + keys.remove(sampleKeys[i]); + } catch (UnsupportedOperationException e) { + // if key.remove is unsupported, just skip this test + return; + } + assertTrue( + "Key should have been removed from the underlying map.", + !map.containsKey(sampleKeys[i])); + } + } + + // TODO: Need: + // testValuesRemovedFromEntrySetAreRemovedFromMap + // same for EntrySet/KeySet/values's + // Iterator.remove, removeAll, retainAll + + + /** + * Utility methods to create an array of Map.Entry objects + * out of the given key and value arrays.

+ * + * @param keys the array of keys + * @param values the array of values + * @return an array of Map.Entry of those keys to those values + */ + private Map.Entry[] makeEntryArray(Object[] keys, Object[] values) { + Map.Entry[] result = new Map.Entry[keys.length]; + for (int i = 0; i < keys.length; i++) { + Map map = makeConfirmedMap(); + map.put(keys[i], values[i]); + result[i] = (Map.Entry) map.entrySet().iterator().next(); + } + return result; + } + + + /** + * Bulk test {@link Map#entrySet()}. This method runs through all of + * the tests in {@link SetAbstractTestCase}. + * After modification operations, {@link #verifyAll()} is invoked to ensure + * that the map and the other collection views are still valid. + * + * @return a {@link SetAbstractTestCase} instance for testing the map's entry set + */ + /* + public BulkTest bulkTestMapEntrySet() { + return new TestMapEntrySet(); + } + */ + + public class TestMapEntrySet extends SetAbstractTestCase { + + // Have to implement manually; entrySet doesn't support addAll + public Object[] getFullElements() { + Object[] k = getSampleKeys(); + Object[] v = getSampleValues(); + return makeEntryArray(k, v); + } + + // Have to implement manually; entrySet doesn't support addAll + public Object[] getOtherElements() { + Object[] k = getOtherKeys(); + Object[] v = getOtherValues(); + return makeEntryArray(k, v); + } + + public Set makeEmptySet() { + return makeEmptyMap().entrySet(); + } + + public Set makeFullSet() { + return makeFullMap().entrySet(); + } + + public boolean isAddSupported() { + // Collection views don't support add operations. + return false; + } + public boolean isRemoveSupported() { + // Entry set should only support remove if map does + return MapAbstractTestCase.this.isRemoveSupported(); + } + public boolean isGetStructuralModify() { + return MapAbstractTestCase.this.isGetStructuralModify(); + } + public boolean isTestSerialization() { + return false; + } + + public void resetFull() { + MapAbstractTestCase.this.resetFull(); + collection = map.entrySet(); + TestMapEntrySet.this.confirmed = MapAbstractTestCase.this.confirmed.entrySet(); + } + + public void resetEmpty() { + MapAbstractTestCase.this.resetEmpty(); + collection = map.entrySet(); + TestMapEntrySet.this.confirmed = MapAbstractTestCase.this.confirmed.entrySet(); + } + + public void testMapEntrySetIteratorEntry() { + resetFull(); + Iterator it = collection.iterator(); + int count = 0; + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + assertEquals(true, MapAbstractTestCase.this.map.containsKey(entry.getKey())); + assertEquals(true, MapAbstractTestCase.this.map.containsValue(entry.getValue())); + if (isGetStructuralModify() == false) { + assertEquals(MapAbstractTestCase.this.map.get(entry.getKey()), entry.getValue()); + } + count++; + } + assertEquals(collection.size(), count); + } + + public void testMapEntrySetIteratorEntrySetValue() { + Object key1 = getSampleKeys()[0]; + Object key2 = (getSampleKeys().length ==1 ? getSampleKeys()[0] : getSampleKeys()[1]); + Object newValue1 = getNewSampleValues()[0]; + Object newValue2 = (getNewSampleValues().length ==1 ? getNewSampleValues()[0] : getNewSampleValues()[1]); + + resetFull(); + // explicitly get entries as sample values/keys are connected for some maps + // such as BeanMap + Iterator it = TestMapEntrySet.this.collection.iterator(); + Map.Entry entry1 = getEntry(it, key1); + it = TestMapEntrySet.this.collection.iterator(); + Map.Entry entry2 = getEntry(it, key2); + Iterator itConfirmed = TestMapEntrySet.this.confirmed.iterator(); + Map.Entry entryConfirmed1 = getEntry(itConfirmed, key1); + itConfirmed = TestMapEntrySet.this.confirmed.iterator(); + Map.Entry entryConfirmed2 = getEntry(itConfirmed, key2); + verifyAll(); + + if (isSetValueSupported() == false) { + try { + entry1.setValue(newValue1); + } catch (UnsupportedOperationException ex) { + } + return; + } + + entry1.setValue(newValue1); + entryConfirmed1.setValue(newValue1); + assertEquals(newValue1, entry1.getValue()); + assertEquals(true, MapAbstractTestCase.this.map.containsKey(entry1.getKey())); + assertEquals(true, MapAbstractTestCase.this.map.containsValue(newValue1)); + assertEquals(newValue1, MapAbstractTestCase.this.map.get(entry1.getKey())); + verifyAll(); + + entry1.setValue(newValue1); + entryConfirmed1.setValue(newValue1); + assertEquals(newValue1, entry1.getValue()); + assertEquals(true, MapAbstractTestCase.this.map.containsKey(entry1.getKey())); + assertEquals(true, MapAbstractTestCase.this.map.containsValue(newValue1)); + assertEquals(newValue1, MapAbstractTestCase.this.map.get(entry1.getKey())); + verifyAll(); + + entry2.setValue(newValue2); + entryConfirmed2.setValue(newValue2); + assertEquals(newValue2, entry2.getValue()); + assertEquals(true, MapAbstractTestCase.this.map.containsKey(entry2.getKey())); + assertEquals(true, MapAbstractTestCase.this.map.containsValue(newValue2)); + assertEquals(newValue2, MapAbstractTestCase.this.map.get(entry2.getKey())); + verifyAll(); + } + + public Map.Entry getEntry(Iterator itConfirmed, Object key) { + Map.Entry entry = null; + while (itConfirmed.hasNext()) { + Map.Entry temp = (Map.Entry) itConfirmed.next(); + if (temp.getKey() == null) { + if (key == null) { + entry = temp; + break; + } + } else if (temp.getKey().equals(key)) { + entry = temp; + break; + } + } + assertNotNull("No matching entry in map for key '" + key + "'", entry); + return entry; + } + + public void testMapEntrySetRemoveNonMapEntry() { + if (isRemoveSupported() == false) return; + resetFull(); + assertEquals(false, getSet().remove(null)); + assertEquals(false, getSet().remove(new Object())); + } + + public void verifyAll() { + super.verifyAll(); + MapAbstractTestCase.this.verifyAll(); + } + } + + + /** + * Bulk test {@link Map#keySet()}. This method runs through all of + * the tests in {@link SetAbstractTestCase}. + * After modification operations, {@link #verifyAll()} is invoked to ensure + * that the map and the other collection views are still valid. + * + * @return a {@link SetAbstractTestCase} instance for testing the map's key set + */ + /* + public BulkTest bulkTestMapKeySet() { + return new TestMapKeySet(); + } + */ + + public class TestMapKeySet extends SetAbstractTestCase { + public Object[] getFullElements() { + return getSampleKeys(); + } + + public Object[] getOtherElements() { + return getOtherKeys(); + } + + public Set makeEmptySet() { + return makeEmptyMap().keySet(); + } + + public Set makeFullSet() { + return makeFullMap().keySet(); + } + + public boolean isNullSupported() { + return MapAbstractTestCase.this.isAllowNullKey(); + } + public boolean isAddSupported() { + return false; + } + public boolean isRemoveSupported() { + return MapAbstractTestCase.this.isRemoveSupported(); + } + public boolean isTestSerialization() { + return false; + } + + public void resetEmpty() { + MapAbstractTestCase.this.resetEmpty(); + collection = map.keySet(); + TestMapKeySet.this.confirmed = MapAbstractTestCase.this.confirmed.keySet(); + } + + public void resetFull() { + MapAbstractTestCase.this.resetFull(); + collection = map.keySet(); + TestMapKeySet.this.confirmed = MapAbstractTestCase.this.confirmed.keySet(); + } + + public void verifyAll() { + super.verifyAll(); + MapAbstractTestCase.this.verifyAll(); + } + } + + + /** + * Bulk test {@link Map#values()}. This method runs through all of + * the tests in {@link CollectionAbstractTestCase}. + * After modification operations, {@link #verifyAll()} is invoked to ensure + * that the map and the other collection views are still valid. + * + * @return a {@link CollectionAbstractTestCase} instance for testing the map's + * values collection + */ + /* + public BulkTest bulkTestMapValues() { + return new TestMapValues(); + } + */ + + public class TestMapValues extends CollectionAbstractTestCase { + public Object[] getFullElements() { + return getSampleValues(); + } + + public Object[] getOtherElements() { + return getOtherValues(); + } + + public Collection makeCollection() { + return makeEmptyMap().values(); + } + + public Collection makeFullCollection() { + return makeFullMap().values(); + } + + public boolean isNullSupported() { + return MapAbstractTestCase.this.isAllowNullKey(); + } + public boolean isAddSupported() { + return false; + } + public boolean isRemoveSupported() { + return MapAbstractTestCase.this.isRemoveSupported(); + } + public boolean isTestSerialization() { + return false; + } + + public boolean areEqualElementsDistinguishable() { + // equal values are associated with different keys, so they are + // distinguishable. + return true; + } + + public Collection makeConfirmedCollection() { + // never gets called, reset methods are overridden + return null; + } + + public Collection makeConfirmedFullCollection() { + // never gets called, reset methods are overridden + return null; + } + + public void resetFull() { + MapAbstractTestCase.this.resetFull(); + collection = map.values(); + TestMapValues.this.confirmed = MapAbstractTestCase.this.confirmed.values(); + } + + public void resetEmpty() { + MapAbstractTestCase.this.resetEmpty(); + collection = map.values(); + TestMapValues.this.confirmed = MapAbstractTestCase.this.confirmed.values(); + } + + public void verifyAll() { + super.verifyAll(); + MapAbstractTestCase.this.verifyAll(); + } + + // TODO: should test that a remove on the values collection view + // removes the proper mapping and not just any mapping that may have + // the value equal to the value returned from the values iterator. + } + + + /** + * Resets the {@link #map}, {@link #entrySet}, {@link #keySet}, + * {@link #values} and {@link #confirmed} fields to empty. + */ + public void resetEmpty() { + this.map = makeEmptyMap(); + views(); + this.confirmed = makeConfirmedMap(); + } + + /** + * Resets the {@link #map}, {@link #entrySet}, {@link #keySet}, + * {@link #values} and {@link #confirmed} fields to full. + */ + public void resetFull() { + this.map = makeFullMap(); + views(); + this.confirmed = makeConfirmedMap(); + Object[] k = getSampleKeys(); + Object[] v = getSampleValues(); + for (int i = 0; i < k.length; i++) { + confirmed.put(k[i], v[i]); + } + } + + + /** + * Resets the collection view fields. + */ + private void views() { + this.keySet = map.keySet(); + this.values = map.values(); + this.entrySet = map.entrySet(); + } + + + /** + * Verifies that {@link #map} is still equal to {@link #confirmed}. + * This method checks that the map is equal to the HashMap, + * and that the map's collection views are still equal to + * the HashMap's collection views. An equals test + * is done on the maps and their collection views; their size and + * isEmpty results are compared; their hashCodes are + * compared; and containsAll tests are run on the + * collection views. + */ + public void verifyAll() { + verifyMap(); + verifyEntrySet(); + verifyKeySet(); + verifyValues(); + } + + public void verifyMap() { + int size = confirmed.size(); + boolean empty = confirmed.isEmpty(); + assertEquals("Map should be same size as HashMap", + size, map.size()); + assertEquals("Map should be empty if HashMap is", + empty, map.isEmpty()); + assertEquals("hashCodes should be the same", + confirmed.hashCode(), map.hashCode()); + // this fails for LRUMap because confirmed.equals() somehow modifies + // map, causing concurrent modification exceptions. + //assertEquals("Map should still equal HashMap", confirmed, map); + // this works though and performs the same verification: + assertTrue("Map should still equal HashMap", map.equals(confirmed)); + // TODO: this should really be reexamined to figure out why LRU map + // behaves like it does (the equals shouldn't modify since all accesses + // by the confirmed collection should be through an iterator, thus not + // causing LRUMap to change). + } + + public void verifyEntrySet() { + int size = confirmed.size(); + boolean empty = confirmed.isEmpty(); + assertEquals("entrySet should be same size as HashMap's" + + "\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), + size, entrySet.size()); + assertEquals("entrySet should be empty if HashMap is" + + "\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), + empty, entrySet.isEmpty()); + assertTrue("entrySet should contain all HashMap's elements" + + "\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), + entrySet.containsAll(confirmed.entrySet())); + assertEquals("entrySet hashCodes should be the same" + + "\nTest: " + entrySet + "\nReal: " + confirmed.entrySet(), + confirmed.entrySet().hashCode(), entrySet.hashCode()); + assertEquals("Map's entry set should still equal HashMap's", + confirmed.entrySet(), entrySet); + } + + public void verifyKeySet() { + int size = confirmed.size(); + boolean empty = confirmed.isEmpty(); + assertEquals("keySet should be same size as HashMap's" + + "\nTest: " + keySet + "\nReal: " + confirmed.keySet(), + size, keySet.size()); + assertEquals("keySet should be empty if HashMap is" + + "\nTest: " + keySet + "\nReal: " + confirmed.keySet(), + empty, keySet.isEmpty()); + assertTrue("keySet should contain all HashMap's elements" + + "\nTest: " + keySet + "\nReal: " + confirmed.keySet(), + keySet.containsAll(confirmed.keySet())); + assertEquals("keySet hashCodes should be the same" + + "\nTest: " + keySet + "\nReal: " + confirmed.keySet(), + confirmed.keySet().hashCode(), keySet.hashCode()); + assertEquals("Map's key set should still equal HashMap's", + confirmed.keySet(), keySet); + } + + public void verifyValues() { + List known = new ArrayList(confirmed.values()); + List test = new ArrayList(values); + + int size = confirmed.size(); + boolean empty = confirmed.isEmpty(); + assertEquals("values should be same size as HashMap's" + + "\nTest: " + test + "\nReal: " + known, + size, values.size()); + assertEquals("values should be empty if HashMap is" + + "\nTest: " + test + "\nReal: " + known, + empty, values.isEmpty()); + assertTrue("values should contain all HashMap's elements" + + "\nTest: " + test + "\nReal: " + known, + test.containsAll(known)); + assertTrue("values should contain all HashMap's elements" + + "\nTest: " + test + "\nReal: " + known, + known.containsAll(test)); + // originally coded to use a HashBag, but now separate jar so... + for (Iterator it = known.iterator(); it.hasNext();) { + boolean removed = test.remove(it.next()); + assertTrue("Map's values should still equal HashMap's", removed); + } + assertTrue("Map's values should still equal HashMap's", test.isEmpty()); + } + + + /** + * Erases any leftover instance variables by setting them to null. + */ + public void tearDown() throws Exception { + map = null; + keySet = null; + entrySet = null; + values = null; + confirmed = null; + } + +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/NullMapTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/NullMapTestCase.java new file mode 100755 index 00000000..c97dcae9 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/NullMapTestCase.java @@ -0,0 +1,187 @@ +package com.twelvemonkeys.util; + + +import java.util.Map; +import java.util.HashMap; + +/** + * NOTE: This TestCase is written especially for NullMap, and is full of dirty + * tricks. A good indication that NullMap is not a good, general-purpose Map + * implementation... + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/NullMapTestCase.java#2 $ + */ +public class NullMapTestCase extends MapAbstractTestCase { + private boolean empty = true; + + public Map makeEmptyMap() { + return new NullMap(); + } + + public Map makeFullMap() { + return new NullMap(); + } + + public Map makeConfirmedMap() { + // Always empty + return new HashMap(); + } + + public void resetEmpty() { + empty = true; + super.resetEmpty(); + } + + public void resetFull() { + empty = false; + super.resetFull(); + } + + public void verifyAll() { + if (empty) { + super.verifyAll(); + } + } + + // Overriden, as this map is always empty + public void testMapIsEmpty() { + resetEmpty(); + assertEquals("Map.isEmpty() should return true with an empty map", + true, map.isEmpty()); + verifyAll(); + resetFull(); + assertEquals("Map.isEmpty() should return true with a full map", + true, map.isEmpty()); + } + + // Overriden, as this map is always empty + public void testMapSize() { + resetEmpty(); + assertEquals("Map.size() should be 0 with an empty map", + 0, map.size()); + verifyAll(); + + resetFull(); + assertEquals("Map.size() should equal the number of entries " + + "in the map", 0, map.size()); + } + + public void testMapContainsKey() { + Object[] keys = getSampleKeys(); + + resetEmpty(); + for(int i = 0; i < keys.length; i++) { + assertTrue("Map must not contain key when map is empty", + !map.containsKey(keys[i])); + } + verifyAll(); + } + + public void testMapContainsValue() { + Object[] values = getSampleValues(); + + resetEmpty(); + for(int i = 0; i < values.length; i++) { + assertTrue("Empty map must not contain value", + !map.containsValue(values[i])); + } + verifyAll(); + } + + public void testMapEquals() { + resetEmpty(); + assertTrue("Empty maps unequal.", map.equals(confirmed)); + verifyAll(); + } + + public void testMapHashCode() { + resetEmpty(); + assertTrue("Empty maps have different hashCodes.", + map.hashCode() == confirmed.hashCode()); + } + + public void testMapGet() { + resetEmpty(); + + Object[] keys = getSampleKeys(); + + for (int i = 0; i < keys.length; i++) { + assertTrue("Empty map.get() should return null.", + map.get(keys[i]) == null); + } + verifyAll(); + } + + public void testMapPut() { + resetEmpty(); + Object[] keys = getSampleKeys(); + Object[] values = getSampleValues(); + Object[] newValues = getNewSampleValues(); + + for (int i = 0; i < keys.length; i++) { + Object o = map.put(keys[i], values[i]); + //confirmed.put(keys[i], values[i]); + verifyAll(); + assertTrue("First map.put should return null", o == null); + } + for (int i = 0; i < keys.length; i++) { + map.put(keys[i], newValues[i]); + //confirmed.put(keys[i], newValues[i]); + verifyAll(); + } + } + + public void testMapToString() { + resetEmpty(); + assertTrue("Empty map toString() should not return null", + map.toString() != null); + verifyAll(); + } + + public void testMapPutAll() { + // TODO: Find a menaingful way to test this + } + + public void testMapRemove() { + resetEmpty(); + + Object[] keys = getSampleKeys(); + for(int i = 0; i < keys.length; i++) { + Object o = map.remove(keys[i]); + assertTrue("First map.remove should return null", o == null); + } + verifyAll(); + } + + //----------------------------------------------------------------------- + public void testEntrySetClearChangesMap() { + } + + public void testKeySetClearChangesMap() { + } + + public void testKeySetRemoveChangesMap() { + } + + public void testValuesClearChangesMap() { + } + + public void testEntrySetContains1() { + } + + public void testEntrySetContains2() { + } + + public void testEntrySetContains3() { + } + + public void testEntrySetRemove1() { + } + + public void testEntrySetRemove2() { + } + + public void testEntrySetRemove3() { + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/ObjectAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/ObjectAbstractTestCase.java new file mode 100755 index 00000000..688acdbe --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/ObjectAbstractTestCase.java @@ -0,0 +1,307 @@ +/* + * Copyright 2001-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twelvemonkeys.util; + +import org.jmock.cglib.MockObjectTestCase; + +import java.io.*; + +/** + * Abstract test class for {@link Object} methods and contracts. + *

+ * To use, simply extend this class, and implement + * the {@link #makeObject()} method. + *

+ * If your {@link Object} fails one of these tests by design, + * you may still use this base set of cases. Simply override the + * test case (method) your {@link Object} fails. + * + * @version $Revision: #2 $ $Date: 2008/07/15 $ + * + * @author Rodney Waldhoff + * @author Stephen Colebourne + * @author Anonymous + */ +public abstract class ObjectAbstractTestCase extends MockObjectTestCase { + + //----------------------------------------------------------------------- + /** + * Implement this method to return the object to test. + * + * @return the object to test + */ + public abstract Object makeObject(); + + /** + * Override this method if a subclass is testing an object + * that cannot serialize an "empty" Collection. + * (e.g. Comparators have no contents) + * + * @return true + */ + public boolean supportsEmptyCollections() { + return true; + } + + /** + * Override this method if a subclass is testing an object + * that cannot serialize a "full" Collection. + * (e.g. Comparators have no contents) + * + * @return true + */ + public boolean supportsFullCollections() { + return true; + } + + /** + * Is serialization testing supported. + * Default is true. + */ + public boolean isTestSerialization() { + return true; + } + + /** + * Returns true to indicate that the collection supports equals() comparisons. + * This implementation returns true; + */ + public boolean isEqualsCheckable() { + return true; + } + + //----------------------------------------------------------------------- + public void testObjectEqualsSelf() { + Object obj = makeObject(); + assertEquals("A Object should equal itself", obj, obj); + } + + public void testEqualsNull() { + Object obj = makeObject(); + assertEquals(false, obj.equals(null)); // make sure this doesn't throw NPE either + } + + public void testObjectHashCodeEqualsSelfHashCode() { + Object obj = makeObject(); + assertEquals("hashCode should be repeatable", obj.hashCode(), obj.hashCode()); + } + + public void testObjectHashCodeEqualsContract() { + Object obj1 = makeObject(); + if (obj1.equals(obj1)) { + assertEquals( + "[1] When two objects are equal, their hashCodes should be also.", + obj1.hashCode(), obj1.hashCode()); + } + Object obj2 = makeObject(); + if (obj1.equals(obj2)) { + assertEquals( + "[2] When two objects are equal, their hashCodes should be also.", + obj1.hashCode(), obj2.hashCode()); + assertTrue( + "When obj1.equals(obj2) is true, then obj2.equals(obj1) should also be true", + obj2.equals(obj1)); + } + } + + public void testSerializeDeserializeThenCompare() throws Exception { + Object obj = makeObject(); + if (obj instanceof Serializable && isTestSerialization()) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + ObjectOutputStream out = new ObjectOutputStream(buffer); + out.writeObject(obj); + out.close(); + + ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())); + Object dest = in.readObject(); + in.close(); + if (isEqualsCheckable()) { + assertEquals("obj != deserialize(serialize(obj))", obj, dest); + } + } + } + + /** + * Sanity check method, makes sure that any Serializable + * class can be serialized and de-serialized in memory, + * using the handy makeObject() method + * + * @throws java.io.IOException + * @throws ClassNotFoundException + */ + public void testSimpleSerialization() throws Exception { + Object o = makeObject(); + if (o instanceof Serializable && isTestSerialization()) { + byte[] objekt = writeExternalFormToBytes((Serializable) o); + Object p = readExternalFormFromBytes(objekt); + } + } + + /** + * Tests serialization by comparing against a previously stored version in CVS. + * If the test object is serializable, confirm that a canonical form exists. + */ + public void testCanonicalEmptyCollectionExists() { + if (supportsEmptyCollections() && isTestSerialization() && !skipSerializedCanonicalTests()) { + Object object = makeObject(); + if (object instanceof Serializable) { + String name = getCanonicalEmptyCollectionName(object); + assertTrue( + "Canonical empty collection (" + name + ") is not in CVS", + new File(name).exists()); + } + } + } + + /** + * Tests serialization by comparing against a previously stored version in CVS. + * If the test object is serializable, confirm that a canonical form exists. + */ + public void testCanonicalFullCollectionExists() { + if (supportsFullCollections() && isTestSerialization() && !skipSerializedCanonicalTests()) { + Object object = makeObject(); + if (object instanceof Serializable) { + String name = getCanonicalFullCollectionName(object); + assertTrue( + "Canonical full collection (" + name + ") is not in CVS", + new File(name).exists()); + } + } + } + + // protected implementation + //----------------------------------------------------------------------- + /** + * Get the version of Collections that this object tries to + * maintain serialization compatibility with. Defaults to 1, the + * earliest Collections version. (Note: some collections did not + * even exist in this version). + * + * This constant makes it possible for TestMap (and other subclasses, + * if necessary) to automatically check CVS for a versionX copy of a + * Serialized object, so we can make sure that compatibility is maintained. + * See, for example, TestMap.getCanonicalFullMapName(Map map). + * Subclasses can override this variable, indicating compatibility + * with earlier Collections versions. + * + * @return The version, or null if this object shouldn't be + * tested for compatibility with previous versions. + */ + public String getCompatibilityVersion() { + return "1"; + } + + protected String getCanonicalEmptyCollectionName(Object object) { + StringBuilder retval = new StringBuilder(); + retval.append("data/test/"); + String colName = object.getClass().getName(); + colName = colName.substring(colName.lastIndexOf(".") + 1, colName.length()); + retval.append(colName); + retval.append(".emptyCollection.version"); + retval.append(getCompatibilityVersion()); + retval.append(".obj"); + return retval.toString(); + } + + protected String getCanonicalFullCollectionName(Object object) { + StringBuilder retval = new StringBuilder(); + retval.append("data/test/"); + String colName = object.getClass().getName(); + colName = colName.substring(colName.lastIndexOf(".") + 1, colName.length()); + retval.append(colName); + retval.append(".fullCollection.version"); + retval.append(getCompatibilityVersion()); + retval.append(".obj"); + return retval.toString(); + } + + /** + * Write a Serializable or Externalizable object as + * a file at the given path. NOT USEFUL as part + * of a unit test; this is just a utility method + * for creating disk-based objects in CVS that can become + * the basis for compatibility tests using + * readExternalFormFromDisk(String path) + * + * @param o Object to serialize + * @param path path to write the serialized Object + * @exception java.io.IOException + */ + protected void writeExternalFormToDisk(Serializable o, String path) throws IOException { + FileOutputStream fileStream = new FileOutputStream(path); + writeExternalFormToStream(o, fileStream); + } + + /** + * Converts a Serializable or Externalizable object to + * bytes. Useful for in-memory tests of serialization + * + * @param o Object to convert to bytes + * @return serialized form of the Object + * @exception java.io.IOException + */ + protected byte[] writeExternalFormToBytes(Serializable o) throws IOException { + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + writeExternalFormToStream(o, byteStream); + return byteStream.toByteArray(); + } + + /** + * Reads a Serialized or Externalized Object from disk. + * Useful for creating compatibility tests between + * different CVS versions of the same class + * + * @param path path to the serialized Object + * @return the Object at the given path + * @exception java.io.IOException + * @exception ClassNotFoundException + */ + protected Object readExternalFormFromDisk(String path) throws IOException, ClassNotFoundException { + FileInputStream stream = new FileInputStream(path); + return readExternalFormFromStream(stream); + } + + /** + * Read a Serialized or Externalized Object from bytes. + * Useful for verifying serialization in memory. + * + * @param b byte array containing a serialized Object + * @return Object contained in the bytes + * @exception java.io.IOException + * @exception ClassNotFoundException + */ + protected Object readExternalFormFromBytes(byte[] b) throws IOException, ClassNotFoundException { + ByteArrayInputStream stream = new ByteArrayInputStream(b); + return readExternalFormFromStream(stream); + } + + protected boolean skipSerializedCanonicalTests() { + return Boolean.getBoolean("org.apache.commons.collections:with-clover"); + } + + // private implementation + //----------------------------------------------------------------------- + private Object readExternalFormFromStream(InputStream stream) throws IOException, ClassNotFoundException { + ObjectInputStream oStream = new ObjectInputStream(stream); + return oStream.readObject(); + } + + private void writeExternalFormToStream(Serializable o, OutputStream stream) throws IOException { + ObjectOutputStream oStream = new ObjectOutputStream(stream); + oStream.writeObject(o); + } + +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/SetAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/SetAbstractTestCase.java new file mode 100755 index 00000000..5bdcc797 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/SetAbstractTestCase.java @@ -0,0 +1,183 @@ +/* + * Copyright 2001-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twelvemonkeys.util; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +/** + * Abstract test class for {@link Set} methods and contracts. + *

+ * Since {@link Set} doesn't stipulate much new behavior that isn't already + * found in {@link Collection}, this class basically just adds tests for + * {@link Set#equals} and {@link Set#hashCode()} along with an updated + * {@link #verifyAll()} that ensures elements do not appear more than once in the + * set. + *

+ * To use, subclass and override the {@link #makeEmptySet()} + * method. You may have to override other protected methods if your + * set is not modifiable, or if your set restricts what kinds of + * elements may be added; see {@link CollectionAbstractTestCase} for more details. + * + * @since Commons Collections 3.0 + * @version $Revision: #2 $ $Date: 2008/07/15 $ + * + * @author Paul Jack + */ +public abstract class SetAbstractTestCase extends CollectionAbstractTestCase { + + //----------------------------------------------------------------------- + /** + * Provides additional verifications for sets. + */ + public void verifyAll() { + super.verifyAll(); + + assertEquals("Sets should be equal", confirmed, collection); + assertEquals("Sets should have equal hashCodes", + confirmed.hashCode(), collection.hashCode()); + Collection set = makeConfirmedCollection(); + Iterator iterator = collection.iterator(); + while (iterator.hasNext()) { + assertTrue("Set.iterator should only return unique elements", + set.add(iterator.next())); + } + } + + //----------------------------------------------------------------------- + /** + * Set equals method is defined. + */ + public boolean isEqualsCheckable() { + return true; + } + + /** + * Returns an empty Set for use in modification testing. + * + * @return a confirmed empty collection + */ + public Collection makeConfirmedCollection() { + return new HashSet(); + } + + /** + * Returns a full Set for use in modification testing. + * + * @return a confirmed full collection + */ + public Collection makeConfirmedFullCollection() { + Collection set = makeConfirmedCollection(); + set.addAll(Arrays.asList(getFullElements())); + return set; + } + + /** + * Makes an empty set. The returned set should have no elements. + * + * @return an empty set + */ + public abstract Set makeEmptySet(); + + /** + * Makes a full set by first creating an empty set and then adding + * all the elements returned by {@link #getFullElements()}. + * + * Override if your set does not support the add operation. + * + * @return a full set + */ + public Set makeFullSet() { + Set set = makeEmptySet(); + set.addAll(Arrays.asList(getFullElements())); + return set; + } + + /** + * Makes an empty collection by invoking {@link #makeEmptySet()}. + * + * @return an empty collection + */ + public final Collection makeCollection() { + return makeEmptySet(); + } + + /** + * Makes a full collection by invoking {@link #makeFullSet()}. + * + * @return a full collection + */ + public final Collection makeFullCollection() { + return makeFullSet(); + } + + //----------------------------------------------------------------------- + /** + * Return the {@link CollectionAbstractTestCase#collection} fixture, but cast as a Set. + */ + public Set getSet() { + return (Set)collection; + } + + /** + * Return the {@link CollectionAbstractTestCase#confirmed} fixture, but cast as a Set. + */ + public Set getConfirmedSet() { + return (Set)confirmed; + } + + //----------------------------------------------------------------------- + /** + * Tests {@link Set#equals(Object)}. + */ + public void testSetEquals() { + resetEmpty(); + assertEquals("Empty sets should be equal", + getSet(), getConfirmedSet()); + verifyAll(); + + Collection set2 = makeConfirmedCollection(); + set2.add("foo"); + assertTrue("Empty set shouldn't equal nonempty set", + !getSet().equals(set2)); + + resetFull(); + assertEquals("Full sets should be equal", getSet(), getConfirmedSet()); + verifyAll(); + + set2.clear(); + set2.addAll(Arrays.asList(getOtherElements())); + assertTrue("Sets with different contents shouldn't be equal", + !getSet().equals(set2)); + } + + /** + * Tests {@link Set#hashCode()}. + */ + public void testSetHashCode() { + resetEmpty(); + assertEquals("Empty sets have equal hashCodes", + getSet().hashCode(), getConfirmedSet().hashCode()); + + resetFull(); + assertEquals("Equal sets have equal hashCodes", + getSet().hashCode(), getConfirmedSet().hashCode()); + } + +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/StringTokenIteratorTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/StringTokenIteratorTestCase.java new file mode 100755 index 00000000..0895e965 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/StringTokenIteratorTestCase.java @@ -0,0 +1,111 @@ +package com.twelvemonkeys.util; + + +import java.util.Iterator; + +/** + * StringTokenIteratorTestCase + *

+ * + * + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/StringTokenIteratorTestCase.java#1 $ + */ +public class StringTokenIteratorTestCase extends TokenIteratorAbstractTestCase { + public void setUp() throws Exception { + super.setUp(); + } + + public void tearDown() throws Exception { + super.tearDown(); + } + + protected TokenIterator createTokenIterator(String pString) { + return new StringTokenIterator(pString); + } + + protected TokenIterator createTokenIterator(String pString, String pDelimiters) { + return new StringTokenIterator(pString, pDelimiters); + } + + public void testEmptyDelimiter() { + Iterator iterator = createTokenIterator("", ""); + assertFalse("Empty string has elements", iterator.hasNext()); + } + + public void testSingleToken() { + Iterator iterator = createTokenIterator("A"); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testSingleTokenEmptyDelimiter() { + Iterator iterator = createTokenIterator("A", ""); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testSingleTokenSingleDelimiter() { + Iterator iterator = createTokenIterator("A", ","); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testSingleSeparatorDefaultDelimiter() { + Iterator iterator = createTokenIterator("A B C D"); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("B", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("C", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("D", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testSingleSeparator() { + Iterator iterator = createTokenIterator("A,B,C", ","); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("B", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("C", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testMultipleSeparatorDefaultDelimiter() { + Iterator iterator = createTokenIterator("A B C\nD\t\t \nE"); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("B", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("C", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("D", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("E", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testMultipleSeparator() { + Iterator iterator = createTokenIterator("A,B,;,C...D, ., ,E", " ,.;:"); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("B", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("C", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("D", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("E", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TimeoutMapTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TimeoutMapTestCase.java new file mode 100755 index 00000000..8cfb9ab1 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TimeoutMapTestCase.java @@ -0,0 +1,632 @@ +/**************************************************** + * * + * (c) 2000-2003 WM-data * + * All rights reserved * + * http://www.twelvemonkeys.no * + * * + * $RCSfile: TimeoutMapTestCase.java,v $ + * @version $Revision: #2 $ + * $Date: 2008/07/15 $ + * * + * @author Last modified by: $Author: haku $ + * * + ****************************************************/ + + +package com.twelvemonkeys.util; + + +import junit.framework.Test; +import junit.framework.TestSuite; + +import java.util.*; + + +/** + * TimeoutMapTestCase + *

+ * + * + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TimeoutMapTestCase.java#2 $ + */ +public class TimeoutMapTestCase extends MapAbstractTestCase { + + /** + * Method suite + * + * @return + */ + public static Test suite() { + return new TestSuite(TimeoutMapTestCase.class); + } + + + public Map makeEmptyMap() { + return new TimeoutMap(60 * 60 * 1000); + } + /* + * The basic Map interface lets one associate keys and values: + */ + + /** + * Method testBasicMap + */ + public void testBasicMap() { + + Map map = new TimeoutMap(60000L); + Object key = "key"; + Object value = new Integer(3); + + map.put(key, value); + assertEquals(value, map.get(key)); + } + + /* + * If there is no value associated with a key, + * the basic Map will return null for that key: + */ + + /** + * Method testBasicMapReturnsNullForMissingKey + */ + public void testBasicMapReturnsNullForMissingKey() { + + Map map = new TimeoutMap(60000L); + + assertNull(map.get("key")); + } + + /* + * One can also explicitly store a null value for + * some key: + */ + + /** + * Method testBasicMapAllowsNull + */ + public void testBasicMapAllowsNull() { + + Map map = new TimeoutMap(60000L); + Object key = "key"; + Object value = null; + + map.put(key, value); + assertNull(map.get(key)); + } + + /** + * Method testBasicMapAllowsMultipleTypes + */ + public void testBasicMapAllowsMultipleTypes() { + + Map map = new TimeoutMap(60000L); + + map.put("key-1", "value-1"); + map.put(new Integer(2), "value-2"); + map.put("key-3", new Integer(3)); + map.put(new Integer(4), new Integer(4)); + map.put(Boolean.FALSE, ""); + assertEquals("value-1", map.get("key-1")); + assertEquals("value-2", map.get(new Integer(2))); + assertEquals(new Integer(3), map.get("key-3")); + assertEquals(new Integer(4), map.get(new Integer(4))); + assertEquals("", map.get(Boolean.FALSE)); + } + + /** + * Method testBasicMapStoresOnlyOneValuePerKey + */ + public void testBasicMapStoresOnlyOneValuePerKey() { + + Map map = new TimeoutMap(60000L); + + assertNull(map.put("key", "value-1")); + assertEquals("value-1", map.get("key")); + assertEquals("value-1", map.put("key", "value-2")); + assertEquals("value-2", map.get("key")); + } + + /** + * Method testBasicMapValuesView + */ + public void testBasicMapValuesView() { + + Map map = new TimeoutMap(60000L); + + assertNull(map.put("key-1", new Integer(1))); + assertNull(map.put("key-2", new Integer(2))); + assertNull(map.put("key-3", new Integer(3))); + assertNull(map.put("key-4", new Integer(4))); + assertEquals(4, map.size()); + + Collection values = map.values(); + assertEquals(4, values.size()); + + Iterator it = values.iterator(); + + assertNotNull(it); + int count = 0; + + while (it.hasNext()) { + Object o = it.next(); + + assertNotNull(o); + assertTrue(o instanceof Integer); + count++; + } + assertEquals(4, count); + } + + /** + * Method testBasicMapKeySetView + */ + public void testBasicMapKeySetView() { + + Map map = new TimeoutMap(60000L); + + assertNull(map.put("key-1", "value-1")); + assertNull(map.put("key-2", "value-2")); + assertNull(map.put("key-3", "value-3")); + assertNull(map.put("key-4", "value-4")); + assertEquals(4, map.size()); + Iterator it = map.keySet().iterator(); + + assertNotNull(it); + int count = 0; + + while (it.hasNext()) { + Object o = it.next(); + + assertNotNull(o); + assertTrue(o instanceof String); + count++; + } + assertEquals(4, count); + } + + /** + * Method testBasicMapEntrySetView + */ + public void testBasicMapEntrySetView() { + + Map map = new TimeoutMap(60000L); + + assertNull(map.put("key-1", new Integer(1))); + assertNull(map.put("key-2", "value-2")); + assertNull(map.put("key-3", new Object())); + assertNull(map.put("key-4", Boolean.FALSE)); + assertEquals(4, map.size()); + Iterator it = map.entrySet().iterator(); + + assertNotNull(it); + int count = 0; + + while (it.hasNext()) { + Object o = it.next(); + + assertNotNull(o); + assertTrue(o instanceof Map.Entry); + count++; + } + assertEquals(4, count); + } + + /** + * Method testBasicMapValuesView + */ + public void testBasicMapValuesViewRemoval() { + + Map map = new TimeoutMap(60000L); + + assertNull(map.put("key-1", new Integer(1))); + assertNull(map.put("key-2", new Integer(2))); + assertNull(map.put("key-3", new Integer(3))); + assertNull(map.put("key-4", new Integer(4))); + assertEquals(4, map.size()); + Iterator it = map.values().iterator(); + + assertNotNull(it); + int count = 0; + + while (it.hasNext()) { + Object o = it.next(); + + assertNotNull(o); + assertTrue(o instanceof Integer); + try { + it.remove(); + } + catch (UnsupportedOperationException e) { + fail("Removal failed"); + } + count++; + } + assertEquals(4, count); + } + + /** + * Method testBasicMapKeySetView + */ + public void testBasicMapKeySetViewRemoval() { + + Map map = new TimeoutMap(60000L); + + assertNull(map.put("key-1", "value-1")); + assertNull(map.put("key-2", "value-2")); + assertNull(map.put("key-3", "value-3")); + assertNull(map.put("key-4", "value-4")); + assertEquals(4, map.size()); + Iterator it = map.keySet().iterator(); + + assertNotNull(it); + int count = 0; + + while (it.hasNext()) { + Object o = it.next(); + + assertNotNull(o); + assertTrue(o instanceof String); + try { + it.remove(); + } + catch (UnsupportedOperationException e) { + fail("Removal failed"); + } + count++; + } + assertEquals(4, count); + } + + /** + * Method testBasicMapEntrySetView + */ + public void testBasicMapEntrySetViewRemoval() { + + Map map = new TimeoutMap(60000L); + + assertNull(map.put("key-1", new Integer(1))); + assertNull(map.put("key-2", "value-2")); + assertNull(map.put("key-3", new Object())); + assertNull(map.put("key-4", Boolean.FALSE)); + assertEquals(4, map.size()); + Iterator it = map.entrySet().iterator(); + + assertNotNull(it); + int count = 0; + + while (it.hasNext()) { + Object o = it.next(); + + assertNotNull(o); + assertTrue(o instanceof Map.Entry); + try { + it.remove(); + } + catch (UnsupportedOperationException e) { + fail("Removal failed"); + } + count++; + } + assertEquals(4, count); + } + + /** + * Method testBasicMapStoresOnlyOneValuePerKey + */ + public void testTimeoutReturnNull() { + + Map map = new TimeoutMap(100L); + + assertNull(map.put("key", "value-1")); + assertEquals("value-1", map.get("key")); + assertNull(map.put("another", "value-2")); + assertEquals("value-2", map.get("another")); + synchronized (this) { + try { + Thread.sleep(110L); + } + catch (InterruptedException e) { + // Continue, but might break the timeout thing below... + } + } + + // Values should now time out + assertNull(map.get("key")); + assertNull(map.get("another")); + } + + /** + * Method testTimeoutIsEmpty + */ + public void testTimeoutIsEmpty() { + + TimeoutMap map = new TimeoutMap(50L); + + assertNull(map.put("key", "value-1")); + assertEquals("value-1", map.get("key")); + assertNull(map.put("another", "value-2")); + assertEquals("value-2", map.get("another")); + synchronized (this) { + try { + Thread.sleep(100L); + } + catch (InterruptedException e) { + // Continue, but might break the timeout thing below... + } + } + + // This for loop should not print anything, if the tests succeed. + Set set = map.keySet(); + assertEquals(0, set.size()); + for (Iterator iterator = set.iterator(); iterator.hasNext(); System.out.println(iterator.next())) { + ; + } + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); + } + + /** + * Method testTimeoutWrapIsEmpty + */ + public void testTimeoutWrapIsEmpty() { + + Map map = new TimeoutMap(new LRUMap(2), null, 100L); + + assertNull(map.put("key", "value-1")); + assertEquals("value-1", map.get("key")); + assertNull(map.put("another", "value-2")); + assertEquals("value-2", map.get("another")); + assertNull(map.put("third", "value-3")); + assertEquals("value-3", map.get("third")); + synchronized (this) { + try { + Thread.sleep(110L); + } + catch (InterruptedException e) { + // Continue, but might break the timeout thing below... + } + } + + // This for loop should not print anything, if the tests succeed. + Set set = map.keySet(); + assertEquals(0, set.size()); + for (Iterator iterator = set.iterator(); iterator.hasNext(); System.out.println(iterator.next())) { + ; + } + assertEquals(0, map.size()); + assertTrue(map.isEmpty()); + } + + /** + * Method testTimeoutWrapReturnNull + */ + public void testTimeoutWrapReturnNull() { + + Map map = new TimeoutMap(new LRUMap(), null, 100L); + + assertNull(map.put("key", "value-1")); + assertEquals("value-1", map.get("key")); + assertNull(map.put("another", "value-2")); + assertEquals("value-2", map.get("another")); + synchronized (this) { + try { + Thread.sleep(110L); + } + catch (InterruptedException e) { + // Continue, but might break the timeout thing below... + } + } + + // Values should now time out + assertNull(map.get("key")); + assertNull(map.get("another")); + } + + /** + * Method testWrapMaxSize + */ + public void testWrapMaxSize() { + + LRUMap lru = new LRUMap(); + + lru.setMaxSize(2); + TimeoutMap map = new TimeoutMap(lru, null, 1000L); + + assertNull(map.put("key", "value-1")); + assertEquals("value-1", map.get("key")); + assertNull(map.put("another", "value-2")); + assertEquals("value-2", map.get("another")); + assertNull(map.put("third", "value-3")); + assertEquals("value-3", map.get("third")); + + // This value should have expired + assertNull(map.get("key")); + + // These should be left + assertEquals("value-2", map.get("another")); + assertEquals("value-3", map.get("third")); + } + + /** + * Method testWrapMapContainingValues + */ + public void testWrapMapContainingValues() { + + Map backing = new TreeMap(); + + backing.put("key", "original"); + TimeoutMap map = null; + + try { + map = new TimeoutMap(backing, backing, 1000L); + Object value = map.put("key", "value-1"); + assertNotNull(value); // Should now have value! + assertEquals("original", value); + } + catch (ClassCastException cce) { + cce.printStackTrace(); + fail("Content not converted to TimedEntries properly!"); + } + assertEquals("value-1", map.get("key")); + assertNull(map.put("another", "value-2")); + assertEquals("value-2", map.get("another")); + assertNull(map.put("third", "value-3")); + assertEquals("value-3", map.get("third")); + } + + public void testIteratorRemove() { + Map map = makeFullMap(); + + for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) { + iterator.remove(); + } + assertEquals(0, map.size()); + + map = makeFullMap(); + + for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) { + iterator.next(); + iterator.remove(); + } + assertEquals(0, map.size()); + } + + public void testIteratorPredictableNext() { + TimeoutMap map = (TimeoutMap) makeFullMap(); + map.setExpiryTime(50l); + assertFalse(map.isEmpty()); + + int count = 0; + for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) { + if (count == 0) { + // NOTE: Only wait fist time, to avoid slooow tests + synchronized (this) { + try { + wait(60l); + } + catch (InterruptedException e) { + } + } + } + + try { + Map.Entry entry = (Map.Entry) iterator.next(); + assertNotNull(entry); + count++; + } + catch (NoSuchElementException nse) { + fail("Elements expire between Interator.hasNext() and Iterator.next()"); + } + } + + assertTrue("Elements expired too early, test did not run as expected.", count > 0); + //assertEquals("Elements did not expire as expected.", 1, count); + } + + public void testIteratorPredictableRemove() { + TimeoutMap map = (TimeoutMap) makeFullMap(); + map.setExpiryTime(50l); + assertFalse(map.isEmpty()); + + int count = 0; + for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) { + if (count == 0) { + // NOTE: Only wait fist time, to avoid slooow tests + synchronized (this) { + try { + wait(60l); + } + catch (InterruptedException e) { + } + } + } + + try { + iterator.remove(); + count++; + } + catch (NoSuchElementException nse) { + fail("Elements expired between Interator.hasNext() and Iterator.remove()"); + } + } + + assertTrue("Elements expired too early, test did not run as expected.", count > 0); + //assertEquals("Elements did not expire as expected.", 1, count); + } + + public void testIteratorPredictableNextRemove() { + TimeoutMap map = (TimeoutMap) makeFullMap(); + map.setExpiryTime(50l); + assertFalse(map.isEmpty()); + + int count = 0; + for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) { + if (count == 0) { + // NOTE: Only wait fist time, to avoid slooow tests + synchronized (this) { + try { + wait(60l); + } + catch (InterruptedException e) { + } + } + } + + try { + Map.Entry entry = (Map.Entry) iterator.next(); + assertNotNull(entry); + } + catch (NoSuchElementException nse) { + fail("Elements expired between Interator.hasNext() and Iterator.next()"); + } + + try { + iterator.remove(); + count++; + } + catch (NoSuchElementException nse) { + fail("Elements expired between Interator.hasNext() and Iterator.remove()"); + } + } + + assertTrue("Elements expired too early, test did not run as expected.", count > 0); + //assertEquals("Elements did not expire as expected.", 1, count); + } + + public void testIteratorPredictableRemovedEntry() { + TimeoutMap map = (TimeoutMap) makeEmptyMap(); + map.setExpiryTime(1000l); // No elements should expire during this test + + map.put("key-1", new Integer(1)); + map.put("key-2", new Integer(2)); + + assertFalse(map.isEmpty()); + + Object removedKey = null; + Object otherKey = null; + Iterator iterator = map.entrySet().iterator(); + assertTrue("Iterator was empty", iterator.hasNext()); + try { + Map.Entry entry = (Map.Entry) iterator.next(); + assertNotNull(entry); + removedKey = entry.getKey(); + otherKey = "key-1".equals(removedKey) ? "key-2" : "key-1"; + } + catch (NoSuchElementException nse) { + fail("Elements expired between Interator.hasNext() and Iterator.next()"); + } + + try { + iterator.remove(); + } + catch (NoSuchElementException nse) { + fail("Elements expired between Interator.hasNext() and Iterator.remove()"); + } + + assertTrue("Wrong entry removed, keySet().iterator() is broken.", !map.containsKey(removedKey)); + assertTrue("Wrong entry removed, keySet().iterator() is broken.", map.containsKey(otherKey)); + } +} + diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TokenIteratorAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TokenIteratorAbstractTestCase.java new file mode 100755 index 00000000..acef4f81 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TokenIteratorAbstractTestCase.java @@ -0,0 +1,52 @@ +package com.twelvemonkeys.util; + +import junit.framework.TestCase; + +import java.util.Iterator; + +/** + * TokenIteratorAbstractTestCase + *

+ * + * + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/TokenIteratorAbstractTestCase.java#1 $ + */ +public abstract class TokenIteratorAbstractTestCase extends TestCase { + protected abstract TokenIterator createTokenIterator(String pString); + + protected abstract TokenIterator createTokenIterator(String pString, String pDelimiters); + + public void testNullString() { + try { + createTokenIterator(null); + fail("Null string parameter not allowed"); + } + catch (IllegalArgumentException e) { + // okay! + } + catch (Throwable t) { + fail(t.getMessage()); + } + } + + public void testNullDelimmiter() { + try { + createTokenIterator("", null); + fail("Null delimiter parameter not allowed"); + } + catch (IllegalArgumentException e) { + // okay! + } + catch (Throwable t) { + fail(t.getMessage()); + } + } + + public void testEmptyString() { + Iterator iterator = createTokenIterator(""); + assertFalse("Empty string has elements", iterator.hasNext()); + } + +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/ConverterTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/ConverterTestCase.java new file mode 100755 index 00000000..d974da3f --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/ConverterTestCase.java @@ -0,0 +1,18 @@ +package com.twelvemonkeys.util.convert; + +import junit.framework.TestCase; + +/** + * ConverterTestCase + *

+ * + * @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/convert/ConverterTestCase.java#1 $ + */ +public class ConverterTestCase extends TestCase { + + public void testMe() { + // TODO: Implement tests + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/DateConverterTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/DateConverterTestCase.java new file mode 100755 index 00000000..006acde8 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/DateConverterTestCase.java @@ -0,0 +1,58 @@ +package com.twelvemonkeys.util.convert; + +import com.twelvemonkeys.lang.DateUtil; + +import java.text.DateFormat; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * DateConverterTestCase + *

+ * + * @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/convert/DateConverterTestCase.java#2 $ + */ +public class DateConverterTestCase extends PropertyConverterAbstractTestCase { + protected final static String FORMAT_STR_1 = "dd.MM.yyyy HH:mm:ss"; + protected final static String FORMAT_STR_2 = "dd-MM-yyyy hh:mm:ss a"; + + protected PropertyConverter makePropertyConverter() { + return new DateConverter(); + } + + protected Conversion[] getTestConversions() { + // The default format doesn't contain milliseconds, so we have to round + long time = System.currentTimeMillis(); + final Date now = new Date(DateUtil.roundToSecond(time)); + DateFormat df = DateFormat.getDateTimeInstance(); + + return new Conversion[] { + new Conversion("01.11.2006 15:26:23", new GregorianCalendar(2006, 10, 1, 15, 26, 23).getTime(), FORMAT_STR_1), + + // This doesn't really work.. But close enough + new Conversion(df.format(now), now), + + // This format is really stupid + new Conversion("01-11-2006 03:27:44 pm", new GregorianCalendar(2006, 10, 1, 15, 27, 44).getTime(), FORMAT_STR_2, "01-11-2006 03:27:44 PM"), + + // These seems to be an hour off (no timezone?)... + new Conversion("42", new Date(42l), "S"), + new Conversion(String.valueOf(time % 1000l), new Date(time % 1000l), "S"), + }; + } + + @Override + public void testConvert() { + TimeZone old = TimeZone.getDefault(); + try { + TimeZone.setDefault(TimeZone.getTimeZone("GMT")); + super.testConvert(); + } + finally { + TimeZone.setDefault(old); + } + } +} \ No newline at end of file diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java new file mode 100755 index 00000000..e5ff4226 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/DefaultConverterTestCase.java @@ -0,0 +1,71 @@ +package com.twelvemonkeys.util.convert; + +/** + * DefaultConverterTestCase + *

+ * + * @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/convert/DefaultConverterTestCase.java#1 $ + */ +public class DefaultConverterTestCase extends PropertyConverterAbstractTestCase { + protected PropertyConverter makePropertyConverter() { + return new DefaultConverter(); + } + + protected Conversion[] getTestConversions() { + //noinspection BooleanConstructorCall + return new Conversion[] { + // Booleans + new Conversion("true", Boolean.TRUE), + new Conversion("TRUE", Boolean.TRUE, null, "true"), + new Conversion("false", Boolean.FALSE), + new Conversion("FALSE", new Boolean(false), null, "false"), + + // Stupid but valid + new Conversion("fooBar", "fooBar"), + //new Conversion("fooBar", new StringBuilder("fooBar")), - StringBuilder does not impl equals()... + + // Stupid test class that reveres chars + new Conversion("fooBar", new FooBar("fooBar")), + + // TODO: More tests + }; + } + + // TODO: Test boolean -> Boolean conversion + + public static class FooBar { + private final String mBar; + + public FooBar(String pFoo) { + if (pFoo == null) { + throw new IllegalArgumentException("pFoo == null"); + } + mBar = reverse(pFoo); + } + + private String reverse(String pFoo) { + StringBuilder buffer = new StringBuilder(pFoo.length()); + + for (int i = pFoo.length() - 1; i >= 0; i--) { + buffer.append(pFoo.charAt(i)); + } + + return buffer.toString(); + } + + public String toString() { + return reverse(mBar); + } + + public boolean equals(Object obj) { + return obj == this || (obj instanceof FooBar && ((FooBar) obj).mBar.equals(mBar)); + } + + public int hashCode() { + return 7 * mBar.hashCode(); + } + } + +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/NumberConverterTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/NumberConverterTestCase.java new file mode 100755 index 00000000..97ddcb44 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/NumberConverterTestCase.java @@ -0,0 +1,42 @@ +package com.twelvemonkeys.util.convert; + +/** + * NumberConverterTestCase + *

+ * + * @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/convert/NumberConverterTestCase.java#2 $ + */ +public class NumberConverterTestCase extends PropertyConverterAbstractTestCase { + protected PropertyConverter makePropertyConverter() { + return new NumberConverter(); + } + + protected Conversion[] getTestConversions() { + return new Conversion[] { + new Conversion("0", 0), + new Conversion("1", 1), + new Conversion("-1001", -1001), + new Conversion("1E3", 1000, null, "1000"), + + new Conversion("-2", -2l), + new Conversion("2000651651854", 2000651651854l), + new Conversion("2E10", 20000000000l, null, "20000000000"), + + new Conversion("3", 3.0f), + new Conversion("3.1", 3.1f), + new Conversion("3.2", 3.2f, "#.#"), + //new Conversion("3,3", new Float(3), "#", "3"), // Seems to need parseIntegerOnly + new Conversion("-3.4", -3.4f), + new Conversion("-3.5E10", -3.5e10f, null, "-35000000512"), + + new Conversion("4", 4.0), + new Conversion("4.1", 4.1), + new Conversion("4.2", 4.2, "#.#"), + //new Conversion("4,3", new Double(4), "#", "4"), // Seems to need parseIntegerOnly + new Conversion("-4.4", -4.4), + new Conversion("-4.5E97", -4.5e97, null, "-45000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + }; + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/PropertyConverterAbstractTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/PropertyConverterAbstractTestCase.java new file mode 100755 index 00000000..2f557027 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/PropertyConverterAbstractTestCase.java @@ -0,0 +1,99 @@ +package com.twelvemonkeys.util.convert; + +import com.twelvemonkeys.lang.ObjectAbstractTestCase; + +/** + * PropertyConverterAbstractTestCase + *

+ * + * @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/convert/PropertyConverterAbstractTestCase.java#2 $ + */ +public abstract class PropertyConverterAbstractTestCase extends ObjectAbstractTestCase { + + protected Object makeObject() { + return makePropertyConverter(); + } + + protected abstract PropertyConverter makePropertyConverter(); + + protected abstract Conversion[] getTestConversions(); + + public void testConvert() { + + PropertyConverter converter = makePropertyConverter(); + + Conversion[] tests = getTestConversions(); + + for (Conversion test : tests) { + Object obj; + try { + obj = converter.toObject(test.original(), test.type(), test.format()); + assertEquals("'" + test.original() + "' convtered to incorrect type", test.type(), obj.getClass()); + assertEquals("'" + test.original() + "' not converted", test.value(), obj); + + String result = converter.toString(test.value(), test.format()); + + assertEquals("'" + test.converted() + "' does not macth", test.converted(), result); + + obj = converter.toObject(result, test.type(), test.format()); + assertEquals("'" + test.original() + "' convtered to incorrect type", test.type(), obj.getClass()); + assertEquals("'" + test.original() + "' did not survive roundrip conversion", test.value(), obj); + } + catch (ConversionException e) { + e.printStackTrace(); + fail("Converting '" + test.original() + "' to " + test.type() + " failed: " + e.getMessage()); + } + } + } + + public static final class Conversion { + private final String mStrVal; + private final Object mObjVal; + private final String mFormat; + private final String mConvertedStrVal; + + public Conversion(String pStrVal, Object pObjVal) { + this(pStrVal, pObjVal, null, null); + } + + public Conversion(String pStrVal, Object pObjVal, String pFormat) { + this(pStrVal, pObjVal, pFormat, null); + } + + public Conversion(String pStrVal, Object pObjVal, String pFormat, String pConvertedStrVal) { + if (pStrVal == null) { + throw new IllegalArgumentException("pStrVal == null"); + } + if (pObjVal == null) { + throw new IllegalArgumentException("pObjVal == null"); + } + + mStrVal = pStrVal; + mObjVal = pObjVal; + mFormat = pFormat; + mConvertedStrVal = pConvertedStrVal != null ? pConvertedStrVal : pStrVal; + } + + public String original() { + return mStrVal; + } + + public Object value() { + return mObjVal; + } + + public Class type() { + return mObjVal.getClass(); + } + + public String format() { + return mFormat; + } + + public String converted() { + return mConvertedStrVal; + } + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/TimeConverterTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/TimeConverterTestCase.java new file mode 100755 index 00000000..f71898b6 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/convert/TimeConverterTestCase.java @@ -0,0 +1,19 @@ +package com.twelvemonkeys.util.convert; + +/** + * TimeConverterTestCase + *

+ * + * @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/convert/TimeConverterTestCase.java#1 $ + */ +public class TimeConverterTestCase extends PropertyConverterAbstractTestCase { + protected PropertyConverter makePropertyConverter() { + return new TimeConverter(); + } + + protected Conversion[] getTestConversions() { + return new Conversion[0];// TODO: Implement + } +} diff --git a/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/regex/RegExTokenIteratorTestCase.java b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/regex/RegExTokenIteratorTestCase.java new file mode 100755 index 00000000..69517f60 --- /dev/null +++ b/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/regex/RegExTokenIteratorTestCase.java @@ -0,0 +1,122 @@ +package com.twelvemonkeys.util.regex; + +import com.twelvemonkeys.util.TokenIterator; +import com.twelvemonkeys.util.TokenIteratorAbstractTestCase; + +import java.util.Iterator; + +/** + * StringTokenIteratorTestCase + *

+ * + * + * + * @author Harald Kuhr + * @version $Id: //depot/branches/personal/haraldk/twelvemonkeys/release-2/twelvemonkeys-core/src/test/java/com/twelvemonkeys/util/regex/RegExTokenIteratorTestCase.java#1 $ + */ +public class RegExTokenIteratorTestCase extends TokenIteratorAbstractTestCase { + public void setUp() throws Exception { + super.setUp(); + } + + public void tearDown() throws Exception { + super.tearDown(); + } + + protected TokenIterator createTokenIterator(String pString) { + return new RegExTokenIterator(pString); + } + + protected TokenIterator createTokenIterator(String pString, String pDelimiters) { + return new RegExTokenIterator(pString, pDelimiters); + } + + public void testEmptyDelimiter() { + // TODO: What is it supposed to match? + /* + Iterator iterator = createTokenIterator("", ".*"); + assertTrue("Empty string has no elements", iterator.hasNext()); + iterator.next(); + assertFalse("Empty string has more then one element", iterator.hasNext()); + */ + } + + public void testSingleToken() { + Iterator iterator = createTokenIterator("A"); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testSingleTokenEmptyDelimiter() { + // TODO: What is it supposed to match? + /* + Iterator iterator = createTokenIterator("A", ".*"); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + */ + } + + public void testSingleTokenSingleDelimiter() { + Iterator iterator = createTokenIterator("A", "[^,]+"); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testSingleSeparatorDefaultDelimiter() { + Iterator iterator = createTokenIterator("A B C D"); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("B", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("C", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("D", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testSingleSeparator() { + Iterator iterator = createTokenIterator("A,B,C", "[^,]+"); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("B", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("C", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testMultipleSeparatorDefaultDelimiter() { + Iterator iterator = createTokenIterator("A B C\nD\t\t \nE"); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("A", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("B", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("C", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("D", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("E", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } + + public void testMultipleSeparator() { + Iterator iterator = createTokenIterator("A,B,;,C...D, ., ,E", "[^ ,.;:]+"); + assertTrue("String has no elements", iterator.hasNext()); + Object o = iterator.next(); + assertEquals("A", o); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("B", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("C", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("D", iterator.next()); + assertTrue("String has no elements", iterator.hasNext()); + assertEquals("E", iterator.next()); + assertFalse("String has more than one element", iterator.hasNext()); + } +} diff --git a/twelvemonkeys-core/src/test/resources/Thumbs-camera.db b/twelvemonkeys-core/src/test/resources/Thumbs-camera.db new file mode 100755 index 0000000000000000000000000000000000000000..1adedbb882a3f42a976a57d4756c126241c142f9 GIT binary patch literal 79360 zcmeF(2UHZ@x*+UEKtQsRkswi$NY0=lNdy$hQ9wi_=L{lABxecIl0ia~bCMhc1SIDy zAfbs3G~NH^yZ5_u@0t70e0R>wnssKKy?E$UT~)iPD0aW^Tm55*bTOlhW(o2)O&Wp; zL80*=*Z30Jn_5 z9e@d723P=AfDO0{hyxq|C%^@813UmPAP3wB_yB%D01yO(0AWA`5Cy~l2|yW;1f&3I zKn8dKJOmyAvcO|N9xwnD07XCvcmg~H3;`8D6;K170nY(-;02%oXaZV*HlPFO0(yWx zU<5b=Hh?kk5-;IGW#dJ??|A$cONBoDy= zGj+c@fY38w6JcJvcJ%`q7Wl!&#m2_M!oGona~+r91_1&74Sak;qMIayM5ILc_#_l0 zq_@b($;kaO0~5Rl3mXp`8;^_-pOEao+|aEMQe5yi^AZz-3381T z1Cta3-2q_$tB(!V_G*RywP9QX>v$ap7w-l>n4k*mAq-5+Ygm|9YYnFM0k1=_NU?9; z;d^-f)^igaCI>Qp|CkJ1=0|1U$km7USp-ZS-{ak&proRvVP#{#%fTrqBrGB-CNBF} zPF_J#>B$QXO)YI5T|Ki`=C3W@SiW^~c5!uc_wWn|3$7i zZeD)D*Yb+Ws_L5By85>Ej?S*`p5DHZ(Vt`E6O&WZOUo;(YwH`oHn-phheyXJr--xj ztA1fXF#n}n|JJj==@%*J*EK9GOe~zMeqmg5129Rkuz!Aw`8?W5>Z_XN|B6XMuXQ_Ukj#5vj6(IPxAG{20d7xnaarDuhJ;VZ zW==nlv|QmD;!yeVllj9F`6n6Wl`O;$6tPxHu6q(V8(E?uOf7WZDxCu z>M$PXXld#R`OR9GhnP7~Tt-8dG9+ct5a|)W#nGo|$ZEaJ-#)P;Il6PEewO#sS9AEX zDaCK-7!7&Hi&8YG^*cL7RD483f&=%_5afjt8WNx)fQH2W`tP6Gt~yl9N9OpV!gE-Wl6@8>2Y;Q+XRpI-Zu+^F&SeS21oFhc+xXF-dT> zxv4fAi_CmF6? z(;qFNU4b!hEr@m7148xX!Oig>NUG;aPE^Q?t_X@9nn{vo4`GC7>H4#ADAv4 zclGV{hgsU}T*h3de6zX=_aE?eLg)&8*3uu~(j4J&wi1n@5i8=!sj;N~>?k5YB9GSt z%j?;E?xs{%`J=HQ(W{ol zBTtuF!+WNc#jacRea-c%0^@0=jqb%up)Vgf@*djUjip+>;U+TFdbwezcODX)dHl@S zQFQE0``qi#Xh`=EbuV`;ZDAgmdv;pfR1K$ycQAzIh;u6RtT{ zHQ!)DR%Wnu$}j)Cgc(~`d9i@-J?<*aN|lQ3GA(Uaupjo@TlyUqB<(i5$xLRfA$xqA zhJL>Tc}*Z8(s(c9;;gt;~LcLMQ4AsCo1nfA$?pF!xjKJ4dp4TWlr z&Y~f2-B-FQc0~5eq=568D!xYj zy^cO@Tf?wYq#HbtNot-Op20!i%Aj8_5xVPZt5%n1fb@l@c0-JBA)34cCy7tye(;PY zwj;Y^CdTWc2;KV)$G_lv<+>Zo#gwd;$~BkAD;}_kV)_|*oofX(SDwH*2TI(MCR?>9?hT^SW1YrZ6n$y7 zUTo8oAXt4|1Zya@wW<&Mg*t+jSX@VPxxaTOnkuoRE5QbMaaEQXC=gN4N356ke^XNM zW51^Hx-T}wb$E0p^V~QGO2nK(On{^eUgg5Ze!pzat~y&%9r z>+-ia?|SG&Q2tl3oDtpwbGxt@pR86igw}+}j9<_2u8SzwXEuzq6WBmlgt#HoU0cC^ ziSgI*_vD5X><5dGul!FHgw8`tEM<+G5Mepgk}*CGCuVmHXGAtn`qN%nP;bT%$szz^Pe=SV=#Fgn332yZzD9WFF++K}}bC$aG zw05L(urQY@eLti@x3y#PBI8u#!2?E5{}=sB4A&NDr)b1pB_30VJ$6dOrnDVvHG3x< z)z@4SccPsQ8_p2fzBIY9E>pKfb ziW_^W*j%J=Td8Fp8b7T0J@EQF)s~hA9r1|{8nTj*mE(mWI{pf zROa6*ZPMiszFb}iHFwMsm2WPVj@|hs6bSncWncA{fSa|)@^&$hFIdJJw4Kbe{ku9W}hHm z<0l$YlNGpNsO)GaZf}!1TP?6oup&B$tr2pfuY-vN=PdCmNs~dW*@P@VttQD`(;MXA z4LSi^BYvM;FWy7Y*Kc1fE@OWo;1xxSO`yj~$tbpR#`nO3Zo=M0r{_wG&}(PX&nlv$ zBzA5qepmL~A3Lt9`39;rasKqHkR)v*tqQYLJB2>QLPLJ_B|+D0 z7G}Wzu?jSVSu?OCAzp^eyUNbS%+l*#LGGIg4#{_+_iTz-u!EhDk_<4ti=0jTU^P29RqfxKx&Z+nGs_!)u)%4mbEzhhgKub5rS`0TG~I~S7fDd zs%kz~@vpW##gDEN-5g~nKb-0^59roxQy_ei{I#?>!YBV!@pqfamikGgLrpyz!e&TZ z(?x$D4JnN`K9{r7mYiOe=Cl$qq91O)x8&k`=T4wxHwoO z4g=m8Us01XWFv#hVGQ&`%+&H`HZ=XXX&X3wtCv|cFU05`|;5$h# zWy#_$sp&@3aP5QOHb05O=>1ic&$j+j_m24rjKo`RzbnpdS)w6TMOLe_Dpt6x@|l@O zjLyx{fF83R#&Gy^X{xRl0jX)u+H+{g0V}tr@J=5Z0!uzcLsH%xs`z?&t6UbKn0_sg zc$;N!O7k|prxSVJR8!+G*dRtu&Sw85F|h0gaba6`l37k zIvPT{#cv{fm^XDWpGTrTbYX&gFr98x<(;E#^}6lHuC16%-6@^a+mc^DHOZhyS%%5QHGs9dcv2Fy55^yDE)K_UEmKmVO&i&JlO? z$TObH*--mF9YSp05BP?L(9CvkV3*a~@ro{>Avg2ghpZ%r~+91(qu0Dz_CKpJkQS~UD}L8UdKIuPPEwTO*kl-)%!scp>k(%`LeI7u^!R-uVU4&&jhZi zovPIcos`6D-g6TwA1zqotfEuZ>CEvtr*ebO>P@QZx2H2ee{ZgnhiSQbKrp$#5XqDI zYF6yC@;1t?@ZxQa`%;f&l%gie`3>;vp1$Mh<9Nvz{=V+p5jhXE4GqEBq&zuQb`Yqf z7_eqeqbtfLaWi>1G_Y-$*QP7zhli+mG;)-z()78j*w*{=?}ujm_84>mLn}tUce=L@ zkNjdLuw^XiJYnAt(GZHa;_)&xjoNlN!8R#e;J8|tX*w}K6vj_F>$%7Z?%C;lg-F4V zy0PVnCG5%V%ao)uhp*&L`--skomx^2BvS0CB%pgcUjZJ_T~l1JAXPhNOFut7|H!*M zR=kw!m&bD5HtV>zTE_P>CBM9d&KucI*Pc^4m5D(d?Quqhs_99cf93Gp)?R(Q!Ru4I z?bs6BC|iU`=^sEt0(l>nvRPEidYuHhy<*;t*pPBR*ZOeb3MEtaAP#uxQ4!<9veh^1 z{b_E3(N{B9P?UNp(JDe+KYx`HtK0MW;|zyrflKBNAs8uO6ubaI-NM?kb`estdtIFr*3Zqy&Cs^0bAHi6z2_Isw<6VuIT2&6zf_@ z&Fe)L9vLrjDifvdds4^VU5^}Rb$&-mQ##6ub?W0!F z%mo;C!00w@O1At+ojXYro8o>iDt``ahT66W3v^4ujYyysTKT28VV1NwcKlC$jSPLb zmZJy}k)xf{V{?%q_lhm^Rp`Du&KNvM<->YTX2SBp?KP7-mPv?M5B0)SIu9C>A$@^{ zT>FiNATGUQyVlQMbKSblgD1q%yD-(KbTK#}SZqZ<)4mATD5$T{ey+T%oGE!jZE8k0 z@w;B~Q4D47CNZk_x$FhsfE~rn1=&NHX15JA0zXwjlQN#c7AsCVsf$`xb zWuwhvY!MGIU0cF!B{w*-63xvkLMJh_lfRjL}zNnBd1tMxzKeXsaF zKT<)Wf>H9_{o+A;V#%_)E~}62vgD7R@D<=7#pJk@46Dbg1tS@70&}!9a!ZycI-v4? z=&3&=PXEBSS5dRyARvedZm&X;|D*W-RjlwTcKe^j|F6QSWWZmE|6k<_K-><(2;2df z0A_#%_%r^0m1nrh0dN3+#{aMK20Y;LRi5E02k=+p|5tg0s~o^ziT_{a6|Qmse;fb5 z${$?i091gh`2W?q{8!`uS9yi29Kux|z!-Q5m;k1L8So1DpT+-ez%*Amfd4H1f0bLf zTCcwv|9=OjCjprrDscPq-?=&_%3p^6@KNr@@B$eUK@g>u1=$f1kSP%c85lW`Re1#P zfL9)ay}%FdJp!*j{Nq)AkarRN<1rt2<}rBn>e0hLG7tZf!?}t-Lyj<>uE}>}{270K z4&u@O81d%^|BOHX5m*0*k3S<1K1wbZchf1{fEy;Y6B9&{A^KIzV`OJS4s&rb9MgN|wV z+Zqu+^JuEo%12pq55T}4MsfcwrFFBjL{zcXw(nMum3eT1+^a6UC}*@Ae~L zx#D58Nl{k!euW;>X`fGgrrQ2BL}!TybGQ1=`9+O6`_;*`ecmBM0Su#MG;g#deSTTs zcHvLI=f)2cP061~MmakVNNo9-?Q3}zUqeGQnD7Y0doX)&`e@ORpH`(~b}g087hgIg zLs>i$JVo1IqJpa=i8I@M7V-TUZR%cj#LU&FEbbT?N*Ig@o_$WRHCwoCQTfrSdrVpT zn|A)TMx52TLe9sy4g2cwoaJq-=Fv<^YX8<#y6pkynO<++Wp^!kG(OlL~V`Z_uuNK0Cc{ z{<1IgmfB@9PZ5s5d}+-CnQ2k>1DN8zcF<@WJ;BA}5G#J%7$1;k*nVAL)k45SgKt%U z5%W9M>j@G6h2qQ}*xqt`yhV~>9u^)&N^MzbXRpq^w5@ZfhcyaXMAps8ewGZ6Z^slM zeDbP=%iI_}*Lq<*h|p~OLJ6xM0p2QWe01Na8A_~18f}VZSc5<3>Q^5JQTmTXa;1;| zCYYyew!|4&zbBnVje1@AO=&bGDS3X$g_re={%qg)8Ra}>U8VLlcNQLcFZz}e2c3DL z;ZdXbEef38`VXsLWccq3wR#J~VMu1Zi*~Nx!7Es|M(uYHQEP^~wFZU}%4+faeQq{% z7tvNdFqP5t!tXmBa$+Ea&B?rP*}GYb)4N3 z@)2!c%r|G?OIKjIBwE-svh#V(_TB&u!TolA&5REcLi7`SWD+u2Z-#`Cb>t*jS8SW{z5d?ROuehx`=sJ-;88pdpLl z=i>$Rg)B8}`pR;0n!d^sxBSmAjR?()rIQMq8VB{pwV9zyZ_k+*H4kR=wh~+44M{a- z&3c1q@{>kyvHRsl(ByCj8Ql~w0jExiBpQwDUKTfJdSql^O&$y(RqLhjGfRzj>^@rE z5L?A|U!3br7o7!kiFtwo7$*{C&{t;jjWWwJL2D!Q`n9+C?-EcEDdGq%o6u|DC8_#^G9@gXLXtG)S-(VQ|}PWYY12`4VIYEf$BfMp3* z_Mu`hBc&I`3OkM{?2h3BL}B;(H0DJ-g9OnhI=^(T796F~)cBA4-k36x_~B^J zRT94pn9PK&+yQwPjZCt`bbS=TX7z;d$$53GZ*TrxLMDsVm=J?=3+Q0e>{E?jqRJ&9 zK_T@8&OQvVh0a3D;ba}cg$AtibSR7=!hy%LJ}3U%W_aK`=Bf3Zu3v|ibihiOkLUD; zO8lpixV1N_!SXw3$hhDF8JC=-h(nnYE_T_s0q^CkyAsOsJ`zw#O}Xmt>qj%G`pQxY z%AbdN?>r~j4yJCUPlNW=Ed=VR^M+Q*JwN2a!it=ULPPYDy}S!ndMylW5`|=nt-ZzS z-V>8=-&=*O_#Y2=-3-&nS&s?K8hSOjp+1&kxz6dvX|Cbv--_JESy2dM{vj!vxHhq) zF1srK?m#yf4|!chx2dtZ>hN{6Kh@cg=w=Tp;Sd&nyxnrsJ=UwCPEV8Y!?Po~-`L#O z$u(o`%nKr!YD#l#%Vqx3=d(Y@pT)TU7~{_e|C{R{1g^&7e@DBjKfwg=|F6`aTzyW& z{nz>%!M|hKS8u&q{?+GJ9N=mluAcj!HEu9L9oXMj?W$hv?`c=d|FiFZb@^AdV;28f zEOk|@_V=`_f-(KGfMh;nEf3D@V^@ojN?y4PYpv=cqp9%*6jS}B`sv@RI=x@0MuT|Hb0E7>avDN|9; z!*(}*jD`@)dNO?QR94q-#2BHi2LCB ztIGWe|B?Agt~rBVbJv?>ue;RFwb@G`M?!VWc8I&ME-6mM1=Sytv_4jX^G|F?cN5Ch z`kYRRNPN89jx#j`bkwFA^;=*_(i)YfJC{D)vn$KZ(2Ri%SQw~8BAJeZAV(JvRs);;UG{b1Ort*QX-(rr|>Xx19c-09^ALI1Z}4$2-XmNVRS(U4vZ z=>CssO60i=icoL?^`vCA#|LN4I4zwF4S65z{SqEoVUc5rqTLTaov(vf9y3pF0Z?PmMoE_b@&a7z|3uaBaggw%WwagTA1F0cXRGe zZKa_u`5l_;sXrLAjIJALUMN|E=69 zFJ$l#*EB=5=x1DPn3l3@;^wEm2)68a>k%I>3c=Ui9~(jKnu%!3(Z}&di<6U4^9dPq z8EVVnS4u9@$+(Obv{hWs)4W_@FGryC-0)9mNPgN?!rFj+M>g+X!@ioq==_Uovj#c} z>o5(r69Up*uJ*zgQ?GA9Eo;z_I6AV;!-MX6 zor|m?wBrrkjdr(aX+vF&-Xw}-jN!+7EK|EsGlwfGZd@dcKFa?3^Q4LuW-KKm;34Z= zEy@r^PZOusRNBNTtadG{JZjk}`Scqm3e&hje;eN2F=V$K!J=O>;a5Jy-G_6Y%RTtj zr)$`_bXk?-HF=JD#NB@Pg(J3N37t>9#%X@nQ6zc@9EE+iuyKyupD^bsi~2+THN%~@ zuP2;^Q7>xOTjZXm26_AVth>KxPa*V=o}E;B#r9raraR`2+7|-E2`0?$MhTPiuRapj z^{Pt=U$#n)5?$r5WgAAEwWGU82@BTbgT;G~KZeBBL4&312#sD8{)%&tT9PU$ym;B;H*qtS!)za$3-Zq`6(Ru& zm%eDoruFd_&5T7S3h$~?q2*l8$$7>Zr%D{SFW z?^}MeilAZ)CVa#=RyKQG3K%Xq=v@@2hK5m3ZPO>=ZsBOiZ+1#ljJ|dXYP{xPbD?4{ zbazoS<$6VebfE_ZLthio*a@P&OBR*++;5Y-%KdbwQ?T%Y$9?XLYow;Wu$JnVupUD3 z{#3p+W8Id^yVKA*Dz$R8T}8xbN6H$s!-s7{vm)VHL93g3F5Uy|Og~Mr#gm>+^Jy!s zQKW=y71?f(J<+4`WX&!wWGmErqe%n7wXE!;ztojw-On&>R%_v2e@*dOpIQbc^ZrqZ zLZ^1|QUs=P#ppbTO$mwNR8boLtpo=&1YSAB$;`b> zHKyD|Lx^0U#CuC4B~?4Zx=gjPlfB8&AP*K%yo%f5DyD_yI@HYNUpoFm8@{4Hm3vlP zJZ9kEwrjJ(zF84vW?1u)f)Ky6uvovJfRMuq1hBd$=JU*=o|a-x#!7LeBg8+{>%SEb zeCtB^Lc{66IpU__r%N0ey}>k(7lmU}-k=!5)1a}sdQVMuCu(Z!voF!FN9x``PW2Bb z+@^DewhbdQr{GENyDuNVMngs#W-hUCyby8YAaR||_b!~Xbo zTS`@TOa_9tW1Ft@`tG_KnI0z#cL%ZHXUS{VRrwC>9lEUayXzU=H_w`fwW02|e6(b# zx<1jN^-Edt!A0)3TAM7aTSIzBD$GmG$@{@W9n-Xxrxe;E&*dkae$Rhw5`kF>6}MN4O_>@{Ho8i^qB`6%}Wr( z9Y0ce)Y^=As!4n3R-!tpdph5(*o!;BCf*~RVLdG$ed%Mn_SBkQ8P*7IMQ&dn-?mt= zmEpX!K9t%L5RdsxqnbW_++vo?*RalhSUcdWtOjprPbV^z_h`f}?#@@>EcEc@txCwS zeyJE)?*Tb{FNQZJiT%yR z)XJv(v;E>;Q`gZ#L*x(Dz&!dl?BA=a8=&m&sV7bQEgx)Flv_gTCz)-{!#YU{>xgdO z0u?U@()&Z#N*-Q3aaZ>vYL1trgnhH)A4oF3^=+Lur4x#CA#<5E-4u2be)$=N?;eal zg`zi{n%tgtb{M)BN)}!=`?#AbbS8kY@AJFZhqoQMHlTz^=24*?t?5qK!28+z;ZL{g zOnbkXN@4pv%1Eru!$t9+@)&BqLV1)JvW?4T>&<)zrHN5p74@S>12sk}RXZbdPXczF zES$q`o7DAEJ$(0IgIcJtPjvict81P+vc%dtxK0hDxxN zR>F$w<%~gyG6cD1RIEf@_nSa2oGUmOQ>36qgQ`zJhWd$uM#fupGz1^{X(o7PMUBdD z@)7SPb9ITr3dJXvg^-DH{QEVQyF8@k%o4=Yncuq0N^;?4D|VD<$kl{Q)g{C$gN64Z z_6v1d0&P>qm35;%6Y4(rpKA->+hfpF@b&Q9RIsHG$x$2jRorI?rAayo9Vdh3 z(Ysyso?iwP6m@FVvDe5`ur;1YXrFo7enPojFBf-nwiPet(W>9jBOEDL2G3&#S}QFm z$r8uC>0!m0lKHnz}fw@$n{m^oX&m$VozZp`Nrv-lZd_gG5%ra}3(V$G zIMdDf{40_Z9Ws@Mvx!jLsydt1SlV~w>N@&XcO+kt_ApCXL5XoS3#gQ)s*60`J-j$I zEo^SKEDlk?RWtJYmp|Q`(-oY%hhV5Gal5CHphewlE-Df75=Sq6rNhRpz-UW}K)?X6 zzv9bG-4M4uk$Ad;ZZprAjSI)op`gRf z#H~Wd5uOzBrFW(eOmQ$Vp*`9&Cif3MXC}$6Iw(E5VR$T~En{I!J+LwqkWN2dLL;1L zBFSxP_*Jqnl&MRqvC)@&zGDi$0REfHsDLw|8~Nes?`wn5a)oytfk`rU-1-IV&*~&6 znx}=m-4++lQBX8QahGeYPE&H^z9DB07{W1+j_YW%kMkZ-=f^gS!0j=kYC6H3T*p?? zkSdqe6F*R08OG-S-Knekd(m}=uup}Pl1fVpq`HU(y@v8(wy|O%ysWxNE6c?PpWfg7 zct7^BL%-5$geJT-Az$F#}={Ks8PY<4Rd`JXH7MBoviRa0 z!+u0>PzxO-MS_1@#?qrL_&?(S^>@anXLQ zw;{X7v)D18P@fCBAFv?%GVQ6y41B=LB)#^weoT%R0AtEm{8=4|lp1m{RuaM`q z9%2+0yRb)rQKkXqhnB?{&y%tu6)(%;twb(i8bzo|Mbhb~vrh~);nzI6MThh*aqvIp zd3foliWv8T0;~O7Yg{kZ15I-%`_~)Jp{l-}VWv-Z6ya<+&8FKL}BXt6TwJ1j}^k32dA>oC)J}lu}f?9={O7ZcuY4k#cuC@e|G?B zs}XCJxpkY3%_H05hWgq`=v-%xIm_;DLgj=$edE`WNb=4>k+ruIYWn(Fe=St}F9Eav z$A14a{`G$?{v+Kq6VLUMFfZfE&VVklg61Kclce62+uE&83}$B1 zy<$qgiC-(exWXaZ0(-srHAv7jr^L?dY@>i{XgBIV;GY_`9mTA>a+7l1>Qv{ZZ zAQz}Z*8oGh&7)7|^>0=v9JSYDnvK4Mq2(`z2-Jfer-B&oTXK}5Ez zN*nIf3xgr=z|jzWrh`(n&C+4~wYkZ1}`Y zpjelJ8ZYozaycWx?2Y2V2li$zJ<*&C?l1E9mDa!Zy}F=*C5<-WnzK#EbY(J$Q7>mT zxuZr&Nl9>?e{68${%ZPNbk$c#Z+Bh3;R98v$8yzU=*>4Np(%U)hicy+Y#ViF&fFhG zPOeSn^Hn!{{eTS%QAP3{5@}k&Zl<#p5yF zO66@s`L1Psufo^!agWvviSwCbj*I%;Pf4V#RpCT|k0KTU-Y;23Wh%W_E( zLqA$NF??MpQ`;edl1tOhTl%tm6g#H9Ssk&(%qC$yCBgn`0b{t9Z^p@|y-aHkQAy3z#plaR>K?~mDM$Dk zK2YOH9_l>~_I0(a4^xm{`SB9^_S6`2tKZ2SztG(E<5_@eeXR})e{Ui+gWl0zuo3&4 z%wWa^-lw;W6~Z%y@Uz|rsaOw*Ay&SMAX(GzepL1DPk_YyAaF)@x-X^N_t(5XHK;$* z#Jf^8nbdin{B72JR?V73rme9!dyWbEl56w+{VLpZ-k!6fHtmxB)A}$qZrcWpchbvP zT~hDJca8C#7x0iTTK11yqJ-$=M<0#;UXvGBQmgD`Tbld%?mlHKq4!1jqSu7We!otL z%Fk8h&<+mP>_TcY|Hh-@ZMlPaiL8_eHP4Z$)CGzo7(PN`xoEtZKp9bamtMf`GK;@o z+Vz?;|JU%{qiZb>Wh^2bTo^*4heTQDwPjR;TLdLFz0X5Frd#;&eGlL!Qh;dBG2nT7 z@dh=MZla#P+0{*52f_8R!>M8s&ih+Kc05UYEQ#GCAfzg6*F~Ql{q2PHPF-uyfcKa= z{6eE?EUS`&w8s!$OFwq9@J^C_sXr=Yr^JeaZC5ku`$1!ygflGZGxduakr#8H728fF z+cv+vdIpP&na?-;&^$`Q+eIu^G@#v0;|eSB(DO~YOt!uq#RGN7eXEyH>N0m~|K^$H zR75YszvZ_7%-`vQeBwXrC;xf<{(t(#1pjC9cl%zvgot2rhF`DzT05=3Ww}S$VwW-O zbM7!V#c15ZL}|X3dIm$5M*EVVk*zLF$r$K_7AJRiX1IUwzX$+V4QK}KpMHo!mUpih zdcd(<;gaQrowwiWe(t*F|1)mW$i?eI)19pgUOck3^{e2U`%4MGYYr*TGPJ73#NCd_ z;HX|BN9CWB;JO)$4wa##^AQp7!7}RD7;$Xr?zh4gx-1v<+s+=w$a>SAh99wA854}e zEB5fVBC|+{W7r*c%u^>$d{%K!v|Gr%a=1rnNsVOXIxBLU|Kc0XYL8N=jQCE8q@S(m z4EyDw#ywvqJ=~#wHov)I#ylUUr)4+2D#n)ffAS!O9C>d%@C!LAp5!T;uKZHnvuR*! zy;ipBN8_w7MZCltX7~D0Pl|OPgu0)9$9OEr*LqT4&cPwEmQT!<4D*ufXy~Mb!~8

@_W0~`*5>*FTpt1u@DtLka-7n26pvK&$uHYs-ZoY$PZ z+CNG%k{r!s9(-uw_%#;GEHV83A~}wKx}&|lM;F)TSu-U|a+eI}+3AVWzHlb=mh()i zjl3mOb$N=%O4=?q?zBT1ba^Mr$RXflznVT_X&qc2^+wBw_yuo$)}f^8$`Yp1=_8b8 zgwMYG!2|EA7YOZPe4H;fMKdevY&+Fbf*Gj1BJ$(F{VlI4)YA2U( z=KbOUU#FFpJlkiKg}t1_P(j7-Oqg zE)#Rxy#s}T*Q&=WzkD_>SRXUOT@Dj2>K{IjdcmaqGyggC<=}2@r+486+q3sxrPLZ{+qRg6F5tSgr$@OL{{xFxhA>$h(*+u+(w29v?n%;hQxbxub z%;#+-$*igO-7P0Avq*=R{DswxW0(a!%f&~(UcKoPrHoMS>n>a!5OLv18k9F0P&6w~*TDYo&WPJ0fu!s zUwgz8#%8u3(ThQK)n-v1%y`dIp zo0)1nx^bvZuEfl3ZjFsw*vlU2VC<359!G$(_-^`^GdAk@=lKQ-Z?06;P0DL$bO@P6*mePSPDLEY%_ZgA9~1RCf>M_4;%lBTDL!!R#bN(g zTZlOWp-po#QkSrG=%=n%#&>OX^-gmwW(ZxNLGoTmxU+(M+0B7>PM}2ZvB!j?nFNWS z@GqQ2B8A|cbp0M%R%@)Ye!j)KumOe{wdLjvxY}~g+~F*>pw$DL%3reHW%7AAQG2&y z{qBv;C7C30ws1|RS%2h~Ii%o5pZs2&mfatg2n6YS8cc`A6^B%{5~ zFEP=RH6_{5EnZYl+`dO*pfA#w;=I7Om7T#b-ooL_w9`E5d=ZVCzA=#gv|3@o4h=D0 zG0u^pt{SUGFfZ5nqajH=bvR@3etO6r_~$_zZswucYHwd=nek>Qb6%nEo23Evw+akS z&M7X_xJ!{LA?)9K35Og~Mk?V1p?7Mlp4-@$a}j9EAJipg8yJfF^SEykjisxL=e;aN6_rae($6r|B&OgHV z>u>7^{Xv)B0|7uF5CjARA;1S96!-{)0pUOd5D7#9(Lf9k3&a8OKmw2mBmtj*WFQ4d z1=4`eKso>gz5p3OCXfYW13AFeHB5QnHXkSez5<0n5l{@205G5wCVXEJ5oiLMffk?@_y)8A?LY_633LJ7Ko8Ih^a1_AcVGY*1crbgz%VcZi~>J_ zG2m+bC&29_aCME~G%y3q0&~DTumCIqOTaR)0;~dSz&fx2`~o(CEnpk?4eS8Bz#gy< zz<~qc5I6#kffL{qKmcdJ)%sk3TO@FK^%(f0P!i*h>+t^h{aOE*-=B3JIkNfXBes=S zkYRff5sq4pRoKWWr~3R$VR|a;p+%LKsTc2}E80l7h?_VM&u+bF&J&zaLgg}-UT(0< zM8kS$rQwf<^^^5=b~29S#p`^Y??D?K@LTDsc-_|u zXs3L;pqok+oY$F88Fn-bh9CDM`b|VWO4GP!skmod_qn&ow8xW(m$0*i?aOQxb0!|q zMn9;qZCt1AWEc0UhIMS^<%4IUD-%M9LbyfysJm77^j!FlUg9`6PTo&Jj+mq|Dd&yv zb1MQ4EW%P>RNi44@~0eX8jU}!@Y|B}*}Gt|l;2+nulb}JhHRCLqip0-0nMnE(=@C#36?#wCSWIH>!^-7cRj!A$p4RSgi89gK zn%^J;5=5&UWMp|GJ)3Pr4?9nZR#j;W=4bY# zU-eRT%BZHlR0i4f_S|p^CsO;er_+oLzdT8`VWIJy**t$-Ub!PQ%gd4-E$~kG-F=+X z78-=dqoU2e`y(K(c%uHbKw!q(ivQu4r-P45)_U0?1QQ?JLpsDCMpW3d8lP?6*podn z`Bn&}l)kI`wqB!NGj7{yN}$NiZNxJ5rRE@2K`V<3ChU3222SW6V`YT3MWvPI1Li|B zyM{>dm(E`^f)nmuOmAO0i?YkSIZ`N@wW^?c$?!Icwso1`FF$Xn_s_hsjfLs!U0zWH^P2^^3kE%>{SLGb zEm_LSP0nV&_@9P=>p*d_JVOeF;!$s~Qs_kDlz(ii zqgBqnPe(oPd*=p`r;S=M8SUW&jPzA2zNCnR<4vDb z5s_AY?ag9kA#D`0FIwuoPCo1^+5}wVe{hyOk-rnk_4KL5bDN2uX`JC~bLVt6-I!|?^5J|yt&`yzPXE<|5Gorj)#PP-%Uia*w(_Hd zJ1^CjwnS&6Nz^8c5e^GAO*X3u-|N8m9v>eSeXvhzfyFV{7F7Wy%#33b$!H2gZ-m~L3BWk5Wy7Vd!R z{W^?@(vBfL?Z}SCw`Fe`Dc0MhBm@bIMxFClnMCfV1ANZ$ky8Xo1XTA|e1|7y$XKz< zxl+_zo&5;1ntvi~UdGv^3u?#2GAcauU-n{%BoP|l^rXxw39R# zj~%k=(j#LqQ3$?*$tVVVU0!FYVXqSzv7ilAqUqCDuWsg49Bg2wtT$TjFFb?G=XjnA zC^@kncSD_4l^LeiUYmAQdj_$XvFxjP4)VwsuVt3ymIr0k@wQ5_>3;dSv-VMmjYhI$ zG-VRA`k>*fI^0+-f1R)UMR=TnNLPcQK)dJ5QWe$lLw(c_kAY$+hYe9{TJ4rjX6fcP zp}@QW?YS*O2?covw_i*5vu-;JZHQOOuNXB%l(M*gHp5!ses}}t=c2Z<+X{XvUuwK^ zPg~sKD^YO$kzb_MV522$(mmMCOG$6g5puA;7#D8uNxv`@i3+B_X=ir(X>8=0@&Cu( zT|ia!?Tg|^0Z|c$-67o|-Hmi@aswOuPfy(Q zA8)+#?mhS3@$NY9{Pq|h7eippwHSNA{Lb~A-!F-qobqU41;VWA$d+}lt%veZHa{P$ z3(iohr+*~J_YyaRS9(>sH-wxv12rtt%ZKOHc3nqidEvQ_`zMk#b;kgVFd|{9n@YV= z-W|Q2{3nc5S)TTh^tCgk%d%E7M>&q$Qw_8$5KSV!5nEacGqLj0 zg9B~F=()d9n?jl#ijA7&E`X8Xux>{(Ro`u z9Dgq3$&F-_nSCegR$7DA8&5g7R21~BEPbM!job51KFNWZ{9h<0K}D^h7fMBTL#hd1Ngw{* zJv~z=loF*jrUGXX$mxA(Xmk1Xtdw0LT3Lh|&P|kE-0V)=fSc>yy(=XztV4wa`z!>x zxFW%VuDG;?4YI)6wT~a|24zzIew=lRaeuw;pjUV$bJyVFp(56G0!<@aED+}PqDXJx zC#i5U2{qccxXumhl(cskSssxg=A#VdH3_O!)%DfXPa>ApTq*rxT4T2Fb``l{1ThdNDOOVkhvN*n9446477$Vzh5-kbYc|MMMyZ&Z?wt_R|oRrSz zL8eAjc&mn6p!zqfXAtX#;z*5q`jKUC{OWZsE>c{Y5BntGTI`kyJ0K~JT5H?ZrB;NO z9fNj`2+7$uduI$J#tleEk7@$JOjgWjc1?s+w9b` zyA?yaJepJXtUh4YsPPSqw{X+!MRzI-Y@1;2Y7$&T(t+9x{VBNr=)VriL-(ujn*h4~wozw4X>RPx? zo{4RWYJV^ir$jx5&AW0|VYK35Hq!9wPywxDEI$+ytVe(PJmFY>;{k1!^v^)oSfg!ep!?Je6YXXM2K(*(0hUFN`&%> z01v8|2Nf9dEClF~N56f;e>qqd(9IH+Sj z66jjAAWf%id(VF|;MI{V8n7)C>Per{O5bgQvfn~?E2Qr*K6uF+Z^g#NMp(3Jq#J!( za^xHAMa2hI3RjBp^q?EM!~Vw=drvE;S5?;L^yJM7h!!GLZP3C{{f3mHCUOyu9j;Xq zW__>5O+vm)i?_X_?GjFTRNEyI&h*KO{D$`$JBf$wdq*35+sb*qZ+oiy^HCN>6v<`%@gXZLc}ej){Cj~h?;xH&|*{ZK?^ zNH$bq!WvoNHfgLtVr(K{jk?SALp5n?`%Z?b$kMzJr?tSm5asGZgVbtxMH#}!M%Z$4 z5_=Lp_w~bwOvV#F$J!UD0TdKuCu8u+laYo({?VBuyV`4)})J) z@zGcdd?zHW`@tO)sYseh;EWrgFDRh>b{%u7C)B3R7&Nor_a>~$*O{EjvK+g#?r4_1 zp@bqz@0o9AjP#*GoJCJa1Cm?`knIYUl*)56Wtq# z`OFU1I}gSFBsn6;QKKsj_NZH;ZUR-;p7CjA!{5gnq(h zPDCM|cX?;X5x2dGIVx&%9^u9!!KsdCWYA1)_^U^_>Fx`lpXO2a@py%uWT0#OJ$HUwWMvG%i6GLMP zXZ{|Z7B4G5nZt44@=&$P#)*aNEQrptBq0lNw5!#6&VWbQ_X-aNL#bL zBs)rVpsGN`yb1e6AN9!ifqlGCmG3D(;j|8-Y|BKocGi({MCNTE)gjw2ln6+lmvgZu zWb&NKBn1YF^JpdMo&+IUT#OVJX;Nbeeyb zx~#KjrC1^7ZEBIrPPy{an8E`;a-@-mQtdzlM`VEeLOo}^)a_;aL`k7d=A0xaXSz9K zbE8FbfHQp(&85xEI|pSNj!O;s4DdWt@x;7q7WvE?#|sn;^3!+=HiNdv-jL7N=qzH* zb}BlAWn7TKO-eD{9>L=nD=?o5O=7vrVq_#h`3~b$>32YAuzQ_HC}WeMMX`itk<55=S*V)t9Nq*9=kio|_@u9U zDZ$)49_bketdAHK9HXdET)#Mn@xk}~vJqUeBc~!*&QA{*wd8kIUbPBoXr(@9A$ZqI z7|2-U^K}Rwc;;PaQa!$^L-O;@>K^Y^f*q_c>F{8N_AD1czEs4vDtE~6A?;~H&BxTk z*~Fsv2hmIT2vj|S93e(@=;xGJm$=F9i;XkeF&*6>2hQF;H!D0YM}=>zm6c@EKhH8U zreO$c+B?T@_uoDZ*h&3j8yaT=$((hG)#!tFOR9XusG&;_|cSA ziPIQu^SVYY!u8wEIa8hK;g5qssi{g{dJ~Zlzx%*&Y7JV!*$~e{&!n`Dc87_{aSJpSJ(X5$x`qzSfKkUfZ2vmKp~2n~Ps4X7x5? zXQ$RdM{nIUV`}Lc!!8M7{KL=KR`7By^t>j8N?r&UdHC>5xJ$GP zPGb=WYWwBStnmsR)?x<|{F|^<+GeYe9p!}GTUZ6YzQ+fzsgNFJ&U!|WxL~Z<;DDh4 zI&;xEj{K0Z_@M_QNk+x3%c{19j#RajmBX8kbA?o_z36!CkBiDXS8wL+$@Ch+qW2nV z-q88@3tU8A@r+s=icRCGH6$D3bx0c+WO?9AJs=&`#7y6<3E94HeB_I|JGV-8Fp*o+ zL=-|_SN%kT;`#Rt3g{yTBE#a(_(gNGUSE$=bHbqYI*;dOeCbEaQjG6^i5w}h*i;r2 z$gU0GpWmlpAu+aQEQT)RflX;_9sT@CoYPAcQjc)1)B@m-^2zo!dQi;mC?f5w%1Zo% znb=ZfVvcRloJ2~sH0zT`^>h~$0{bHe??d1FMu==h$qVZ8<8`c~ll$Gq@QI5Z?q~bK zx-c;_w|nV1(m~3Hhf(iHjY@Bf;w*YnD3q*TeO}6PqoPAN1Q)`7CpOJpx}bj@)#PdS zj5S5baEMomk5_2qv!C43?f9tP zLfZ7sl@D1BRXNa6x1TAhfhLRCuQv%^n$>5&>qtHezHN!yO(yR3$>k!D%B4d8)k6k3 z!%v(P=f$?Oy9HU2s(4;sTcU!lnzcthzl|7|4O@;itOqeuiN!RRgXS=)*eIKVGznXS zs^o~DM@RuHCb ziOxO&TCNbI1%2 zppsd8)Vz?trj<$mp~Pl>BZhP+dsj`9&2o-y*5cUNVf9Cu2eej|hmgsbVSQ;0-i!JR z<%=3-i)~dK7MnDvSn}EJ(rY~RaTQ({dBb2)%0b;Hu+BhPM2pWmNYlAO#d<_Nc(&1J za6n}>UO8stpI?vZ5;IS5oSx-K=8|PLT*^vxz1P#}ku+H`ZZnwb|JkcGt#PMVNq`19 z7=Sq-b|s7+@`EEN>~4&h)0NtJvO)x*A*-aU-%-x%ke{8URxFdok$mQFhh#^*6&S;B zGxN%B+;=A>3=J-eYIe5Irg>$2^^iPZ1y5`;M0MP0vEqxp$#B$AcjA!kcoN$xz4&ac z%9y(pRc~5Szr8*+Zg2iA65MKo-i+A#Bv_s$jWY|2GRvd-IF2-~h$r&KauF^zXXMfs zGSb(I3e2MNM!39N%FlE7=kYx~mG0y!C3e{fb$Sr^HY!%KDa}=1=Vb|srJmj1$X<&S z;onos8h47a`w%bs&KBNPFL0@5uov9Qf5hgR7T9Y%yY)uCsD2jBq8JmK^R1WjX@_z! zYxF?jmQ_?@d^hElab&i@p6V&9;_J_&m=+oZ4}&k^uNx-Tj&Sjq-@A1m=|W|D$4;wV zBaIX|qM=T0m+Hg8DVfl}KPUR@{D%cBV*WGCe+*In->U!nyKUZW0JLR8K?Bf%I{*f7 z7q|!92QYyL02Y7^-~bN+TmTQi2M7Q{fCwN4NB~lR3?K(6fJXo&0Q%dZPy>$v8sK-1 z5*;|w0}KEo@C0}YFagW}3&0An0nY$-fCJzJxBzZ|2jB(x0DeFK5Cnt(VL${B1;l{o zzzaYecnQ1$BmhZ33Xle5fZx|92ac}+c|ZYB1l|BjfHI&0r~+z$I-mh)0$PAJpabXv zdVoG)02l&q0VBW|Fab;fGr%0M04xD300LM8Hh?W)2fPF9f#28X2#!vGGvETa0&aji z-~o67UVt~?13-cIz`t<+LBwqP8hvyP(8*%JU)FhaF8UI zIK%ZxS|q3la~9PyZ-|JUaFA`*MCL1wuiQb@ibmdW`69}XN@ zFbSFfX&^>g_s~YWoT;4bqb`pxCquW$dMz=zAtdjv@9kP9nVB`r|G-aC$Il_v>me6c z4v#jUbZM@{4lVvp17{30Or#_oesKd`RhG*^(!h6L&CK(LJFMosNbd@{$oh8uqhLn$ zmhUcP&}n3Xt;?fcJ6#--CU8hlXELI=Y{6)%yE`QSdpN;!jj1wW^K-NU(}wcl5 zQKZ;^#bV_zU8#_IvKtxN#9)akH)$Ntb(Qzyp?8NP{3r{u4I{nj_IVoL``xPA#idk= zM$*=6U|xa$c0OWu=^y|}zkI|?dmS^bN) zfo-Z^`DOB$sh;%RnGh)O;Qng+9cD1;gKHU;xE7!j8}j^qieJSgSzq8X-t*a|S~J_( ziTc%uugb)Vj_8zHR&Tl1dDbhr-+#S#pj{2c=6OHh~o_b31j;xY8(x zG^cmyh0v}~e)i^ZWugxci{ooSY5Ri#Ni8WA@!s0+Jc#HlrH&p}+)GAom2%;(c8y75 zGFS-N?)A3;GLrAc=}${!KM7&qMM`+huR0c5C3&C4f0HXPLy5uq^4llv8WXLhXQ>CwzE-1wvTDJ)u7Xw=j$H_^kJKqaRo2w+ELyz=@}(b z=D$EmH+oTwp>ue>5#Zc6z-fBy6@0%C{vacwoyuM-iEw+vXI9Zj+S;21MEISM+L62&w|;^X%g5mfF&+>F%XkGpHBXzjaUUyzJf zY;MFIX?N@%8i1}@Y(o`Gyf|52eXg*1?d~t=*mud_!>kclt6=SLVe2H+fvo~d%sQt1 zx!&B`6VdECifxsKuQhK4OjY924vX(IKkcTaiA6zR`0c#eA3`Vxc6gZy;j8BK;h)_TwC(IW+s10zi(3Gpul;@n@blKw4QdL^394Pu!w|u%$&q;TSszsz;@R=sg{7~DMn`I| z$zStnSn!sbE#2DDKYhURSiUY~FAZ81%jm{+<|C@XbZbZ(>$^fmQtyw-$L?<*OgSCj zagpUMIism~6xC$yXpo?g^`u$~mppefwNgx7bpt6a#8)TkfY>nP5wTNoNau_jdK40- zG0=>?tD^|d38L-fvPpq{4@78;r-`aH!J7)KK@APOI4XZP10?OL96TUPFg#mFQt2Q7 zgR01*hG|Pub)?Kv^MdKbJHjyY>c(OfDQ{~SedPG#QyZl(^xAoCCcDXw=WYeW>>AnI zGDVs4PBgnQc}1^spxGa0QW?Bn^RN_?>}P3&G-Vs(AuN{2GgNNaogl27_5gIVDo?9C zb64Zh#!I)sK_xlRR^FeU{e{v>&zhM}A68(p#@^wyKyp|ysWJT0j?_KW5dA!R)1U&q zP}|AnawstR>=nte(WeJDXcRuqE*odjazgHd{>wYwf=3c}=^aQ_A0Vet94|GjmxB}P z`w%Jl>*$UL~sFVX2PrWCS-w9mox zJgL$ofSy+Q~F7iBoInbu>nf>|rFyv|WjzOP9R7z<8oJ zO=rTw-fG&_6(4S*Hjd{ylQJcyp4@FW2N`0Ye11St_MP_V+?v~lSuTxnIBy1T#n>t$ zD|Df^_CaEiLRv6orr{Whv(yal!(EJ*2l|s=tdvAPh*y=0)Js)-D=RDMyxZk**bhd=he{$JexdV$&w zMYR>iXL04SdLvir`y?ZMLlvo)A;A{9F7;s()kJLv3#XluyZ+6B)4=2J?)N%8v6HWu z-9}YMAN$(0krJcSm5?9{*7MY(Z&)ftVdMcB3JV?D8+}9tM1QN68X~W=KdYI<_stZ7C@j_Wu2NR!$4W@!R)2>dbej)2uoI z3~P!duO7vVuKf^2*_ojVVlla*z_LiVJ7{bhT4R0!t79pw2A`kO#Xc5WTZV+j{!2ZOTN?LtR$nlK(?6~aHDsKtvVl}d#HdHS2?FKROIzprT!^ zG{|XzKO=7^AHzlVbLiuX-cO7?W{^@EWh6l}&QbiSS2|#-c2tu_#EA2NCDxd^`r3P) zs?*@3s{y8$sbh{jPO?$I_JWH+LsQ_K27%KPHk5g?mBvo0Q2d)=!5B-yx;9MNkoXaF zl+0z$xvNie-pu{Z1NRSFFULC*JZj>(-nV67w`*iPc$ttdJd@Y)wB@QWE6a_y^>l-s)1Aye1BRjv{EwrrNj4otcEsQtZ~@=wI-$#C{`W@wKVhbxTI6Q zR4P@jq}x5m4b{`NQjA9}v-*z<+?Iu9vRB?bZW`6S2Nt#K zq#lTektzn)?!qYKB|BvXwV~=v zaWT&$?U^SL;?&slk`9``P=tL5cSj>dH0aC6)(X(?%bW^oo-(Ul%seWZsE49@S=Ahd zg|U29Kfe1Ke<+7=gU|ln$}OS$D@c_Up2N7$wXDaT(@JwmTj6Z3y%Bd+;{+HY>$=e# z1ShHB(%0#2*n9E8x*Vt@bxTMMTzK|o+u;#tEtlHohnVd6(V>u~MwXO4G-j_lPKKYS zC>bM+zqAqT97zGWVIHX@4@w&BAlskDxx-aYf-#C3v!fyWv_LT%l2n{5jCos@!D3ud zoujM&Vi&uB@#I!h8Jn zo&7pBKXv*WF2AjiOTm+v;;k%`5mEBHKb50nQ%?mL;&=4nPWBSdE_ss@0u&g`aO)=C+zqQ?PkxrzV%LQciy3k|{* z95QIDX(1>C%5z>I{OVe}QadI`P(iR_a>AJV;oT$^vWTnR5i$xV9=|nO|yrJU9Om*laY ze4|yV{#rb=Jf|+oI=me8sal%11mhDV*TBD8S*A=t74>>qkE`85rd3<>NF>=K-Kooo z^u^Gf+GeVLoi;u{kvmoc;ZxCQ)a#{4XUBA`h4;`8%`9=x%ynYQ#mxQAV#lsBHV%RW z-!+#;EIrtNO$I+eZMXJvq=|ZO=yCq#PTum?!=r8)s*NKUosEDKD4YILfLtl=M|zvs zNK+rG@^Bol4@5aSBTTU(q%O>88J{9?&R1UxPV-X|_JF@n3gucxY+1h%tN9XNx-HLm zz#dBELt^mat<8#@@+9w(c)Dc}P7=if)SH7Nm?J8G{zMFW=Tp_ZrxMog@lk~fo7Hmm z#_!IKB3E99>=!i&yA>}yOt%c~-rS?&S?{&HD%HQ1CD?}WXS*}W|G9?AU*~W0f6U)# z#Y_M1&)+!y8}g3{K`JrPKahVc3)UUC{*r&p`QI@9?^JT!e_(tEFuvYj@~^r68^$LF zW0U*?_8@XNpZ9>x8jbXFcCVp-|_I%a*dS-uzDdX^%=7 zdU+BhE;YJ4k2UlL@Rg(GtsT;6 zD@?~o;$}qmF>N9F`p#d&1&D7!s9-2q$l@Y zly0TpRC*?A2^6@KsLse`4Th?Zf?g2;YrYwb=dbd*Bq>6UA{m+@%13bvSE`vmGpHM< zwP=V95w7SnjEI%l@|bNbR$XOn7XN7U;P1)MB*hI;ggPe*dt z;&=LBxpx{}byKvhxW1)i;8#R^qzt?OdBs^xMMgceO#QhesEgGnFtK}#L{}8~={$RN z0@rrR@-G$2YLu}Jb<@9eofKnyPhz=Yg?H>7J+&~lM>DS?_PboKr^0a)y;2$3kC)__ z$&4u3+g6Qv|8xPbq7mU}Wfa76^n{g~Hg%v!EC4d?FRc=`ig6(FKwCMwsmnR5g&dK( zgocY=o_jI`cGhbjr$VJ7RYKpqFE$OIYHuqA)p=zPyht5!qVlXpcroIY9LHjMZsPj2 zs45mI9P|wpx1m{Y13^Wiov-sGjz`5CnMi_qqBLH&WdAhQOdqjpj}vGx85x9#^QG@u zx0K=mc4q^h#DrLhmz=44y;WhN@va-p2F{zskY{d_r!8t4&Re6u(y*&)!^1dsYA+*S zF+@4`Y^~Cl)z()h=uU?*Qf?N>oEpEpmWD%T_WTOv^uhD44SoNWR9-3~%jgQrp9Y;= zT^`3a_^O(f_2iye_uQ)Qe#AX*GQ3svL~^TFD5LMphHKJ^mDISox-<+c9!?B(AQl_b zz$|X5SE~KqdEVin5x^GzjNohfElR>_vOYHXNjm+CXGRrA7rH#tG z#aT=ae2b(lfqi}B@KY|YtdhAWY?5KRUYKn3P^^P|N2tbWUB#iSBv>O)fd6)_BH_+A zok3&V;5(8}eTctxz-ikK_&BcX@Ek`E9{5y_htb@ z7{}oi{p9pOpAk;8Lh|hKm`zM?eZUJQ_#D}#hFVy0wB4$1I#0aX%@HWYGpWx7XKd+# zqU!2|wf{olEZK#uvpWkg)K>9Y$uOK6Oys=$Y8C2UFm0t&-6PEKBBCSqgP8DSIwD35 z*FaAeNvD&N{x+%huvssJ0djBFx@v?ozgm|vy%>^)D9wHG*4*Xe`;i)5?4?O(LCo2s zsgXA!{)1GlGyW6Jg|$pRE^G%gjb>dhBRm=Xea(+%UhZO0O_YlgB9zQaj@w@dcVOg5 z6z@n@-I15+UhQFOYNI6_4Jyo1{xM9OgzKE+TDp|>O!<`q^lpLZgXXu+xI>o`1%*NL z-DnT3bdIt1KUdzIQ+uk3VQg{D>wmUcGvHlZbBoka=|PftVD4wI^th5q=kN^0Vi#FSGBcxzTc-8>`{l$Pm^Y`+Y+{SKZ`+eoYOp>l z`e+2}b4}=ZzT_I~K0nWQ-FnHbC#Vip2NR75%0zizswHBV3$o^Fv*yzv#Ka z4LMRI+%7`|^|);?ug6AS59`4~eW+#ZvwBP+x7+AvjVx6i?g(n2=jOL%&$c&UwXYr( z@3NANdi7|+?N2ohGq#8RwqOh9DgXc1U+4dS^nbqlKQjOK1#`g5C_Bq2|yz78At+>f!}!wsoyQ90G9Q2sj2#fZx}929DPpk|L;HgfB(_{`;Y$LkN@cZ{jc5so9%z7{>ndEf8+nP z{PTa|_2&!98Bui07sRqK8>-ulG46t4V9RG4*pEHZAFjb{+gJ(*t8wISnXsh ze%_qDo7&j_Q{If?WR~YIl<1UiTZexd=BZUn<$6OkMhZGTRuM%uBC# znffntpSbn#Z$(e@9E=QP-B)s^b-jdsu1{x;(McC;P{Wi8*%3Bh9$nsZ@iY{Bx7hIU z^6S=x6OuXIVrOe>OpPYz(dY8uZLxBx7Ei@sKJDzBd>p>lBV)EzJ(Q5@*HKeATl%o& zXQ*2~cM$2$q6rw&h5Ibp9JmfH)rsMq3ie0oy>-n~vu%(pH-nCcauqW4t$sKAE!U8o zaVLU`<|KD&1Hq@a@8KNg2~d06UutaBHt{x3(8V@(7TJDONcn1n<#RNo{9(Yvi@@cH z^wO<~JhyP4$;<%)gJ-cJ_tQjGVn7S4Nz&Ei=@Xsqen|4W*(%=%u93=ba5}-$(^YL* zUPY5crR$%lk=OT|d^l`{H)(~c_C~r=7N`BN#vIsO3OFOb42prwRmYp#pXM$k=pGDn zh!0HO3L4tdp4mH?@}C>2?u{xnWBh`e)OnrGI;#>kFI_96S%WdkvpZVf)@zp);jI%w z3Qv&v%xcYM8i_?-X8XpzvP|OwY{JZnZ_0o#B&N|ECZtl|Vl#{8HWJ;$xDUnpGC5E; z81DE@zp`?~#=iUQaA2-z+^Lo%y=vBOdV9pV=eBqFp~hYk;d&9xZ0)G|n+F-$%Ppvr zewP}0dE2x~qs2c;K6TLeoKQ7o^5RT>z_Lcr%2YQZJk35mk&&g8a@mFqytQ#7um;sW z!V=xLLq{}WLBP-OlcA}@NqU)vo)3;s2>n^AXM|o* zr5tH;bVaX4k&1McuGFV2D~DN(Zn`WG)Gr+9gqj61P;3_Ck8coz~M^$?{@Nt7&eSE7Hf3 z7E$Jz@_o!RzdAEcQ(hnHsOm|za?P;3SF~fcVJsb#IxaMz*i>@+Qu#C4o%lyetjaMn z>J87&#RoTZ5}?Qep^UjJd&taTcvu&Tv>;BqJ>vc;t2>#F|JX^-OZU&#&jdt{bxD$J zu}AbF8?!$a@#Q^bbm8H4sx?VBI_?=9dbxd4)1~te3TG2$U*h~AxZwB zBjk?Mic~;yySBAA0d`?YL z{0OSIIMLwRPS#yE!gH_B-zJ$@Wa{J^LPi|-DnSRI(7oV{T^P-O#9MsHJ;vQ%DLDnj zt@v28PE%L_>yypM&{fyz+D6in@FQ0O_80gRoq_gBgYr&wzEI)rjzlp5)2?L$2?Y-4 z!o_a4kdQC|+Is$U5>2)*{=s%ZJ zhHF6u+>JqVMInY5*#(|dHMt^E?Y+6x1+DLQT2l0`Q4*t~ihYJz4mX(aq~DUWx2K9G z;-baxT)8zUQpVg?bjk~_ZeNAiLWfrj7C2A7?_8#6xM0U*DGe$i)TW&UEQ=x5uIUdN zm{}t}KbQ>ccNK+jY!W~(3+mt^pvM!oww~Z_wBA0(Ym_e3n~S39icoJM@#>E@__7&H zLe#%d#0Cr8u8D7O$)&FnbvJ$x+QADg$Kqrc#t)nvCDVoJpV|(^Bb9Pua)n>>7S1tK z4ouxR?=+X^gw0s0-%H02O1gtltQtVoJ(=tlvSfu@_lZv`9y~?aAe?l2DOcW9Q9Z#O zW_g+Pe&p6ovBo0@A^dB|d z5|>uI3EHg71MSoo*z=m$i%Q4>OwbPucdtQ;a6i;J(a!jSl4v=uo+{^Sj@tx2#l$&i zbNziW0(;qjf2)^wU3+&Ze_iE6ipzA0PXg35XLh@er;o_uC;xkArd44a1?^h94`dXU ztMXRd@>wJ_@sEh@!E;>|9{CrA^PMJoWSL4pvY1_-6_9KMzao+G#rJoi<&V1o(qIid~yiHcgTVJlYmHc|l z^fGTy8IOv{uVn{^_K5Lfj{dt`C+&n)R)q?#rqi6~(C&5xjh5VoF}E^} zG*p2aD&P6-d75C7^Cthcqw}xp_oIL8-~1n0zyBAWKO=e<1c$3W)}!2_J4889D;jC! zTQc{bY)^mabQ0x=o^88EwQp>9Bdp#ehpYAG&+})AyQx(EH>is**pj_rx-G_5Bb`S& z4+j|f*8)~iN9qsAL@H(yS;^!KpTEG&eWJ0Yx7Y!(G>I4yQi`l#%0JuqN;uH5Y=@9y zWnE8{@yMstJF2=(`GsjUdxaxrp_#WRQME%xHIKDQIw|``}D!agpQ;X!R&N~ZhiJQ@sa{VBs?18 zW1$P0vtRphJENG=-lM}j;!TwaGWv^up(q~n#-`}1RSUn!$R^n-gm?O02Nz4{a@g@7 ze>=m-MPf@hhQ*y!Z0CPciRhnhKNFkQSY}IKeOg%rQT4aY7#UY*wtUFRRSkdWvE*_N zlNu2gdAr;k>^MI)BVqbzAi3QrnD@iW0+U-^hv_4AuxToZ>fr{V6&<5RuKuj@N!vMS zxZU>Uvnj^<&%>fJ!>^JZTuxCuDI8A-FZLlJ`EWcd#keiQJg|oc>h7BdiBI}i)vB+i zm&m?hdxrxtDl8HHQocUSEAqCa3z#U+mLdxx;8$~7Ngos`Uz@MD6eJmh*dSGM7u;1k&Y80$+#PA(J`SG`K0)OPKg1h8pn*&itVnXEoDuGgHjihCy-dxXQ)WYI z{xGi|-OGV4xI5tKkj%h_VBk0ZQI+haVtYKK0HMa1{+KO0PNqv@aQMQ|M{Vhb_HceE z5Y{xrKUc^nMNJ`iIoTY$qPlhUR77d9=v1@4vROYXy6cRgs1F8fwG?Y9&JP7AuX8xeY zL(-7izU2P?C(ddw_%&RxTrlX&Fju#CBv`5vle==}L4Y)-Ad62R5V-T)M`ia`H;gh( zOBidVz~(uToOw)7`sn%+)z==XAeh|vh8tZt#gf!BF>wyvs#2r$hw+m#(X%G4rdr)1 z;uIU3@5BT*Tbk4kyhOaQB?y)W#wl&!a++7B??;O=`|HB`?4qV+E}CUehwg1wiQ*pO zHLykNDxgW@mUqFawh$9lm4{ca1-V`C*~Um2+LPKxkJwOGd7_ZqIJa7nc_pd#h}1`s zqV@+-vU{$U7H_B%S~9VZ%v_f9cUoy@)Q?Zvwb`%>E-y$i&wc=TGuq2d)LUTg@+!o( zOuNo+w~!tBhWRU8EVQVSezc-bSg?icW3a~tNl)&9&Ff3-yoLZ$RgAG>uFLhxmW4RA zXI#T;3Y^|i`=BNCsoO!rGgm_clPFo2v~3T+%ZX=U6*~qb-1R|zDxplO_R&HiN9pKD zamu@JTJk_ zy*%|hrFoU%hern-N2N=0kvlpDU=v_M(nRhme%JjDl6}$$Oej<3YU5qE94TV-hDPP7 zwGt9xC!WYdsCJrrgN&&ckx-3sh=kfNl=zT)=*>z=1a^`tf%3zh%e~1WFEvV}Zz#3v zoRhTfiAgevNf7=#o}ur@_Y2DzKzKlzPUPn01`&36@m+DQG?C!VqYdA4)CG0Go8vO4 z_M4O)$0HTt#a_a?`C85;-x^Oh4G|nz6@0`wIKpLwk|Ke2BlVErc5Pwx&_1#LUns!? z?|cXvBKyOibF!E})|jZ*$#TA!CPjsMZW+9a^)5;tOOY@zSBw`r70}b#tAw~jV`vf6 zQMmAIrI&96ve8#gJdw56W zwX<)riOWFjqZL*w=*uSmVl?3#TBI_9`itIMYrD5dtorZGVnyXjQcc@pR^!=8$Y=mEC z&CKUx>dB&Go&^QmGu^76X!0W_WTGgW-rDq9C)&?i@g^>Y;OcEjpJQ;u{H(p%6gEHP zhl$C*Vw|avYxCdeHru-yIwkb`g7}59cb<@hok1G1VM$_Fx9!21wtAt~Og#G`e)Kd> zl{sD46_$L)=^q3LmQd~cTT1zJGHk7 zk6LSXrXA$Xc3f9)@H=Wr$d~mNeT$j{JQ^z_!<-X7y*E{Q?=1i`Q=Q3Rc9uJ4uve}; z6T!Yxa^JT#CH>wlAr>9^Q@6QGk*aNZ5Bm`tY1`ru&ZqpGIA!*J=!3|-A{#l~in4%} zbt(DK1c9wCo}-OWXVOzB@uI%M?(Y8GImZeYLU%JZre)weq(N zmcPz_i~dRLxAFYf^N&$*z>fo@9sKF_x1Y@4|L7Ft9@PG&{xfb&?0`eZr|B`>s_g^3X-|qeQ58!X3{QZwa!T1kkoJf@CK>l+IWHkoVYkJq_HBa2HpLAVO%^LDk7 z*RHZBse%jlifzUn+zDHKM&s#kFc4)57zy;X8#hg~1i7+PA&>zd<2>@=yv<)Iv`bh| z`4&3GW(ZI1+PHtAgv_?I@n5)@kJ4%MCRy)5JjqK~_^>f*VK8CxuLcO{ig{bIh%EZY z?HOzBfdV>T-)c)z$pc9t94gr>3fGn+Xw;D#y(LW5+mL(tEHPnF_&hJ>HI;QNcl%q9 z_2N)ZgNhEnPJz6pA)B*=o>dBs?~YW)#-bBF`lOLb9#LWAZr0)w>D+r8VH0D#Rjp4s zCK(Ays;aIS#h^9WnM6+q-jdJc;zi%V(T|hT8i@)+kyTh7l(%Cos;NfzE`Ot^qy0I9 zl-_Z%QdciAsdic0X*ucTPZwl?Lg(vNsOw^pL!EkV%g;6(kIfTHhWhH(sIU#5aUPrg z)x4?A8l8G|j=eYT47JTO3LWHU?(o+LG_YNJ$ZKsQXJIvvqQFg>is`-)AluRtE=*aL$hAH$!?BQoHAOGs^rqB*-|Z(Jux zPqEA0jc6iw%#Hy1n72dJR906F);fOPG04b>HlD%C?Z`^63Sy4xX#0r7g1fZGi5HAH zuhsop9Ll@uC@55xA{@B`d(p5fFNV)txr(%iNbh|-6<+xoXKW#T6vWuXtj(Ww#qvdx zU1mMM*W`)}mN43Q*OXx39}N!AS>w$BO;Wcl5iy&9aXor+!Z5a#S^W{(uehkVJ4CKhS8j;QRmDyAQZ1 zwtP?EEl~wTq6DQ81e7QsIVmbpL}DXRKypr!lSq`DM4}|gO^^%%k~2!qIY_3P9Noix z_s*L)bDg)lvwLS}Z?~WCr@D)CtfK5s+jUOW|ED9ZblD#5LH(s1@vT6hg&Fk)-z zi+v)^pdJ%R5rX1$Iq5)7JWFIw3ydvO*6^&>QdEoYohmDkMAgW5$OfKmOk7$MD6H$S z=R9uUN+R$)_GJ7n(@bkODF zLRJ`WsdNR##CY|Pe+Vk|d-7)9X(&8w8L#ph?$&%3b)d3a@?Oklis>C*L8Z89xmg-{ z$KC8}z3f$fy! zsymxLnhb_1KGaiePl&X{1Cx7jQeS(^nD+1;%w_YO&?Vjuc=Ci2v$_MV`U%aLzDqJcYmrx)`T zaR_VzddbC^&AWzR`Rs||#`CyEFg7)?J(9=6cz318g5}-jt>~8CB%?BJi)N~{&@X#* zH!JE|yIcnJ5XXw22U3ek30gG}u?iN`VFO!PGohxfNlP}1agHk z9)*bniTZuP`sEKVpXEn{O=fEN)%0CuYsTlx?Si#Xyvm0=s=B>9eQ$a_geA%9qp@mK zf!)NurQtK9k)XE!xcBz_`{!@xFKVd!Z&Bm-3W`d9VEvRWXEwY89}?*Im+o~p<9gWw4cKiiO|oK^o5?~pA>|;ksK1wPmPGEtN7==Z)r+)@bf$dlliVn-dp>A zAtyyGa~h5aj8_Q3qMWAd#?KR)(LDPjmNo61kChzgn-O`vD=Xi#zBs%Pa7VL>eU_|T zGQe-YLyS0U)3lcEAN~P(W6&tm^X{mZ$GciQWLN88*T}(rj^2Ovambp5Wj-|>p#ufS z!D?}s&&j1cM;8-fgQy7T#vr&`4?p_ZCsy7TE{Ec8t&%gcSLd+kPF6XNzfjD#O8SfN zufql1&3VTdPPY|V-Dp%V_H@v@PvlW&xY9f+By|2XFtgRqa>8ZJcKN=$?@rz@OW|no zhtk$%b#v2&!g+TJTQE*F!}iufU$iyWv>ig;!LhD2;O%BQtoGQ-D!QDV&zu@|9~WKljnSv zXlXqv+k3gS^G;K+FNL=Ifq`{)IEHR?;Ol7;Y|lh%OH0RQrscz$Yq#EmvFXG8J;^QZ zlq4eCu{bj^Bbw5pSjV~e4a|$9R`E%*>miy}UVB@mR8cdF+dm+N%C7hdY?Z0of=}mW z&?Wap&y<2(x5SY5T}l)VVBc`CKV-PZhxJzb4Ti5c+4K4>4rfk2zj>q_RnoToNS|~f zaDZ(6Bm3CvX(W4-$wMjMIW+8p_VvnYOFGs9lpQ)7QJOs1j)qtz!v+ zs@wF1X_|L1Km+T~luLTiy;0uel5`}vBrest=&YA*5%%`Tn={1=r6~g zjXSMw-akBaH7>q7uH$lh#YR~-*y;sGPHFW3dRFUf-p==zhV6XpA3EZ9@=md(HN@NjJ^o#sGzuKzU zWG*Lk@v)obffRKO{nyZG?}}305J!^QJU4$3dZB7($Sina8jsD%-=Mm(d?e8_ECt-O zNCR7vePf;yr`5N#El(mz%dBULc`>JrA=paGF{%L!M;dhM7&C^4?Diimon0Dl-fYoi zbsf9dw?4#>dANPbv?<`M%yZ?@Fz@QB7B<)nciu~P!h@PUXGg+8X*H|sfp3_Ckbz!P zk=h3o$Guan937$e*R}Y-Sn6l#g~J;MBGdV+S(xdcBJL2*NM8?#R=eXJJ8KbHCPgE0 z|5Y0{LqK^B`WS>V-bPbiz}%{XyiywHRoyF>_*Z#Q|0#+SwqpSYx)Lz#+T~$$BR%ku zU~rixjX95?Tb4IVH=4eTOcdXZk@J=h?mDHc zlei-xF(npf%N#GFX#Dn~~#Y zIMc;oEDp7?jkO!SI1?=6%h1_|*JBmiTa3WKNEp1A8?dF$6Dg=!nE#u^kpr$VDr@j;t@jQ|aRZg{1x6+n- zc^R~%5EAV4Yx3%hkGJp4>tr<(4^waBPoR@$Zlv8V)OVXB&-ax$VoLf49)qY+X<0P1F=VPK&D+m0Wh^5aqY@u>WGRDL`vKOU7IUyjO;N9D(( z^5g$}`SG4$4)`_T1-t>g0Uy8@@B{pT0N^bU2m}GaKnM^DgaP3|1P}>C0nxxaAO?63 z!~$_ZJdgk+0_XGBAHXshNC8rTG$0-L2xI`6Ko*b<`UD4uK=!7?4H>^*_M!4EO;+!0XGnxHy+_aPY1WUd6jYa0LhF zD(O`MB4QE}5?uUiWTeDogv2DoKgK{l4mt(~CI;puOw3EfcsO|f^Y^X())JK;kA{wc ziG}_D&-o)NKOU7IkIIin<;SD)<5BtXsQh?Temp8ao@EJ}O5=$3-zYzx@BhH{=is#D zm(z*!<@q&5|J@@AoO^u#_4@z25RU(@`9J(Q76k2o`TF|?uzlWN>z}j#3$}j(w*UDr z_3!`q-=7wo_x)CXA9em`WNc$=_sZVE(aGzLw~w!%e?WLdWK{IKnD?Xl$MoORMs~%HZ`}jwzc>6^$!dV4UhbZ@4rUoUmVwbJ}ODrfc1^JoL>3t@CQV{ z%8X)f*VGTG?Vu7-LB|kyM3T?@jg57dFw;=OWk{gl0iTYO8`qG7yF%qL4P1K|-Vi<> z)G}Jbky5d%I1xM~KCp33%AaD`6ggn;&)dwu0}m@X_SEs)dzuY7EXx9=YxWxCs?OSi zNLZd=F>eX5n4yjxyyyb@bNJ@Q0@32Wa#oXx?o!pT#qF{lO0lIvDmgz!= zw9;~1d+VFb0{i4}xB9d1?^Suda;PNSF6^WTiLRTyl%*!Ytg*Q3)K{QBQ9UGQYeiNX z%KMB%SI7jt@n|7^pjeFsZcRpX7C1*+YaFmD|8DI9UXG{d_U04liBplChK@;;KVGE2 zPfyn^1L1M@?12909^Bqo9eDkWqWKFOXa!Ztx9>LNxlopNOv1Z2^FB4tolRI#XjBJ= zuUD5pyXEH1cNlWa)n~XRGD4_S9j8m!B%!61?nESxOWvn~leAVIuyR=!>50BJJ_p?# z%q*`V4WO$myA8X3zi#Qe`weT-7x4E)dE;X)$;gC^K=&%Oo8zONbbUqfx|ctM^cEO? zm*?mESmDDvu|dO3rfbRocc0Aq0V!c^>g0{(7?~~>zkz?O#NfjH%|{X*t51vQc$guoR`k&%0albWYt zHO>}nEb#&_x6jtK8_PcQ>^Zl}3+{vu7cQ?>D-cRYl~@mbJ$x?0Fd`x3WYBpQ`5d$B zZPjsuen6Y4&Kz!0q42nAZjk885l@Kqs7J2U*}rJgS|rEfa)(JQ`%b*^2DId1b|}=C4boyOlQ-vBG5Y*g9G+I{N8n9;=~c z^`9pT-C zw-v=CiOZrB<1HR7)&4d>&6V(I8*X zJTc6xUk8B--?xgGvSYfk=us}PH0Ef3d1)iHCD?0_!|=3Yj2v@fQj)>^01^KKQW4ul zx@Vmhwha`Aw6AhYj%nqDjqQFPVj#3QlI#F z6)wj|bJFdD(3BdcQbvy5o$n3?67Z&XFGCvpbgmeG|C zer2{tDRC$%aVqq_YZwqX{pqlGQa5iM1GM@m}j{`I^_R{KQXcHOb%XEJ$JV;F{ToGTpK zE_S<3^8PI0UAfD~VKLFIRL8m@ANkf5cUfegyzj#?gymfIKSDgI9-K!K5Hfi(~F->(^2F@u>WGRDL`v zKOU7IkIIin<;VY<=EqAyWFX=YEy!I64}=r^VTDLRo`E&z4?_S9m4Sj7-?HwAwOnw9 zJb>H- z!82E|9ZQHg#NhmI2R%XUjq@kJZs)J}i39Y}`gNcFwh!h{`*8lW57tln-1})C?4S1G z`e~nEhIfDW_u=_zpI?SVf2R*8IIcfC2EPpB{%#-kpZ57Bdh>Vt{QOv-pJ)8uzt8!5 z=4bnc_tQQId-1wa96;3hx=oPTcI z0!uo89$)}&19t#MfC*p*SO8Xl4Y&)i0~`P+a1Y=DxB(u37vKZ<0Rcb|5CVh&5#T=X z01yQp0*`>lfEXYSNC1+66mY(O>0l`XJOyNdXMh|a4=4bNfD)h#zyKBCIiL!t0qTGT zpb2OJF92;o2hasx0(yWxU;r2bMu0J30+<43fH`0RSOQjnHDCkS0(QVFz#eb_&iC61 zES&)tz!h)<+yM_jP8*xXikQPQEcsuPKlmq}KRZNkbVoNABx1JbG=3&s=y^LeX-V-! zBlR)s+ePu$-_c(xt>WP~Qr)Y1Hfh^+v(E8Slk=6hV3t2wd<3|fsB^YA*v%^1T%{qN!(%~r?F;;)^~ z%A$25Rw!Hw)_KS8({7UtTz||bK<}^-89qS8v=Byp!z8os+CDm$`horiPM{zw->R3I zoKZf;3ldu4{uRl4Mq>Pd9v%bf8DFk%YkP1hGhtCD8aMg8D2Iz3--s5T|0)bw9fkTa zzdXKX7=9*)1)s(+3wj) z#+2Q=(BJO)$%m=m)nu{l?4HSJ_38qnQFK0K>l^ECY;=s{*=p(MhzvjNp_>SyjSRZz zJdeTNoAlyxeGFt6x*4(y8iieYO33m=QU-in=15(pvQyVZXtUNEdo3InJ(~;}{5YLX z)yY>{)iu83Fk;)=tdI$c+0V_;w?t>(_#=$?D;sd60-}2{A!#!lmpBPKUcMIiaVzm z!Ag$Y^3&l}5oa=__=MIjs)+#?B zf^H;heIY_Hx=;P{IhdCvcllLznUoGkZ{!VDxud%nmv03IGAAoh?V|8iv zg23h36NOo>t*G}EEZfZ^a^tbfL9F)e!50;y_~=6_TQRKphoA_F)1-zg>(Tz|?C8Cf z(}^n?ztgQ2FcO;6L6C}KbM^jj$oP!MIwibV(#O)vS|gl^!ZB7) z=`&naAtEmcU%4BZ&D$c;q|!kgp5bWWDun;6q}s2Lc{R`=&t(h|(&VK!G&wl3ZDBgx zDoC1_f6d{`js{Dzk1~%9UTRXMkVyxIg}{c6q`9Qf>UT>AbMbXLq#$E>jo+!0B>E0l ztBbma`T-S!KR)Z!BqcItOW_v9Vzq4^n~ynpj5mgrR=oHnNhvOErbnl$C8`k_{7?Cd zho)V)oL$-8xTujPlWvYnS$~+b-f41jK)murKt(GI(}GBO<4AqX!hM>bD!`|#nylQ3 zZ69FvuU$(lX*pqhmdITG5b1@H)H*c6Xqo?5=~xn)IGyt}NKZV~K>9R$lqF+CH-P3- zIrDr**J6fn1+QC&IOFl|yQ?1t#~FsZCt4g^PLU>E!nayiDs8C~ zi=@Q+Jw(FbC%1&sdR^T`w<)uGzD94fyUH(Qykn_;X;6^GvXerqP?ikunHg6WX8AVb z7g{F4U4R{GlBwlgCNEjUr-*(!qrur|Em^Osf)t8#O0sRSC4bO^RZ$D=RBPbz62da> z3K|Z_pk6FIv9n3SoAhvhUCSKBW}p`FNyNZwFS7qQb!pR&-?Fwacp7)(2?b&kz0uUg zh9=D8h132AtgM;ktH@SK=n^vjmKmSz56HELe3WugUy~X{dTTn+<-bL7yP3#`rv}Ov z2UG^?%n)6V8TSNPHZ%)j`MA6t;*e8Gkr~a;c(q4F5)E0pT~_X-Ze&^yw9RIv<%hVD zqDe-6__5b<(a$!K`8Mdh*@F>xo9`%Q-Fa;47#)^7`MpfqO4o7^88Z7YU?ZxzFCYKIM~}tCB_yT( z;Q7C6(tS()`P3z)fgOUK1O!guCVeilTgsdDqjsM=-%;6mga}jD+7CgcIuG!5*xZ;L ztZ4&XQ|*P@Wn487Yz=6uFhkk4>nBS$u$>N=d+uiWkyltg@lVzJndIs%5@f>0OhNscHf7(K8(D-&aK>y`I9)-JK#C z@3IZ}0Sd3*Gj0fK+&y8}C<|gWcj3z#s|bvlNzjNGzCI7@(Jzk~f~Q26maRCkLSg%K zg$Z^q%b0A?PkW!@r#sSDVGf2OrLTx$b>wGaeYRJRS2gBLiBBae4t_)BdkABqORve( zX`^NA%p^mfD%;5yxx_$vd_6mfYrT^1)Iwh7;nSjW`71BflRmWUz_IIMna@}dP3y=z zhPqZ52Bp`$juxw+_zuFy3cVZAV(g=7!3FCpiV+@nCJBo_AJ`b{_%S1Ivry9}bhU~2 zn2dN!Cje!rHWY#! z1gT9IavS`xEaP5o>}WZg{)o1-sLtom;;HM_nlEHm+|#vvK}E9FNQ0(E{6VHD@mE+9;$w}`4UeSb5AuttR~$P>9G^0gy@<1MEqO& z_m3gFxc6I@G`(jNxS^y*;l)M^TFsm-PTOWgiYg^ll+Njw9Lg@fA=s~ZaUt)v*mAo- zYR8@#=a4N6xo&M)VIW>K;+nhlHIaT84*W}%Tt!{;ev=c-hc%jw_(|TN%13B0=P6T< zpg8|&&3&vZb0=Na@)1i|xhW^z*m5&(cR|{)Z&x0V=K05H3H~OE{&O%_HtyGf#fT= zN#a3a<^yemS8qIe%hj(;583hKj3I}6p9Oe#L+eMq2WxXH7~Nj6ZjOG|Z+RH(%;4>5 zfE<0eh7BG3BusK3XHc-+_&~4;JL55YRjd?CMzVFTjj^VImZZ-wH(mbgH`-VNyA1on z=|nd9N7n8abA)kgwe1LcjvnXa`c1TATs2nP#@l#ba<)(HtR#ZH%sHX?-fUirdurYx z1g6k>QjxDM$>_8r>=tKi+%i0BUU*|KOsMoBa}vjy;1H2e`|}AQ?`DeLxtO*yLKE!z zNp>|^`q7!{3MJ92ljH7;Nt)j9wT;r!Iika`Q1>_6C#&;H%|Kj!Hz$j(w(}Qta3tbHcpAv*)a%Q;z-5nq7%oltLvRlGg z_yH~g7b>2)-6wsZGJ5jr4Enii=)|Gb>W)I+H*c5Iids9NZ@ZBxDHXFj&hZu@C1j(c zDg;H(>*|gMvGQFx)W%}I7)f+x={@d6o@ct~&K9vE|$7)}@<+ zpBWi1-EL8%$Yc_F+RR2WHQwNbzDRf2c193@lqFdyI`C_ASYld?3_1O#k^k0>TGA@L zP0s|afo@F4NYNULABLHk-SBa&u^O*&4hdglA?tH#Q$bkm!nRUZdVX3#>$iD~-^Qjn z!F<`@jtgJ=b*%UA+ur+2{nI~d{_pwm`ez>hJNI8a0ppJRzf8iN&wc*!+jj)ppZfLw zkALQO|E=#|PwV`)?vGslGuua%5sAOT1Ol7J6DGLQnK0%^ec^(yC3k3Z%J{2SM= zM&*B_^1o5}->Cd=RQ@+A{~MM6jmrOC_*dkA|Lt58+4;c#U)^8xpYu}x^*)F{a(>_+ HZR>vk303os literal 0 HcmV?d00001 diff --git a/twelvemonkeys-core/src/test/resources/sunflower.jpg b/twelvemonkeys-core/src/test/resources/sunflower.jpg new file mode 100755 index 0000000000000000000000000000000000000000..de2a7a644adda387e2eae956a7db7496b79e9ce3 GIT binary patch literal 25094 zcmbrl1ymeO*DgA^y96h}f)m_5xXa)Y+=mP<0V22tcL+K-8QdYbhTt+t@E}8QmpkwK z{pbAaoOSM6cimIHs%q8V-POglQd?5gL*=idNARYes=01`41paO9Kp5eqs+K%pC z?w*eB?_ctBzX6EIsc4|U0skbzf6f3PA&&p5|J9xUtHb=aL=^wXP_h4QA^oTIn(*Hi zHsZSfB;WM=(O2_Hg(1TaCA|Kl9Nev>=yqyWNy?*7j+&`0Et zi2MKm1S2ZKaR3DX>G^*h4P}5b0_hXrwW5=&-Ai8&CoeBM&`WFom$J6*)^;!d=NOf} z6hI8p^DzMbUj@MSpN^59VSrEoE+!@>7A7th7A_GEHVzRP0WK~983hRm83_pm5$?a~ zzpDQ<{`V3I4+jSiACC|ppO6$EAD{G}0-y9hmk|Ab>EO8sK!k~~R38P20f0<|ghGV$ zyb3@Cpg;E`Nh46AA^n>Wfd5G-h_?_G4IKj$3mXUVj{UDnBmgqX|4GTk56`~n`Ir{po1lk`qDPHj8| z$qVbgu)Iyyw^isZ@)CM64cxRls}Jp)@!mRDe4SDp){xpiyM0m8I54+!X<+XYo>tm4 zIKO)(tYqlm8<7ra9$MJDCITQMBhaG$iwOfAP4Hg`uXu@35eUExBxrn}5=s9-@I!a% zgrCWJ!{c+3ydIg2r@$#Q`ab|LSOj~g5g>|Q*eU>pyzDmrCxriZ1h4-a!t*i!7vzygwg z^)a-F2zmOTy-?Z#3u9%c>Z-snvBAr_br{H%A2!F z30-&fTO4c)ETw?~gBvw_h21*wJtXfDsf+;!z)E^ZW>#=a%*u{nF!y-6LhscOdT0wV8;4c z*$f34om$5hiP&y!ZEmKMYGf+yq;yvuZFoD_tSpLWnBFP=ObU*D1`Je-of!ue!yKB* zz_gI2FZvx8CV7!)l8~iVgTqcKo&o;wTbiVN9xScbo!qUV zK`pk=00;V&i1U!&Zm4#vZ1D%oqPhE|koo$%wmulj7xzn4N)q-iIsBSEwV^Re#oLJ?Z#idY?FsL9V_ke4(o3%WrKtFocgk153}moF7;#` zpc#uz$(n1&FEgITYDYGaPQLgzWEi|}my6Y-9j_K>Run}kG7pxgm%SMz(g5=Obla?z0NM|7vxp=svgTF=uLVrJ+yuL|0d zyZ{7bySKh}cJ{VXJ(f<5_c>22LEqO8+#cZBWR-0T1_#1I*>2^ZM+9w*0EJ4OU0r{@ zyCAvF+iAqYM1-t< zL^Q+=JOlF8B|y!v`We_?pjhLA*&xg4o!x;Ycto}_dw%s^`?xX-1IJMLR& zY~+G(Ii&g4)#K>S(r~~VZ*X+1*ujviuHMEhI7MGlgWBfG^~t3Cl+`9pKAom2izJG8 zbF((H^W?*A5y{x@;493P?>n0jxLZ8G%I;cPO3s6`AyiBySe$#->(4W-^TCj9!(;<3 zWtTdf)oKGbsT@4mbw~Yd$-2CCkD$~i&tpgLBZ=?rGv?8Xz)no0jV z|6%XXuSniCnX!7MSMj=`Ca6AGO@GBiFn zPuEul?_HoC%lAe>#?s1!jDJA2??HdJi`J9(mn=4U-W~q@Nq0)tQ>dba$OM<317wt1BHpC*6;E z6YfmYd@!3?eogQ8Df7I;06T42DM?uvX-00=s%-qF&kV*j8yO0DDabo?Uw5mm7dtQU zkwNCZH_g<@(e2nNkn&;6#A@n?w>!Pu=;~kRC0dHyp*gaSw}M-(Qor^HiaH&Rw{@4%(i7T(bK2Chz{1wd2=^ z1QwoWh!E8_?&f4rp!GA8Je7_qcNx7L=yN zk6xY#IGyO%xOimToteP0POQ3fhuT4t9vS_#=y28@Nu|>Oy4H9tO*^CACYXF>T~?lq z%cJxO_&(+ZB*+MY&%d79H8lLradAXi%l(1wZ^1#edR9d<%NX4*u^G+0lpiCk4Krlw zVNeu(UYtJYD3HS@YSoB%6>`0N_Sc#y0#NRv_)wkC;_jme24LaS`lcw(*YisLT9Oi1 zkpQ^hvM9LMYup0AAP~H}xVq>9^aPtFWMLUxHTI9}V+yL1Nt~A=2G_e?tk8qb_K+1^y<{Y$CRb(L zmKGYsnuMyr7i&n4(xXAO*_=or5cE|&L4j{|W@%M~46*D$xf zL~dNilf_@Vjhc7|_+jH|qHi;X10v3*R_*PM>^SYJxGtQQ7aE8+T^#5Yjx-;>v@~u< zB-egPa6bp?#C*z)GOeSMe`LREEslH@=VDhJrd7V)eX_a;u9cN*S4 zAPYxXbbIex_MwIH)eh#P*>@npfrFrmZ1!RMhd@eC>40X8ggasg&VWymq8wWF2Yl#QBfE=2J@_bGY5G{dhB-=RZm>Jl~m z^)o=a>|VI?%Ia`D!1U+hUNF2{>E7v8ui@j~i{RXt=lRufyC9Q_!vg{LD&o7)ud7p& zO_H$SFDuoLCV%`M8A)5D=ZA*@Ffb9EYC(_sw%YyCWTL1tfY-druZs3ZlWYQBld${W zU{CTZe4y|a^iqTf*#uKu>$sJlwJh3nG*KdS_*AvZxM?+Y!3|U~7%_h$pXK+mMybrp z>RxWp;n<%!jrI(vH-YizKERY$8;fqog4nK)oX;Ot@+z~2 z_=Wk#7WY8k9fb)|Ob-GyYp4MX$5!Gc6#XZ(!YGjf0!B!gujpQ=8^t? zaT)*60{`JM&{0uQ(NNLR(9p5a5g7*)9UT(~7aJP~8ylDKKg7j;U?Kqh(w^m*mZS^WA)1TYB59|k#SyaVQuR7KnJmKEzdGRpNu_LgJGguN+`Bpiy+N6t8D1jNxHp=L?2Vl^0{ z68jBrd_<*V@a#c0=P>|CKmWQorV&8b?v21k=CQ-^vpU$qenr0g%^g?L(%SB=+wKoa zJ=qxYOZ9kzWDfDXr74y#;Q~i%<(lv|BIkx`ux+g@Jas%#WF`_ME1-s@-3W)Jgeev( z^3@tEEUIUP?^|-}rLd}JbSV)q zO*O7BEmTKCf>$tmjyHw6(b7p9siMibOv)W%9HR&cXzbOrVcZ;!I-FVjqaJZQ~tRSAk#aGOoqrQKeS2w2HtG<6FHtVB1z@Zyh3<>{|z0i6DA{n+;luV zOVMoYW$R0h$`wAw*=!tdvI$?6)8UYtGj?ephvJ8VN)}iE(F;rQ-k#zwK<(gGEHg{k z1kHUH@mbUKBF{@M>WVC0RFp$ToXSv$GdN~UR8A+fy?(5X$jdIsK1+z%Jmu!s3>}e+ zdHKx5b`E?p+;sXjPKoq~)2_Xt1Lw$&P3FZ0$dgN9EU{H|)R!wbXQz{OCoQCb;j?OJ z=8By{yc0upRSNA}L3Su*ALFsjIB?bFm?W>(-od|;%k;&F6ol$$^Q$XmZ~YoaqcI8IoVIO< ze~r5Zr@X5)mi}aviCz>x&pDAqU5CB-b~p=Dk-QLX#*CF#ofS!)jPR9CSQ%EGA$G>+ zAWpGwAySsiZs0~`hSps43YXeE?a$gtpU8iWE~3@ETZ&zg%Z9V`2#`r zVq$_ao3Y@6ZIIzkh#KqKIh{YOnRWDt+oyzLU^frMwR(10*`h1^YUTLVD?3`{L8UEv zSfsz<+;W`wNN{t(<|D%BTKl~rq?wsP(>K=Q{f?)S{dg6kXhsPrTb3Y>M|nu2$KT@% zxaP#MME6qXYAfAP=1}MIPifUcIMuaAGQ6TRBwrd_-6I7#=W z=^{Ux11H*PN{aFmg)w4dpdq;SkI1QGI6BCVy|V<*vlpj*;h#R@_W*9*0p1`{wt#R2 z<%w+V1RUrR3?#M=EGw9D8m{=4b6%(UiZ2Defd>wZk&79Va~s$*mRAo^<(&3dw0mq# z2(xuv!19;5#X*v!*wYoKV`B7#_>7~X3g45=8bBynhZ1pvkd3Q+Ut+NpZ+C&PxVCTA zUUAe_Z~5B3{_zWtz2_B-nQ7E-0dXcL*jsL=JxIvix(Vvmwp%zt6J;k&=#RfkZ%ia$|23iaVjcQ^P@m2ugF!mRD}*P@W-1UZ+4#w1DE}NFE||iH0{4y zY9DY5=(I29uBZw^)knE9r1Y&4Rl+M#yE7CK1LMxTcPTH^K!FNK0kUR@+l_UklaoX+ zy1Tf1KAEaC$B}7$L(JrgH=3%arDmwD=NJu_ZLeUSv?IjRpJWb3SJY@)gUDRnJwqyZ zCN=N`o8Yjp7Sq_sf0 z6UJtoC0qB@#!7v{9R-!KH0O)lH_2A9taO!XdhJ?)X{j_MQ{%yI0rUTAocd zE`oe1butM;VlUI2ELnfli`y@K$ul3W&h|GC@_f91SXimvBlTO!;E4e6C;$kz?3m`&BC0q5W#c2D2Q}z1G>JsO zx_0vmgUspG)~CDd{p|D|E`{1zxXCj>mv?pY8Nd%$8X9t*ICwWYxMQo}kdP+3i^32T zyy8FCsJCRk7$Cyv$`-kZ4+cC!q^v$fT1ZmHsU&4eStMugQ;(*!xYwH8~lw1N~NbwW0Zi)I6Z1k z%5=`Knf&4l*d;s4xMNzV6cYXcogp^|D(h5Q)`qp(>@75nMJYIQ1HLiH#4^|6JPMScaxIO zXMDWtxRzU#+iA@8U_G}Kd`@>!M2GbZIGtR+jxnfFrBK`CymoRIV3Vw>6zuq-Pf~ze znxmj}NU_J5#WkqLi3n$`3a$+MH}0>vsdGnsoi^tCc7b#q|Eevw6-aa-%??ygdjdB0 zLvmcC4$Pjm8ski$}^``A?$1T-AI7=?=z$&M1zt9Cpm6pjRnxXeZKM)srzTCO} zw&C=!|I?bd0~i`-*Q$^vVibZ|RVYRVI6z?+cd`J^PQ-BuwY{J_EDD0J^l=+Iv2P|y zI>E>d(-O^<1g^$qA6nUDev{3VAq#}h6+6wH+6Wyt8EkLP+8w2Bff~Fi9-1~_O$+=M zN?dbxJAMWQvGKYBc;;-|z=>|UIHT0<*@kbk@8Kc1(^nA)I0LQjV$Xn-G+6EH7FOnI zx~x2r37}+c8tYmk#nHvJ2BvBy4J8QLwp8`|1+A>7hrUb7X9l^TS-MN9KlTqIL#;T; zUZqA4gWiA4TlUqz-j=}elUo|)N7dC;CR(#{Fxcm8%64aVE!!54{a>^1m>?J~2b|`T zPHOQ5F2pAwb7%LQ+qeGsCQuQ*;%9}2^DI^L>O`pL2Wv2iMD(ayx>c?xQ@+Yj`H-cR z(}#OEf5K+AA?_m&(}b~J&oii?FVpv2d5fN3D{aJh{&knTytx;pUB{K_uy)_L-Gt_y zQSHgI_yt%jsF60~gLwfm@6>EDk{KCF`A4L0k4Av!g^LD)N)i=JL=Y+V2tW`Hpa7+F z5Ldk`Pkks`iY5rildAHN26~zXI1UKZtoZegKLgaByce$!F#r(VY`b`q*A;B&Tdlc; zTe~fu;LfbU*LHHJ8YZLJcB(uF_Qw0l1g@N#gBqbUe_yx0W77>D)cA9kRZcb72>zDC zOUAQImbeI%KHZ$R@RfQo71h~=!1rF(&w#!iHu1a>a0BNxZTG{AC!saCa#Ss?Xr1)u z;Wlumi&5>g@Kp_U!}?R%joOuK@gd&Qbx5=437O-=m_oczx{6hK12senE*wXDg?~{U$HR)9zS4uK|a3>*e zwbWLvL(tASR4IStkn2=o;F+rwCov)1`?t~6aUzZdg2qY?o)0!RA9OfYTas0H*Pj7t z?up~jS`~SD)I=AM-i%47866)9Ds}a;Kd6FW@gfly8ypP@kn93 zCZ+f_E5Y`*xPbmnzES+7o_$iTQXvat$)BLo?2+CBjjEblV^6>C7rqVTD@C6dN4V2Ofr@pWoGP%|G8T= zSe#D`h6}kVu8_X0>&4-;S&h|H)peV@2rXr%0;2$E)sayk0D6R18VB8oAt53_>X+mH z{t;il2i5K8Vy(}$hiX88%>GBD9U4Jmz$8tG;GOeYkq(!_eWsgL`;We-qju2_$->o6 zdF|1}C|?Zg>-hWDb&E+)+UlGn(Z+aqY&DJ8LF8Mo*Tqcq!u`t4fZ4Sy#F$e~HzC4z zFXue@X>I#zB#@D|+h)BzByHKqp^>M2S`s{5`pH{Si|1q6&RnE-z&%xQMG2*k9cSG& zi<`$I*O-w+eP~O;jVEzmxA46p(eMZuKbMPUUk^SjkbK=}BmOjJ?_BA9qvk7+mR(^i znLC~9-(q>qeG+o^9ab!WOp)3%K9xK6V&^xX9pa0#+-y%}YHG)RUH&MQJ00%7nkvSi zczjj2WHy3(k)%Hoh1&xuY%C$zc#JijF*9V$bfxs+=8hGDt?;{fkSf_3idj_YJ4^di z4u!ig1*B`nVU1_SQMExW^76(bmS^pH;D{j8G1Es*o+>JKRm=oFL2~_|-m~5i`ksYO^txN>T~PSM!HXxY*WAnU@U1j+D5n)>e*sS4TeB zH+Y>Ejb({;C=^PbnP!TmW4EoPzMQ!SH;)B>4^N^q^EwG@e2U*&p3JjgQ~o&{B$-d< z0X%&MDF0E8b;@`xSQ{_)1iS<#8%uN9H-eAiM%@GXVp7z(EMK?Va~o-Lv{6OS$|iZg zL7E*yc$)uk7aASEzw&~>6q!sBoQ5`EtWZTWfjGQ{pLB!daoc@_Huqkq@fw#Le+F{2 zhs~{SO$-0&k!S@b)a0N`IOKWW9@d1^)6XHN76g)RT5zZ;e}&RH+(hr$SyX`D@RO9v&$tPoW!CA*Cw1eC zEbRImx)s|w?sb?8!&rS?6amR#?YC_jf_j3ONZW$v_tm?DbkW;YBfddWHj417;c;Iy z+ms>B0M)NGfOkDvB;~4~sd)yjdFM<9fcy-*(*kCrcMRiHVZ+H5&e6Mu48~zE)^`WS zE1mKKwib==1D!plhGRI`_7t@-(%C<2NV1&R-&O{_sb7rD6%BfZnt68q2B7>ANN| z>-ybQg8ts_h<()FO)ZiA?K#xwp%D!687vQWPv3=gV%f*t2Xn)RkMA8&YG+IbcWQ8# znCWPe9~k;=iupc~_Fy>Neg7EpJiU6v!W8Rl9%TaK6oLaO3Yd(@`cRP#6ry5o0b-SZ z27>Iyv>VyXA*rc*6MbKeAZFN?E70MM!h}-tZ>&rxmeD!j&784}=>0HyDtsRuct*%&@G&o#`4rd^AOP#S69uU3-W$Jgs+z z`bJZgF&#wb#F>6n8K+9}+{m-l6AQi)BJV>#?Elp+IM3CjBtN(FE&dyCd{K7U?W?(C zdf~o_Sgrs~qq`4GSIlpvR`$HDyci2&!xhPb-#I$hY@=RCCoYWUnJHK=`vrW^zYTtM zo~~lJ>RyrJ5g7L^oPkX+ndNjGhl6DEp=G<|7_D z_NV6=m}|~w-bOI#?=iBRCG8vsvnV^>v#%kQggA1ED+o{8M@_{R2A^G59s#y|R$eCi zFhKc*q@2RvYYQi51?n@?D7dR|ci%NOF11p2;1F>rLH>fx#nTZaGk30A918W^nfeL@ zZWLy0);@~w&Fci~KCLj)E)L!9=5!dx=*%%U)#J@iYgWy&}R4;xK6`|2s#-R=Zi`@pnvsF(i&E(@#nLnp)Lkt_OYLIfhlC) z=vZ0?eC&2@KLAI0O+4~|jdfqTE!?TERzn1fan7|X2R}=iOmhixl64#j2DXLZi{UZ1 z8@wia-S#vE6$e8xD7u)>Dco|cB&E=()rh{19bJ4G=UvyL(ex1EgV%waufRQ~SucCZ zIH}1e6&jIt4lsQV!liILu~NjbFiGf>AENdt zXL{#M#-!;pcrrzh`Zb&kL)L+8QY>{$>)s!#WeBNZ`YxzL3)b0E`y{+rOr~Upz(I6NP?VsySv6;ic3H1}19t`c@D9^LYTrdsZRso$D zW;ckab=9T@C7D*G0e5uCm^ar!J8b28q_aVFZO`L|ig6lT-Tyb!0P z1r(~ZBVf|%FqbMeo<8ZfQor3ZFDS{uOJB*F$Kt30nVRK{ z5oSiEH+Ur$cFGtUTZKH!m(MGe z;Mjk`9{phpx;q~ID?nP2gc&`$XD74ai;QQ92ia*UH4{72Hy((##2QANv;oJ&i;F)( zOK$U7?~%}>k!V=iJqk|7o;Zar&IZU`2kY#kpz6&{E@T6^r+9qA*mqQU-GMao6;fu0 zbd=O-7IyK|=gWqQvszXRl-8NviWQ?(%9ANjn%POpX8^Rjq7wu1XnP$f!RXy;gFEf7 zA3P%M?SeM0NF9owUnH#z!3_+X;B;To?x+WHKCOrT-Uv2;WWmO66b&5G^F=YG?l^yZvC4F#y@_?8Z-DvX-I8$g zqiExCU=+N%nW?!A_RCvOv4rc@@Jm$U7&)dO3pbJJ?Rn-hjiw^H$q(F(xP)WgcY^*+ zpQQHNOck&=zjInEWhnx_RPcSlRMn9LNjs2zKRlOc8c&TV5Go?`qKLaG+iU;8}CZtTU9TKRMPq7 z9M*CDvqrWk(zi)vvGYtyHQ^D0r<-qXCaK7nv2oiJaw`02TBr!FMACW5Fnq3dKA_2t zCtO>L4u5zjVf>btYG+d7m>0l z3cBarlz3g1s@&O~ZJ?lXmIj*KQT_gd$jll;Crjor_S;)AQt(Fg|4BkCou zDVs|sl2Om^n1fTM8>_UcR@J)SZmW0Q zve}hgV$bBWhWU2gi-)nK^@2Zi+c=AlcDkb(by7h2&EEWv4obtY<{lVZGcWZ7ZN2uMFYkecO zl!+cjiFR~Qg~yO|@I_@uJ$0JL1)s;-yoY}qKT4w2t(%rPJFlGK3q{9=fhsXI`z5H7 zt(23LwV>a=0I2LhgitYwo~L)Er4zA$e|EiY*1$!wl{4w=<|$}0>$UK>>+K-}cs%d# z23;JkAY*0u?@?!h%O^xchG*AD@cpE{@q$Y|YoNBMn_|{Zrd`&kPmU!=t~vnk4bt<1 zL)-N;zzZ~8DgWxb@>J?ZldpoCnVKinW6RGump#v@DL>UH=I?RP_7ykAG`A)V@z4A0 zgrdIYCdR}uik!yI#WmdnW3k&QCGovRb3}%na~%_mDt-nki(glw8|}t&0PJfk$`{}N zV6a@p`5eMv+0ePf461tUoqd5)%77Ic8Z<59$?qU>VAokQb&_c4=SucnD;rfw8J!);SblWzMEw|grW^( z$=n%oRET>1NtCbiWY@eH@lZK~CTAP8edGyHZgZI0WuY&XY(a4vV(b9 zdPJ%!1fgazl5uTe2HHSn$QPt*GXk;4jfvRydI2k<@%F~1WkJhJH@8WgmPQTq4}t)( z#N-=+knw)rnbAG8*WBC9+`*JVwU~Qg`Dm#5F>6wqT9P;G9Hhy zvp&Yx%{7g5Qz1=v3l~@7B7`6Uut=P2G%E|sHzW;BD1^Q<_Qge4lde}7S)gvhrSe2; z04%wBXg6R-^am(ThsyLP!-qv_OwE(iQ z^vCh31l(|!OZKbG`M6#+7*{b(K;k&y4dy>@@UJS22557V0kb#0wW9wW$79B>T^1

lns-LzR?X4%WlXW$zIWq87i(DT0%$V4+6m*CB z4DX?WS!GRE&~!3$v1lF-c;%RY@&rdCG7tG{>D6<7n{QKVe*$rXfHf(G#>{a&DSSx9 z`d|8I14Q(wupr;RZO;EHm`lL^ita`8A;hTnm!p?6b}btZO8Ij&i2| z#xC?*BaM-9=SqaUZ7VVVI6lTgA8spHmUy{odrWi0s&odzhuoVhBSKX+9`tRCL(i8A z@(E(UMiDROv8aiMn^v88Q5_`hOEhO@@#@05Ik0EXUeJN>3>)vLc3XKaT;qi^}6GlgzVRS_ZV5noZmM^OU!eap;@ z=Z6;aLFoe}#Qzr}W_c5+K>g6w0)wrDyqB-L_oohQX3fEg6wU>m3t?HMqQj+dLnd!ScjNjxsiib+>vMzZ#)I6jDdAM413t%W}WmRT!P`PtmMRK0!Y&cSckNj(|MN4vsxdD9I} z7F8pB15@rBj~0ZlQX=$L)SHeOtf%=Z360G$c;__e+rtO3#p%xJO*)i1JMC}=xTdpu zWACu&P{t7JR2vPc(sV|=5{|Bex~6S#lZdd9G&$LWu-hUB@HEprRZ60KmCCf;`Fp-5 zv>kE3HWer3;tY=-2cYI-hYUpX?WDh=IbVzu@uV0vwvM5#E@BURtq1nJPN1x1v~mY9PRpXp!5ZzhbBQtGQqu!28qOPOGMim>4yXjCbD{ZI?~{XGs#<%OPgNX2u;~LfU8mOJd4y`1h|pFWB_v zyz+s*a)~n^yH+Hmh;aX}$Y`g$yIgr8!~r%RG=iGA(kveYGwKr<89+=bR3Pm868Wuh z?OBjgr|QEOzMc74nV`|GtiWcgHi1?84k5ztYsxdr;ScTU$Uv!@B0Q6>!>6B|#^LEY zu?EJzB9fBRIL$oEKQ`M}-g@r^_}X5%Sh4Jh)ajMUQd7*#B{|JXQEkHG%^{)ngG7^S za5C-#2f-MI&1euVilMm3UrqpBAmKP@y=rZpqy58a=))Y=JDB*gs=qMf2mqvB@a^7dIT*$Kb z*PA%bkLt*Mt*%0yEi!P~9aJomshwzKu=9u^j#-r~sdF<-T3lLhglU)bEAYiOqjFa( zzC)UC^Y@pwd12{HPRnpEU8jMeEB0r|{rmD#&!(hvEn8Bo2ieb*&84SfUfl`rkz|!A z?HaC*T*Wztu39eA>AZ1^8fwg_n$JIv57NB z$&1?$MbuGaWxw2fS?xYbwi1Vri&2%Aomi!bnx@g1Gk=;fZOiJByON)f82b3_$`2`G z*&Neg`1=uj)++O?wzBU9n>zYz(r1_|vZGVx)n*mMz$m(q3gl#pW%{V3Z#iT6ha#^J zv2QFeEVeLAOtoR`rDO(_&04PLV~S8mLHU-tnBE`8STT7GM`UN@P&yIimHx!OVojX) z%}l3d^o8A*-}fiIyvP}|7emg*`&I!7nwIyxwB_#FrZHarF05_!#_2*EFYm{O?n7J% zHAP>`lGd}Q;9rIBxvUp;HKg=0Mb}k9lZxL^5fD7Cf~U{QE1PJT>RFHPJ}Bb1iu-zU za&#EEc9psD!IynG*i0_C(+MM&*(4o|(PVGD>NewkDuA`lw(D+MwocNvCJMK@DjCJw zTEYWIcH)uTapNaxu$>uEXW8==#RA`eqcwJ&z`n7jNg7S5MA8>`_wT_f4^i%XUF}>@yl7;8!-DbPGMFu06fJNCPabD zilPoctj?QrKi+CW#nQplK0cPw#}Lcf=DYsB8Y1ir4JU;^5jVEuxN!KcPShKlG!7)k zR6@jLl&s8JhWA!UheR56Kd*Yk1=*!5>Ih5rkNEgf7m@xRDH`>~QBe3)?eF9l&uinzDobg3@opSH<#*hV-U?c|r6vtHqL4CWjwtBT9+CXc5)Kq@&iLvUO8K<*99R%$XI>gptkuVwC~G|3DYTAsZh7Y5xIb|JM+d{b6r<=l z4%o>JEwI)BGs+kTfYRi^nr`d?;&0S4i*60#R0 z5YpF$ome)sVZ#S-m3RHR=)_!T&e*>KFl!j&96m2AZx_YTC`O;Ax(>y^1jQF;zh^%^ zo_1ZBAHH2SYaj6F#xeo)orFYC1meQu=H@1~+?rat6BVuJEI#Iufh+Q6V#vLO zxsQNU$J&0Y5$i8_X{l*gt26i}$h)=fK5iMTGk0|&V!jN5?IGQPU`Uo*li$_S*P%dM zT<({C2^nHVD#dzVgwt31lYXr?Ta0{ zQldfK?a={`0+t*2WihtBkr@Ms5Z@Jj*4B*Cob|sNX zS*oD7I}bnI_!+e6#=WRnw*;fREV*axMHFt%b9n4BZsNDzQtme0Ab8!QSmAaYuCWrD zqkPguEF1G9W=&YJrnd-2k=a)-h>GOpCs`T~{{YYXXUum!?%-?Fv9=&^r&4DDD^c*I zhnL~Tu7(wrdWzQ`2G_Ip_R+Jo;oEK2++WUaZWT!&$iM@nA1d)W7nOB3oN7YajcL+A z-ANi@<4(XRARGVi7cZu}8md&mu znqMhGS06!dw}KcVSYU}_g#{8g)GC4MQIN*6Nh%^TGrBvgXziOcx!oDmNJ_;VYpL0Q z)k;)QW#(>NM{@LFs=+FmM)mc-f0FsXlB`ztD5>V?FJ) z*zJ4m&6SL%TU&WvFwIRtk&5XwCca)cb*9{E%JWoo9(Gh$Yr1KU)X#4q(=o_s0OaA1 zqx;y8P^%wU=xsG&aAQQbw=WbVQbGqa_^zU9=1jzkvAVL#XesCVIxI6=+#OEnQqF z)m}c#B5es;k-gj7P;a)ixScFfQN`uhi5}xdpC?{GI&lCh;a8jPuB<-Y#+_&qRVOMGq{hB)PjN5$0yA3ec0KHEBgH%lNfpveLyUF0(T0#IaCf$islZ2OIzn z00%q(fB)9lyaqSSvYFqV`FpZzvD&P~-TNS8DLRrcPGH8o%yj+a^J6zvuU6h(l2}`B z69byw?pZ{W2@H*r>eNZ6fGwG(G39$U)=8Fxw#nK1*6Vm}p@E~(r>!J_qM&1uH5s2B zBbyM)@ybqtE(&6 z(vU14Dp(rnaK(i|;Z=TL!;90@`HC@ph4B?73ObpD(SthIGDr1dOM6B$*GjZdJpc@K zWkywgpzO|xq-o{Th(?W1klcR?Zyjs%Tlv_)* zS9^cye$x^Nx5NC)c$lkAXA>w$WYk#o?gpy9oU}*ORBc+inCItuPE?NxZWiOyw7kdK z8y3lGg&|EQu17mDEV^r~ln3pL3zBl%Dtjn>BDw|>+sz*;bhXEmFo`G%6^pGd(Q zNj@j)0~OFbZ}61PlOn5D)*? z*Qbg(A(A;5qB4-~tWbsVA%zP2akq6R7gt1P$7!3}!)p!o)5aCh&h)xgB)A8ZyD=@V zO*LxA52(giH!o<=M5GI2BP^tiNmdM8Wn8Mi3HD-~r6fHhKQhKN=NKzRty;AogyFf^ zqN4UzB#k8SBr6(^W)m~({{VngIJ>=WNYz>2C6A=_KVq&M=QYwjR4vV%3N+lu7P4zF zk6l6Impd;XN@2?TYj5)glG<0hhibRn_WAA7X{DK56lyHost74985q=&Nh#Fj_xW^5 zZgPz<-3cVoneM%%d1WF@IpJ1aVuUyxD?$ZEH1yZhq>)-y+C2xmZj;-~^45`uo`lIW z*F$SaZHNpgHVaXe>Efg2EybhIGGV0hvRuO_JIIOI-}d2uS5 z>JW|IZuWp95Vz+|AV}f2{xi4TO5jg3qF9m30#9h!*)Im=JVrUR6pbB&uvXJthbl49 zJ}htY-30A#N&BTs*JCUZHI0I}BMJcF0O5cEr~7)a0sqk0^r;je5ON@jjBd$QQJIO* zqHcY&ePs7i(i!a9LnKp6k&G|^hNrI_roWfU*7D#kTbh^gAitVe#Aw@;Q5?$!Mk6Cn zgkv|$mr(kyv3NJDI%!M!T#UmiFZCMYDO-I=GcK}IX*2F~JaODjp-1LTAfhOz4>n}@ z3|GAJ^kdXZ1)HRrF9noLkxH?|O(m8`8s3@nAMmf25W>rDuNFPZiinEdw_c#>+bJVY z=C~DRAX0$vYWi_zF-VS2sIS74N4T}*aK{_FYEVX#naEUSkoDqsB-IAN(UZ2CumC8b%B;uJOhs-}yDqWPqC{I- zUfRfwVC(ah6}V;8s42lKRX??jdv=s@dq`zofSZ$Vw{XTMxe}BmHAtXj)E-grk46?! zQhuQ^BCj@S;d@`1xH26&R#Z_P2^^vG4Ec-`qg0^UP*-i;uPoqQRJgWs%IH?A87Zo@ zDpiMrj8&D6m4hXX&W24L*J>Md`;>P*8+cc#ImX*0i3k_K$mhzH{{X9r zY{GZtNTMl8K(K5(6~&zQ7q|ThFJ{sa;a2>#12hH6@%UA7(dcwa%EuWpoy(bttY{G( zOaO4e0Mh^*1^@<_KtKP{*y)}(jKE4Hh|##zjtF3L1ROGOQcXRKROsqvz0=v>DL9fF zVJuEgnMnYX#39Ucar{{3b($$zZKwHVxXDl28CYyDEa?hcBuhY9iKze%bH~D%#=Uln zX!ydcx5y#3w9#yxD|8%$)`q0>6XI$y1-nmdv6Cfo>MNU>j(E{7?$x?{0W`0PucsCh zYoLj*Sk@PmNK7{nx`Ixa+f4@=P@E)^ds7AcMc|WixRApIt6a5n(x!vj-ck0PLpa4^ z-3P}r*oTE}Vgz5@M-%wKpPESj0K4TMy@q9z;Y5*1ZDxkaHsiJJbJ?1tuo&2~FE)w_ z9a@0Q9~@V)Zc%jW{6zA7LwO+IRkX;gsxn0=l&`RXmNZgiScR1*gd~jc&(qJRq>2!0 zQUxoYh9yyS+9Mgn!Z9B50P?A(ck7j)|-uC5`-(QBKNN&feH@tAv&w7wkKKt~-J-zIEH=wa2s$>ZRXCE&U5WLx62}`Btw!3U zRS^uN-qF=E#I+kkse+=NOaO2g02&+sbufT-oG2gv(%C$5x@Lx40tnyOTVLB7dxg}5 zh6H3)hXx#T7_X?)#_V#-VE9WZO^JV$-WzMpV6!^GEQ|?dW;#hZSJ7N?y3Hemu~|`i zGgJ}1+#jULLDSV&Q$tcXP-loEfQ+x)ZRvFSv@Lj+Ag)K#!w-z{89^DQ?d`qd0gd#T zmbVSlQhjW79Y5muu`7(Eu-)+roL8(va_`O7YjI;~1a>x{P%L$>e{NW9y(D7l_n5Q1 z+Myk`-ODV>vNIQN5t*;65=~)%*gQ>k3%i?6$JyRHr;0{-j;NTSU>&^}#=+cc*Z51D zB8JfSFLHUE+G~8jifU=&;l~P7j9G4*aSAvhxh^Z%wbRUA)i@M+c0QzptEzC(YrLJmQAU`v~ zQ%d;agOBDky)zTJZq<Q4KQD8C1l8sBz zI_f=R%%8Q19l1nRj=>k6^2Df-8b~MwTpnLzcw#wPsO-&c4=-oOzq~WvTrzFZ^ChfC zjPG77-V_+x<-D;&DUAz<6NYZ$Q+J2$;e$^$hwcT!3 zD>@5%SCO9GR3}UhixU|Nyat?09Zc6q+=#nuAO8SC_Z6glJ>u3a*PAIpTWJD-1wSnc z9LB8E79p*DC~JXz+J3rrJ?x@8sU_RMfn|B7Lhj4>H`SqoX`pHlXiA0v`&5R)V8Dmn`+6zS zrL_F1RZr!vO6QM-F^{d~ic2-TA9S7p zb|}V*8l-6;FODUh$_;~~E*-_(`(3`;&3|+thDEM|d58w4gNfmYon-Jtu6Pmu07!fC zPU5w^+ix7b^@FsLNJes6yYiFwqGO=_){F0BsL&03Q6_1sUwPGphp$F5R8o*q`8BKA@ zwH!mRQhika0Dc7~wxADZeSK|kuW3Ax+$WLXc2*yRp{^uXTIryT7mc6lcY19*UqJ5i zv|~|Um{pneHy(LaO};7K>I-|PQqN)~GRmipnrKFMrxiqOXxi37OBR#r z7|}KJmcQ%AywRFQ_eXVYeYLbnxL-9+Yi({k^GiZP zFQ9x+?k+3dyr8PbB~#A5&EEaR+_yqq#^m%=)*DEwkj7{MDwR2!d6=`~67Hf?b!4d* zkD+t!55C^Ut{%`^mX}_pD#g~FI{8_N{LJ~i?Od3(pqtFssBF=;_+wa%iQl$0+#^|H zAuglxzoZQ4f2$dM!$IcnM~m$fTe(abPMCEj>4f~&)S(Z&z*dEkH+itaXzk=hp;O%b%#oUKav)p27>HyNtnB$fxD zBJUp4k`|qJdq9!IW|dl0S4hCAEg2#~LGI|+_i?r6$E<>fnmg}$ZT|p%+_#qnA-j=9 zv}AL1Vo;HSJPwpiUwc+C^%qp9Qdp`LavbI4UqH+BlS7EAhXg1pKm_{yKUNl?4viwG zE*a98Tm%3X)yq(P4REvyoYJ*S~%+q!8=E zpyqgZVzjMLLc{+6=Nqp}PtIKH(fm)Dp1oeCuBV!n(fMD8@8Q|y>-d2G*46%YC15iw zO+8f=%k^UHxuv?`nNe6~W#65rymuY^m$P4mh9oItZeu+-&P)}^b~Jh&ax8vrKH}+I zokprxC04K`R_KhwaP9raZrx_vCji{U+|mQe%D^(ZlS8G|mA`8dXH=-y8XM2j{?L>3z%r~zcM>JMT>GX9^%y3Q{0B$qOW61iuU z!l?vl&(aU03p7n>C0M6oQ%;z{k>T(>L= zg0K!Qa;-fB7c`nF-gVgy@f7YQ{7Wspyl_S%K|nuQ$4z3)ZAUChvy+xAITTV(KxV}7 z1J{YFG{}Cj_obcnoO=${Bv*Q5S?=cq6jn;~(Hn;zX&FBU$Bf-xrEsC}Mlm(qHpo$r z%-nIW6$jD>x5craD~?{INRbKhD;6OD-WzMZ1x&HuFG~eZRqbrWM<>4W^=!;^o$3#^Q8wo%R z4~W75VxfUn6;>1@)Nt0Sg;*JvarWGHyOg4zl1`|P8Y^c(j!#){%DCdT7M3cbzTeuG z)gJYSOwg=sNC8ru&EesU>$1$~O$9|#(X(w<*DytV97_bs9yS^3Q@G- z3Ek=@Zmw-CZ6&(6b%xg643fy%DxwOKe#~zqlA^&C+dTE%e^+S9Qd7gS&a9yF*m^wSxWjW!No z&1Mm(roPckHZl^TPZ&(dB=`(DNGKyCzk*ZRaP));s$W9>zwN@%107Txqc3h0N`MLC zSJ#oJ^2oPPtb9x%9Sra zXa4{gmGugUc&stdG*c7}hJd2B5K=#V?v0CJw_A(SO!K{dso!rQ z?5tGVZK`yHRwGi58tF}6hZXGURB?MkR~s1G{Z{&iZEFp!uIlkyz>5?yEHKeg017H& zCxG!d)Q+lgR76=tuRf)B-tDu`wr;oPYbn`FhK?xMgl#}-MMkA~;a;Y4RAOZ-EGkJE z{{TiJ0Kr-^tPYY5K^#Z34XCs|HT1`H-1i>Pw79;BBMLfJxkC$8Pu4RYCjmtA~ zifoOl7O>=S#Iz8uZ{2~}+vBvmhS?KbF(s~|tTPrqEb*1s!~D&7hI(jAY8iaukPGIX|o_HmZv zx=8bqTQ6{1Lgd)oavq?cPUeTk|BZx=5nb;@Z;>mk`kkK zGOB4g3eW-Jf`D{{g#Z&@MjcQlJ43X$PT}3-v9toT*DEn0QZdDt;%hdX!hcCWa&ZH+R6 zuShu-nTk3zZUdu@E{rM&J_H^k7N|8ci`;)oJ6m+uC7gfCu8#+|0K`wO6Gi_3!!cr| z-3%qOb&gZ(GVT8W=?{08hSoO|L0+znb^F~uO8YV0Tdgs#y~XmGuV~?T z_~GIVw?CNZK8<=Zt$G?b{r-CVKYJKl^1f2yrfB;gh_$5RC|^AHEQST^?#Xa zr@gP2ML*O0y;{BebNXLZTL`6xHrm)mq^|-2RF5f0E17`6|?F=yiT?Re$-8rd0Sk zf0vDo7Y|3?86!tHxG*WgfFGv<0EZ4h!Zs8^Cc=P=`0>Jki{V5NZU>JH09Cv1Px8)h z<+^?E=Jn~)p5~ryUxS+!^*lX~e0)rI{{TelfAo5M&;I~+rkx+BO@D7++{RAtgXi?f HNPqv?Ak}qc literal 0 HcmV?d00001 diff --git a/twelvemonkeys-core/todo.txt b/twelvemonkeys-core/todo.txt new file mode 100755 index 00000000..56fcb530 --- /dev/null +++ b/twelvemonkeys-core/todo.txt @@ -0,0 +1,10 @@ +- Move some of the imageio classes and test cases to a new submodule imageio-util (imageio-core) + - ImageReaderBase + - ImageWriterBase + - Maybe something in the adapter folder? +- Test cases for the OLE2Compound stuff +- Move encoder/decoder streams to separate subpackage "enc" +- Test cases for encoder/decoders +- Test cases in general +- Remove lots of the old imageio code +- Remove some of the old SQL stuff? \ No newline at end of file