From 98e7e333bbb6bcdb1d332e58f253d3dcc372fa32 Mon Sep 17 00:00:00 2001 From: Konloch Date: Mon, 6 Jul 2015 08:07:29 -0600 Subject: [PATCH] some changes --- libs/gson-2.3.1.jar | Bin 210856 -> 0 bytes .../apache/commons/codec/BinaryDecoder.java | 38 + .../apache/commons/codec/BinaryEncoder.java | 38 + .../apache/commons/codec/CharEncoding.java | 113 + src/org/apache/commons/codec/Charsets.java | 156 ++ src/org/apache/commons/codec/Decoder.java | 47 + .../commons/codec/DecoderException.java | 86 + src/org/apache/commons/codec/Encoder.java | 44 + .../commons/codec/EncoderException.java | 89 + .../apache/commons/codec/StringDecoder.java | 38 + .../apache/commons/codec/StringEncoder.java | 38 + .../codec/StringEncoderComparator.java | 91 + .../apache/commons/codec/binary/Base32.java | 539 ++++ .../codec/binary/Base32InputStream.java | 85 + .../codec/binary/Base32OutputStream.java | 89 + .../apache/commons/codec/binary/Base64.java | 786 ++++++ .../codec/binary/Base64InputStream.java | 88 + .../codec/binary/Base64OutputStream.java | 92 + .../commons/codec/binary/BaseNCodec.java | 525 ++++ .../codec/binary/BaseNCodecInputStream.java | 211 ++ .../codec/binary/BaseNCodecOutputStream.java | 176 ++ .../commons/codec/binary/BinaryCodec.java | 301 +++ .../codec/binary/CharSequenceUtils.java | 79 + src/org/apache/commons/codec/binary/Hex.java | 443 ++++ .../commons/codec/binary/StringUtils.java | 422 +++ .../apache/commons/codec/binary/package.html | 21 + src/org/apache/commons/codec/digest/B64.java | 79 + .../apache/commons/codec/digest/Crypt.java | 151 ++ .../commons/codec/digest/DigestUtils.java | 1140 ++++++++ .../commons/codec/digest/HmacAlgorithms.java | 94 + .../commons/codec/digest/HmacUtils.java | 794 ++++++ .../apache/commons/codec/digest/Md5Crypt.java | 302 +++ .../codec/digest/MessageDigestAlgorithms.java | 81 + .../commons/codec/digest/Sha2Crypt.java | 545 ++++ .../commons/codec/digest/UnixCrypt.java | 413 +++ .../apache/commons/codec/digest/package.html | 24 + .../codec/language/AbstractCaverphone.java | 80 + .../commons/codec/language/Caverphone.java | 105 + .../commons/codec/language/Caverphone1.java | 127 + .../commons/codec/language/Caverphone2.java | 131 + .../codec/language/ColognePhonetic.java | 445 ++++ .../codec/language/DaitchMokotoffSoundex.java | 561 ++++ .../codec/language/DoubleMetaphone.java | 1009 +++++++ .../language/MatchRatingApproachEncoder.java | 426 +++ .../commons/codec/language/Metaphone.java | 430 +++ .../apache/commons/codec/language/Nysiis.java | 319 +++ .../codec/language/RefinedSoundex.java | 205 ++ .../commons/codec/language/Soundex.java | 254 ++ .../commons/codec/language/SoundexUtils.java | 124 + .../codec/language/bm/BeiderMorseEncoder.java | 181 ++ .../commons/codec/language/bm/Lang.java | 231 ++ .../commons/codec/language/bm/Languages.java | 295 +++ .../commons/codec/language/bm/NameType.java | 53 + .../codec/language/bm/PhoneticEngine.java | 529 ++++ .../codec/language/bm/ResourceConstants.java | 37 + .../commons/codec/language/bm/Rule.java | 720 +++++ .../commons/codec/language/bm/RuleType.java | 50 + .../commons/codec/language/bm/package.html | 21 + .../commons/codec/language/package.html | 21 + src/org/apache/commons/codec/net/BCodec.java | 251 ++ src/org/apache/commons/codec/net/QCodec.java | 358 +++ .../codec/net/QuotedPrintableCodec.java | 602 +++++ .../commons/codec/net/RFC1522Codec.java | 186 ++ .../apache/commons/codec/net/URLCodec.java | 368 +++ src/org/apache/commons/codec/net/Utils.java | 50 + src/org/apache/commons/codec/net/package.html | 23 + src/org/apache/commons/codec/overview.html | 29 + src/org/apache/commons/codec/package.html | 100 + src/org/imgscalr/AsyncScalr.java | 594 +++++ src/org/imgscalr/Scalr.java | 2349 +++++++++++++++++ .../club/bytecodeviewer/BytecodeViewer.java | 93 +- .../club/bytecodeviewer/JarUtils.java | 2 + .../club/bytecodeviewer/Settings.java | 2 + .../club/bytecodeviewer/gui/AboutWindow.java | 2 +- .../club/bytecodeviewer/gui/BootScreen.java | 281 ++ .../club/bytecodeviewer/gui/ClassViewer.java | 1 - .../bytecodeviewer/gui/MainViewerGUI.java | 14 +- .../CompiledJavaPluginLaunchStrategy.java | 1 + 78 files changed, 19872 insertions(+), 46 deletions(-) delete mode 100644 libs/gson-2.3.1.jar create mode 100644 src/org/apache/commons/codec/BinaryDecoder.java create mode 100644 src/org/apache/commons/codec/BinaryEncoder.java create mode 100644 src/org/apache/commons/codec/CharEncoding.java create mode 100644 src/org/apache/commons/codec/Charsets.java create mode 100644 src/org/apache/commons/codec/Decoder.java create mode 100644 src/org/apache/commons/codec/DecoderException.java create mode 100644 src/org/apache/commons/codec/Encoder.java create mode 100644 src/org/apache/commons/codec/EncoderException.java create mode 100644 src/org/apache/commons/codec/StringDecoder.java create mode 100644 src/org/apache/commons/codec/StringEncoder.java create mode 100644 src/org/apache/commons/codec/StringEncoderComparator.java create mode 100644 src/org/apache/commons/codec/binary/Base32.java create mode 100644 src/org/apache/commons/codec/binary/Base32InputStream.java create mode 100644 src/org/apache/commons/codec/binary/Base32OutputStream.java create mode 100644 src/org/apache/commons/codec/binary/Base64.java create mode 100644 src/org/apache/commons/codec/binary/Base64InputStream.java create mode 100644 src/org/apache/commons/codec/binary/Base64OutputStream.java create mode 100644 src/org/apache/commons/codec/binary/BaseNCodec.java create mode 100644 src/org/apache/commons/codec/binary/BaseNCodecInputStream.java create mode 100644 src/org/apache/commons/codec/binary/BaseNCodecOutputStream.java create mode 100644 src/org/apache/commons/codec/binary/BinaryCodec.java create mode 100644 src/org/apache/commons/codec/binary/CharSequenceUtils.java create mode 100644 src/org/apache/commons/codec/binary/Hex.java create mode 100644 src/org/apache/commons/codec/binary/StringUtils.java create mode 100644 src/org/apache/commons/codec/binary/package.html create mode 100644 src/org/apache/commons/codec/digest/B64.java create mode 100644 src/org/apache/commons/codec/digest/Crypt.java create mode 100644 src/org/apache/commons/codec/digest/DigestUtils.java create mode 100644 src/org/apache/commons/codec/digest/HmacAlgorithms.java create mode 100644 src/org/apache/commons/codec/digest/HmacUtils.java create mode 100644 src/org/apache/commons/codec/digest/Md5Crypt.java create mode 100644 src/org/apache/commons/codec/digest/MessageDigestAlgorithms.java create mode 100644 src/org/apache/commons/codec/digest/Sha2Crypt.java create mode 100644 src/org/apache/commons/codec/digest/UnixCrypt.java create mode 100644 src/org/apache/commons/codec/digest/package.html create mode 100644 src/org/apache/commons/codec/language/AbstractCaverphone.java create mode 100644 src/org/apache/commons/codec/language/Caverphone.java create mode 100644 src/org/apache/commons/codec/language/Caverphone1.java create mode 100644 src/org/apache/commons/codec/language/Caverphone2.java create mode 100644 src/org/apache/commons/codec/language/ColognePhonetic.java create mode 100644 src/org/apache/commons/codec/language/DaitchMokotoffSoundex.java create mode 100644 src/org/apache/commons/codec/language/DoubleMetaphone.java create mode 100644 src/org/apache/commons/codec/language/MatchRatingApproachEncoder.java create mode 100644 src/org/apache/commons/codec/language/Metaphone.java create mode 100644 src/org/apache/commons/codec/language/Nysiis.java create mode 100644 src/org/apache/commons/codec/language/RefinedSoundex.java create mode 100644 src/org/apache/commons/codec/language/Soundex.java create mode 100644 src/org/apache/commons/codec/language/SoundexUtils.java create mode 100644 src/org/apache/commons/codec/language/bm/BeiderMorseEncoder.java create mode 100644 src/org/apache/commons/codec/language/bm/Lang.java create mode 100644 src/org/apache/commons/codec/language/bm/Languages.java create mode 100644 src/org/apache/commons/codec/language/bm/NameType.java create mode 100644 src/org/apache/commons/codec/language/bm/PhoneticEngine.java create mode 100644 src/org/apache/commons/codec/language/bm/ResourceConstants.java create mode 100644 src/org/apache/commons/codec/language/bm/Rule.java create mode 100644 src/org/apache/commons/codec/language/bm/RuleType.java create mode 100644 src/org/apache/commons/codec/language/bm/package.html create mode 100644 src/org/apache/commons/codec/language/package.html create mode 100644 src/org/apache/commons/codec/net/BCodec.java create mode 100644 src/org/apache/commons/codec/net/QCodec.java create mode 100644 src/org/apache/commons/codec/net/QuotedPrintableCodec.java create mode 100644 src/org/apache/commons/codec/net/RFC1522Codec.java create mode 100644 src/org/apache/commons/codec/net/URLCodec.java create mode 100644 src/org/apache/commons/codec/net/Utils.java create mode 100644 src/org/apache/commons/codec/net/package.html create mode 100644 src/org/apache/commons/codec/overview.html create mode 100644 src/org/apache/commons/codec/package.html create mode 100644 src/org/imgscalr/AsyncScalr.java create mode 100644 src/org/imgscalr/Scalr.java create mode 100644 src/the/bytecode/club/bytecodeviewer/gui/BootScreen.java diff --git a/libs/gson-2.3.1.jar b/libs/gson-2.3.1.jar deleted file mode 100644 index 250132c197467d7ddef3b7f40a98fdc036163b66..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 210856 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≪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

eMUo@%PPi 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 OZ$92#YJ45aW%wUD7hlN$ diff --git a/src/org/apache/commons/codec/BinaryDecoder.java b/src/org/apache/commons/codec/BinaryDecoder.java new file mode 100644 index 00000000..546ff765 --- /dev/null +++ b/src/org/apache/commons/codec/BinaryDecoder.java @@ -0,0 +1,38 @@ +/* + * 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 org.apache.commons.codec; + +/** + * Defines common decoding methods for byte array decoders. + * + * @version $Id$ + */ +public interface BinaryDecoder extends Decoder { + + /** + * Decodes a byte array and returns the results as a byte array. + * + * @param source + * A byte array which has been encoded with the appropriate encoder + * @return a byte array that contains decoded content + * @throws DecoderException + * A decoder exception is thrown if a Decoder encounters a failure condition during the decode process. + */ + byte[] decode(byte[] source) throws DecoderException; +} + diff --git a/src/org/apache/commons/codec/BinaryEncoder.java b/src/org/apache/commons/codec/BinaryEncoder.java new file mode 100644 index 00000000..65f92cc8 --- /dev/null +++ b/src/org/apache/commons/codec/BinaryEncoder.java @@ -0,0 +1,38 @@ +/* + * 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 org.apache.commons.codec; + +/** + * Defines common encoding methods for byte array encoders. + * + * @version $Id$ + */ +public interface BinaryEncoder extends Encoder { + + /** + * Encodes a byte array and return the encoded data as a byte array. + * + * @param source + * Data to be encoded + * @return A byte array containing the encoded data + * @throws EncoderException + * thrown if the Encoder encounters a failure condition during the encoding process. + */ + byte[] encode(byte[] source) throws EncoderException; +} + diff --git a/src/org/apache/commons/codec/CharEncoding.java b/src/org/apache/commons/codec/CharEncoding.java new file mode 100644 index 00000000..39ec98e7 --- /dev/null +++ b/src/org/apache/commons/codec/CharEncoding.java @@ -0,0 +1,113 @@ +/* + * 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 org.apache.commons.codec; + +/** + * Character encoding names required of every implementation of the Java platform. + * + * From the Java documentation Standard charsets: + *

+ * Every implementation of the Java platform is required to support the following character encodings. Consult the + * release documentation for your implementation to see if any other encodings are supported. Consult the release + * documentation for your implementation to see if any other encodings are supported. + *

+ * + *
    + *
  • US-ASCII
    + * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode character set.
  • + *
  • ISO-8859-1
    + * ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.
  • + *
  • UTF-8
    + * Eight-bit Unicode Transformation Format.
  • + *
  • UTF-16BE
    + * Sixteen-bit Unicode Transformation Format, big-endian byte order.
  • + *
  • UTF-16LE
    + * Sixteen-bit Unicode Transformation Format, little-endian byte order.
  • + *
  • UTF-16
    + * Sixteen-bit Unicode Transformation Format, byte order specified by a mandatory initial byte-order mark (either order + * accepted on input, big-endian used on output.)
  • + *
+ * + * This perhaps would best belong in the [lang] project. Even if a similar interface is defined in [lang], it is not + * foreseen that [codec] would be made to depend on [lang]. + * + *

+ * This class is immutable and thread-safe. + *

+ * + * @see Standard charsets + * @since 1.4 + * @version $Id$ + */ +public class CharEncoding { + /** + * CharEncodingISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1. + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + */ + public static final String ISO_8859_1 = "ISO-8859-1"; + + /** + * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set. + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + */ + public static final String US_ASCII = "US-ASCII"; + + /** + * Sixteen-bit Unicode Transformation Format, The byte order specified by a mandatory initial byte-order mark + * (either order accepted on input, big-endian used on output) + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + */ + public static final String UTF_16 = "UTF-16"; + + /** + * Sixteen-bit Unicode Transformation Format, big-endian byte order. + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + */ + public static final String UTF_16BE = "UTF-16BE"; + + /** + * Sixteen-bit Unicode Transformation Format, little-endian byte order. + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + */ + public static final String UTF_16LE = "UTF-16LE"; + + /** + * Eight-bit Unicode Transformation Format. + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + */ + public static final String UTF_8 = "UTF-8"; +} diff --git a/src/org/apache/commons/codec/Charsets.java b/src/org/apache/commons/codec/Charsets.java new file mode 100644 index 00000000..d57b3af5 --- /dev/null +++ b/src/org/apache/commons/codec/Charsets.java @@ -0,0 +1,156 @@ +/* + * 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 org.apache.commons.codec; + +import java.nio.charset.Charset; + +/** + * Charsets required of every implementation of the Java platform. + * + * From the Java documentation Standard + * charsets: + *

+ * Every implementation of the Java platform is required to support the following character encodings. Consult the + * release documentation for your implementation to see if any other encodings are supported. Consult the release + * documentation for your implementation to see if any other encodings are supported. + *

+ * + *
    + *
  • US-ASCII
    + * Seven-bit ASCII, a.k.a. ISO646-US, a.k.a. the Basic Latin block of the Unicode character set.
  • + *
  • ISO-8859-1
    + * ISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1.
  • + *
  • UTF-8
    + * Eight-bit Unicode Transformation Format.
  • + *
  • UTF-16BE
    + * Sixteen-bit Unicode Transformation Format, big-endian byte order.
  • + *
  • UTF-16LE
    + * Sixteen-bit Unicode Transformation Format, little-endian byte order.
  • + *
  • UTF-16
    + * Sixteen-bit Unicode Transformation Format, byte order specified by a mandatory initial byte-order mark (either order + * accepted on input, big-endian used on output.)
  • + *
+ * + * This perhaps would best belong in the Commons Lang project. Even if a similar class is defined in Commons Lang, it is + * not foreseen that Commons Codec would be made to depend on Commons Lang. + * + *

+ * This class is immutable and thread-safe. + *

+ * + * @see Standard charsets + * @since 1.7 + * @version $Id: CharEncoding.java 1173287 2011-09-20 18:16:19Z ggregory $ + */ +public class Charsets { + + // + // This class should only contain Charset instances for required encodings. This guarantees that it will load + // correctly and without delay on all Java platforms. + // + + /** + * Returns the given Charset or the default Charset if the given Charset is null. + * + * @param charset + * A charset or null. + * @return the given Charset or the default Charset if the given Charset is null + */ + public static Charset toCharset(final Charset charset) { + return charset == null ? Charset.defaultCharset() : charset; + } + + /** + * Returns a Charset for the named charset. If the name is null, return the default Charset. + * + * @param charset + * The name of the requested charset, may be null. + * @return a Charset for the named charset + * @throws java.nio.charset.UnsupportedCharsetException + * If the named charset is unavailable + */ + public static Charset toCharset(final String charset) { + return charset == null ? Charset.defaultCharset() : Charset.forName(charset); + } + + /** + * CharEncodingISO Latin Alphabet No. 1, a.k.a. ISO-LATIN-1. + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + * @deprecated Use Java 7's {@link java.nio.charset.StandardCharsets.ISO_8859_1} instead + */ + @Deprecated + public static final Charset ISO_8859_1 = Charset.forName(CharEncoding.ISO_8859_1); + + /** + * Seven-bit ASCII, also known as ISO646-US, also known as the Basic Latin block of the Unicode character set. + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + * @deprecated Use Java 7's {@link java.nio.charset.StandardCharsets.US_ASCII} instead + */ + @Deprecated + public static final Charset US_ASCII = Charset.forName(CharEncoding.US_ASCII); + + /** + * Sixteen-bit Unicode Transformation Format, The byte order specified by a mandatory initial byte-order mark + * (either order accepted on input, big-endian used on output) + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + * @deprecated Use Java 7's {@link java.nio.charset.StandardCharsets.UTF_16} instead + */ + @Deprecated + public static final Charset UTF_16 = Charset.forName(CharEncoding.UTF_16); + + /** + * Sixteen-bit Unicode Transformation Format, big-endian byte order. + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + * @deprecated Use Java 7's {@link java.nio.charset.StandardCharsets.UTF_16BE} instead + */ + @Deprecated + public static final Charset UTF_16BE = Charset.forName(CharEncoding.UTF_16BE); + + /** + * Sixteen-bit Unicode Transformation Format, little-endian byte order. + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + * @deprecated Use Java 7's {@link java.nio.charset.StandardCharsets.UTF_16LE} instead + */ + @Deprecated + public static final Charset UTF_16LE = Charset.forName(CharEncoding.UTF_16LE); + + /** + * Eight-bit Unicode Transformation Format. + *

+ * Every implementation of the Java platform is required to support this character encoding. + * + * @see Standard charsets + * @deprecated Use Java 7's {@link java.nio.charset.StandardCharsets.UTF_8} + */ + @Deprecated + public static final Charset UTF_8 = Charset.forName(CharEncoding.UTF_8); +} diff --git a/src/org/apache/commons/codec/Decoder.java b/src/org/apache/commons/codec/Decoder.java new file mode 100644 index 00000000..9f3ba609 --- /dev/null +++ b/src/org/apache/commons/codec/Decoder.java @@ -0,0 +1,47 @@ +/* + * 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 org.apache.commons.codec; + +/** + * Provides the highest level of abstraction for Decoders. + *

+ * This is the sister interface of {@link Encoder}. All Decoders implement this common generic interface. + * Allows a user to pass a generic Object to any Decoder implementation in the codec package. + *

+ * One of the two interfaces at the center of the codec package. + * + * @version $Id$ + */ +public interface Decoder { + + /** + * Decodes an "encoded" Object and returns a "decoded" Object. Note that the implementation of this interface will + * try to cast the Object parameter to the specific type expected by a particular Decoder implementation. If a + * {@link ClassCastException} occurs this decode method will throw a DecoderException. + * + * @param source + * the object to decode + * @return a 'decoded" object + * @throws DecoderException + * a decoder exception can be thrown for any number of reasons. Some good candidates are that the + * parameter passed to this method is null, a param cannot be cast to the appropriate type for a + * specific encoder. + */ + Object decode(Object source) throws DecoderException; +} + diff --git a/src/org/apache/commons/codec/DecoderException.java b/src/org/apache/commons/codec/DecoderException.java new file mode 100644 index 00000000..9d51b044 --- /dev/null +++ b/src/org/apache/commons/codec/DecoderException.java @@ -0,0 +1,86 @@ +/* + * 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 org.apache.commons.codec; + +/** + * Thrown when there is a failure condition during the decoding process. This exception is thrown when a {@link Decoder} + * encounters a decoding specific exception such as invalid data, or characters outside of the expected range. + * + * @version $Id$ + */ +public class DecoderException extends Exception { + + /** + * Declares the Serial Version Uid. + * + * @see Always Declare Serial Version Uid + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. The cause is not initialized, and may + * subsequently be initialized by a call to {@link #initCause}. + * + * @since 1.4 + */ + public DecoderException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently + * be initialized by a call to {@link #initCause}. + * + * @param message + * The detail message which is saved for later retrieval by the {@link #getMessage()} method. + */ + public DecoderException(final String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + *

+ * Note that the detail message associated with cause is not automatically incorporated into this + * exception's detail message. + * + * @param message + * The detail message which is saved for later retrieval by the {@link #getMessage()} method. + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public DecoderException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause and a detail message of (cause==null ? + * null : cause.toString()) (which typically contains the class and detail message of cause). + * This constructor is useful for exceptions that are little more than wrappers for other throwables. + * + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public DecoderException(final Throwable cause) { + super(cause); + } +} diff --git a/src/org/apache/commons/codec/Encoder.java b/src/org/apache/commons/codec/Encoder.java new file mode 100644 index 00000000..c7e99eb1 --- /dev/null +++ b/src/org/apache/commons/codec/Encoder.java @@ -0,0 +1,44 @@ +/* + * 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 org.apache.commons.codec; + +/** + * Provides the highest level of abstraction for Encoders. + *

+ * This is the sister interface of {@link Decoder}. Every implementation of Encoder provides this + * common generic interface which allows a user to pass a generic Object to any Encoder implementation + * in the codec package. + * + * @version $Id$ + */ +public interface Encoder { + + /** + * Encodes an "Object" and returns the encoded content as an Object. The Objects here may just be + * byte[] or Strings depending on the implementation used. + * + * @param source + * An object to encode + * @return An "encoded" Object + * @throws EncoderException + * An encoder exception is thrown if the encoder experiences a failure condition during the encoding + * process. + */ + Object encode(Object source) throws EncoderException; +} + diff --git a/src/org/apache/commons/codec/EncoderException.java b/src/org/apache/commons/codec/EncoderException.java new file mode 100644 index 00000000..7115a48d --- /dev/null +++ b/src/org/apache/commons/codec/EncoderException.java @@ -0,0 +1,89 @@ +/* + * 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 org.apache.commons.codec; + +/** + * Thrown when there is a failure condition during the encoding process. This exception is thrown when an + * {@link Encoder} encounters a encoding specific exception such as invalid data, inability to calculate a checksum, + * characters outside of the expected range. + * + * @version $Id$ + */ +public class EncoderException extends Exception { + + /** + * Declares the Serial Version Uid. + * + * @see Always Declare Serial Version Uid + */ + private static final long serialVersionUID = 1L; + + /** + * Constructs a new exception with null as its detail message. The cause is not initialized, and may + * subsequently be initialized by a call to {@link #initCause}. + * + * @since 1.4 + */ + public EncoderException() { + super(); + } + + /** + * Constructs a new exception with the specified detail message. The cause is not initialized, and may subsequently + * be initialized by a call to {@link #initCause}. + * + * @param message + * a useful message relating to the encoder specific error. + */ + public EncoderException(final String message) { + super(message); + } + + /** + * Constructs a new exception with the specified detail message and cause. + * + *

+ * Note that the detail message associated with cause is not automatically incorporated into this + * exception's detail message. + *

+ * + * @param message + * The detail message which is saved for later retrieval by the {@link #getMessage()} method. + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public EncoderException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * Constructs a new exception with the specified cause and a detail message of (cause==null ? + * null : cause.toString()) (which typically contains the class and detail message of cause). + * This constructor is useful for exceptions that are little more than wrappers for other throwables. + * + * @param cause + * The cause which is saved for later retrieval by the {@link #getCause()} method. A null + * value is permitted, and indicates that the cause is nonexistent or unknown. + * @since 1.4 + */ + public EncoderException(final Throwable cause) { + super(cause); + } +} diff --git a/src/org/apache/commons/codec/StringDecoder.java b/src/org/apache/commons/codec/StringDecoder.java new file mode 100644 index 00000000..c455a5ba --- /dev/null +++ b/src/org/apache/commons/codec/StringDecoder.java @@ -0,0 +1,38 @@ +/* + * 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 org.apache.commons.codec; + +/** + * Defines common decoding methods for String decoders. + * + * @version $Id$ + */ +public interface StringDecoder extends Decoder { + + /** + * Decodes a String and returns a String. + * + * @param source + * the String to decode + * @return the encoded String + * @throws DecoderException + * thrown if there is an error condition during the Encoding process. + */ + String decode(String source) throws DecoderException; +} + diff --git a/src/org/apache/commons/codec/StringEncoder.java b/src/org/apache/commons/codec/StringEncoder.java new file mode 100644 index 00000000..d8b128ee --- /dev/null +++ b/src/org/apache/commons/codec/StringEncoder.java @@ -0,0 +1,38 @@ +/* + * 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 org.apache.commons.codec; + +/** + * Defines common encoding methods for String encoders. + * + * @version $Id$ + */ +public interface StringEncoder extends Encoder { + + /** + * Encodes a String and returns a String. + * + * @param source + * the String to encode + * @return the encoded String + * @throws EncoderException + * thrown if there is an error condition during the encoding process. + */ + String encode(String source) throws EncoderException; +} + diff --git a/src/org/apache/commons/codec/StringEncoderComparator.java b/src/org/apache/commons/codec/StringEncoderComparator.java new file mode 100644 index 00000000..2a190b44 --- /dev/null +++ b/src/org/apache/commons/codec/StringEncoderComparator.java @@ -0,0 +1,91 @@ +/* + * 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 org.apache.commons.codec; + +import java.util.Comparator; + +/** + * Compares Strings using a {@link StringEncoder}. This comparator is used to sort Strings by an encoding scheme such as + * Soundex, Metaphone, etc. This class can come in handy if one need to sort Strings by an encoded form of a name such + * as Soundex. + * + *

This class is immutable and thread-safe.

+ * + * @version $Id$ + */ +@SuppressWarnings("rawtypes") +// TODO ought to implement Comparator but that's not possible whilst maintaining binary compatibility. +public class StringEncoderComparator implements Comparator { + + /** + * Internal encoder instance. + */ + private final StringEncoder stringEncoder; + + /** + * Constructs a new instance. + * + * @deprecated Creating an instance without a {@link StringEncoder} leads to a {@link NullPointerException}. Will be + * removed in 2.0. + */ + @Deprecated + public StringEncoderComparator() { + this.stringEncoder = null; // Trying to use this will cause things to break + } + + /** + * Constructs a new instance with the given algorithm. + * + * @param stringEncoder + * the StringEncoder used for comparisons. + */ + public StringEncoderComparator(final StringEncoder stringEncoder) { + this.stringEncoder = stringEncoder; + } + + /** + * Compares two strings based not on the strings themselves, but on an encoding of the two strings using the + * StringEncoder this Comparator was created with. + * + * If an {@link EncoderException} is encountered, return 0. + * + * @param o1 + * the object to compare + * @param o2 + * the object to compare to + * @return the Comparable.compareTo() return code or 0 if an encoding error was caught. + * @see Comparable + */ + @Override + public int compare(final Object o1, final Object o2) { + + int compareCode = 0; + + try { + @SuppressWarnings("unchecked") // May fail with CCE if encode returns something that is not Comparable + // However this was always the case. + final Comparable> s1 = (Comparable>) this.stringEncoder.encode(o1); + final Comparable s2 = (Comparable) this.stringEncoder.encode(o2); + compareCode = s1.compareTo(s2); + } catch (final EncoderException ee) { + compareCode = 0; + } + return compareCode; + } + +} diff --git a/src/org/apache/commons/codec/binary/Base32.java b/src/org/apache/commons/codec/binary/Base32.java new file mode 100644 index 00000000..e40d6525 --- /dev/null +++ b/src/org/apache/commons/codec/binary/Base32.java @@ -0,0 +1,539 @@ +/* + * 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 org.apache.commons.codec.binary; + +/** + * Provides Base32 encoding and decoding as defined by RFC 4648. + * + *

+ * The class can be parameterized in the following manner with various constructors: + *

+ *
    + *
  • Whether to use the "base32hex" variant instead of the default "base32"
  • + *
  • Line length: Default 76. Line length that aren't multiples of 8 will still essentially end up being multiples of + * 8 in the encoded data. + *
  • Line separator: Default is CRLF ("\r\n")
  • + *
+ *

+ * This class operates directly on byte streams, and not character streams. + *

+ *

+ * This class is thread-safe. + *

+ * + * @see RFC 4648 + * + * @since 1.5 + * @version $Id$ + */ +public class Base32 extends BaseNCodec { + + /** + * BASE32 characters are 5 bits in length. + * They are formed by taking a block of five octets to form a 40-bit string, + * which is converted into eight BASE32 characters. + */ + private static final int BITS_PER_ENCODED_BYTE = 5; + private static final int BYTES_PER_ENCODED_BLOCK = 8; + private static final int BYTES_PER_UNENCODED_BLOCK = 5; + + /** + * Chunk separator per RFC 2045 section 2.1. + * + * @see RFC 2045 section 2.1 + */ + private static final byte[] CHUNK_SEPARATOR = {'\r', '\n'}; + + /** + * This array is a lookup table that translates Unicode characters drawn from the "Base32 Alphabet" (as specified + * in Table 3 of RFC 4648) into their 5-bit positive integer equivalents. Characters that are not in the Base32 + * alphabet but fall within the bounds of the array are translated to -1. + */ + private static final byte[] DECODE_TABLE = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 20-2f + -1, -1, 26, 27, 28, 29, 30, 31, -1, -1, -1, -1, -1, -1, -1, -1, // 30-3f 2-7 + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4f A-O + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // 50-5a P-Z + }; + + /** + * This array is a lookup table that translates 5-bit positive integer index values into their "Base32 Alphabet" + * equivalents as specified in Table 3 of RFC 4648. + */ + private static final byte[] ENCODE_TABLE = { + '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', + '2', '3', '4', '5', '6', '7', + }; + + /** + * This array is a lookup table that translates Unicode characters drawn from the "Base32 Hex Alphabet" (as + * specified in Table 4 of RFC 4648) into their 5-bit positive integer equivalents. Characters that are not in the + * Base32 Hex alphabet but fall within the bounds of the array are translated to -1. + */ + private static final byte[] HEX_DECODE_TABLE = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 20-2f + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1, // 30-3f 2-7 + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 40-4f A-O + 25, 26, 27, 28, 29, 30, 31, // 50-57 P-V + }; + + /** + * This array is a lookup table that translates 5-bit positive integer index values into their + * "Base32 Hex Alphabet" equivalents as specified in Table 4 of RFC 4648. + */ + private static final byte[] HEX_ENCODE_TABLE = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + }; + + /** Mask used to extract 5 bits, used when encoding Base32 bytes */ + private static final int MASK_5BITS = 0x1f; + + // The static final fields above are used for the original static byte[] methods on Base32. + // The private member fields below are used with the new streaming approach, which requires + // some state be preserved between calls of encode() and decode(). + + /** + * Place holder for the bytes we're dealing with for our based logic. + * Bitwise operations store and extract the encoding or decoding from this variable. + */ + + /** + * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. + * decodeSize = {@link #BYTES_PER_ENCODED_BLOCK} - 1 + lineSeparator.length; + */ + private final int decodeSize; + + /** + * Decode table to use. + */ + private final byte[] decodeTable; + + /** + * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. + * encodeSize = {@link #BYTES_PER_ENCODED_BLOCK} + lineSeparator.length; + */ + private final int encodeSize; + + /** + * Encode table to use. + */ + private final byte[] encodeTable; + + /** + * Line separator for encoding. Not used when decoding. Only used if lineLength > 0. + */ + private final byte[] lineSeparator; + + /** + * Creates a Base32 codec used for decoding and encoding. + *

+ * When encoding the line length is 0 (no chunking). + *

+ * + */ + public Base32() { + this(false); + } + + /** + * Creates a Base32 codec used for decoding and encoding. + *

+ * When encoding the line length is 0 (no chunking). + *

+ * @param pad byte used as padding byte. + */ + public Base32(final byte pad) { + this(false, pad); + } + + /** + * Creates a Base32 codec used for decoding and encoding. + *

+ * When encoding the line length is 0 (no chunking). + *

+ * @param useHex if {@code true} then use Base32 Hex alphabet + */ + public Base32(final boolean useHex) { + this(0, null, useHex, PAD_DEFAULT); + } + + /** + * Creates a Base32 codec used for decoding and encoding. + *

+ * When encoding the line length is 0 (no chunking). + *

+ * @param useHex if {@code true} then use Base32 Hex alphabet + * @param pad byte used as padding byte. + */ + public Base32(final boolean useHex, final byte pad) { + this(0, null, useHex, pad); + } + + /** + * Creates a Base32 codec used for decoding and encoding. + *

+ * When encoding the line length is given in the constructor, the line separator is CRLF. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 8). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + */ + public Base32(final int lineLength) { + this(lineLength, CHUNK_SEPARATOR); + } + + /** + * Creates a Base32 codec used for decoding and encoding. + *

+ * When encoding the line length and line separator are given in the constructor. + *

+ *

+ * Line lengths that aren't multiples of 8 will still essentially end up being multiples of 8 in the encoded data. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 8). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of bytes. + * @throws IllegalArgumentException + * The provided lineSeparator included some Base32 characters. That's not going to work! + */ + public Base32(final int lineLength, final byte[] lineSeparator) { + this(lineLength, lineSeparator, false, PAD_DEFAULT); + } + + /** + * Creates a Base32 / Base32 Hex codec used for decoding and encoding. + *

+ * When encoding the line length and line separator are given in the constructor. + *

+ *

+ * Line lengths that aren't multiples of 8 will still essentially end up being multiples of 8 in the encoded data. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 8). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of bytes. + * @param useHex + * if {@code true}, then use Base32 Hex alphabet, otherwise use Base32 alphabet + * @throws IllegalArgumentException + * The provided lineSeparator included some Base32 characters. That's not going to work! Or the + * lineLength > 0 and lineSeparator is null. + */ + public Base32(final int lineLength, final byte[] lineSeparator, final boolean useHex) { + this(lineLength, lineSeparator, useHex, PAD_DEFAULT); + } + + /** + * Creates a Base32 / Base32 Hex codec used for decoding and encoding. + *

+ * When encoding the line length and line separator are given in the constructor. + *

+ *

+ * Line lengths that aren't multiples of 8 will still essentially end up being multiples of 8 in the encoded data. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 8). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of bytes. + * @param useHex + * if {@code true}, then use Base32 Hex alphabet, otherwise use Base32 alphabet + * @param pad byte used as padding byte. + * @throws IllegalArgumentException + * The provided lineSeparator included some Base32 characters. That's not going to work! Or the + * lineLength > 0 and lineSeparator is null. + */ + public Base32(final int lineLength, final byte[] lineSeparator, final boolean useHex, final byte pad) { + super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, lineLength, + lineSeparator == null ? 0 : lineSeparator.length, pad); + if (useHex) { + this.encodeTable = HEX_ENCODE_TABLE; + this.decodeTable = HEX_DECODE_TABLE; + } else { + this.encodeTable = ENCODE_TABLE; + this.decodeTable = DECODE_TABLE; + } + if (lineLength > 0) { + if (lineSeparator == null) { + throw new IllegalArgumentException("lineLength " + lineLength + " > 0, but lineSeparator is null"); + } + // Must be done after initializing the tables + if (containsAlphabetOrPad(lineSeparator)) { + final String sep = StringUtils.newStringUtf8(lineSeparator); + throw new IllegalArgumentException("lineSeparator must not contain Base32 characters: [" + sep + "]"); + } + this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length; + this.lineSeparator = new byte[lineSeparator.length]; + System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length); + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + this.decodeSize = this.encodeSize - 1; + + if (isInAlphabet(pad) || isWhiteSpace(pad)) { + throw new IllegalArgumentException("pad must not be in alphabet or whitespace"); + } + } + + /** + *

+ * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once + * with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1" + * call is not necessary when decoding, but it doesn't hurt, either. + *

+ *

+ * Ignores all non-Base32 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are + * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in, + * garbage-out philosophy: it will not check the provided data for validity. + *

+ * + * @param in + * byte[] array of ascii data to Base32 decode. + * @param inPos + * Position to start reading data from. + * @param inAvail + * Amount of bytes available from input for encoding. + * @param context the context to be used + * + * Output is written to {@link Context#buffer} as 8-bit octets, using {@link Context#pos} as the buffer position + */ + @Override + void decode(final byte[] in, int inPos, final int inAvail, final Context context) { + // package protected for access from I/O streams + + if (context.eof) { + return; + } + if (inAvail < 0) { + context.eof = true; + } + for (int i = 0; i < inAvail; i++) { + final byte b = in[inPos++]; + if (b == pad) { + // We're done. + context.eof = true; + break; + } else { + final byte[] buffer = ensureBufferSize(decodeSize, context); + if (b >= 0 && b < this.decodeTable.length) { + final int result = this.decodeTable[b]; + if (result >= 0) { + context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK; + // collect decoded bytes + context.lbitWorkArea = (context.lbitWorkArea << BITS_PER_ENCODED_BYTE) + result; + if (context.modulus == 0) { // we can output the 5 bytes + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 32) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 24) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) (context.lbitWorkArea & MASK_8BITS); + } + } + } + } + } + + // Two forms of EOF as far as Base32 decoder is concerned: actual + // EOF (-1) and first time '=' character is encountered in stream. + // This approach makes the '=' padding characters completely optional. + if (context.eof && context.modulus >= 2) { // if modulus < 2, nothing to do + final byte[] buffer = ensureBufferSize(decodeSize, context); + + // we ignore partial bytes, i.e. only multiples of 8 count + switch (context.modulus) { + case 2 : // 10 bits, drop 2 and output one byte + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 2) & MASK_8BITS); + break; + case 3 : // 15 bits, drop 7 and output 1 byte + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 7) & MASK_8BITS); + break; + case 4 : // 20 bits = 2*8 + 4 + context.lbitWorkArea = context.lbitWorkArea >> 4; // drop 4 bits + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS); + break; + case 5 : // 25bits = 3*8 + 1 + context.lbitWorkArea = context.lbitWorkArea >> 1; + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS); + break; + case 6 : // 30bits = 3*8 + 6 + context.lbitWorkArea = context.lbitWorkArea >> 6; + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS); + break; + case 7 : // 35 = 4*8 +3 + context.lbitWorkArea = context.lbitWorkArea >> 3; + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 24) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 16) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.lbitWorkArea) & MASK_8BITS); + break; + default: + // modulus can be 0-7, and we excluded 0,1 already + throw new IllegalStateException("Impossible modulus "+context.modulus); + } + } + } + + /** + *

+ * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with + * the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, so flush last + * remaining bytes (if not multiple of 5). + *

+ * + * @param in + * byte[] array of binary data to Base32 encode. + * @param inPos + * Position to start reading data from. + * @param inAvail + * Amount of bytes available from input for encoding. + * @param context the context to be used + */ + @Override + void encode(final byte[] in, int inPos, final int inAvail, final Context context) { + // package protected for access from I/O streams + + if (context.eof) { + return; + } + // inAvail < 0 is how we're informed of EOF in the underlying data we're + // encoding. + if (inAvail < 0) { + context.eof = true; + if (0 == context.modulus && lineLength == 0) { + return; // no leftovers to process and not using chunking + } + final byte[] buffer = ensureBufferSize(encodeSize, context); + final int savedPos = context.pos; + switch (context.modulus) { // % 5 + case 0 : + break; + case 1 : // Only 1 octet; take top 5 bits then remainder + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 3) & MASK_5BITS]; // 8-1*5 = 3 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea << 2) & MASK_5BITS]; // 5-3=2 + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + break; + case 2 : // 2 octets = 16 bits to use + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 11) & MASK_5BITS]; // 16-1*5 = 11 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 6) & MASK_5BITS]; // 16-2*5 = 6 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 1) & MASK_5BITS]; // 16-3*5 = 1 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea << 4) & MASK_5BITS]; // 5-1 = 4 + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + break; + case 3 : // 3 octets = 24 bits to use + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 19) & MASK_5BITS]; // 24-1*5 = 19 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 14) & MASK_5BITS]; // 24-2*5 = 14 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 9) & MASK_5BITS]; // 24-3*5 = 9 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 4) & MASK_5BITS]; // 24-4*5 = 4 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea << 1) & MASK_5BITS]; // 5-4 = 1 + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + break; + case 4 : // 4 octets = 32 bits to use + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 27) & MASK_5BITS]; // 32-1*5 = 27 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 22) & MASK_5BITS]; // 32-2*5 = 22 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 17) & MASK_5BITS]; // 32-3*5 = 17 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 12) & MASK_5BITS]; // 32-4*5 = 12 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 7) & MASK_5BITS]; // 32-5*5 = 7 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 2) & MASK_5BITS]; // 32-6*5 = 2 + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea << 3) & MASK_5BITS]; // 5-2 = 3 + buffer[context.pos++] = pad; + break; + default: + throw new IllegalStateException("Impossible modulus "+context.modulus); + } + context.currentLinePos += context.pos - savedPos; // keep track of current line position + // if currentPos == 0 we are at the start of a line, so don't add CRLF + if (lineLength > 0 && context.currentLinePos > 0){ // add chunk separator if required + System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); + context.pos += lineSeparator.length; + } + } else { + for (int i = 0; i < inAvail; i++) { + final byte[] buffer = ensureBufferSize(encodeSize, context); + context.modulus = (context.modulus+1) % BYTES_PER_UNENCODED_BLOCK; + int b = in[inPos++]; + if (b < 0) { + b += 256; + } + context.lbitWorkArea = (context.lbitWorkArea << 8) + b; // BITS_PER_BYTE + if (0 == context.modulus) { // we have enough bytes to create our output + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 35) & MASK_5BITS]; + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 30) & MASK_5BITS]; + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 25) & MASK_5BITS]; + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 20) & MASK_5BITS]; + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 15) & MASK_5BITS]; + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 10) & MASK_5BITS]; + buffer[context.pos++] = encodeTable[(int)(context.lbitWorkArea >> 5) & MASK_5BITS]; + buffer[context.pos++] = encodeTable[(int)context.lbitWorkArea & MASK_5BITS]; + context.currentLinePos += BYTES_PER_ENCODED_BLOCK; + if (lineLength > 0 && lineLength <= context.currentLinePos) { + System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); + context.pos += lineSeparator.length; + context.currentLinePos = 0; + } + } + } + } + } + + /** + * Returns whether or not the {@code octet} is in the Base32 alphabet. + * + * @param octet + * The value to test + * @return {@code true} if the value is defined in the the Base32 alphabet {@code false} otherwise. + */ + @Override + public boolean isInAlphabet(final byte octet) { + return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1; + } +} diff --git a/src/org/apache/commons/codec/binary/Base32InputStream.java b/src/org/apache/commons/codec/binary/Base32InputStream.java new file mode 100644 index 00000000..844f9a06 --- /dev/null +++ b/src/org/apache/commons/codec/binary/Base32InputStream.java @@ -0,0 +1,85 @@ +/* + * 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 org.apache.commons.codec.binary; + +import java.io.InputStream; + +/** + * Provides Base32 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength + * is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate + * constructor. + *

+ * The default behaviour of the Base32InputStream is to DECODE, whereas the default behaviour of the Base32OutputStream + * is to ENCODE, but this behaviour can be overridden by using a different constructor. + *

+ *

+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode + * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc). + *

+ * + * @version $Id$ + * @see RFC 4648 + * @since 1.5 + */ +public class Base32InputStream extends BaseNCodecInputStream { + + /** + * Creates a Base32InputStream such that all data read is Base32-decoded from the original provided InputStream. + * + * @param in + * InputStream to wrap. + */ + public Base32InputStream(final InputStream in) { + this(in, false); + } + + /** + * Creates a Base32InputStream such that all data read is either Base32-encoded or Base32-decoded from the original + * provided InputStream. + * + * @param in + * InputStream to wrap. + * @param doEncode + * true if we should encode all data read from us, false if we should decode. + */ + public Base32InputStream(final InputStream in, final boolean doEncode) { + super(in, new Base32(false), doEncode); + } + + /** + * Creates a Base32InputStream such that all data read is either Base32-encoded or Base32-decoded from the original + * provided InputStream. + * + * @param in + * InputStream to wrap. + * @param doEncode + * true if we should encode all data read from us, false if we should decode. + * @param lineLength + * If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to + * nearest multiple of 4). If lineLength <= 0, the encoded data is not divided into lines. If doEncode + * is false, lineLength is ignored. + * @param lineSeparator + * If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n). + * If lineLength <= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored. + */ + public Base32InputStream(final InputStream in, final boolean doEncode, + final int lineLength, final byte[] lineSeparator) { + super(in, new Base32(lineLength, lineSeparator), doEncode); + } + +} diff --git a/src/org/apache/commons/codec/binary/Base32OutputStream.java b/src/org/apache/commons/codec/binary/Base32OutputStream.java new file mode 100644 index 00000000..b2319315 --- /dev/null +++ b/src/org/apache/commons/codec/binary/Base32OutputStream.java @@ -0,0 +1,89 @@ +/* + * 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 org.apache.commons.codec.binary; + +import java.io.OutputStream; + +/** + * Provides Base32 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength + * is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate + * constructor. + *

+ * The default behaviour of the Base32OutputStream is to ENCODE, whereas the default behaviour of the Base32InputStream + * is to DECODE. But this behaviour can be overridden by using a different constructor. + *

+ *

+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode + * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc). + *

+ *

+ * Note: It is mandatory to close the stream after the last byte has been written to it, otherwise the + * final padding will be omitted and the resulting data will be incomplete/inconsistent. + *

+ * + * @version $Id$ + * @see RFC 4648 + * @since 1.5 + */ +public class Base32OutputStream extends BaseNCodecOutputStream { + + /** + * Creates a Base32OutputStream such that all data written is Base32-encoded to the original provided OutputStream. + * + * @param out + * OutputStream to wrap. + */ + public Base32OutputStream(final OutputStream out) { + this(out, true); + } + + /** + * Creates a Base32OutputStream such that all data written is either Base32-encoded or Base32-decoded to the + * original provided OutputStream. + * + * @param out + * OutputStream to wrap. + * @param doEncode + * true if we should encode all data written to us, false if we should decode. + */ + public Base32OutputStream(final OutputStream out, final boolean doEncode) { + super(out, new Base32(false), doEncode); + } + + /** + * Creates a Base32OutputStream such that all data written is either Base32-encoded or Base32-decoded to the + * original provided OutputStream. + * + * @param out + * OutputStream to wrap. + * @param doEncode + * true if we should encode all data written to us, false if we should decode. + * @param lineLength + * If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to + * nearest multiple of 4). If lineLength <= 0, the encoded data is not divided into lines. If doEncode + * is false, lineLength is ignored. + * @param lineSeparator + * If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n). + * If lineLength <= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored. + */ + public Base32OutputStream(final OutputStream out, final boolean doEncode, + final int lineLength, final byte[] lineSeparator) { + super(out, new Base32(lineLength, lineSeparator), doEncode); + } + +} diff --git a/src/org/apache/commons/codec/binary/Base64.java b/src/org/apache/commons/codec/binary/Base64.java new file mode 100644 index 00000000..742c433c --- /dev/null +++ b/src/org/apache/commons/codec/binary/Base64.java @@ -0,0 +1,786 @@ +/* + * 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 org.apache.commons.codec.binary; + +import java.math.BigInteger; + +/** + * Provides Base64 encoding and decoding as defined by RFC 2045. + * + *

+ * This class implements section 6.8. Base64 Content-Transfer-Encoding from RFC 2045 Multipurpose + * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies by Freed and Borenstein. + *

+ *

+ * The class can be parameterized in the following manner with various constructors: + *

+ *
    + *
  • URL-safe mode: Default off.
  • + *
  • Line length: Default 76. Line length that aren't multiples of 4 will still essentially end up being multiples of + * 4 in the encoded data. + *
  • Line separator: Default is CRLF ("\r\n")
  • + *
+ *

+ * The URL-safe parameter is only applied to encode operations. Decoding seamlessly handles both modes. + *

+ *

+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only + * encode/decode character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, + * UTF-8, etc). + *

+ *

+ * This class is thread-safe. + *

+ * + * @see RFC 2045 + * @since 1.0 + * @version $Id$ + */ +public class Base64 extends BaseNCodec { + + /** + * BASE32 characters are 6 bits in length. + * They are formed by taking a block of 3 octets to form a 24-bit string, + * which is converted into 4 BASE64 characters. + */ + private static final int BITS_PER_ENCODED_BYTE = 6; + private static final int BYTES_PER_UNENCODED_BLOCK = 3; + private static final int BYTES_PER_ENCODED_BLOCK = 4; + + /** + * Chunk separator per RFC 2045 section 2.1. + * + *

+ * N.B. The next major release may break compatibility and make this field private. + *

+ * + * @see RFC 2045 section 2.1 + */ + static final byte[] CHUNK_SEPARATOR = {'\r', '\n'}; + + /** + * This array is a lookup table that translates 6-bit positive integer index values into their "Base64 Alphabet" + * equivalents as specified in Table 1 of RFC 2045. + * + * Thanks to "commons" project in ws.apache.org for this code. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + */ + private static final byte[] STANDARD_ENCODE_TABLE = { + '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', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' + }; + + /** + * This is a copy of the STANDARD_ENCODE_TABLE above, but with + and / + * changed to - and _ to make the encoded Base64 results more URL-SAFE. + * This table is only used when the Base64's mode is set to URL-SAFE. + */ + private static final byte[] URL_SAFE_ENCODE_TABLE = { + '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', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' + }; + + /** + * This array is a lookup table that translates Unicode characters drawn from the "Base64 Alphabet" (as specified + * in Table 1 of RFC 2045) into their 6-bit positive integer equivalents. Characters that are not in the Base64 + * alphabet but fall within the bounds of the array are translated to -1. + * + * Note: '+' and '-' both decode to 62. '/' and '_' both decode to 63. This means decoder seamlessly handles both + * URL_SAFE and STANDARD base64. (The encoder, on the other hand, needs to know ahead of time what to emit). + * + * Thanks to "commons" project in ws.apache.org for this code. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + */ + private static final byte[] DECODE_TABLE = { + // 0 1 2 3 4 5 6 7 8 9 A B C D E F + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 00-0f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 10-1f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, 62, -1, 63, // 20-2f + - / + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, // 30-3f 0-9 + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 40-4f A-O + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, // 50-5f P-Z _ + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 60-6f a-o + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51 // 70-7a p-z + }; + + /** + * Base64 uses 6-bit fields. + */ + /** Mask used to extract 6 bits, used when encoding */ + private static final int MASK_6BITS = 0x3f; + + // The static final fields above are used for the original static byte[] methods on Base64. + // The private member fields below are used with the new streaming approach, which requires + // some state be preserved between calls of encode() and decode(). + + /** + * Encode table to use: either STANDARD or URL_SAFE. Note: the DECODE_TABLE above remains static because it is able + * to decode both STANDARD and URL_SAFE streams, but the encodeTable must be a member variable so we can switch + * between the two modes. + */ + private final byte[] encodeTable; + + // Only one decode table currently; keep for consistency with Base32 code + private final byte[] decodeTable = DECODE_TABLE; + + /** + * Line separator for encoding. Not used when decoding. Only used if lineLength > 0. + */ + private final byte[] lineSeparator; + + /** + * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. + * decodeSize = 3 + lineSeparator.length; + */ + private final int decodeSize; + + /** + * Convenience variable to help us determine when our buffer is going to run out of room and needs resizing. + * encodeSize = 4 + lineSeparator.length; + */ + private final int encodeSize; + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length is 0 (no chunking), and the encoding table is STANDARD_ENCODE_TABLE. + *

+ * + *

+ * When decoding all variants are supported. + *

+ */ + public Base64() { + this(0); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in the given URL-safe mode. + *

+ * When encoding the line length is 76, the line separator is CRLF, and the encoding table is STANDARD_ENCODE_TABLE. + *

+ * + *

+ * When decoding all variants are supported. + *

+ * + * @param urlSafe + * if true, URL-safe encoding is used. In most cases this should be set to + * false. + * @since 1.4 + */ + public Base64(final boolean urlSafe) { + this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length is given in the constructor, the line separator is CRLF, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @since 1.4 + */ + public Base64(final int lineLength) { + this(lineLength, CHUNK_SEPARATOR); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length and line separator are given in the constructor, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of bytes. + * @throws IllegalArgumentException + * Thrown when the provided lineSeparator included some base64 characters. + * @since 1.4 + */ + public Base64(final int lineLength, final byte[] lineSeparator) { + this(lineLength, lineSeparator, false); + } + + /** + * Creates a Base64 codec used for decoding (all modes) and encoding in URL-unsafe mode. + *

+ * When encoding the line length and line separator are given in the constructor, and the encoding table is + * STANDARD_ENCODE_TABLE. + *

+ *

+ * Line lengths that aren't multiples of 4 will still essentially end up being multiples of 4 in the encoded data. + *

+ *

+ * When decoding all variants are supported. + *

+ * + * @param lineLength + * Each line of encoded data will be at most of the given length (rounded down to nearest multiple of + * 4). If lineLength <= 0, then the output will not be divided into lines (chunks). Ignored when + * decoding. + * @param lineSeparator + * Each line of encoded data will end with this sequence of bytes. + * @param urlSafe + * Instead of emitting '+' and '/' we emit '-' and '_' respectively. urlSafe is only applied to encode + * operations. Decoding seamlessly handles both modes. + * Note: no padding is added when using the URL-safe alphabet. + * @throws IllegalArgumentException + * The provided lineSeparator included some base64 characters. That's not going to work! + * @since 1.4 + */ + public Base64(final int lineLength, final byte[] lineSeparator, final boolean urlSafe) { + super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, + lineLength, + lineSeparator == null ? 0 : lineSeparator.length); + // TODO could be simplified if there is no requirement to reject invalid line sep when length <=0 + // @see test case Base64Test.testConstructors() + if (lineSeparator != null) { + if (containsAlphabetOrPad(lineSeparator)) { + final String sep = StringUtils.newStringUtf8(lineSeparator); + throw new IllegalArgumentException("lineSeparator must not contain base64 characters: [" + sep + "]"); + } + if (lineLength > 0){ // null line-sep forces no chunking rather than throwing IAE + this.encodeSize = BYTES_PER_ENCODED_BLOCK + lineSeparator.length; + this.lineSeparator = new byte[lineSeparator.length]; + System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length); + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + } else { + this.encodeSize = BYTES_PER_ENCODED_BLOCK; + this.lineSeparator = null; + } + this.decodeSize = this.encodeSize - 1; + this.encodeTable = urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE; + } + + /** + * Returns our current encode mode. True if we're URL-SAFE, false otherwise. + * + * @return true if we're in URL-SAFE mode, false otherwise. + * @since 1.4 + */ + public boolean isUrlSafe() { + return this.encodeTable == URL_SAFE_ENCODE_TABLE; + } + + /** + *

+ * Encodes all of the provided data, starting at inPos, for inAvail bytes. Must be called at least twice: once with + * the data to encode, and once with inAvail set to "-1" to alert encoder that EOF has been reached, to flush last + * remaining bytes (if not multiple of 3). + *

+ *

Note: no padding is added when encoding using the URL-safe alphabet.

+ *

+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

+ * + * @param in + * byte[] array of binary data to base64 encode. + * @param inPos + * Position to start reading data from. + * @param inAvail + * Amount of bytes available from input for encoding. + * @param context + * the context to be used + */ + @Override + void encode(final byte[] in, int inPos, final int inAvail, final Context context) { + if (context.eof) { + return; + } + // inAvail < 0 is how we're informed of EOF in the underlying data we're + // encoding. + if (inAvail < 0) { + context.eof = true; + if (0 == context.modulus && lineLength == 0) { + return; // no leftovers to process and not using chunking + } + final byte[] buffer = ensureBufferSize(encodeSize, context); + final int savedPos = context.pos; + switch (context.modulus) { // 0-2 + case 0 : // nothing to do here + break; + case 1 : // 8 bits = 6 + 2 + // top 6 bits: + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 2) & MASK_6BITS]; + // remaining 2: + buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 4) & MASK_6BITS]; + // URL-SAFE skips the padding to further reduce size. + if (encodeTable == STANDARD_ENCODE_TABLE) { + buffer[context.pos++] = pad; + buffer[context.pos++] = pad; + } + break; + + case 2 : // 16 bits = 6 + 6 + 4 + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 10) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 4) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 2) & MASK_6BITS]; + // URL-SAFE skips the padding to further reduce size. + if (encodeTable == STANDARD_ENCODE_TABLE) { + buffer[context.pos++] = pad; + } + break; + default: + throw new IllegalStateException("Impossible modulus "+context.modulus); + } + context.currentLinePos += context.pos - savedPos; // keep track of current line position + // if currentPos == 0 we are at the start of a line, so don't add CRLF + if (lineLength > 0 && context.currentLinePos > 0) { + System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); + context.pos += lineSeparator.length; + } + } else { + for (int i = 0; i < inAvail; i++) { + final byte[] buffer = ensureBufferSize(encodeSize, context); + context.modulus = (context.modulus+1) % BYTES_PER_UNENCODED_BLOCK; + int b = in[inPos++]; + if (b < 0) { + b += 256; + } + context.ibitWorkArea = (context.ibitWorkArea << 8) + b; // BITS_PER_BYTE + if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 18) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 12) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 6) & MASK_6BITS]; + buffer[context.pos++] = encodeTable[context.ibitWorkArea & MASK_6BITS]; + context.currentLinePos += BYTES_PER_ENCODED_BLOCK; + if (lineLength > 0 && lineLength <= context.currentLinePos) { + System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length); + context.pos += lineSeparator.length; + context.currentLinePos = 0; + } + } + } + } + } + + /** + *

+ * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once + * with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1" + * call is not necessary when decoding, but it doesn't hurt, either. + *

+ *

+ * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are + * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in, + * garbage-out philosophy: it will not check the provided data for validity. + *

+ *

+ * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach. + * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/ + *

+ * + * @param in + * byte[] array of ascii data to base64 decode. + * @param inPos + * Position to start reading data from. + * @param inAvail + * Amount of bytes available from input for encoding. + * @param context + * the context to be used + */ + @Override + void decode(final byte[] in, int inPos, final int inAvail, final Context context) { + if (context.eof) { + return; + } + if (inAvail < 0) { + context.eof = true; + } + for (int i = 0; i < inAvail; i++) { + final byte[] buffer = ensureBufferSize(decodeSize, context); + final byte b = in[inPos++]; + if (b == pad) { + // We're done. + context.eof = true; + break; + } else { + if (b >= 0 && b < DECODE_TABLE.length) { + final int result = DECODE_TABLE[b]; + if (result >= 0) { + context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK; + context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result; + if (context.modulus == 0) { + buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS); + } + } + } + } + } + + // Two forms of EOF as far as base64 decoder is concerned: actual + // EOF (-1) and first time '=' character is encountered in stream. + // This approach makes the '=' padding characters completely optional. + if (context.eof && context.modulus != 0) { + final byte[] buffer = ensureBufferSize(decodeSize, context); + + // We have some spare bits remaining + // Output all whole multiples of 8 bits and ignore the rest + switch (context.modulus) { +// case 0 : // impossible, as excluded above + case 1 : // 6 bits - ignore entirely + // TODO not currently tested; perhaps it is impossible? + break; + case 2 : // 12 bits = 8 + 4 + context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits + buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS); + break; + case 3 : // 18 bits = 8 + 8 + 2 + context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits + buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS); + buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS); + break; + default: + throw new IllegalStateException("Impossible modulus "+context.modulus); + } + } + } + + /** + * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the + * method treats whitespace as valid. + * + * @param arrayOctet + * byte array to test + * @return true if all bytes are valid characters in the Base64 alphabet or if the byte array is empty; + * false, otherwise + * @deprecated 1.5 Use {@link #isBase64(byte[])}, will be removed in 2.0. + */ + @Deprecated + public static boolean isArrayByteBase64(final byte[] arrayOctet) { + return isBase64(arrayOctet); + } + + /** + * Returns whether or not the octet is in the base 64 alphabet. + * + * @param octet + * The value to test + * @return true if the value is defined in the the base 64 alphabet, false otherwise. + * @since 1.4 + */ + public static boolean isBase64(final byte octet) { + return octet == PAD_DEFAULT || (octet >= 0 && octet < DECODE_TABLE.length && DECODE_TABLE[octet] != -1); + } + + /** + * Tests a given String to see if it contains only valid characters within the Base64 alphabet. Currently the + * method treats whitespace as valid. + * + * @param base64 + * String to test + * @return true if all characters in the String are valid characters in the Base64 alphabet or if + * the String is empty; false, otherwise + * @since 1.5 + */ + public static boolean isBase64(final String base64) { + return isBase64(StringUtils.getBytesUtf8(base64)); + } + + /** + * Tests a given byte array to see if it contains only valid characters within the Base64 alphabet. Currently the + * method treats whitespace as valid. + * + * @param arrayOctet + * byte array to test + * @return true if all bytes are valid characters in the Base64 alphabet or if the byte array is empty; + * false, otherwise + * @since 1.5 + */ + public static boolean isBase64(final byte[] arrayOctet) { + for (int i = 0; i < arrayOctet.length; i++) { + if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) { + return false; + } + } + return true; + } + + /** + * Encodes binary data using the base64 algorithm but does not chunk the output. + * + * @param binaryData + * binary data to encode + * @return byte[] containing Base64 characters in their UTF-8 representation. + */ + public static byte[] encodeBase64(final byte[] binaryData) { + return encodeBase64(binaryData, false); + } + + /** + * Encodes binary data using the base64 algorithm but does not chunk the output. + * + * NOTE: We changed the behaviour of this method from multi-line chunking (commons-codec-1.4) to + * single-line non-chunking (commons-codec-1.5). + * + * @param binaryData + * binary data to encode + * @return String containing Base64 characters. + * @since 1.4 (NOTE: 1.4 chunked the output, whereas 1.5 does not). + */ + public static String encodeBase64String(final byte[] binaryData) { + return StringUtils.newStringUtf8(encodeBase64(binaryData, false)); + } + + /** + * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The + * url-safe variation emits - and _ instead of + and / characters. + * Note: no padding is added. + * @param binaryData + * binary data to encode + * @return byte[] containing Base64 characters in their UTF-8 representation. + * @since 1.4 + */ + public static byte[] encodeBase64URLSafe(final byte[] binaryData) { + return encodeBase64(binaryData, false, true); + } + + /** + * Encodes binary data using a URL-safe variation of the base64 algorithm but does not chunk the output. The + * url-safe variation emits - and _ instead of + and / characters. + * Note: no padding is added. + * @param binaryData + * binary data to encode + * @return String containing Base64 characters + * @since 1.4 + */ + public static String encodeBase64URLSafeString(final byte[] binaryData) { + return StringUtils.newStringUtf8(encodeBase64(binaryData, false, true)); + } + + /** + * Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks + * + * @param binaryData + * binary data to encode + * @return Base64 characters chunked in 76 character blocks + */ + public static byte[] encodeBase64Chunked(final byte[] binaryData) { + return encodeBase64(binaryData, true); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if true this encoder will chunk the base64 output into 76 character blocks + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE} + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked) { + return encodeBase64(binaryData, isChunked, false); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if true this encoder will chunk the base64 output into 76 character blocks + * @param urlSafe + * if true this encoder will emit - and _ instead of the usual + and / characters. + * Note: no padding is added when encoding using the URL-safe alphabet. + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE} + * @since 1.4 + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe) { + return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE); + } + + /** + * Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks. + * + * @param binaryData + * Array containing binary data to encode. + * @param isChunked + * if true this encoder will chunk the base64 output into 76 character blocks + * @param urlSafe + * if true this encoder will emit - and _ instead of the usual + and / characters. + * Note: no padding is added when encoding using the URL-safe alphabet. + * @param maxResultSize + * The maximum result size to accept. + * @return Base64-encoded data. + * @throws IllegalArgumentException + * Thrown when the input array needs an output array bigger than maxResultSize + * @since 1.4 + */ + public static byte[] encodeBase64(final byte[] binaryData, final boolean isChunked, + final boolean urlSafe, final int maxResultSize) { + if (binaryData == null || binaryData.length == 0) { + return binaryData; + } + + // Create this so can use the super-class method + // Also ensures that the same roundings are performed by the ctor and the code + final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, CHUNK_SEPARATOR, urlSafe); + final long len = b64.getEncodedLength(binaryData); + if (len > maxResultSize) { + throw new IllegalArgumentException("Input array too big, the output array would be bigger (" + + len + + ") than the specified maximum size of " + + maxResultSize); + } + + return b64.encode(binaryData); + } + + /** + * Decodes a Base64 String into octets. + *

+ * Note: this method seamlessly handles data encoded in URL-safe or normal mode. + *

+ * + * @param base64String + * String containing Base64 data + * @return Array containing decoded data. + * @since 1.4 + */ + public static byte[] decodeBase64(final String base64String) { + return new Base64().decode(base64String); + } + + /** + * Decodes Base64 data into octets. + *

+ * Note: this method seamlessly handles data encoded in URL-safe or normal mode. + *

+ * + * @param base64Data + * Byte array containing Base64 data + * @return Array containing decoded data. + */ + public static byte[] decodeBase64(final byte[] base64Data) { + return new Base64().decode(base64Data); + } + + // Implementation of the Encoder Interface + + // Implementation of integer encoding used for crypto + /** + * Decodes a byte64-encoded integer according to crypto standards such as W3C's XML-Signature. + * + * @param pArray + * a byte array containing base64 character data + * @return A BigInteger + * @since 1.4 + */ + public static BigInteger decodeInteger(final byte[] pArray) { + return new BigInteger(1, decodeBase64(pArray)); + } + + /** + * Encodes to a byte64-encoded integer according to crypto standards such as W3C's XML-Signature. + * + * @param bigInt + * a BigInteger + * @return A byte array containing base64 character data + * @throws NullPointerException + * if null is passed in + * @since 1.4 + */ + public static byte[] encodeInteger(final BigInteger bigInt) { + if (bigInt == null) { + throw new NullPointerException("encodeInteger called with null parameter"); + } + return encodeBase64(toIntegerBytes(bigInt), false); + } + + /** + * Returns a byte-array representation of a BigInteger without sign bit. + * + * @param bigInt + * BigInteger to be converted + * @return a byte array representation of the BigInteger parameter + */ + static byte[] toIntegerBytes(final BigInteger bigInt) { + int bitlen = bigInt.bitLength(); + // round bitlen + bitlen = ((bitlen + 7) >> 3) << 3; + final byte[] bigBytes = bigInt.toByteArray(); + + if (((bigInt.bitLength() % 8) != 0) && (((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) { + return bigBytes; + } + // set up params for copying everything but sign bit + int startSrc = 0; + int len = bigBytes.length; + + // if bigInt is exactly byte-aligned, just skip signbit in copy + if ((bigInt.bitLength() % 8) == 0) { + startSrc = 1; + len--; + } + final int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec + final byte[] resizedBytes = new byte[bitlen / 8]; + System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len); + return resizedBytes; + } + + /** + * Returns whether or not the octet is in the Base64 alphabet. + * + * @param octet + * The value to test + * @return true if the value is defined in the the Base64 alphabet false otherwise. + */ + @Override + protected boolean isInAlphabet(final byte octet) { + return octet >= 0 && octet < decodeTable.length && decodeTable[octet] != -1; + } + +} diff --git a/src/org/apache/commons/codec/binary/Base64InputStream.java b/src/org/apache/commons/codec/binary/Base64InputStream.java new file mode 100644 index 00000000..5d926bde --- /dev/null +++ b/src/org/apache/commons/codec/binary/Base64InputStream.java @@ -0,0 +1,88 @@ +/* + * 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 org.apache.commons.codec.binary; + +import java.io.InputStream; + +/** + * Provides Base64 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength + * is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate + * constructor. + *

+ * The default behaviour of the Base64InputStream is to DECODE, whereas the default behaviour of the Base64OutputStream + * is to ENCODE, but this behaviour can be overridden by using a different constructor. + *

+ *

+ * This class implements section 6.8. Base64 Content-Transfer-Encoding from RFC 2045 Multipurpose + * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies by Freed and Borenstein. + *

+ *

+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode + * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc). + *

+ * + * @version $Id$ + * @see RFC 2045 + * @since 1.4 + */ +public class Base64InputStream extends BaseNCodecInputStream { + + /** + * Creates a Base64InputStream such that all data read is Base64-decoded from the original provided InputStream. + * + * @param in + * InputStream to wrap. + */ + public Base64InputStream(final InputStream in) { + this(in, false); + } + + /** + * Creates a Base64InputStream such that all data read is either Base64-encoded or Base64-decoded from the original + * provided InputStream. + * + * @param in + * InputStream to wrap. + * @param doEncode + * true if we should encode all data read from us, false if we should decode. + */ + public Base64InputStream(final InputStream in, final boolean doEncode) { + super(in, new Base64(false), doEncode); + } + + /** + * Creates a Base64InputStream such that all data read is either Base64-encoded or Base64-decoded from the original + * provided InputStream. + * + * @param in + * InputStream to wrap. + * @param doEncode + * true if we should encode all data read from us, false if we should decode. + * @param lineLength + * If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to + * nearest multiple of 4). If lineLength <= 0, the encoded data is not divided into lines. If doEncode + * is false, lineLength is ignored. + * @param lineSeparator + * If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n). + * If lineLength <= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored. + */ + public Base64InputStream(final InputStream in, final boolean doEncode, + final int lineLength, final byte[] lineSeparator) { + super(in, new Base64(lineLength, lineSeparator), doEncode); + } +} diff --git a/src/org/apache/commons/codec/binary/Base64OutputStream.java b/src/org/apache/commons/codec/binary/Base64OutputStream.java new file mode 100644 index 00000000..35d3e3c3 --- /dev/null +++ b/src/org/apache/commons/codec/binary/Base64OutputStream.java @@ -0,0 +1,92 @@ +/* + * 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 org.apache.commons.codec.binary; + +import java.io.OutputStream; + +/** + * Provides Base64 encoding and decoding in a streaming fashion (unlimited size). When encoding the default lineLength + * is 76 characters and the default lineEnding is CRLF, but these can be overridden by using the appropriate + * constructor. + *

+ * The default behaviour of the Base64OutputStream is to ENCODE, whereas the default behaviour of the Base64InputStream + * is to DECODE. But this behaviour can be overridden by using a different constructor. + *

+ *

+ * This class implements section 6.8. Base64 Content-Transfer-Encoding from RFC 2045 Multipurpose + * Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies by Freed and Borenstein. + *

+ *

+ * Since this class operates directly on byte streams, and not character streams, it is hard-coded to only encode/decode + * character encodings which are compatible with the lower 127 ASCII chart (ISO-8859-1, Windows-1252, UTF-8, etc). + *

+ *

+ * Note: It is mandatory to close the stream after the last byte has been written to it, otherwise the + * final padding will be omitted and the resulting data will be incomplete/inconsistent. + *

+ * + * @version $Id$ + * @see RFC 2045 + * @since 1.4 + */ +public class Base64OutputStream extends BaseNCodecOutputStream { + + /** + * Creates a Base64OutputStream such that all data written is Base64-encoded to the original provided OutputStream. + * + * @param out + * OutputStream to wrap. + */ + public Base64OutputStream(final OutputStream out) { + this(out, true); + } + + /** + * Creates a Base64OutputStream such that all data written is either Base64-encoded or Base64-decoded to the + * original provided OutputStream. + * + * @param out + * OutputStream to wrap. + * @param doEncode + * true if we should encode all data written to us, false if we should decode. + */ + public Base64OutputStream(final OutputStream out, final boolean doEncode) { + super(out,new Base64(false), doEncode); + } + + /** + * Creates a Base64OutputStream such that all data written is either Base64-encoded or Base64-decoded to the + * original provided OutputStream. + * + * @param out + * OutputStream to wrap. + * @param doEncode + * true if we should encode all data written to us, false if we should decode. + * @param lineLength + * If doEncode is true, each line of encoded data will contain lineLength characters (rounded down to + * nearest multiple of 4). If lineLength <= 0, the encoded data is not divided into lines. If doEncode + * is false, lineLength is ignored. + * @param lineSeparator + * If doEncode is true, each line of encoded data will be terminated with this byte sequence (e.g. \r\n). + * If lineLength <= 0, the lineSeparator is not used. If doEncode is false lineSeparator is ignored. + */ + public Base64OutputStream(final OutputStream out, final boolean doEncode, + final int lineLength, final byte[] lineSeparator) { + super(out, new Base64(lineLength, lineSeparator), doEncode); + } +} diff --git a/src/org/apache/commons/codec/binary/BaseNCodec.java b/src/org/apache/commons/codec/binary/BaseNCodec.java new file mode 100644 index 00000000..8d73442a --- /dev/null +++ b/src/org/apache/commons/codec/binary/BaseNCodec.java @@ -0,0 +1,525 @@ +/* + * 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 org.apache.commons.codec.binary; + +import java.util.Arrays; + +import org.apache.commons.codec.BinaryDecoder; +import org.apache.commons.codec.BinaryEncoder; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; + +/** + * Abstract superclass for Base-N encoders and decoders. + * + *

+ * This class is thread-safe. + *

+ * + * @version $Id$ + */ +public abstract class BaseNCodec implements BinaryEncoder, BinaryDecoder { + + /** + * Holds thread context so classes can be thread-safe. + * + * This class is not itself thread-safe; each thread must allocate its own copy. + * + * @since 1.7 + */ + static class Context { + + /** + * Place holder for the bytes we're dealing with for our based logic. + * Bitwise operations store and extract the encoding or decoding from this variable. + */ + int ibitWorkArea; + + /** + * Place holder for the bytes we're dealing with for our based logic. + * Bitwise operations store and extract the encoding or decoding from this variable. + */ + long lbitWorkArea; + + /** + * Buffer for streaming. + */ + byte[] buffer; + + /** + * Position where next character should be written in the buffer. + */ + int pos; + + /** + * Position where next character should be read from the buffer. + */ + int readPos; + + /** + * Boolean flag to indicate the EOF has been reached. Once EOF has been reached, this object becomes useless, + * and must be thrown away. + */ + boolean eof; + + /** + * Variable tracks how many characters have been written to the current line. Only used when encoding. We use + * it to make sure each encoded line never goes beyond lineLength (if lineLength > 0). + */ + int currentLinePos; + + /** + * Writes to the buffer only occur after every 3/5 reads when encoding, and every 4/8 reads when decoding. This + * variable helps track that. + */ + int modulus; + + Context() { + } + + /** + * Returns a String useful for debugging (especially within a debugger.) + * + * @return a String useful for debugging. + */ + @SuppressWarnings("boxing") // OK to ignore boxing here + @Override + public String toString() { + return String.format("%s[buffer=%s, currentLinePos=%s, eof=%s, ibitWorkArea=%s, lbitWorkArea=%s, " + + "modulus=%s, pos=%s, readPos=%s]", this.getClass().getSimpleName(), Arrays.toString(buffer), + currentLinePos, eof, ibitWorkArea, lbitWorkArea, modulus, pos, readPos); + } + } + + /** + * EOF + * + * @since 1.7 + */ + static final int EOF = -1; + + /** + * MIME chunk size per RFC 2045 section 6.8. + * + *

+ * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any + * equal signs. + *

+ * + * @see RFC 2045 section 6.8 + */ + public static final int MIME_CHUNK_SIZE = 76; + + /** + * PEM chunk size per RFC 1421 section 4.3.2.4. + * + *

+ * The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any + * equal signs. + *

+ * + * @see RFC 1421 section 4.3.2.4 + */ + public static final int PEM_CHUNK_SIZE = 64; + + private static final int DEFAULT_BUFFER_RESIZE_FACTOR = 2; + + /** + * Defines the default buffer size - currently {@value} + * - must be large enough for at least one encoded block+separator + */ + private static final int DEFAULT_BUFFER_SIZE = 8192; + + /** Mask used to extract 8 bits, used in decoding bytes */ + protected static final int MASK_8BITS = 0xff; + + /** + * Byte used to pad output. + */ + protected static final byte PAD_DEFAULT = '='; // Allow static access to default + + /** + * @deprecated Use {@link #pad}. Will be removed in 2.0. + */ + @Deprecated + protected final byte PAD = PAD_DEFAULT; // instance variable just in case it needs to vary later + + protected final byte pad; // instance variable just in case it needs to vary later + + /** Number of bytes in each full block of unencoded data, e.g. 4 for Base64 and 5 for Base32 */ + private final int unencodedBlockSize; + + /** Number of bytes in each full block of encoded data, e.g. 3 for Base64 and 8 for Base32 */ + private final int encodedBlockSize; + + /** + * Chunksize for encoding. Not used when decoding. + * A value of zero or less implies no chunking of the encoded data. + * Rounded down to nearest multiple of encodedBlockSize. + */ + protected final int lineLength; + + /** + * Size of chunk separator. Not used unless {@link #lineLength} > 0. + */ + private final int chunkSeparatorLength; + + /** + * Note lineLength is rounded down to the nearest multiple of {@link #encodedBlockSize} + * If chunkSeparatorLength is zero, then chunking is disabled. + * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3) + * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4) + * @param lineLength if > 0, use chunking with a length lineLength + * @param chunkSeparatorLength the chunk separator length, if relevant + */ + protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, + final int lineLength, final int chunkSeparatorLength) { + this(unencodedBlockSize, encodedBlockSize, lineLength, chunkSeparatorLength, PAD_DEFAULT); + } + + /** + * Note lineLength is rounded down to the nearest multiple of {@link #encodedBlockSize} + * If chunkSeparatorLength is zero, then chunking is disabled. + * @param unencodedBlockSize the size of an unencoded block (e.g. Base64 = 3) + * @param encodedBlockSize the size of an encoded block (e.g. Base64 = 4) + * @param lineLength if > 0, use chunking with a length lineLength + * @param chunkSeparatorLength the chunk separator length, if relevant + * @param pad byte used as padding byte. + */ + protected BaseNCodec(final int unencodedBlockSize, final int encodedBlockSize, + final int lineLength, final int chunkSeparatorLength, final byte pad) { + this.unencodedBlockSize = unencodedBlockSize; + this.encodedBlockSize = encodedBlockSize; + final boolean useChunking = lineLength > 0 && chunkSeparatorLength > 0; + this.lineLength = useChunking ? (lineLength / encodedBlockSize) * encodedBlockSize : 0; + this.chunkSeparatorLength = chunkSeparatorLength; + + this.pad = pad; + } + + /** + * Returns true if this object has buffered data for reading. + * + * @param context the context to be used + * @return true if there is data still available for reading. + */ + boolean hasData(final Context context) { // package protected for access from I/O streams + return context.buffer != null; + } + + /** + * Returns the amount of buffered data available for reading. + * + * @param context the context to be used + * @return The amount of buffered data available for reading. + */ + int available(final Context context) { // package protected for access from I/O streams + return context.buffer != null ? context.pos - context.readPos : 0; + } + + /** + * Get the default buffer size. Can be overridden. + * + * @return {@link #DEFAULT_BUFFER_SIZE} + */ + protected int getDefaultBufferSize() { + return DEFAULT_BUFFER_SIZE; + } + + /** + * Increases our buffer by the {@link #DEFAULT_BUFFER_RESIZE_FACTOR}. + * @param context the context to be used + */ + private byte[] resizeBuffer(final Context context) { + if (context.buffer == null) { + context.buffer = new byte[getDefaultBufferSize()]; + context.pos = 0; + context.readPos = 0; + } else { + final byte[] b = new byte[context.buffer.length * DEFAULT_BUFFER_RESIZE_FACTOR]; + System.arraycopy(context.buffer, 0, b, 0, context.buffer.length); + context.buffer = b; + } + return context.buffer; + } + + /** + * Ensure that the buffer has room for size bytes + * + * @param size minimum spare space required + * @param context the context to be used + * @return the buffer + */ + protected byte[] ensureBufferSize(final int size, final Context context){ + if ((context.buffer == null) || (context.buffer.length < context.pos + size)){ + return resizeBuffer(context); + } + return context.buffer; + } + + /** + * Extracts buffered data into the provided byte[] array, starting at position bPos, up to a maximum of bAvail + * bytes. Returns how many bytes were actually extracted. + *

+ * Package protected for access from I/O streams. + * + * @param b + * byte[] array to extract the buffered data into. + * @param bPos + * position in byte[] array to start extraction at. + * @param bAvail + * amount of bytes we're allowed to extract. We may extract fewer (if fewer are available). + * @param context + * the context to be used + * @return The number of bytes successfully extracted into the provided byte[] array. + */ + int readResults(final byte[] b, final int bPos, final int bAvail, final Context context) { + if (context.buffer != null) { + final int len = Math.min(available(context), bAvail); + System.arraycopy(context.buffer, context.readPos, b, bPos, len); + context.readPos += len; + if (context.readPos >= context.pos) { + context.buffer = null; // so hasData() will return false, and this method can return -1 + } + return len; + } + return context.eof ? EOF : 0; + } + + /** + * Checks if a byte value is whitespace or not. + * Whitespace is taken to mean: space, tab, CR, LF + * @param byteToCheck + * the byte to check + * @return true if byte is whitespace, false otherwise + */ + protected static boolean isWhiteSpace(final byte byteToCheck) { + switch (byteToCheck) { + case ' ' : + case '\n' : + case '\r' : + case '\t' : + return true; + default : + return false; + } + } + + /** + * Encodes an Object using the Base-N algorithm. This method is provided in order to satisfy the requirements of + * the Encoder interface, and will throw an EncoderException if the supplied object is not of type byte[]. + * + * @param obj + * Object to encode + * @return An object (of type byte[]) containing the Base-N encoded data which corresponds to the byte[] supplied. + * @throws EncoderException + * if the parameter supplied is not of type byte[] + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (!(obj instanceof byte[])) { + throw new EncoderException("Parameter supplied to Base-N encode is not a byte[]"); + } + return encode((byte[]) obj); + } + + /** + * Encodes a byte[] containing binary data, into a String containing characters in the Base-N alphabet. + * Uses UTF8 encoding. + * + * @param pArray + * a byte array containing binary data + * @return A String containing only Base-N character data + */ + public String encodeToString(final byte[] pArray) { + return StringUtils.newStringUtf8(encode(pArray)); + } + + /** + * Encodes a byte[] containing binary data, into a String containing characters in the appropriate alphabet. + * Uses UTF8 encoding. + * + * @param pArray a byte array containing binary data + * @return String containing only character data in the appropriate alphabet. + */ + public String encodeAsString(final byte[] pArray){ + return StringUtils.newStringUtf8(encode(pArray)); + } + + /** + * Decodes an Object using the Base-N algorithm. This method is provided in order to satisfy the requirements of + * the Decoder interface, and will throw a DecoderException if the supplied object is not of type byte[] or String. + * + * @param obj + * Object to decode + * @return An object (of type byte[]) containing the binary data which corresponds to the byte[] or String + * supplied. + * @throws DecoderException + * if the parameter supplied is not of type byte[] + */ + @Override + public Object decode(final Object obj) throws DecoderException { + if (obj instanceof byte[]) { + return decode((byte[]) obj); + } else if (obj instanceof String) { + return decode((String) obj); + } else { + throw new DecoderException("Parameter supplied to Base-N decode is not a byte[] or a String"); + } + } + + /** + * Decodes a String containing characters in the Base-N alphabet. + * + * @param pArray + * A String containing Base-N character data + * @return a byte array containing binary data + */ + public byte[] decode(final String pArray) { + return decode(StringUtils.getBytesUtf8(pArray)); + } + + /** + * Decodes a byte[] containing characters in the Base-N alphabet. + * + * @param pArray + * A byte array containing Base-N character data + * @return a byte array containing binary data + */ + @Override + public byte[] decode(final byte[] pArray) { + if (pArray == null || pArray.length == 0) { + return pArray; + } + final Context context = new Context(); + decode(pArray, 0, pArray.length, context); + decode(pArray, 0, EOF, context); // Notify decoder of EOF. + final byte[] result = new byte[context.pos]; + readResults(result, 0, result.length, context); + return result; + } + + /** + * Encodes a byte[] containing binary data, into a byte[] containing characters in the alphabet. + * + * @param pArray + * a byte array containing binary data + * @return A byte array containing only the basen alphabetic character data + */ + @Override + public byte[] encode(final byte[] pArray) { + if (pArray == null || pArray.length == 0) { + return pArray; + } + final Context context = new Context(); + encode(pArray, 0, pArray.length, context); + encode(pArray, 0, EOF, context); // Notify encoder of EOF. + final byte[] buf = new byte[context.pos - context.readPos]; + readResults(buf, 0, buf.length, context); + return buf; + } + + // package protected for access from I/O streams + abstract void encode(byte[] pArray, int i, int length, Context context); + + // package protected for access from I/O streams + abstract void decode(byte[] pArray, int i, int length, Context context); + + /** + * Returns whether or not the octet is in the current alphabet. + * Does not allow whitespace or pad. + * + * @param value The value to test + * + * @return true if the value is defined in the current alphabet, false otherwise. + */ + protected abstract boolean isInAlphabet(byte value); + + /** + * Tests a given byte array to see if it contains only valid characters within the alphabet. + * The method optionally treats whitespace and pad as valid. + * + * @param arrayOctet byte array to test + * @param allowWSPad if true, then whitespace and PAD are also allowed + * + * @return true if all bytes are valid characters in the alphabet or if the byte array is empty; + * false, otherwise + */ + public boolean isInAlphabet(final byte[] arrayOctet, final boolean allowWSPad) { + for (int i = 0; i < arrayOctet.length; i++) { + if (!isInAlphabet(arrayOctet[i]) && + (!allowWSPad || (arrayOctet[i] != pad) && !isWhiteSpace(arrayOctet[i]))) { + return false; + } + } + return true; + } + + /** + * Tests a given String to see if it contains only valid characters within the alphabet. + * The method treats whitespace and PAD as valid. + * + * @param basen String to test + * @return true if all characters in the String are valid characters in the alphabet or if + * the String is empty; false, otherwise + * @see #isInAlphabet(byte[], boolean) + */ + public boolean isInAlphabet(final String basen) { + return isInAlphabet(StringUtils.getBytesUtf8(basen), true); + } + + /** + * Tests a given byte array to see if it contains any characters within the alphabet or PAD. + * + * Intended for use in checking line-ending arrays + * + * @param arrayOctet + * byte array to test + * @return true if any byte is a valid character in the alphabet or PAD; false otherwise + */ + protected boolean containsAlphabetOrPad(final byte[] arrayOctet) { + if (arrayOctet == null) { + return false; + } + for (final byte element : arrayOctet) { + if (pad == element || isInAlphabet(element)) { + return true; + } + } + return false; + } + + /** + * Calculates the amount of space needed to encode the supplied array. + * + * @param pArray byte[] array which will later be encoded + * + * @return amount of space needed to encoded the supplied array. + * Returns a long since a max-len array will require > Integer.MAX_VALUE + */ + public long getEncodedLength(final byte[] pArray) { + // Calculate non-chunked size - rounded up to allow for padding + // cast to long is needed to avoid possibility of overflow + long len = ((pArray.length + unencodedBlockSize-1) / unencodedBlockSize) * (long) encodedBlockSize; + if (lineLength > 0) { // We're using chunking + // Round up to nearest multiple + len += ((len + lineLength-1) / lineLength) * chunkSeparatorLength; + } + return len; + } +} diff --git a/src/org/apache/commons/codec/binary/BaseNCodecInputStream.java b/src/org/apache/commons/codec/binary/BaseNCodecInputStream.java new file mode 100644 index 00000000..30b2cb3a --- /dev/null +++ b/src/org/apache/commons/codec/binary/BaseNCodecInputStream.java @@ -0,0 +1,211 @@ +/* + * 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 org.apache.commons.codec.binary; + +import static org.apache.commons.codec.binary.BaseNCodec.EOF; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.codec.binary.BaseNCodec.Context; + +/** + * Abstract superclass for Base-N input streams. + * + * @since 1.5 + * @version $Id$ + */ +public class BaseNCodecInputStream extends FilterInputStream { + + private final BaseNCodec baseNCodec; + + private final boolean doEncode; + + private final byte[] singleByte = new byte[1]; + + private final Context context = new Context(); + + protected BaseNCodecInputStream(final InputStream in, final BaseNCodec baseNCodec, final boolean doEncode) { + super(in); + this.doEncode = doEncode; + this.baseNCodec = baseNCodec; + } + + /** + * {@inheritDoc} + * + * @return 0 if the {@link InputStream} has reached EOF, + * 1 otherwise + * @since 1.7 + */ + @Override + public int available() throws IOException { + // Note: the logic is similar to the InflaterInputStream: + // as long as we have not reached EOF, indicate that there is more + // data available. As we do not know for sure how much data is left, + // just return 1 as a safe guess. + + return context.eof ? 0 : 1; + } + + /** + * Marks the current position in this input stream. + *

The {@link #mark} method of {@link BaseNCodecInputStream} does nothing.

+ * + * @param readLimit the maximum limit of bytes that can be read before the mark position becomes invalid. + * @since 1.7 + */ + @Override + public synchronized void mark(final int readLimit) { + } + + /** + * {@inheritDoc} + * + * @return always returns false + */ + @Override + public boolean markSupported() { + return false; // not an easy job to support marks + } + + /** + * Reads one byte from this input stream. + * + * @return the byte as an integer in the range 0 to 255. Returns -1 if EOF has been reached. + * @throws IOException + * if an I/O error occurs. + */ + @Override + public int read() throws IOException { + int r = read(singleByte, 0, 1); + while (r == 0) { + r = read(singleByte, 0, 1); + } + if (r > 0) { + final byte b = singleByte[0]; + return b < 0 ? 256 + b : b; + } + return EOF; + } + + /** + * Attempts to read len bytes into the specified b array starting at offset + * from this InputStream. + * + * @param b + * destination byte array + * @param offset + * where to start writing the bytes + * @param len + * maximum number of bytes to read + * + * @return number of bytes read + * @throws IOException + * if an I/O error occurs. + * @throws NullPointerException + * if the byte array parameter is null + * @throws IndexOutOfBoundsException + * if offset, len or buffer size are invalid + */ + @Override + public int read(final byte b[], final int offset, final int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (offset < 0 || len < 0) { + throw new IndexOutOfBoundsException(); + } else if (offset > b.length || offset + len > b.length) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } else { + int readLen = 0; + /* + Rationale for while-loop on (readLen == 0): + ----- + Base32.readResults() usually returns > 0 or EOF (-1). In the + rare case where it returns 0, we just keep trying. + + This is essentially an undocumented contract for InputStream + implementors that want their code to work properly with + java.io.InputStreamReader, since the latter hates it when + InputStream.read(byte[]) returns a zero. Unfortunately our + readResults() call must return 0 if a large amount of the data + being decoded was non-base32, so this while-loop enables proper + interop with InputStreamReader for that scenario. + ----- + This is a fix for CODEC-101 + */ + while (readLen == 0) { + if (!baseNCodec.hasData(context)) { + final byte[] buf = new byte[doEncode ? 4096 : 8192]; + final int c = in.read(buf); + if (doEncode) { + baseNCodec.encode(buf, 0, c, context); + } else { + baseNCodec.decode(buf, 0, c, context); + } + } + readLen = baseNCodec.readResults(b, offset, len, context); + } + return readLen; + } + } + + /** + * Repositions this stream to the position at the time the mark method was last called on this input stream. + *

+ * The {@link #reset} method of {@link BaseNCodecInputStream} does nothing except throw an {@link IOException}. + * + * @throws IOException if this method is invoked + * @since 1.7 + */ + @Override + public synchronized void reset() throws IOException { + throw new IOException("mark/reset not supported"); + } + + /** + * {@inheritDoc} + * + * @throws IllegalArgumentException if the provided skip length is negative + * @since 1.7 + */ + @Override + public long skip(final long n) throws IOException { + if (n < 0) { + throw new IllegalArgumentException("Negative skip length: " + n); + } + + // skip in chunks of 512 bytes + final byte[] b = new byte[512]; + long todo = n; + + while (todo > 0) { + int len = (int) Math.min(b.length, todo); + len = this.read(b, 0, len); + if (len == EOF) { + break; + } + todo -= len; + } + + return n - todo; + } +} diff --git a/src/org/apache/commons/codec/binary/BaseNCodecOutputStream.java b/src/org/apache/commons/codec/binary/BaseNCodecOutputStream.java new file mode 100644 index 00000000..90d2f535 --- /dev/null +++ b/src/org/apache/commons/codec/binary/BaseNCodecOutputStream.java @@ -0,0 +1,176 @@ +/* + * 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 org.apache.commons.codec.binary; + +import static org.apache.commons.codec.binary.BaseNCodec.EOF; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.apache.commons.codec.binary.BaseNCodec.Context; + +/** + * Abstract superclass for Base-N output streams. + *

+ * To write the EOF marker without closing the stream, call {@link #eof()} or use an Apache Commons IO CloseShieldOutputStream. + *

+ * + * @since 1.5 + * @version $Id$ + */ +public class BaseNCodecOutputStream extends FilterOutputStream { + + private final boolean doEncode; + + private final BaseNCodec baseNCodec; + + private final byte[] singleByte = new byte[1]; + + private final Context context = new Context(); + + // TODO should this be protected? + public BaseNCodecOutputStream(final OutputStream out, final BaseNCodec basedCodec, final boolean doEncode) { + super(out); + this.baseNCodec = basedCodec; + this.doEncode = doEncode; + } + + /** + * Writes the specified byte to this output stream. + * + * @param i + * source byte + * @throws IOException + * if an I/O error occurs. + */ + @Override + public void write(final int i) throws IOException { + singleByte[0] = (byte) i; + write(singleByte, 0, 1); + } + + /** + * Writes len bytes from the specified b array starting at offset to this + * output stream. + * + * @param b + * source byte array + * @param offset + * where to start reading the bytes + * @param len + * maximum number of bytes to write + * + * @throws IOException + * if an I/O error occurs. + * @throws NullPointerException + * if the byte array parameter is null + * @throws IndexOutOfBoundsException + * if offset, len or buffer size are invalid + */ + @Override + public void write(final byte b[], final int offset, final int len) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (offset < 0 || len < 0) { + throw new IndexOutOfBoundsException(); + } else if (offset > b.length || offset + len > b.length) { + throw new IndexOutOfBoundsException(); + } else if (len > 0) { + if (doEncode) { + baseNCodec.encode(b, offset, len, context); + } else { + baseNCodec.decode(b, offset, len, context); + } + flush(false); + } + } + + /** + * Flushes this output stream and forces any buffered output bytes to be written out to the stream. If propagate is + * true, the wrapped stream will also be flushed. + * + * @param propagate + * boolean flag to indicate whether the wrapped OutputStream should also be flushed. + * @throws IOException + * if an I/O error occurs. + */ + private void flush(final boolean propagate) throws IOException { + final int avail = baseNCodec.available(context); + if (avail > 0) { + final byte[] buf = new byte[avail]; + final int c = baseNCodec.readResults(buf, 0, avail, context); + if (c > 0) { + out.write(buf, 0, c); + } + } + if (propagate) { + out.flush(); + } + } + + /** + * Flushes this output stream and forces any buffered output bytes to be written out to the stream. + * + * @throws IOException + * if an I/O error occurs. + */ + @Override + public void flush() throws IOException { + flush(true); + } + + /** + * Closes this output stream and releases any system resources associated with the stream. + *

+ * To write the EOF marker without closing the stream, call {@link #eof()} or use an + * Apache Commons IO CloseShieldOutputStream. + *

+ * + * @throws IOException + * if an I/O error occurs. + */ + @Override + public void close() throws IOException { + eof(); + flush(); + out.close(); + } + + /** + * Writes EOF. + * + * @throws IOException + * if an I/O error occurs. + * @since 1.11 + */ + public void eof() throws IOException { + // Notify encoder of EOF (-1). + if (doEncode) { + baseNCodec.encode(singleByte, 0, EOF, context); + } else { + baseNCodec.decode(singleByte, 0, EOF, context); + } + } + +} diff --git a/src/org/apache/commons/codec/binary/BinaryCodec.java b/src/org/apache/commons/codec/binary/BinaryCodec.java new file mode 100644 index 00000000..54f8a943 --- /dev/null +++ b/src/org/apache/commons/codec/binary/BinaryCodec.java @@ -0,0 +1,301 @@ +/* + * 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 org.apache.commons.codec.binary; + +import org.apache.commons.codec.BinaryDecoder; +import org.apache.commons.codec.BinaryEncoder; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; + +/** + * Converts between byte arrays and strings of "0"s and "1"s. + * + *

This class is immutable and thread-safe.

+ * + * TODO: may want to add more bit vector functions like and/or/xor/nand + * TODO: also might be good to generate boolean[] from byte[] et cetera. + * + * @since 1.3 + * @version $Id$ + */ +public class BinaryCodec implements BinaryDecoder, BinaryEncoder { + /* + * tried to avoid using ArrayUtils to minimize dependencies while using these empty arrays - dep is just not worth + * it. + */ + /** Empty char array. */ + private static final char[] EMPTY_CHAR_ARRAY = new char[0]; + + /** Empty byte array. */ + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** Mask for bit 0 of a byte. */ + private static final int BIT_0 = 1; + + /** Mask for bit 1 of a byte. */ + private static final int BIT_1 = 0x02; + + /** Mask for bit 2 of a byte. */ + private static final int BIT_2 = 0x04; + + /** Mask for bit 3 of a byte. */ + private static final int BIT_3 = 0x08; + + /** Mask for bit 4 of a byte. */ + private static final int BIT_4 = 0x10; + + /** Mask for bit 5 of a byte. */ + private static final int BIT_5 = 0x20; + + /** Mask for bit 6 of a byte. */ + private static final int BIT_6 = 0x40; + + /** Mask for bit 7 of a byte. */ + private static final int BIT_7 = 0x80; + + private static final int[] BITS = {BIT_0, BIT_1, BIT_2, BIT_3, BIT_4, BIT_5, BIT_6, BIT_7}; + + /** + * Converts an array of raw binary data into an array of ASCII 0 and 1 characters. + * + * @param raw + * the raw binary data to convert + * @return 0 and 1 ASCII character bytes one for each bit of the argument + * @see org.apache.commons.codec.BinaryEncoder#encode(byte[]) + */ + @Override + public byte[] encode(final byte[] raw) { + return toAsciiBytes(raw); + } + + /** + * Converts an array of raw binary data into an array of ASCII 0 and 1 chars. + * + * @param raw + * the raw binary data to convert + * @return 0 and 1 ASCII character chars one for each bit of the argument + * @throws EncoderException + * if the argument is not a byte[] + * @see org.apache.commons.codec.Encoder#encode(Object) + */ + @Override + public Object encode(final Object raw) throws EncoderException { + if (!(raw instanceof byte[])) { + throw new EncoderException("argument not a byte array"); + } + return toAsciiChars((byte[]) raw); + } + + /** + * Decodes a byte array where each byte represents an ASCII '0' or '1'. + * + * @param ascii + * each byte represents an ASCII '0' or '1' + * @return the raw encoded binary where each bit corresponds to a byte in the byte array argument + * @throws DecoderException + * if argument is not a byte[], char[] or String + * @see org.apache.commons.codec.Decoder#decode(Object) + */ + @Override + public Object decode(final Object ascii) throws DecoderException { + if (ascii == null) { + return EMPTY_BYTE_ARRAY; + } + if (ascii instanceof byte[]) { + return fromAscii((byte[]) ascii); + } + if (ascii instanceof char[]) { + return fromAscii((char[]) ascii); + } + if (ascii instanceof String) { + return fromAscii(((String) ascii).toCharArray()); + } + throw new DecoderException("argument not a byte array"); + } + + /** + * Decodes a byte array where each byte represents an ASCII '0' or '1'. + * + * @param ascii + * each byte represents an ASCII '0' or '1' + * @return the raw encoded binary where each bit corresponds to a byte in the byte array argument + * @see org.apache.commons.codec.Decoder#decode(Object) + */ + @Override + public byte[] decode(final byte[] ascii) { + return fromAscii(ascii); + } + + /** + * Decodes a String where each char of the String represents an ASCII '0' or '1'. + * + * @param ascii + * String of '0' and '1' characters + * @return the raw encoded binary where each bit corresponds to a byte in the byte array argument + * @see org.apache.commons.codec.Decoder#decode(Object) + */ + public byte[] toByteArray(final String ascii) { + if (ascii == null) { + return EMPTY_BYTE_ARRAY; + } + return fromAscii(ascii.toCharArray()); + } + + // ------------------------------------------------------------------------ + // + // static codec operations + // + // ------------------------------------------------------------------------ + /** + * Decodes a char array where each char represents an ASCII '0' or '1'. + * + * @param ascii + * each char represents an ASCII '0' or '1' + * @return the raw encoded binary where each bit corresponds to a char in the char array argument + */ + public static byte[] fromAscii(final char[] ascii) { + if (ascii == null || ascii.length == 0) { + return EMPTY_BYTE_ARRAY; + } + // get length/8 times bytes with 3 bit shifts to the right of the length + final byte[] l_raw = new byte[ascii.length >> 3]; + /* + * We decr index jj by 8 as we go along to not recompute indices using multiplication every time inside the + * loop. + */ + for (int ii = 0, jj = ascii.length - 1; ii < l_raw.length; ii++, jj -= 8) { + for (int bits = 0; bits < BITS.length; ++bits) { + if (ascii[jj - bits] == '1') { + l_raw[ii] |= BITS[bits]; + } + } + } + return l_raw; + } + + /** + * Decodes a byte array where each byte represents an ASCII '0' or '1'. + * + * @param ascii + * each byte represents an ASCII '0' or '1' + * @return the raw encoded binary where each bit corresponds to a byte in the byte array argument + */ + public static byte[] fromAscii(final byte[] ascii) { + if (isEmpty(ascii)) { + return EMPTY_BYTE_ARRAY; + } + // get length/8 times bytes with 3 bit shifts to the right of the length + final byte[] l_raw = new byte[ascii.length >> 3]; + /* + * We decr index jj by 8 as we go along to not recompute indices using multiplication every time inside the + * loop. + */ + for (int ii = 0, jj = ascii.length - 1; ii < l_raw.length; ii++, jj -= 8) { + for (int bits = 0; bits < BITS.length; ++bits) { + if (ascii[jj - bits] == '1') { + l_raw[ii] |= BITS[bits]; + } + } + } + return l_raw; + } + + /** + * Returns true if the given array is null or empty (size 0.) + * + * @param array + * the source array + * @return true if the given array is null or empty (size 0.) + */ + private static boolean isEmpty(final byte[] array) { + return array == null || array.length == 0; + } + + /** + * Converts an array of raw binary data into an array of ASCII 0 and 1 character bytes - each byte is a truncated + * char. + * + * @param raw + * the raw binary data to convert + * @return an array of 0 and 1 character bytes for each bit of the argument + * @see org.apache.commons.codec.BinaryEncoder#encode(byte[]) + */ + public static byte[] toAsciiBytes(final byte[] raw) { + if (isEmpty(raw)) { + return EMPTY_BYTE_ARRAY; + } + // get 8 times the bytes with 3 bit shifts to the left of the length + final byte[] l_ascii = new byte[raw.length << 3]; + /* + * We decr index jj by 8 as we go along to not recompute indices using multiplication every time inside the + * loop. + */ + for (int ii = 0, jj = l_ascii.length - 1; ii < raw.length; ii++, jj -= 8) { + for (int bits = 0; bits < BITS.length; ++bits) { + if ((raw[ii] & BITS[bits]) == 0) { + l_ascii[jj - bits] = '0'; + } else { + l_ascii[jj - bits] = '1'; + } + } + } + return l_ascii; + } + + /** + * Converts an array of raw binary data into an array of ASCII 0 and 1 characters. + * + * @param raw + * the raw binary data to convert + * @return an array of 0 and 1 characters for each bit of the argument + * @see org.apache.commons.codec.BinaryEncoder#encode(byte[]) + */ + public static char[] toAsciiChars(final byte[] raw) { + if (isEmpty(raw)) { + return EMPTY_CHAR_ARRAY; + } + // get 8 times the bytes with 3 bit shifts to the left of the length + final char[] l_ascii = new char[raw.length << 3]; + /* + * We decr index jj by 8 as we go along to not recompute indices using multiplication every time inside the + * loop. + */ + for (int ii = 0, jj = l_ascii.length - 1; ii < raw.length; ii++, jj -= 8) { + for (int bits = 0; bits < BITS.length; ++bits) { + if ((raw[ii] & BITS[bits]) == 0) { + l_ascii[jj - bits] = '0'; + } else { + l_ascii[jj - bits] = '1'; + } + } + } + return l_ascii; + } + + /** + * Converts an array of raw binary data into a String of ASCII 0 and 1 characters. + * + * @param raw + * the raw binary data to convert + * @return a String of 0 and 1 characters representing the binary data + * @see org.apache.commons.codec.BinaryEncoder#encode(byte[]) + */ + public static String toAsciiString(final byte[] raw) { + return new String(toAsciiChars(raw)); + } +} diff --git a/src/org/apache/commons/codec/binary/CharSequenceUtils.java b/src/org/apache/commons/codec/binary/CharSequenceUtils.java new file mode 100644 index 00000000..b886a826 --- /dev/null +++ b/src/org/apache/commons/codec/binary/CharSequenceUtils.java @@ -0,0 +1,79 @@ +/* + * 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 org.apache.commons.codec.binary; + +/** + *

+ * Operations on {@link CharSequence} that are null safe. + *

+ *

+ * Copied from Apache Commons Lang r1586295 on April 10, 2014 (day of 3.3.2 release). + *

+ * + * @see CharSequence + * @since 1.10 + */ +public class CharSequenceUtils { + + /** + * Green implementation of regionMatches. + * + * @param cs + * the CharSequence to be processed + * @param ignoreCase + * whether or not to be case insensitive + * @param thisStart + * the index to start on the cs CharSequence + * @param substring + * the CharSequence to be looked for + * @param start + * the index to start on the substring CharSequence + * @param length + * character length of the region + * @return whether the region matched + */ + static boolean regionMatches(final CharSequence cs, final boolean ignoreCase, final int thisStart, + final CharSequence substring, final int start, final int length) { + if (cs instanceof String && substring instanceof String) { + return ((String) cs).regionMatches(ignoreCase, thisStart, (String) substring, start, length); + } + int index1 = thisStart; + int index2 = start; + int tmpLen = length; + + while (tmpLen-- > 0) { + char c1 = cs.charAt(index1++); + char c2 = substring.charAt(index2++); + + if (c1 == c2) { + continue; + } + + if (!ignoreCase) { + return false; + } + + // The same check as in String.regionMatches(): + if (Character.toUpperCase(c1) != Character.toUpperCase(c2) && + Character.toLowerCase(c1) != Character.toLowerCase(c2)) { + return false; + } + } + + return true; + } +} diff --git a/src/org/apache/commons/codec/binary/Hex.java b/src/org/apache/commons/codec/binary/Hex.java new file mode 100644 index 00000000..51857fe0 --- /dev/null +++ b/src/org/apache/commons/codec/binary/Hex.java @@ -0,0 +1,443 @@ +/* + * 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 org.apache.commons.codec.binary; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import org.apache.commons.codec.BinaryDecoder; +import org.apache.commons.codec.BinaryEncoder; +import org.apache.commons.codec.CharEncoding; +import org.apache.commons.codec.Charsets; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; + +/** + * Converts hexadecimal Strings. The charset used for certain operation can be set, the default is set in + * {@link #DEFAULT_CHARSET_NAME} + * + * This class is thread-safe. + * + * @since 1.1 + * @version $Id$ + */ +public class Hex implements BinaryEncoder, BinaryDecoder { + + /** + * Default charset name is {@link Charsets#UTF_8} + * + * @since 1.7 + */ + public static final Charset DEFAULT_CHARSET = Charsets.UTF_8; + + /** + * Default charset name is {@link CharEncoding#UTF_8} + * + * @since 1.4 + */ + public static final String DEFAULT_CHARSET_NAME = CharEncoding.UTF_8; + + /** + * Used to build output as Hex + */ + private static final char[] DIGITS_LOWER = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + /** + * Used to build output as Hex + */ + private static final char[] DIGITS_UPPER = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + /** + * Converts an array of characters representing hexadecimal values into an array of bytes of those same values. The + * returned array will be half the length of the passed array, as it takes two characters to represent any given + * byte. An exception is thrown if the passed char array has an odd number of elements. + * + * @param data + * An array of characters containing hexadecimal digits + * @return A byte array containing binary data decoded from the supplied char array. + * @throws DecoderException + * Thrown if an odd number or illegal of characters is supplied + */ + public static byte[] decodeHex(final char[] data) throws DecoderException { + + final int len = data.length; + + if ((len & 0x01) != 0) { + throw new DecoderException("Odd number of characters."); + } + + final byte[] out = new byte[len >> 1]; + + // two characters form the hex value. + for (int i = 0, j = 0; j < len; i++) { + int f = toDigit(data[j], j) << 4; + j++; + f = f | toDigit(data[j], j); + j++; + out[i] = (byte) (f & 0xFF); + } + + return out; + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data + * a byte[] to convert to Hex characters + * @return A char[] containing hexadecimal characters + */ + public static char[] encodeHex(final byte[] data) { + return encodeHex(data, true); + } + + /** + * Converts a byte buffer into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data + * a byte buffer to convert to Hex characters + * @return A char[] containing hexadecimal characters + * @since 1.11 + */ + public static char[] encodeHex(final ByteBuffer data) { + return encodeHex(data, true); + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data + * a byte[] to convert to Hex characters + * @param toLowerCase + * true converts to lowercase, false to uppercase + * @return A char[] containing hexadecimal characters + * @since 1.4 + */ + public static char[] encodeHex(final byte[] data, final boolean toLowerCase) { + return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + /** + * Converts a byte buffer into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data + * a byte buffer to convert to Hex characters + * @param toLowerCase + * true converts to lowercase, false to uppercase + * @return A char[] containing hexadecimal characters + * @since 1.11 + */ + public static char[] encodeHex(final ByteBuffer data, final boolean toLowerCase) { + return encodeHex(data, toLowerCase ? DIGITS_LOWER : DIGITS_UPPER); + } + + /** + * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data + * a byte[] to convert to Hex characters + * @param toDigits + * the output alphabet + * @return A char[] containing hexadecimal characters + * @since 1.4 + */ + protected static char[] encodeHex(final byte[] data, final char[] toDigits) { + final int l = data.length; + final char[] out = new char[l << 1]; + // two characters form the hex value. + for (int i = 0, j = 0; i < l; i++) { + out[j++] = toDigits[(0xF0 & data[i]) >>> 4]; + out[j++] = toDigits[0x0F & data[i]]; + } + return out; + } + + /** + * Converts a byte buffer into an array of characters representing the hexadecimal values of each byte in order. + * The returned array will be double the length of the passed array, as it takes two characters to represent any + * given byte. + * + * @param data + * a byte buffer to convert to Hex characters + * @param toDigits + * the output alphabet + * @return A char[] containing hexadecimal characters + * @since 1.11 + */ + protected static char[] encodeHex(final ByteBuffer data, final char[] toDigits) { + return encodeHex(data.array(), toDigits); + } + + /** + * Converts an array of bytes into a String representing the hexadecimal values of each byte in order. The returned + * String will be double the length of the passed array, as it takes two characters to represent any given byte. + * + * @param data + * a byte[] to convert to Hex characters + * @return A String containing hexadecimal characters + * @since 1.4 + */ + public static String encodeHexString(final byte[] data) { + return new String(encodeHex(data)); + } + + /** + * Converts a byte buffer into a String representing the hexadecimal values of each byte in order. The returned + * String will be double the length of the passed array, as it takes two characters to represent any given byte. + * + * @param data + * a byte buffer to convert to Hex characters + * @return A String containing hexadecimal characters + * @since 1.11 + */ + public static String encodeHexString(final ByteBuffer data) { + return new String(encodeHex(data)); + } + + /** + * Converts a hexadecimal character to an integer. + * + * @param ch + * A character to convert to an integer digit + * @param index + * The index of the character in the source + * @return An integer + * @throws DecoderException + * Thrown if ch is an illegal hex character + */ + protected static int toDigit(final char ch, final int index) throws DecoderException { + final int digit = Character.digit(ch, 16); + if (digit == -1) { + throw new DecoderException("Illegal hexadecimal character " + ch + " at index " + index); + } + return digit; + } + + private final Charset charset; + + /** + * Creates a new codec with the default charset name {@link #DEFAULT_CHARSET} + */ + public Hex() { + // use default encoding + this.charset = DEFAULT_CHARSET; + } + + /** + * Creates a new codec with the given Charset. + * + * @param charset + * the charset. + * @since 1.7 + */ + public Hex(final Charset charset) { + this.charset = charset; + } + + /** + * Creates a new codec with the given charset name. + * + * @param charsetName + * the charset name. + * @throws java.nio.charset.UnsupportedCharsetException + * If the named charset is unavailable + * @since 1.4 + * @since 1.7 throws UnsupportedCharsetException if the named charset is unavailable + */ + public Hex(final String charsetName) { + this(Charset.forName(charsetName)); + } + + /** + * Converts an array of character bytes representing hexadecimal values into an array of bytes of those same values. + * The returned array will be half the length of the passed array, as it takes two characters to represent any given + * byte. An exception is thrown if the passed char array has an odd number of elements. + * + * @param array + * An array of character bytes containing hexadecimal digits + * @return A byte array containing binary data decoded from the supplied byte array (representing characters). + * @throws DecoderException + * Thrown if an odd number of characters is supplied to this function + * @see #decodeHex(char[]) + */ + @Override + public byte[] decode(final byte[] array) throws DecoderException { + return decodeHex(new String(array, getCharset()).toCharArray()); + } + + /** + * Converts a buffer of character bytes representing hexadecimal values into an array of bytes of those same values. + * The returned array will be half the length of the passed array, as it takes two characters to represent any given + * byte. An exception is thrown if the passed char array has an odd number of elements. + * + * @param buffer + * An array of character bytes containing hexadecimal digits + * @return A byte array containing binary data decoded from the supplied byte array (representing characters). + * @throws DecoderException + * Thrown if an odd number of characters is supplied to this function + * @see #decodeHex(char[]) + * @since 1.11 + */ + public byte[] decode(final ByteBuffer buffer) throws DecoderException { + return decodeHex(new String(buffer.array(), getCharset()).toCharArray()); + } + + /** + * Converts a String or an array of character bytes representing hexadecimal values into an array of bytes of those + * same values. The returned array will be half the length of the passed String or array, as it takes two characters + * to represent any given byte. An exception is thrown if the passed char array has an odd number of elements. + * + * @param object + * A String, ByteBuffer, byte[], or an array of character bytes containing hexadecimal digits + * @return A byte array containing binary data decoded from the supplied byte array (representing characters). + * @throws DecoderException + * Thrown if an odd number of characters is supplied to this function or the object is not a String or + * char[] + * @see #decodeHex(char[]) + */ + @Override + public Object decode(final Object object) throws DecoderException { + if (object instanceof String) { + return decode(((String) object).toCharArray()); + } else if (object instanceof byte[]) { + return decode((byte[]) object); + } else if (object instanceof ByteBuffer) { + return decode((ByteBuffer) object); + } else { + try { + return decodeHex((char[]) object); + } catch (final ClassCastException e) { + throw new DecoderException(e.getMessage(), e); + } + } + } + + /** + * Converts an array of bytes into an array of bytes for the characters representing the hexadecimal values of each + * byte in order. The returned array will be double the length of the passed array, as it takes two characters to + * represent any given byte. + *

+ * The conversion from hexadecimal characters to the returned bytes is performed with the charset named by + * {@link #getCharset()}. + *

+ * + * @param array + * a byte[] to convert to Hex characters + * @return A byte[] containing the bytes of the hexadecimal characters + * @since 1.7 No longer throws IllegalStateException if the charsetName is invalid. + * @see #encodeHex(byte[]) + */ + @Override + public byte[] encode(final byte[] array) { + return encodeHexString(array).getBytes(this.getCharset()); + } + + /** + * Converts byte buffer into an array of bytes for the characters representing the hexadecimal values of each + * byte in order. The returned array will be double the length of the passed array, as it takes two characters to + * represent any given byte. + *

+ * The conversion from hexadecimal characters to the returned bytes is performed with the charset named by + * {@link #getCharset()}. + *

+ * + * @param array + * a byte buffer to convert to Hex characters + * @return A byte[] containing the bytes of the hexadecimal characters + * @see #encodeHex(byte[]) + * @since 1.11 + */ + public byte[] encode(final ByteBuffer array) { + return encodeHexString(array).getBytes(this.getCharset()); + } + + /** + * Converts a String or an array of bytes into an array of characters representing the hexadecimal values of each + * byte in order. The returned array will be double the length of the passed String or array, as it takes two + * characters to represent any given byte. + *

+ * The conversion from hexadecimal characters to bytes to be encoded to performed with the charset named by + * {@link #getCharset()}. + *

+ * + * @param object + * a String, ByteBuffer, or byte[] to convert to Hex characters + * @return A char[] containing hexadecimal characters + * @throws EncoderException + * Thrown if the given object is not a String or byte[] + * @see #encodeHex(byte[]) + */ + @Override + public Object encode(final Object object) throws EncoderException { + byte[] byteArray; + if (object instanceof String) { + byteArray = ((String) object).getBytes(this.getCharset()); + } else if (object instanceof ByteBuffer) { + byteArray = ((ByteBuffer) object).array(); + } else { + try { + byteArray = (byte[]) object; + } catch (final ClassCastException e) { + throw new EncoderException(e.getMessage(), e); + } + } + return encodeHex(byteArray); + } + + /** + * Gets the charset. + * + * @return the charset. + * @since 1.7 + */ + public Charset getCharset() { + return this.charset; + } + + /** + * Gets the charset name. + * + * @return the charset name. + * @since 1.4 + */ + public String getCharsetName() { + return this.charset.name(); + } + + /** + * Returns a string representation of the object, which includes the charset name. + * + * @return a string representation of the object. + */ + @Override + public String toString() { + return super.toString() + "[charsetName=" + this.charset + "]"; + } +} diff --git a/src/org/apache/commons/codec/binary/StringUtils.java b/src/org/apache/commons/codec/binary/StringUtils.java new file mode 100644 index 00000000..84a2a727 --- /dev/null +++ b/src/org/apache/commons/codec/binary/StringUtils.java @@ -0,0 +1,422 @@ +/* + * 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 org.apache.commons.codec.binary; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import org.apache.commons.codec.CharEncoding; +import org.apache.commons.codec.Charsets; + +/** + * Converts String to and from bytes using the encodings required by the Java specification. These encodings are + * specified in + * Standard charsets. + * + *

This class is immutable and thread-safe.

+ * + * @see CharEncoding + * @see Standard charsets + * @version $Id$ + * @since 1.4 + */ +public class StringUtils { + + /** + *

+ * Compares two CharSequences, returning true if they represent equal sequences of characters. + *

+ * + *

+ * nulls are handled without exceptions. Two null references are considered to be equal. + * The comparison is case sensitive. + *

+ * + *
+     * StringUtils.equals(null, null)   = true
+     * StringUtils.equals(null, "abc")  = false
+     * StringUtils.equals("abc", null)  = false
+     * StringUtils.equals("abc", "abc") = true
+     * StringUtils.equals("abc", "ABC") = false
+     * 
+ * + *

+ * Copied from Apache Commons Lang r1583482 on April 10, 2014 (day of 3.3.2 release). + *

+ * + * @see Object#equals(Object) + * @param cs1 + * the first CharSequence, may be null + * @param cs2 + * the second CharSequence, may be null + * @return true if the CharSequences are equal (case-sensitive), or both null + * @since 1.10 + */ + public static boolean equals(final CharSequence cs1, final CharSequence cs2) { + if (cs1 == cs2) { + return true; + } + if (cs1 == null || cs2 == null) { + return false; + } + if (cs1 instanceof String && cs2 instanceof String) { + return cs1.equals(cs2); + } + return CharSequenceUtils.regionMatches(cs1, false, 0, cs2, 0, Math.max(cs1.length(), cs2.length())); + } + + /** + * Calls {@link String#getBytes(Charset)} + * + * @param string + * The string to encode (if null, return null). + * @param charset + * The {@link Charset} to encode the String + * @return the encoded bytes + */ + private static byte[] getBytes(final String string, final Charset charset) { + if (string == null) { + return null; + } + return string.getBytes(charset); + } + + /** + * Calls {@link String#getBytes(Charset)} + * + * @param string + * The string to encode (if null, return null). + * @param charset + * The {@link Charset} to encode the String + * @return the encoded bytes + * @since 1.11 + */ + private static ByteBuffer getByteBuffer(final String string, final Charset charset) { + if (string == null) { + return null; + } + return ByteBuffer.wrap(string.getBytes(charset)); + } + + /** + * Encodes the given string into a byte buffer using the UTF-8 charset, storing the result into a new byte + * array. + * + * @param string + * the String to encode, may be null + * @return encoded bytes, or null if the input string was null + * @throws NullPointerException + * Thrown if {@link Charsets#UTF_8} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @see Standard charsets + * @see #getBytesUnchecked(String, String) + * @since 1.11 + */ + public static ByteBuffer getByteBufferUtf8(final String string) { + return getByteBuffer(string, Charsets.UTF_8); + } + + /** + * Encodes the given string into a sequence of bytes using the ISO-8859-1 charset, storing the result into a new + * byte array. + * + * @param string + * the String to encode, may be null + * @return encoded bytes, or null if the input string was null + * @throws NullPointerException + * Thrown if {@link Charsets#ISO_8859_1} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + * @see Standard charsets + * @see #getBytesUnchecked(String, String) + */ + public static byte[] getBytesIso8859_1(final String string) { + return getBytes(string, Charsets.ISO_8859_1); + } + + + /** + * Encodes the given string into a sequence of bytes using the named charset, storing the result into a new byte + * array. + *

+ * This method catches {@link UnsupportedEncodingException} and rethrows it as {@link IllegalStateException}, which + * should never happen for a required charset name. Use this method when the encoding is required to be in the JRE. + *

+ * + * @param string + * the String to encode, may be null + * @param charsetName + * The name of a required {@link java.nio.charset.Charset} + * @return encoded bytes, or null if the input string was null + * @throws IllegalStateException + * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen for a + * required charset name. + * @see CharEncoding + * @see String#getBytes(String) + */ + public static byte[] getBytesUnchecked(final String string, final String charsetName) { + if (string == null) { + return null; + } + try { + return string.getBytes(charsetName); + } catch (final UnsupportedEncodingException e) { + throw StringUtils.newIllegalStateException(charsetName, e); + } + } + + /** + * Encodes the given string into a sequence of bytes using the US-ASCII charset, storing the result into a new byte + * array. + * + * @param string + * the String to encode, may be null + * @return encoded bytes, or null if the input string was null + * @throws NullPointerException + * Thrown if {@link Charsets#US_ASCII} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + * @see Standard charsets + * @see #getBytesUnchecked(String, String) + */ + public static byte[] getBytesUsAscii(final String string) { + return getBytes(string, Charsets.US_ASCII); + } + + /** + * Encodes the given string into a sequence of bytes using the UTF-16 charset, storing the result into a new byte + * array. + * + * @param string + * the String to encode, may be null + * @return encoded bytes, or null if the input string was null + * @throws NullPointerException + * Thrown if {@link Charsets#UTF_16} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + * @see Standard charsets + * @see #getBytesUnchecked(String, String) + */ + public static byte[] getBytesUtf16(final String string) { + return getBytes(string, Charsets.UTF_16); + } + + /** + * Encodes the given string into a sequence of bytes using the UTF-16BE charset, storing the result into a new byte + * array. + * + * @param string + * the String to encode, may be null + * @return encoded bytes, or null if the input string was null + * @throws NullPointerException + * Thrown if {@link Charsets#UTF_16BE} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + * @see Standard charsets + * @see #getBytesUnchecked(String, String) + */ + public static byte[] getBytesUtf16Be(final String string) { + return getBytes(string, Charsets.UTF_16BE); + } + + /** + * Encodes the given string into a sequence of bytes using the UTF-16LE charset, storing the result into a new byte + * array. + * + * @param string + * the String to encode, may be null + * @return encoded bytes, or null if the input string was null + * @throws NullPointerException + * Thrown if {@link Charsets#UTF_16LE} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + * @see Standard charsets + * @see #getBytesUnchecked(String, String) + */ + public static byte[] getBytesUtf16Le(final String string) { + return getBytes(string, Charsets.UTF_16LE); + } + + /** + * Encodes the given string into a sequence of bytes using the UTF-8 charset, storing the result into a new byte + * array. + * + * @param string + * the String to encode, may be null + * @return encoded bytes, or null if the input string was null + * @throws NullPointerException + * Thrown if {@link Charsets#UTF_8} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + * @see Standard charsets + * @see #getBytesUnchecked(String, String) + */ + public static byte[] getBytesUtf8(final String string) { + return getBytes(string, Charsets.UTF_8); + } + + private static IllegalStateException newIllegalStateException(final String charsetName, + final UnsupportedEncodingException e) { + return new IllegalStateException(charsetName + ": " + e); + } + + /** + * Constructs a new String by decoding the specified array of bytes using the given charset. + * + * @param bytes + * The bytes to be decoded into characters + * @param charset + * The {@link Charset} to encode the String + * @return A new String decoded from the specified array of bytes using the given charset, + * or null if the input byte array was null. + * @throws NullPointerException + * Thrown if {@link Charsets#UTF_8} is not initialized, which should never happen since it is + * required by the Java platform specification. + */ + private static String newString(final byte[] bytes, final Charset charset) { + return bytes == null ? null : new String(bytes, charset); + } + + /** + * Constructs a new String by decoding the specified array of bytes using the given charset. + *

+ * This method catches {@link UnsupportedEncodingException} and re-throws it as {@link IllegalStateException}, which + * should never happen for a required charset name. Use this method when the encoding is required to be in the JRE. + *

+ * + * @param bytes + * The bytes to be decoded into characters, may be null + * @param charsetName + * The name of a required {@link java.nio.charset.Charset} + * @return A new String decoded from the specified array of bytes using the given charset, + * or null if the input byte array was null. + * @throws IllegalStateException + * Thrown when a {@link UnsupportedEncodingException} is caught, which should never happen for a + * required charset name. + * @see CharEncoding + * @see String#String(byte[], String) + */ + public static String newString(final byte[] bytes, final String charsetName) { + if (bytes == null) { + return null; + } + try { + return new String(bytes, charsetName); + } catch (final UnsupportedEncodingException e) { + throw StringUtils.newIllegalStateException(charsetName, e); + } + } + + /** + * Constructs a new String by decoding the specified array of bytes using the ISO-8859-1 charset. + * + * @param bytes + * The bytes to be decoded into characters, may be null + * @return A new String decoded from the specified array of bytes using the ISO-8859-1 charset, or + * null if the input byte array was null. + * @throws NullPointerException + * Thrown if {@link Charsets#ISO_8859_1} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + */ + public static String newStringIso8859_1(final byte[] bytes) { + return new String(bytes, Charsets.ISO_8859_1); + } + + /** + * Constructs a new String by decoding the specified array of bytes using the US-ASCII charset. + * + * @param bytes + * The bytes to be decoded into characters + * @return A new String decoded from the specified array of bytes using the US-ASCII charset, + * or null if the input byte array was null. + * @throws NullPointerException + * Thrown if {@link Charsets#US_ASCII} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + */ + public static String newStringUsAscii(final byte[] bytes) { + return new String(bytes, Charsets.US_ASCII); + } + + /** + * Constructs a new String by decoding the specified array of bytes using the UTF-16 charset. + * + * @param bytes + * The bytes to be decoded into characters + * @return A new String decoded from the specified array of bytes using the UTF-16 charset + * or null if the input byte array was null. + * @throws NullPointerException + * Thrown if {@link Charsets#UTF_16} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + */ + public static String newStringUtf16(final byte[] bytes) { + return new String(bytes, Charsets.UTF_16); + } + + /** + * Constructs a new String by decoding the specified array of bytes using the UTF-16BE charset. + * + * @param bytes + * The bytes to be decoded into characters + * @return A new String decoded from the specified array of bytes using the UTF-16BE charset, + * or null if the input byte array was null. + * @throws NullPointerException + * Thrown if {@link Charsets#UTF_16BE} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + */ + public static String newStringUtf16Be(final byte[] bytes) { + return new String(bytes, Charsets.UTF_16BE); + } + + /** + * Constructs a new String by decoding the specified array of bytes using the UTF-16LE charset. + * + * @param bytes + * The bytes to be decoded into characters + * @return A new String decoded from the specified array of bytes using the UTF-16LE charset, + * or null if the input byte array was null. + * @throws NullPointerException + * Thrown if {@link Charsets#UTF_16LE} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + */ + public static String newStringUtf16Le(final byte[] bytes) { + return new String(bytes, Charsets.UTF_16LE); + } + + /** + * Constructs a new String by decoding the specified array of bytes using the UTF-8 charset. + * + * @param bytes + * The bytes to be decoded into characters + * @return A new String decoded from the specified array of bytes using the UTF-8 charset, + * or null if the input byte array was null. + * @throws NullPointerException + * Thrown if {@link Charsets#UTF_8} is not initialized, which should never happen since it is + * required by the Java platform specification. + * @since As of 1.7, throws {@link NullPointerException} instead of UnsupportedEncodingException + */ + public static String newStringUtf8(final byte[] bytes) { + return newString(bytes, Charsets.UTF_8); + } + +} diff --git a/src/org/apache/commons/codec/binary/package.html b/src/org/apache/commons/codec/binary/package.html new file mode 100644 index 00000000..13345ece --- /dev/null +++ b/src/org/apache/commons/codec/binary/package.html @@ -0,0 +1,21 @@ + + + + Base64, Base32, Binary, and Hexadecimal String encoding and decoding. + + diff --git a/src/org/apache/commons/codec/digest/B64.java b/src/org/apache/commons/codec/digest/B64.java new file mode 100644 index 00000000..93523e84 --- /dev/null +++ b/src/org/apache/commons/codec/digest/B64.java @@ -0,0 +1,79 @@ +/* + * 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 org.apache.commons.codec.digest; + +import java.util.Random; + +/** + * Base64 like method to convert binary bytes into ASCII chars. + * + * TODO: Can Base64 be reused? + * + *

+ * This class is immutable and thread-safe. + *

+ * + * @version $Id$ + * @since 1.7 + */ +class B64 { + + /** + * Table with characters for Base64 transformation. + */ + static final String B64T = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + + /** + * Base64 like conversion of bytes to ASCII chars. + * + * @param b2 + * A byte from the result. + * @param b1 + * A byte from the result. + * @param b0 + * A byte from the result. + * @param outLen + * The number of expected output chars. + * @param buffer + * Where the output chars is appended to. + */ + static void b64from24bit(final byte b2, final byte b1, final byte b0, final int outLen, + final StringBuilder buffer) { + // The bit masking is necessary because the JVM byte type is signed! + int w = ((b2 << 16) & 0x00ffffff) | ((b1 << 8) & 0x00ffff) | (b0 & 0xff); + // It's effectively a "for" loop but kept to resemble the original C code. + int n = outLen; + while (n-- > 0) { + buffer.append(B64T.charAt(w & 0x3f)); + w >>= 6; + } + } + + /** + * Generates a string of random chars from the B64T set. + * + * @param num + * Number of chars to generate. + */ + static String getRandomSalt(final int num) { + final StringBuilder saltString = new StringBuilder(); + for (int i = 1; i <= num; i++) { + saltString.append(B64T.charAt(new Random().nextInt(B64T.length()))); + } + return saltString.toString(); + } +} diff --git a/src/org/apache/commons/codec/digest/Crypt.java b/src/org/apache/commons/codec/digest/Crypt.java new file mode 100644 index 00000000..cd0bce69 --- /dev/null +++ b/src/org/apache/commons/codec/digest/Crypt.java @@ -0,0 +1,151 @@ +/* + * 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 org.apache.commons.codec.digest; + +import org.apache.commons.codec.Charsets; + +/** + * GNU libc crypt(3) compatible hash method. + *

+ * See {@link #crypt(String, String)} for further details. + *

+ * This class is immutable and thread-safe. + * + * @version $Id$ + * @since 1.7 + */ +public class Crypt { + + /** + * Encrypts a password in a crypt(3) compatible way. + *

+ * A random salt and the default algorithm (currently SHA-512) are used. See {@link #crypt(String, String)} for + * details. + * + * @param keyBytes + * plaintext password + * @return hash value + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String crypt(final byte[] keyBytes) { + return crypt(keyBytes, null); + } + + /** + * Encrypts a password in a crypt(3) compatible way. + *

+ * If no salt is provided, a random salt and the default algorithm (currently SHA-512) will be used. See + * {@link #crypt(String, String)} for details. + * + * @param keyBytes + * plaintext password + * @param salt + * salt value + * @return hash value + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String crypt(final byte[] keyBytes, final String salt) { + if (salt == null) { + return Sha2Crypt.sha512Crypt(keyBytes); + } else if (salt.startsWith(Sha2Crypt.SHA512_PREFIX)) { + return Sha2Crypt.sha512Crypt(keyBytes, salt); + } else if (salt.startsWith(Sha2Crypt.SHA256_PREFIX)) { + return Sha2Crypt.sha256Crypt(keyBytes, salt); + } else if (salt.startsWith(Md5Crypt.MD5_PREFIX)) { + return Md5Crypt.md5Crypt(keyBytes, salt); + } else { + return UnixCrypt.crypt(keyBytes, salt); + } + } + + /** + * Calculates the digest using the strongest crypt(3) algorithm. + *

+ * A random salt and the default algorithm (currently SHA-512) are used. + * + * @see #crypt(String, String) + * @param key + * plaintext password + * @return hash value + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String crypt(final String key) { + return crypt(key, null); + } + + /** + * Encrypts a password in a crypt(3) compatible way. + *

+ * The exact algorithm depends on the format of the salt string: + *

    + *
  • SHA-512 salts start with {@code $6$} and are up to 16 chars long. + *
  • SHA-256 salts start with {@code $5$} and are up to 16 chars long + *
  • MD5 salts start with {@code $1$} and are up to 8 chars long + *
  • DES, the traditional UnixCrypt algorithm is used with only 2 chars + *
  • Only the first 8 chars of the passwords are used in the DES algorithm! + *
+ * The magic strings {@code "$apr1$"} and {@code "$2a$"} are not recognized by this method as its output should be + * identical with that of the libc implementation. + *

+ * The rest of the salt string is drawn from the set {@code [a-zA-Z0-9./]} and is cut at the maximum length of if a + * {@code "$"} sign is encountered. It is therefore valid to enter a complete hash value as salt to e.g. verify a + * password with: + * + *

+     * storedPwd.equals(crypt(enteredPwd, storedPwd))
+     * 
+ *

+ * The resulting string starts with the marker string ({@code $6$}), continues with the salt value and ends with a + * {@code "$"} sign followed by the actual hash value. For DES the string only contains the salt and actual hash. + * It's total length is dependent on the algorithm used: + *

    + *
  • SHA-512: 106 chars + *
  • SHA-256: 63 chars + *
  • MD5: 34 chars + *
  • DES: 13 chars + *
+ *

+ * Example: + * + *

+     *      crypt("secret", "$1$xxxx") => "$1$xxxx$aMkevjfEIpa35Bh3G4bAc."
+     *      crypt("secret", "xx") => "xxWAum7tHdIUw"
+     * 
+ *

+ * This method comes in a variation that accepts a byte[] array to support input strings that are not encoded in + * UTF-8 but e.g. in ISO-8859-1 where equal characters result in different byte values. + * + * @see "The man page of the libc crypt (3) function." + * @param key + * plaintext password as entered by the used + * @param salt + * salt value + * @return hash value, i.e. encrypted password including the salt string + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. * + */ + public static String crypt(final String key, final String salt) { + return crypt(key.getBytes(Charsets.UTF_8), salt); + } +} diff --git a/src/org/apache/commons/codec/digest/DigestUtils.java b/src/org/apache/commons/codec/digest/DigestUtils.java new file mode 100644 index 00000000..f5dc9a35 --- /dev/null +++ b/src/org/apache/commons/codec/digest/DigestUtils.java @@ -0,0 +1,1140 @@ +/* + * 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 org.apache.commons.codec.digest; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.binary.StringUtils; + +/** + * Operations to simplify common {@link java.security.MessageDigest} tasks. + * This class is immutable and thread-safe. + * + * @version $Id$ + */ +public class DigestUtils { + + private static final int STREAM_BUFFER_LENGTH = 1024; + + /** + * Read through an ByteBuffer and returns the digest for the data + * + * @param digest + * The MessageDigest to use (e.g. MD5) + * @param data + * Data to digest + * @return the digest + * @throws IOException + * On error reading from the stream + */ + private static byte[] digest(final MessageDigest messageDigest, final ByteBuffer data) { + messageDigest.update(data); + return messageDigest.digest(); + } + + /** + * Read through an InputStream and returns the digest for the data + * + * @param digest + * The MessageDigest to use (e.g. MD5) + * @param data + * Data to digest + * @return the digest + * @throws IOException + * On error reading from the stream + */ + private static byte[] digest(final MessageDigest digest, final InputStream data) throws IOException { + return updateDigest(digest, data).digest(); + } + + /** + * Returns a MessageDigest for the given algorithm. + * + * @param algorithm + * the name of the algorithm requested. See Appendix A in the Java Cryptography Architecture Reference Guide for information about standard + * algorithm names. + * @return A digest instance. + * @see MessageDigest#getInstance(String) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught. + */ + public static MessageDigest getDigest(final String algorithm) { + try { + return MessageDigest.getInstance(algorithm); + } catch (final NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Returns an MD2 MessageDigest. + * + * @return An MD2 digest instance. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught, which should never happen because MD2 is a + * built-in algorithm + * @see MessageDigestAlgorithms#MD2 + * @since 1.7 + */ + public static MessageDigest getMd2Digest() { + return getDigest(MessageDigestAlgorithms.MD2); + } + + /** + * Returns an MD5 MessageDigest. + * + * @return An MD5 digest instance. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught, which should never happen because MD5 is a + * built-in algorithm + * @see MessageDigestAlgorithms#MD5 + */ + public static MessageDigest getMd5Digest() { + return getDigest(MessageDigestAlgorithms.MD5); + } + + /** + * Returns an SHA-1 digest. + * + * @return An SHA-1 digest instance. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught, which should never happen because SHA-1 is a + * built-in algorithm + * @see MessageDigestAlgorithms#SHA_1 + * @since 1.7 + */ + public static MessageDigest getSha1Digest() { + return getDigest(MessageDigestAlgorithms.SHA_1); + } + + /** + * Returns an SHA-224 digest. + *

+ * Java 8 only. + *

+ * + * @return An SHA-224 digest instance. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught on Java 7 and older, SHA-224 is a built-in + * algorithm on Java 8 + * @see MessageDigestAlgorithms#SHA_224 + */ + public static MessageDigest getSha224Digest() { + return getDigest(MessageDigestAlgorithms.SHA_224); + } + + /** + * Returns an SHA-256 digest. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @return An SHA-256 digest instance. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught, which should never happen because SHA-256 is a + * built-in algorithm + * @see MessageDigestAlgorithms#SHA_256 + */ + public static MessageDigest getSha256Digest() { + return getDigest(MessageDigestAlgorithms.SHA_256); + } + + /** + * Returns an SHA-384 digest. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @return An SHA-384 digest instance. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught, which should never happen because SHA-384 is a + * built-in algorithm + * @see MessageDigestAlgorithms#SHA_384 + */ + public static MessageDigest getSha384Digest() { + return getDigest(MessageDigestAlgorithms.SHA_384); + } + + /** + * Returns an SHA-512 digest. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @return An SHA-512 digest instance. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught, which should never happen because SHA-512 is a + * built-in algorithm + * @see MessageDigestAlgorithms#SHA_512 + */ + public static MessageDigest getSha512Digest() { + return getDigest(MessageDigestAlgorithms.SHA_512); + } + + /** + * Returns an SHA-1 digest. + * + * @return An SHA-1 digest instance. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught + * @deprecated Use {@link #getSha1Digest()} + */ + @Deprecated + public static MessageDigest getShaDigest() { + return getSha1Digest(); + } + + /** + * Calculates the MD2 digest and returns the value as a 16 element byte[]. + * + * @param data + * Data to digest + * @return MD2 digest + * @since 1.7 + */ + public static byte[] md2(final byte[] data) { + return getMd2Digest().digest(data); + } + + /** + * Calculates the MD2 digest and returns the value as a 16 element byte[]. + * + * @param data + * Data to digest + * @return MD2 digest + * @since 1.11 + */ + public static byte[] md2(final ByteBuffer data) { + return digest(getMd2Digest(), data); + } + + /** + * Calculates the MD2 digest and returns the value as a 16 element byte[]. + * + * @param data + * Data to digest + * @return MD2 digest + * @throws IOException + * On error reading from the stream + * @since 1.7 + */ + public static byte[] md2(final InputStream data) throws IOException { + return digest(getMd2Digest(), data); + } + + /** + * Calculates the MD2 digest and returns the value as a 16 element byte[]. + * + * @param data + * Data to digest; converted to bytes using {@link StringUtils#getBytesUtf8(String)} + * @return MD2 digest + * @since 1.7 + */ + public static byte[] md2(final String data) { + return md2(StringUtils.getBytesUtf8(data)); + } + + /** + * Calculates the MD2 digest and returns the value as a 32 character hex string. + * + * @param data + * Data to digest + * @return MD2 digest as a hex string + * @since 1.7 + */ + public static String md2Hex(final byte[] data) { + return Hex.encodeHexString(md2(data)); + } + + /** + * Calculates the MD2 digest and returns the value as a 32 character hex string. + * + * @param data + * Data to digest + * @return MD2 digest as a hex string + * @since 1.11 + */ + public static String md2Hex(final ByteBuffer data) { + return Hex.encodeHexString(md2(data)); + } + + /** + * Calculates the MD2 digest and returns the value as a 32 character hex string. + * + * @param data + * Data to digest + * @return MD2 digest as a hex string + * @throws IOException + * On error reading from the stream + * @since 1.7 + */ + public static String md2Hex(final InputStream data) throws IOException { + return Hex.encodeHexString(md2(data)); + } + + /** + * Calculates the MD2 digest and returns the value as a 32 character hex string. + * + * @param data + * Data to digest + * @return MD2 digest as a hex string + * @since 1.7 + */ + public static String md2Hex(final String data) { + return Hex.encodeHexString(md2(data)); + } + + /** + * Calculates the MD5 digest and returns the value as a 16 element byte[]. + * + * @param data + * Data to digest + * @return MD5 digest + */ + public static byte[] md5(final byte[] data) { + return getMd5Digest().digest(data); + } + + /** + * Calculates the MD5 digest and returns the value as a 16 element byte[]. + * + * @param data + * Data to digest + * @return MD5 digest + * @since 1.11 + */ + public static byte[] md5(final ByteBuffer data) { + return digest(getMd5Digest(), data); + } + + /** + * Calculates the MD5 digest and returns the value as a 16 element byte[]. + * + * @param data + * Data to digest + * @return MD5 digest + * @throws IOException + * On error reading from the stream + * @since 1.4 + */ + public static byte[] md5(final InputStream data) throws IOException { + return digest(getMd5Digest(), data); + } + + /** + * Calculates the MD5 digest and returns the value as a 16 element byte[]. + * + * @param data + * Data to digest; converted to bytes using {@link StringUtils#getBytesUtf8(String)} + * @return MD5 digest + */ + public static byte[] md5(final String data) { + return md5(StringUtils.getBytesUtf8(data)); + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param data + * Data to digest + * @return MD5 digest as a hex string + */ + public static String md5Hex(final byte[] data) { + return Hex.encodeHexString(md5(data)); + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param data + * Data to digest + * @return MD5 digest as a hex string + * @since 1.11 + */ + public static String md5Hex(final ByteBuffer data) { + return Hex.encodeHexString(md5(data)); + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param data + * Data to digest + * @return MD5 digest as a hex string + * @throws IOException + * On error reading from the stream + * @since 1.4 + */ + public static String md5Hex(final InputStream data) throws IOException { + return Hex.encodeHexString(md5(data)); + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param data + * Data to digest + * @return MD5 digest as a hex string + */ + public static String md5Hex(final String data) { + return Hex.encodeHexString(md5(data)); + } + + /** + * Calculates the SHA-1 digest and returns the value as a byte[]. + * + * @param data + * Data to digest + * @return SHA-1 digest + * @deprecated Use {@link #sha1(byte[])} + */ + @Deprecated + public static byte[] sha(final byte[] data) { + return sha1(data); + } + +/** + * Calculates the SHA-1 digest and returns the value as a byte[]. + * + * @param data + * Data to digest + * @return SHA-1 digest + * @throws IOException + * On error reading from the stream + * @since 1.4 + * @deprecated Use {@link #sha1(InputStream)} + */ +@Deprecated +public static byte[] sha(final InputStream data) throws IOException { + return sha1(data); +} + + /** + * Calculates the SHA-1 digest and returns the value as a byte[]. + * + * @param data + * Data to digest + * @return SHA-1 digest + * @deprecated Use {@link #sha1(String)} + */ + @Deprecated + public static byte[] sha(final String data) { + return sha1(data); + } + + /** + * Calculates the SHA-1 digest and returns the value as a byte[]. + * + * @param data + * Data to digest + * @return SHA-1 digest + * @since 1.7 + */ + public static byte[] sha1(final byte[] data) { + return getSha1Digest().digest(data); + } + + /** + * Calculates the SHA-1 digest and returns the value as a byte[]. + * + * @param data + * Data to digest + * @return SHA-1 digest + * @since 1.11 + */ + public static byte[] sha1(final ByteBuffer data) { + return digest(getSha1Digest(), data); + } + + /** + * Calculates the SHA-1 digest and returns the value as a byte[]. + * + * @param data + * Data to digest + * @return SHA-1 digest + * @throws IOException + * On error reading from the stream + * @since 1.7 + */ + public static byte[] sha1(final InputStream data) throws IOException { + return digest(getSha1Digest(), data); + } + + /** + * Calculates the SHA-1 digest and returns the value as a byte[]. + * + * @param data + * Data to digest; converted to bytes using {@link StringUtils#getBytesUtf8(String)} + * @return SHA-1 digest + */ + public static byte[] sha1(final String data) { + return sha1(StringUtils.getBytesUtf8(data)); + } + + /** + * Calculates the SHA-1 digest and returns the value as a hex string. + * + * @param data + * Data to digest + * @return SHA-1 digest as a hex string + * @since 1.7 + */ + public static String sha1Hex(final byte[] data) { + return Hex.encodeHexString(sha1(data)); + } + + /** + * Calculates the SHA-1 digest and returns the value as a hex string. + * + * @param data + * Data to digest + * @return SHA-1 digest as a hex string + * @since 1.11 + */ + public static String sha1Hex(final ByteBuffer data) { + return Hex.encodeHexString(sha1(data)); + } + + /** + * Calculates the SHA-1 digest and returns the value as a hex string. + * + * @param data + * Data to digest + * @return SHA-1 digest as a hex string + * @throws IOException + * On error reading from the stream + * @since 1.7 + */ + public static String sha1Hex(final InputStream data) throws IOException { + return Hex.encodeHexString(sha1(data)); + } + + /** + * Calculates the SHA-1 digest and returns the value as a hex string. + * + * @param data + * Data to digest + * @return SHA-1 digest as a hex string + * @since 1.7 + */ + public static String sha1Hex(final String data) { + return Hex.encodeHexString(sha1(data)); + } + + /** + * Calculates the SHA-224 digest and returns the value as a byte[]. + *

+ * Throws a {@link IllegalArgumentException} on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-224 digest + * @throws IllegalArgumentException thrown on JRE versions prior to 1.8.0. + * @since 1.11 + */ + public static byte[] sha224(final byte[] data) { + return getSha224Digest().digest(data); + } + + /** + * Calculates the SHA-224 digest and returns the value as a byte[]. + * + * @param data + * Data to digest + * @return SHA-224 digest + * @throws IllegalArgumentException thrown on JRE versions prior to 1.8.0. + * @since 1.11 + */ + public static byte[] sha224(final ByteBuffer data) { + return digest(getSha224Digest(), data); + } + + /** + * Calculates the SHA-224 digest and returns the value as a byte[]. + *

+ * Throws a {@link IllegalArgumentException} on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-224 digest + * @throws IOException + * On error reading from the stream + * @throws IllegalArgumentException thrown on JRE versions prior to 1.8.0. + * @since 1.11 + */ + public static byte[] sha224(final InputStream data) throws IOException { + return digest(getSha224Digest(), data); + } + + /** + * Calculates the SHA-224 digest and returns the value as a byte[]. + *

+ * Throws a {@link IllegalArgumentException} on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest; converted to bytes using {@link StringUtils#getBytesUtf8(String)} + * @return SHA-224 digest + * @throws IllegalArgumentException thrown on JRE versions prior to 1.8.0. + * @since 1.11 + */ + public static byte[] sha224(final String data) { + return sha224(StringUtils.getBytesUtf8(data)); + } + + /** + * Calculates the SHA-224 digest and returns the value as a hex string. + *

+ * Throws a {@link IllegalArgumentException} on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-224 digest as a hex string + * @throws IllegalArgumentException thrown on JRE versions prior to 1.8.0. + * @since 1.11 + */ + public static String sha224Hex(final byte[] data) { + return Hex.encodeHexString(sha224(data)); + } + + /** + * Calculates the SHA-224 digest and returns the value as a hex string. + *

+ * Throws a {@link IllegalArgumentException} on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-224 digest as a hex string + * @throws IllegalArgumentException thrown on JRE versions prior to 1.8.0. + * @since 1.11 + */ + public static String sha224Hex(final ByteBuffer data) { + return Hex.encodeHexString(sha224(data)); + } + + /** + * Calculates the SHA-224 digest and returns the value as a hex string. + *

+ * Throws a {@link IllegalArgumentException} on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-224 digest as a hex string + * @throws IOException + * On error reading from the stream + * @throws IllegalArgumentException thrown on JRE versions prior to 1.8.0. + * @since 1.11 + */ + public static String sha224Hex(final InputStream data) throws IOException { + return Hex.encodeHexString(sha224(data)); + } + + /** + * Calculates the SHA-224 digest and returns the value as a hex string. + *

+ * Throws a {@link IllegalArgumentException} on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-224 digest as a hex string + * @throws IllegalArgumentException thrown on JRE versions prior to 1.8.0. + * @since 1.11 + */ + public static String sha224Hex(final String data) { + return Hex.encodeHexString(sha224(data)); + } + + /** + * Calculates the SHA-256 digest and returns the value as a byte[]. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-256 digest + * @since 1.4 + */ + public static byte[] sha256(final byte[] data) { + return getSha256Digest().digest(data); + } + + /** + * Calculates the SHA-256 digest and returns the value as a byte[]. + * + * @param data + * Data to digest + * @return SHA-256 digest + * @since 1.11 + */ + public static byte[] sha256(final ByteBuffer data) { + return digest(getSha256Digest(), data); + } + + /** + * Calculates the SHA-256 digest and returns the value as a byte[]. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-256 digest + * @throws IOException + * On error reading from the stream + * @since 1.4 + */ + public static byte[] sha256(final InputStream data) throws IOException { + return digest(getSha256Digest(), data); + } + + /** + * Calculates the SHA-256 digest and returns the value as a byte[]. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest; converted to bytes using {@link StringUtils#getBytesUtf8(String)} + * @return SHA-256 digest + * @since 1.4 + */ + public static byte[] sha256(final String data) { + return sha256(StringUtils.getBytesUtf8(data)); + } + + /** + * Calculates the SHA-256 digest and returns the value as a hex string. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-256 digest as a hex string + * @since 1.4 + */ + public static String sha256Hex(final byte[] data) { + return Hex.encodeHexString(sha256(data)); + } + + /** + * Calculates the SHA-256 digest and returns the value as a hex string. + * + * @param data + * Data to digest + * @return SHA-256 digest as a hex string + * @since 1.11 + */ + public static String sha256Hex(final ByteBuffer data) { + return Hex.encodeHexString(sha256(data)); + } + + /** + * Calculates the SHA-256 digest and returns the value as a hex string. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-256 digest as a hex string + * @throws IOException + * On error reading from the stream + * @since 1.4 + */ + public static String sha256Hex(final InputStream data) throws IOException { + return Hex.encodeHexString(sha256(data)); + } + + /** + * Calculates the SHA-256 digest and returns the value as a hex string. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-256 digest as a hex string + * @since 1.4 + */ + public static String sha256Hex(final String data) { + return Hex.encodeHexString(sha256(data)); + } + + /** + * Calculates the SHA-384 digest and returns the value as a byte[]. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-384 digest + * @since 1.4 + */ + public static byte[] sha384(final byte[] data) { + return getSha384Digest().digest(data); + } + + /** + * Calculates the SHA-384 digest and returns the value as a byte[]. + * + * @param data + * Data to digest + * @return SHA-384 digest + * @since 1.11 + */ + public static byte[] sha384(final ByteBuffer data) { + return digest(getSha384Digest(), data); + } + + /** + * Calculates the SHA-384 digest and returns the value as a byte[]. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-384 digest + * @throws IOException + * On error reading from the stream + * @since 1.4 + */ + public static byte[] sha384(final InputStream data) throws IOException { + return digest(getSha384Digest(), data); + } + + /** + * Calculates the SHA-384 digest and returns the value as a byte[]. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest; converted to bytes using {@link StringUtils#getBytesUtf8(String)} + * @return SHA-384 digest + * @since 1.4 + */ + public static byte[] sha384(final String data) { + return sha384(StringUtils.getBytesUtf8(data)); + } + + /** + * Calculates the SHA-384 digest and returns the value as a hex string. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-384 digest as a hex string + * @since 1.4 + */ + public static String sha384Hex(final byte[] data) { + return Hex.encodeHexString(sha384(data)); + } + + /** + * Calculates the SHA-384 digest and returns the value as a hex string. + * + * @param data + * Data to digest + * @return SHA-384 digest as a hex string + * @since 1.11 + */ + public static String sha384Hex(final ByteBuffer data) { + return Hex.encodeHexString(sha384(data)); + } + + /** + * Calculates the SHA-384 digest and returns the value as a hex string. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-384 digest as a hex string + * @throws IOException + * On error reading from the stream + * @since 1.4 + */ + public static String sha384Hex(final InputStream data) throws IOException { + return Hex.encodeHexString(sha384(data)); + } + + /** + * Calculates the SHA-384 digest and returns the value as a hex string. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-384 digest as a hex string + * @since 1.4 + */ + public static String sha384Hex(final String data) { + return Hex.encodeHexString(sha384(data)); + } + + /** + * Calculates the SHA-512 digest and returns the value as a byte[]. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-512 digest + * @since 1.4 + */ + public static byte[] sha512(final byte[] data) { + return getSha512Digest().digest(data); + } + + /** + * Calculates the SHA-512 digest and returns the value as a byte[]. + * + * @param data + * Data to digest + * @return SHA-512 digest + * @since 1.11 + */ + public static byte[] sha512(final ByteBuffer data) { + return digest(getSha512Digest(), data); + } + + /** + * Calculates the SHA-512 digest and returns the value as a byte[]. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-512 digest + * @throws IOException + * On error reading from the stream + * @since 1.4 + */ + public static byte[] sha512(final InputStream data) throws IOException { + return digest(getSha512Digest(), data); + } + + /** + * Calculates the SHA-512 digest and returns the value as a byte[]. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest; converted to bytes using {@link StringUtils#getBytesUtf8(String)} + * @return SHA-512 digest + * @since 1.4 + */ + public static byte[] sha512(final String data) { + return sha512(StringUtils.getBytesUtf8(data)); + } + + /** + * Calculates the SHA-512 digest and returns the value as a hex string. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-512 digest as a hex string + * @since 1.4 + */ + public static String sha512Hex(final byte[] data) { + return Hex.encodeHexString(sha512(data)); + } + + /** + * Calculates the SHA-512 digest and returns the value as a hex string. + * + * @param data + * Data to digest + * @return SHA-512 digest as a hex string + * @since 1.11 + */ + public static String sha512Hex(final ByteBuffer data) { + return Hex.encodeHexString(sha512(data)); + } + + /** + * Calculates the SHA-512 digest and returns the value as a hex string. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-512 digest as a hex string + * @throws IOException + * On error reading from the stream + * @since 1.4 + */ + public static String sha512Hex(final InputStream data) throws IOException { + return Hex.encodeHexString(sha512(data)); + } + + /** + * Calculates the SHA-512 digest and returns the value as a hex string. + *

+ * Throws a RuntimeException on JRE versions prior to 1.4.0. + *

+ * + * @param data + * Data to digest + * @return SHA-512 digest as a hex string + * @since 1.4 + */ + public static String sha512Hex(final String data) { + return Hex.encodeHexString(sha512(data)); + } + + /** + * Calculates the SHA-1 digest and returns the value as a hex string. + * + * @param data + * Data to digest + * @return SHA-1 digest as a hex string + * @deprecated Use {@link #sha1Hex(byte[])} + */ + @Deprecated + public static String shaHex(final byte[] data) { + return sha1Hex(data); + } + + /** + * Calculates the SHA-1 digest and returns the value as a hex string. + * + * @param data + * Data to digest + * @return SHA-1 digest as a hex string + * @throws IOException + * On error reading from the stream + * @since 1.4 + * @deprecated Use {@link #sha1Hex(InputStream)} + */ + @Deprecated + public static String shaHex(final InputStream data) throws IOException { + return sha1Hex(data); + } + + /** + * Calculates the SHA-1 digest and returns the value as a hex string. + * + * @param data + * Data to digest + * @return SHA-1 digest as a hex string + * @deprecated Use {@link #sha1Hex(String)} + */ + @Deprecated + public static String shaHex(final String data) { + return sha1Hex(data); + } + + /** + * Updates the given {@link MessageDigest}. + * + * @param messageDigest + * the {@link MessageDigest} to update + * @param valueToDigest + * the value to update the {@link MessageDigest} with + * @return the updated {@link MessageDigest} + * @since 1.7 + */ + public static MessageDigest updateDigest(final MessageDigest messageDigest, final byte[] valueToDigest) { + messageDigest.update(valueToDigest); + return messageDigest; + } + + /** + * Updates the given {@link MessageDigest}. + * + * @param messageDigest + * the {@link MessageDigest} to update + * @param valueToDigest + * the value to update the {@link MessageDigest} with + * @return the updated {@link MessageDigest} + * @since 1.11 + */ + public static MessageDigest updateDigest(final MessageDigest messageDigest, final ByteBuffer valueToDigest) { + messageDigest.update(valueToDigest); + return messageDigest; + } + + /** + * Reads through an InputStream and updates the digest for the data + * + * @param digest + * The MessageDigest to use (e.g. MD5) + * @param data + * Data to digest + * @return the digest + * @throws IOException + * On error reading from the stream + * @since 1.8 + */ + public static MessageDigest updateDigest(final MessageDigest digest, final InputStream data) throws IOException { + final byte[] buffer = new byte[STREAM_BUFFER_LENGTH]; + int read = data.read(buffer, 0, STREAM_BUFFER_LENGTH); + + while (read > -1) { + digest.update(buffer, 0, read); + read = data.read(buffer, 0, STREAM_BUFFER_LENGTH); + } + + return digest; + } + + /** + * Updates the given {@link MessageDigest}. + * + * @param messageDigest + * the {@link MessageDigest} to update + * @param valueToDigest + * the value to update the {@link MessageDigest} with; + * converted to bytes using {@link StringUtils#getBytesUtf8(String)} + * @return the updated {@link MessageDigest} + * @since 1.7 + */ + public static MessageDigest updateDigest(final MessageDigest messageDigest, final String valueToDigest) { + messageDigest.update(StringUtils.getBytesUtf8(valueToDigest)); + return messageDigest; + } +} diff --git a/src/org/apache/commons/codec/digest/HmacAlgorithms.java b/src/org/apache/commons/codec/digest/HmacAlgorithms.java new file mode 100644 index 00000000..5dd30d86 --- /dev/null +++ b/src/org/apache/commons/codec/digest/HmacAlgorithms.java @@ -0,0 +1,94 @@ +/* + * 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 org.apache.commons.codec.digest; + +/** + * Standard {@link HmacUtils} algorithm names from the Java Cryptography Architecture Standard Algorithm Name + * Documentation. + * + *

+ * Note: Not all JCE implementations supports all algorithms in this enum. + *

+ * + * @see Java Cryptography + * Architecture Standard Algorithm Name Documentation + * @since 1.10 + * @version $Id$ + */ +public enum HmacAlgorithms { + + /** + * The HmacMD5 Message Authentication Code (MAC) algorithm specified in RFC 2104 and RFC 1321. + *

+ * Every implementation of the Java platform is required to support this standard Mac algorithm. + *

+ */ + HMAC_MD5("HmacMD5"), + + /** + * The HmacSHA1 Message Authentication Code (MAC) algorithm specified in RFC 2104 and FIPS PUB 180-2. + *

+ * Every implementation of the Java platform is required to support this standard Mac algorithm. + *

+ */ + HMAC_SHA_1("HmacSHA1"), + + /** + * The HmacSHA256 Message Authentication Code (MAC) algorithm specified in RFC 2104 and FIPS PUB 180-2. + *

+ * Every implementation of the Java platform is required to support this standard Mac algorithm. + *

+ */ + HMAC_SHA_256("HmacSHA256"), + + /** + * The HmacSHA384 Message Authentication Code (MAC) algorithm specified in RFC 2104 and FIPS PUB 180-2. + *

+ * Every implementation of the Java platform is not required to support this Mac algorithm. + *

+ */ + HMAC_SHA_384("HmacSHA384"), + + /** + * The HmacSHA512 Message Authentication Code (MAC) algorithm specified in RFC 2104 and FIPS PUB 180-2. + *

+ * Every implementation of the Java platform is not required to support this Mac algorithm. + *

+ */ + HMAC_SHA_512("HmacSHA512"); + + private final String algorithm; + + private HmacAlgorithms(final String algorithm) { + this.algorithm = algorithm; + } + + /** + * The algorithm name + * + * @see Java + * Cryptography Architecture Sun Providers Documentation + * @return The algorithm name ("HmacSHA512" for example) + */ + @Override + public String toString() { + return algorithm; + } + +} diff --git a/src/org/apache/commons/codec/digest/HmacUtils.java b/src/org/apache/commons/codec/digest/HmacUtils.java new file mode 100644 index 00000000..425db4ab --- /dev/null +++ b/src/org/apache/commons/codec/digest/HmacUtils.java @@ -0,0 +1,794 @@ +/* + * 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 org.apache.commons.codec.digest; + +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.codec.binary.StringUtils; + +/** + * Simplifies common {@link javax.crypto.Mac} tasks. This class is immutable and thread-safe. + * + * + *

+ * Note: Not all JCE implementations supports all algorithms. If not supported, an IllegalArgumentException is + * thrown. + *

+ * + * @since 1.10 + * @version $Id$ + */ +public final class HmacUtils { + + private static final int STREAM_BUFFER_LENGTH = 1024; + + /** + * Returns an initialized Mac for the HmacMD5 algorithm. + *

+ * Every implementation of the Java platform is required to support this standard Mac algorithm. + *

+ * + * @param key + * They key for the keyed digest (must not be null) + * @return A Mac instance initialized with the given key. + * @see Mac#getInstance(String) + * @see Mac#init(Key) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static Mac getHmacMd5(final byte[] key) { + return getInitializedMac(HmacAlgorithms.HMAC_MD5, key); + } + + /** + * Returns an initialized Mac for the HmacSHA1 algorithm. + *

+ * Every implementation of the Java platform is required to support this standard Mac algorithm. + *

+ * + * @param key + * They key for the keyed digest (must not be null) + * @return A Mac instance initialized with the given key. + * @see Mac#getInstance(String) + * @see Mac#init(Key) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static Mac getHmacSha1(final byte[] key) { + return getInitializedMac(HmacAlgorithms.HMAC_SHA_1, key); + } + + /** + * Returns an initialized Mac for the HmacSHA256 algorithm. + *

+ * Every implementation of the Java platform is required to support this standard Mac algorithm. + *

+ * + * @param key + * They key for the keyed digest (must not be null) + * @return A Mac instance initialized with the given key. + * @see Mac#getInstance(String) + * @see Mac#init(Key) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static Mac getHmacSha256(final byte[] key) { + return getInitializedMac(HmacAlgorithms.HMAC_SHA_256, key); + } + + /** + * Returns an initialized Mac for the HmacSHA384 algorithm. + *

+ * Every implementation of the Java platform is not required to support this Mac algorithm. + *

+ * + * @param key + * They key for the keyed digest (must not be null) + * @return A Mac instance initialized with the given key. + * @see Mac#getInstance(String) + * @see Mac#init(Key) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static Mac getHmacSha384(final byte[] key) { + return getInitializedMac(HmacAlgorithms.HMAC_SHA_384, key); + } + + /** + * Returns an initialized Mac for the HmacSHA512 algorithm. + *

+ * Every implementation of the Java platform is not required to support this Mac algorithm. + *

+ * + * @param key + * They key for the keyed digest (must not be null) + * @return A Mac instance initialized with the given key. + * @see Mac#getInstance(String) + * @see Mac#init(Key) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static Mac getHmacSha512(final byte[] key) { + return getInitializedMac(HmacAlgorithms.HMAC_SHA_512, key); + } + + /** + * Returns an initialized Mac for the given algorithm. + * + * @param algorithm + * the name of the algorithm requested. See Appendix + * A in the Java Cryptography Architecture Reference Guide for information about standard algorithm + * names. + * @param key + * They key for the keyed digest (must not be null) + * @return A Mac instance initialized with the given key. + * @see Mac#getInstance(String) + * @see Mac#init(Key) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static Mac getInitializedMac(final HmacAlgorithms algorithm, final byte[] key) { + return getInitializedMac(algorithm.toString(), key); + } + + /** + * Returns an initialized Mac for the given algorithm. + * + * @param algorithm + * the name of the algorithm requested. See Appendix + * A in the Java Cryptography Architecture Reference Guide for information about standard algorithm + * names. + * @param key + * They key for the keyed digest (must not be null) + * @return A Mac instance initialized with the given key. + * @see Mac#getInstance(String) + * @see Mac#init(Key) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static Mac getInitializedMac(final String algorithm, final byte[] key) { + + if (key == null) { + throw new IllegalArgumentException("Null key"); + } + + try { + final SecretKeySpec keySpec = new SecretKeySpec(key, algorithm); + final Mac mac = Mac.getInstance(algorithm); + mac.init(keySpec); + return mac; + } catch (final NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } catch (final InvalidKeyException e) { + throw new IllegalArgumentException(e); + } + } + + // hmacMd5 + + /** + * Returns a HmacMD5 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacMD5 MAC for the given key and value + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacMd5(final byte[] key, final byte[] valueToDigest) { + try { + return getHmacMd5(key).doFinal(valueToDigest); + } catch (final IllegalStateException e) { + // cannot happen + throw new IllegalArgumentException(e); + } + } + + /** + * Returns a HmacMD5 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest + *

+ * The InputStream must not be null and will not be closed + *

+ * @return HmacMD5 MAC for the given key and value + * @throws IOException + * If an I/O error occurs. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacMd5(final byte[] key, final InputStream valueToDigest) throws IOException { + return updateHmac(getHmacMd5(key), valueToDigest).doFinal(); + } + + /** + * Returns a HmacMD5 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacMD5 MAC for the given key and value + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacMd5(final String key, final String valueToDigest) { + return hmacMd5(StringUtils.getBytesUtf8(key), StringUtils.getBytesUtf8(valueToDigest)); + } + + /** + * Returns a HmacMD5 Message Authentication Code (MAC) as a hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacMD5 MAC for the given key and value as a hex string (lowercase) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacMd5Hex(final byte[] key, final byte[] valueToDigest) { + return Hex.encodeHexString(hmacMd5(key, valueToDigest)); + } + + /** + * Returns a HmacMD5 Message Authentication Code (MAC) as a hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest + *

+ * The InputStream must not be null and will not be closed + *

+ * @return HmacMD5 MAC for the given key and value as a hex string (lowercase) + * @throws IOException + * If an I/O error occurs. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacMd5Hex(final byte[] key, final InputStream valueToDigest) throws IOException { + return Hex.encodeHexString(hmacMd5(key, valueToDigest)); + } + + /** + * Returns a HmacMD5 Message Authentication Code (MAC) as a hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacMD5 MAC for the given key and value as a hex string (lowercase) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacMd5Hex(final String key, final String valueToDigest) { + return Hex.encodeHexString(hmacMd5(key, valueToDigest)); + } + + // hmacSha1 + + /** + * Returns a HmacSHA1 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA1 MAC for the given key and value + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha1(final byte[] key, final byte[] valueToDigest) { + try { + return getHmacSha1(key).doFinal(valueToDigest); + } catch (final IllegalStateException e) { + // cannot happen + throw new IllegalArgumentException(e); + } + } + + /** + * Returns a HmacSHA1 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest + *

+ * The InputStream must not be null and will not be closed + *

+ * @return HmacSHA1 MAC for the given key and value + * @throws IOException + * If an I/O error occurs. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha1(final byte[] key, final InputStream valueToDigest) throws IOException { + return updateHmac(getHmacSha1(key), valueToDigest).doFinal(); + } + + /** + * Returns a HmacSHA1 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA1 MAC for the given key and value + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha1(final String key, final String valueToDigest) { + return hmacSha1(StringUtils.getBytesUtf8(key), StringUtils.getBytesUtf8(valueToDigest)); + } + + /** + * Returns a HmacSHA1 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA1 MAC for the given key and value as hex string (lowercase) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha1Hex(final byte[] key, final byte[] valueToDigest) { + return Hex.encodeHexString(hmacSha1(key, valueToDigest)); + } + + /** + * Returns a HmacSHA1 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest + *

+ * The InputStream must not be null and will not be closed + *

+ * @return HmacSHA1 MAC for the given key and value as hex string (lowercase) + * @throws IOException + * If an I/O error occurs. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha1Hex(final byte[] key, final InputStream valueToDigest) throws IOException { + return Hex.encodeHexString(hmacSha1(key, valueToDigest)); + } + + /** + * Returns a HmacSHA1 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA1 MAC for the given key and value as hex string (lowercase) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha1Hex(final String key, final String valueToDigest) { + return Hex.encodeHexString(hmacSha1(key, valueToDigest)); + } + + // hmacSha256 + + /** + * Returns a HmacSHA256 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA256 MAC for the given key and value + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha256(final byte[] key, final byte[] valueToDigest) { + try { + return getHmacSha256(key).doFinal(valueToDigest); + } catch (final IllegalStateException e) { + // cannot happen + throw new IllegalArgumentException(e); + } + } + + /** + * Returns a HmacSHA256 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest + *

+ * The InputStream must not be null and will not be closed + *

+ * @return HmacSHA256 MAC for the given key and value + * @throws IOException + * If an I/O error occurs. +s * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha256(final byte[] key, final InputStream valueToDigest) throws IOException { + return updateHmac(getHmacSha256(key), valueToDigest).doFinal(); + } + + /** + * Returns a HmacSHA256 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA256 MAC for the given key and value + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha256(final String key, final String valueToDigest) { + return hmacSha256(StringUtils.getBytesUtf8(key), StringUtils.getBytesUtf8(valueToDigest)); + } + + /** + * Returns a HmacSHA256 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA256 MAC for the given key and value as hex string (lowercase) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha256Hex(final byte[] key, final byte[] valueToDigest) { + return Hex.encodeHexString(hmacSha256(key, valueToDigest)); + } + + /** + * Returns a HmacSHA256 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest + *

+ * The InputStream must not be null and will not be closed + *

+ * @return HmacSHA256 MAC for the given key and value as hex string (lowercase) + * @throws IOException + * If an I/O error occurs. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha256Hex(final byte[] key, final InputStream valueToDigest) throws IOException { + return Hex.encodeHexString(hmacSha256(key, valueToDigest)); + } + + /** + * Returns a HmacSHA256 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA256 MAC for the given key and value as hex string (lowercase) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha256Hex(final String key, final String valueToDigest) { + return Hex.encodeHexString(hmacSha256(key, valueToDigest)); + } + + // hmacSha384 + + /** + * Returns a HmacSHA384 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA384 MAC for the given key and value + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha384(final byte[] key, final byte[] valueToDigest) { + try { + return getHmacSha384(key).doFinal(valueToDigest); + } catch (final IllegalStateException e) { + // cannot happen + throw new IllegalArgumentException(e); + } + } + + /** + * Returns a HmacSHA384 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest + *

+ * The InputStream must not be null and will not be closed + *

+ * @return HmacSHA384 MAC for the given key and value + * @throws IOException + * If an I/O error occurs. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha384(final byte[] key, final InputStream valueToDigest) throws IOException { + return updateHmac(getHmacSha384(key), valueToDigest).doFinal(); + } + + /** + * Returns a HmacSHA384 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA384 MAC for the given key and value + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha384(final String key, final String valueToDigest) { + return hmacSha384(StringUtils.getBytesUtf8(key), StringUtils.getBytesUtf8(valueToDigest)); + } + + /** + * Returns a HmacSHA384 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA384 MAC for the given key and value as hex string (lowercase) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha384Hex(final byte[] key, final byte[] valueToDigest) { + return Hex.encodeHexString(hmacSha384(key, valueToDigest)); + } + + /** + * Returns a HmacSHA384 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest + *

+ * The InputStream must not be null and will not be closed + *

+ * @return HmacSHA384 MAC for the given key and value as hex string (lowercase) + * @throws IOException + * If an I/O error occurs. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha384Hex(final byte[] key, final InputStream valueToDigest) throws IOException { + return Hex.encodeHexString(hmacSha384(key, valueToDigest)); + } + + /** + * Returns a HmacSHA384 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA384 MAC for the given key and value as hex string (lowercase) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha384Hex(final String key, final String valueToDigest) { + return Hex.encodeHexString(hmacSha384(key, valueToDigest)); + } + + // hmacSha512 + + /** + * Returns a HmacSHA512 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA512 MAC for the given key and value + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha512(final byte[] key, final byte[] valueToDigest) { + try { + return getHmacSha512(key).doFinal(valueToDigest); + } catch (final IllegalStateException e) { + // cannot happen + throw new IllegalArgumentException(e); + } + } + + /** + * Returns a HmacSHA512 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest + *

+ * The InputStream must not be null and will not be closed + *

+ * @return HmacSHA512 MAC for the given key and value + * @throws IOException + * If an I/O error occurs. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha512(final byte[] key, final InputStream valueToDigest) throws IOException { + return updateHmac(getHmacSha512(key), valueToDigest).doFinal(); + } + + /** + * Returns a HmacSHA512 Message Authentication Code (MAC) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA512 MAC for the given key and value + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static byte[] hmacSha512(final String key, final String valueToDigest) { + return hmacSha512(StringUtils.getBytesUtf8(key), StringUtils.getBytesUtf8(valueToDigest)); + } + + /** + * Returns a HmacSHA512 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA512 MAC for the given key and value as hex string (lowercase) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha512Hex(final byte[] key, final byte[] valueToDigest) { + return Hex.encodeHexString(hmacSha512(key, valueToDigest)); + } + + /** + * Returns a HmacSHA512 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest + *

+ * The InputStream must not be null and will not be closed + *

+ * @return HmacSHA512 MAC for the given key and value as hex string (lowercase) + * @throws IOException + * If an I/O error occurs. + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha512Hex(final byte[] key, final InputStream valueToDigest) throws IOException { + return Hex.encodeHexString(hmacSha512(key, valueToDigest)); + } + + /** + * Returns a HmacSHA512 Message Authentication Code (MAC) as hex string (lowercase) for the given key and value. + * + * @param key + * They key for the keyed digest (must not be null) + * @param valueToDigest + * The value (data) which should to digest (maybe empty or null) + * @return HmacSHA512 MAC for the given key and value as hex string (lowercase) + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught or key is null or key is invalid. + */ + public static String hmacSha512Hex(final String key, final String valueToDigest) { + return Hex.encodeHexString(hmacSha512(key, valueToDigest)); + } + + // update + + /** + * Updates the given {@link Mac}. This generates a digest for valueToDigest and the key the Mac was initialized + * + * @param mac + * the initialized {@link Mac} to update + * @param valueToDigest + * the value to update the {@link Mac} with (maybe null or empty) + * @return the updated {@link Mac} + * @throws IllegalStateException + * if the Mac was not initialized + * @since 1.x + */ + public static Mac updateHmac(final Mac mac, final byte[] valueToDigest) { + mac.reset(); + mac.update(valueToDigest); + return mac; + } + + /** + * Updates the given {@link Mac}. This generates a digest for valueToDigest and the key the Mac was initialized + * + * @param mac + * the initialized {@link Mac} to update + * @param valueToDigest + * the value to update the {@link Mac} with + *

+ * The InputStream must not be null and will not be closed + *

+ * @return the updated {@link Mac} + * @throws IOException + * If an I/O error occurs. + * @throws IllegalStateException + * If the Mac was not initialized + * @since 1.x + */ + public static Mac updateHmac(final Mac mac, final InputStream valueToDigest) throws IOException { + mac.reset(); + final byte[] buffer = new byte[STREAM_BUFFER_LENGTH]; + int read = valueToDigest.read(buffer, 0, STREAM_BUFFER_LENGTH); + + while (read > -1) { + mac.update(buffer, 0, read); + read = valueToDigest.read(buffer, 0, STREAM_BUFFER_LENGTH); + } + + return mac; + } + + /** + * Updates the given {@link Mac}. This generates a digest for valueToDigest and the key the Mac was initialized + * + * @param mac + * the initialized {@link Mac} to update + * @param valueToDigest + * the value to update the {@link Mac} with (maybe null or empty) + * @return the updated {@link Mac} + * @throws IllegalStateException + * if the Mac was not initialized + * @since 1.x + */ + public static Mac updateHmac(final Mac mac, final String valueToDigest) { + mac.reset(); + mac.update(StringUtils.getBytesUtf8(valueToDigest)); + return mac; + } +} diff --git a/src/org/apache/commons/codec/digest/Md5Crypt.java b/src/org/apache/commons/codec/digest/Md5Crypt.java new file mode 100644 index 00000000..09625496 --- /dev/null +++ b/src/org/apache/commons/codec/digest/Md5Crypt.java @@ -0,0 +1,302 @@ +/* + * 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 org.apache.commons.codec.digest; + +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.codec.Charsets; + +/** + * The libc crypt() "$1$" and Apache "$apr1$" MD5-based hash algorithm. + *

+ * Based on the public domain ("beer-ware") C implementation from Poul-Henning Kamp which was found at: + * crypt-md5.c @ freebsd.org
+ *

+ * Source: + * + *

+ * $FreeBSD: src/lib/libcrypt/crypt-md5.c,v 1.1 1999/01/21 13:50:09 brandon Exp $
+ * 
+ *

+ * Conversion to Kotlin and from there to Java in 2012. + *

+ * The C style comments are from the original C code, the ones with "//" from the port. + *

+ * This class is immutable and thread-safe. + * + * @version $Id$ + * @since 1.7 + */ +public class Md5Crypt { + + /** The Identifier of the Apache variant. */ + static final String APR1_PREFIX = "$apr1$"; + + /** The number of bytes of the final hash. */ + private static final int BLOCKSIZE = 16; + + /** The Identifier of this crypt() variant. */ + static final String MD5_PREFIX = "$1$"; + + /** The number of rounds of the big loop. */ + private static final int ROUNDS = 1000; + + /** + * See {@link #apr1Crypt(String, String)} for details. + * + * @param keyBytes + * plaintext string to hash. + * @return the hash value + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. * + */ + public static String apr1Crypt(final byte[] keyBytes) { + return apr1Crypt(keyBytes, APR1_PREFIX + B64.getRandomSalt(8)); + } + + /** + * See {@link #apr1Crypt(String, String)} for details. + * + * @param keyBytes + * plaintext string to hash. + * @param salt An APR1 salt. + * @return the hash value + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String apr1Crypt(final byte[] keyBytes, String salt) { + // to make the md5Crypt regex happy + if (salt != null && !salt.startsWith(APR1_PREFIX)) { + salt = APR1_PREFIX + salt; + } + return Md5Crypt.md5Crypt(keyBytes, salt, APR1_PREFIX); + } + + /** + * See {@link #apr1Crypt(String, String)} for details. + * + * @param keyBytes + * plaintext string to hash. + * @return the hash value + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String apr1Crypt(final String keyBytes) { + return apr1Crypt(keyBytes.getBytes(Charsets.UTF_8)); + } + + /** + * Generates an Apache htpasswd compatible "$apr1$" MD5 based hash value. + *

+ * The algorithm is identical to the crypt(3) "$1$" one but produces different outputs due to the different salt + * prefix. + * + * @param keyBytes + * plaintext string to hash. + * @param salt + * salt string including the prefix and optionally garbage at the end. Will be generated randomly if + * null. + * @return the hash value + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String apr1Crypt(final String keyBytes, final String salt) { + return apr1Crypt(keyBytes.getBytes(Charsets.UTF_8), salt); + } + + /** + * Generates a libc6 crypt() compatible "$1$" hash value. + *

+ * See {@link Crypt#crypt(String, String)} for details. + * + * @param keyBytes + * plaintext string to hash. + * @return the hash value + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String md5Crypt(final byte[] keyBytes) { + return md5Crypt(keyBytes, MD5_PREFIX + B64.getRandomSalt(8)); + } + + /** + * Generates a libc crypt() compatible "$1$" MD5 based hash value. + *

+ * See {@link Crypt#crypt(String, String)} for details. + * + * @param keyBytes + * plaintext string to hash. + * @param salt + * salt string including the prefix and optionally garbage at the end. Will be generated randomly if + * null. + * @return the hash value + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String md5Crypt(final byte[] keyBytes, final String salt) { + return md5Crypt(keyBytes, salt, MD5_PREFIX); + } + + /** + * Generates a libc6 crypt() "$1$" or Apache htpasswd "$apr1$" hash value. + *

+ * See {@link Crypt#crypt(String, String)} or {@link #apr1Crypt(String, String)} for details. + * + * @param keyBytes + * plaintext string to hash. + * @param salt May be null. + * @param prefix salt prefix + * @return the hash value + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String md5Crypt(final byte[] keyBytes, final String salt, final String prefix) { + final int keyLen = keyBytes.length; + + // Extract the real salt from the given string which can be a complete hash string. + String saltString; + if (salt == null) { + saltString = B64.getRandomSalt(8); + } else { + final Pattern p = Pattern.compile("^" + prefix.replace("$", "\\$") + "([\\.\\/a-zA-Z0-9]{1,8}).*"); + final Matcher m = p.matcher(salt); + if (m == null || !m.find()) { + throw new IllegalArgumentException("Invalid salt value: " + salt); + } + saltString = m.group(1); + } + final byte[] saltBytes = saltString.getBytes(Charsets.UTF_8); + + final MessageDigest ctx = DigestUtils.getMd5Digest(); + + /* + * The password first, since that is what is most unknown + */ + ctx.update(keyBytes); + + /* + * Then our magic string + */ + ctx.update(prefix.getBytes(Charsets.UTF_8)); + + /* + * Then the raw salt + */ + ctx.update(saltBytes); + + /* + * Then just as many characters of the MD5(pw,salt,pw) + */ + MessageDigest ctx1 = DigestUtils.getMd5Digest(); + ctx1.update(keyBytes); + ctx1.update(saltBytes); + ctx1.update(keyBytes); + byte[] finalb = ctx1.digest(); + int ii = keyLen; + while (ii > 0) { + ctx.update(finalb, 0, ii > 16 ? 16 : ii); + ii -= 16; + } + + /* + * Don't leave anything around in vm they could use. + */ + Arrays.fill(finalb, (byte) 0); + + /* + * Then something really weird... + */ + ii = keyLen; + final int j = 0; + while (ii > 0) { + if ((ii & 1) == 1) { + ctx.update(finalb[j]); + } else { + ctx.update(keyBytes[j]); + } + ii >>= 1; + } + + /* + * Now make the output string + */ + final StringBuilder passwd = new StringBuilder(prefix + saltString + "$"); + finalb = ctx.digest(); + + /* + * and now, just to make sure things don't run too fast On a 60 Mhz Pentium this takes 34 msec, so you would + * need 30 seconds to build a 1000 entry dictionary... + */ + for (int i = 0; i < ROUNDS; i++) { + ctx1 = DigestUtils.getMd5Digest(); + if ((i & 1) != 0) { + ctx1.update(keyBytes); + } else { + ctx1.update(finalb, 0, BLOCKSIZE); + } + + if (i % 3 != 0) { + ctx1.update(saltBytes); + } + + if (i % 7 != 0) { + ctx1.update(keyBytes); + } + + if ((i & 1) != 0) { + ctx1.update(finalb, 0, BLOCKSIZE); + } else { + ctx1.update(keyBytes); + } + finalb = ctx1.digest(); + } + + // The following was nearly identical to the Sha2Crypt code. + // Again, the buflen is not really needed. + // int buflen = MD5_PREFIX.length() - 1 + salt_string.length() + 1 + BLOCKSIZE + 1; + B64.b64from24bit(finalb[0], finalb[6], finalb[12], 4, passwd); + B64.b64from24bit(finalb[1], finalb[7], finalb[13], 4, passwd); + B64.b64from24bit(finalb[2], finalb[8], finalb[14], 4, passwd); + B64.b64from24bit(finalb[3], finalb[9], finalb[15], 4, passwd); + B64.b64from24bit(finalb[4], finalb[10], finalb[5], 4, passwd); + B64.b64from24bit((byte) 0, (byte) 0, finalb[11], 2, passwd); + + /* + * Don't leave anything around in vm they could use. + */ + // Is there a better way to do this with the JVM? + ctx.reset(); + ctx1.reset(); + Arrays.fill(keyBytes, (byte) 0); + Arrays.fill(saltBytes, (byte) 0); + Arrays.fill(finalb, (byte) 0); + + return passwd.toString(); + } +} diff --git a/src/org/apache/commons/codec/digest/MessageDigestAlgorithms.java b/src/org/apache/commons/codec/digest/MessageDigestAlgorithms.java new file mode 100644 index 00000000..981dfe82 --- /dev/null +++ b/src/org/apache/commons/codec/digest/MessageDigestAlgorithms.java @@ -0,0 +1,81 @@ +/* + * 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 org.apache.commons.codec.digest; + +import java.security.MessageDigest; + +/** + * Standard {@link MessageDigest} algorithm names from the Java Cryptography Architecture Standard Algorithm Name + * Documentation. + *

+ * This class is immutable and thread-safe. + *

+ * TODO 2.0 This should be an enum. + * + * @see Java Cryptography + * Architecture Standard Algorithm Name Documentation + * @since 1.7 + * @version $Id$ + */ +public class MessageDigestAlgorithms { + + private MessageDigestAlgorithms() { + // cannot be instantiated. + } + + /** + * The MD2 message digest algorithm defined in RFC 1319. + */ + public static final String MD2 = "MD2"; + + /** + * The MD5 message digest algorithm defined in RFC 1321. + */ + public static final String MD5 = "MD5"; + + /** + * The SHA-1 hash algorithm defined in the FIPS PUB 180-2. + */ + public static final String SHA_1 = "SHA-1"; + + /** + * The SHA-224 hash algorithm defined in the FIPS PUB 180-4. + *

+ * Java 8 only. + *

+ * + * @since 1.11 + */ + public static final String SHA_224 = "SHA-224"; + + /** + * The SHA-256 hash algorithm defined in the FIPS PUB 180-2. + */ + public static final String SHA_256 = "SHA-256"; + + /** + * The SHA-384 hash algorithm defined in the FIPS PUB 180-2. + */ + public static final String SHA_384 = "SHA-384"; + + /** + * The SHA-512 hash algorithm defined in the FIPS PUB 180-2. + */ + public static final String SHA_512 = "SHA-512"; + +} diff --git a/src/org/apache/commons/codec/digest/Sha2Crypt.java b/src/org/apache/commons/codec/digest/Sha2Crypt.java new file mode 100644 index 00000000..6e568d7e --- /dev/null +++ b/src/org/apache/commons/codec/digest/Sha2Crypt.java @@ -0,0 +1,545 @@ +/* + * 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 org.apache.commons.codec.digest; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.codec.Charsets; + +/** + * SHA2-based Unix crypt implementation. + *

+ * Based on the C implementation released into the Public Domain by Ulrich Drepper <drepper@redhat.com> + * http://www.akkadia.org/drepper/SHA-crypt.txt + *

+ * Conversion to Kotlin and from there to Java in 2012 by Christian Hammers <ch@lathspell.de> and likewise put + * into the Public Domain. + *

+ * This class is immutable and thread-safe. + * + * @version $Id$ + * @since 1.7 + */ +public class Sha2Crypt { + + /** Default number of rounds if not explicitly specified. */ + private static final int ROUNDS_DEFAULT = 5000; + + /** Maximum number of rounds. */ + private static final int ROUNDS_MAX = 999999999; + + /** Minimum number of rounds. */ + private static final int ROUNDS_MIN = 1000; + + /** Prefix for optional rounds specification. */ + private static final String ROUNDS_PREFIX = "rounds="; + + /** The number of bytes the final hash value will have (SHA-256 variant). */ + private static final int SHA256_BLOCKSIZE = 32; + + /** The prefixes that can be used to identify this crypt() variant (SHA-256). */ + static final String SHA256_PREFIX = "$5$"; + + /** The number of bytes the final hash value will have (SHA-512 variant). */ + private static final int SHA512_BLOCKSIZE = 64; + + /** The prefixes that can be used to identify this crypt() variant (SHA-512). */ + static final String SHA512_PREFIX = "$6$"; + + /** The pattern to match valid salt values. */ + private static final Pattern SALT_PATTERN = Pattern + .compile("^\\$([56])\\$(rounds=(\\d+)\\$)?([\\.\\/a-zA-Z0-9]{1,16}).*"); + + /** + * Generates a libc crypt() compatible "$5$" hash value with random salt. + *

+ * See {@link Crypt#crypt(String, String)} for details. + * + * @param keyBytes + * plaintext to hash + * @return complete hash value + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String sha256Crypt(final byte[] keyBytes) { + return sha256Crypt(keyBytes, null); + } + + /** + * Generates a libc6 crypt() compatible "$5$" hash value. + *

+ * See {@link Crypt#crypt(String, String)} for details. + * + * @param keyBytes + * plaintext to hash + * @param salt + * real salt value without prefix or "rounds=" + * @return complete hash value including salt + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String sha256Crypt(final byte[] keyBytes, String salt) { + if (salt == null) { + salt = SHA256_PREFIX + B64.getRandomSalt(8); + } + return sha2Crypt(keyBytes, salt, SHA256_PREFIX, SHA256_BLOCKSIZE, MessageDigestAlgorithms.SHA_256); + } + + /** + * Generates a libc6 crypt() compatible "$5$" or "$6$" SHA2 based hash value. + *

+ * This is a nearly line by line conversion of the original C function. The numbered comments are from the algorithm + * description, the short C-style ones from the original C code and the ones with "Remark" from me. + *

+ * See {@link Crypt#crypt(String, String)} for details. + * + * @param keyBytes + * plaintext to hash + * @param salt + * real salt value without prefix or "rounds=" + * @param saltPrefix + * either $5$ or $6$ + * @param blocksize + * a value that differs between $5$ and $6$ + * @param algorithm + * {@link MessageDigest} algorithm identifier string + * @return complete hash value including prefix and salt + * @throws IllegalArgumentException + * if the given salt is null or does not match the allowed pattern + * @throws IllegalArgumentException + * when a {@link NoSuchAlgorithmException} is caught + * @see MessageDigestAlgorithms + */ + private static String sha2Crypt(final byte[] keyBytes, final String salt, final String saltPrefix, + final int blocksize, final String algorithm) { + + final int keyLen = keyBytes.length; + + // Extracts effective salt and the number of rounds from the given salt. + int rounds = ROUNDS_DEFAULT; + boolean roundsCustom = false; + if (salt == null) { + throw new IllegalArgumentException("Salt must not be null"); + } + + final Matcher m = SALT_PATTERN.matcher(salt); + if (m == null || !m.find()) { + throw new IllegalArgumentException("Invalid salt value: " + salt); + } + if (m.group(3) != null) { + rounds = Integer.parseInt(m.group(3)); + rounds = Math.max(ROUNDS_MIN, Math.min(ROUNDS_MAX, rounds)); + roundsCustom = true; + } + final String saltString = m.group(4); + final byte[] saltBytes = saltString.getBytes(Charsets.UTF_8); + final int saltLen = saltBytes.length; + + // 1. start digest A + // Prepare for the real work. + MessageDigest ctx = DigestUtils.getDigest(algorithm); + + // 2. the password string is added to digest A + /* + * Add the key string. + */ + ctx.update(keyBytes); + + // 3. the salt string is added to digest A. This is just the salt string + // itself without the enclosing '$', without the magic salt_prefix $5$ and + // $6$ respectively and without the rounds= specification. + // + // NB: the MD5 algorithm did add the $1$ salt_prefix. This is not deemed + // necessary since it is a constant string and does not add security + // and /possibly/ allows a plain text attack. Since the rounds= + // specification should never be added this would also create an + // inconsistency. + /* + * The last part is the salt string. This must be at most 16 characters and it ends at the first `$' character + * (for compatibility with existing implementations). + */ + ctx.update(saltBytes); + + // 4. start digest B + /* + * Compute alternate sha512 sum with input KEY, SALT, and KEY. The final result will be added to the first + * context. + */ + MessageDigest altCtx = DigestUtils.getDigest(algorithm); + + // 5. add the password to digest B + /* + * Add key. + */ + altCtx.update(keyBytes); + + // 6. add the salt string to digest B + /* + * Add salt. + */ + altCtx.update(saltBytes); + + // 7. add the password again to digest B + /* + * Add key again. + */ + altCtx.update(keyBytes); + + // 8. finish digest B + /* + * Now get result of this (32 bytes) and add it to the other context. + */ + byte[] altResult = altCtx.digest(); + + // 9. For each block of 32 or 64 bytes in the password string (excluding + // the terminating NUL in the C representation), add digest B to digest A + /* + * Add for any character in the key one byte of the alternate sum. + */ + /* + * (Remark: the C code comment seems wrong for key length > 32!) + */ + int cnt = keyBytes.length; + while (cnt > blocksize) { + ctx.update(altResult, 0, blocksize); + cnt -= blocksize; + } + + // 10. For the remaining N bytes of the password string add the first + // N bytes of digest B to digest A + ctx.update(altResult, 0, cnt); + + // 11. For each bit of the binary representation of the length of the + // password string up to and including the highest 1-digit, starting + // from to lowest bit position (numeric value 1): + // + // a) for a 1-digit add digest B to digest A + // + // b) for a 0-digit add the password string + // + // NB: this step differs significantly from the MD5 algorithm. It + // adds more randomness. + /* + * Take the binary representation of the length of the key and for every 1 add the alternate sum, for every 0 + * the key. + */ + cnt = keyBytes.length; + while (cnt > 0) { + if ((cnt & 1) != 0) { + ctx.update(altResult, 0, blocksize); + } else { + ctx.update(keyBytes); + } + cnt >>= 1; + } + + // 12. finish digest A + /* + * Create intermediate result. + */ + altResult = ctx.digest(); + + // 13. start digest DP + /* + * Start computation of P byte sequence. + */ + altCtx = DigestUtils.getDigest(algorithm); + + // 14. for every byte in the password (excluding the terminating NUL byte + // in the C representation of the string) + // + // add the password to digest DP + /* + * For every character in the password add the entire password. + */ + for (int i = 1; i <= keyLen; i++) { + altCtx.update(keyBytes); + } + + // 15. finish digest DP + /* + * Finish the digest. + */ + byte[] tempResult = altCtx.digest(); + + // 16. produce byte sequence P of the same length as the password where + // + // a) for each block of 32 or 64 bytes of length of the password string + // the entire digest DP is used + // + // b) for the remaining N (up to 31 or 63) bytes use the first N + // bytes of digest DP + /* + * Create byte sequence P. + */ + final byte[] pBytes = new byte[keyLen]; + int cp = 0; + while (cp < keyLen - blocksize) { + System.arraycopy(tempResult, 0, pBytes, cp, blocksize); + cp += blocksize; + } + System.arraycopy(tempResult, 0, pBytes, cp, keyLen - cp); + + // 17. start digest DS + /* + * Start computation of S byte sequence. + */ + altCtx = DigestUtils.getDigest(algorithm); + + // 18. repeast the following 16+A[0] times, where A[0] represents the first + // byte in digest A interpreted as an 8-bit unsigned value + // + // add the salt to digest DS + /* + * For every character in the password add the entire password. + */ + for (int i = 1; i <= 16 + (altResult[0] & 0xff); i++) { + altCtx.update(saltBytes); + } + + // 19. finish digest DS + /* + * Finish the digest. + */ + tempResult = altCtx.digest(); + + // 20. produce byte sequence S of the same length as the salt string where + // + // a) for each block of 32 or 64 bytes of length of the salt string + // the entire digest DS is used + // + // b) for the remaining N (up to 31 or 63) bytes use the first N + // bytes of digest DS + /* + * Create byte sequence S. + */ + // Remark: The salt is limited to 16 chars, how does this make sense? + final byte[] sBytes = new byte[saltLen]; + cp = 0; + while (cp < saltLen - blocksize) { + System.arraycopy(tempResult, 0, sBytes, cp, blocksize); + cp += blocksize; + } + System.arraycopy(tempResult, 0, sBytes, cp, saltLen - cp); + + // 21. repeat a loop according to the number specified in the rounds= + // specification in the salt (or the default value if none is + // present). Each round is numbered, starting with 0 and up to N-1. + // + // The loop uses a digest as input. In the first round it is the + // digest produced in step 12. In the latter steps it is the digest + // produced in step 21.h. The following text uses the notation + // "digest A/C" to describe this behavior. + /* + * Repeatedly run the collected hash value through sha512 to burn CPU cycles. + */ + for (int i = 0; i <= rounds - 1; i++) { + // a) start digest C + /* + * New context. + */ + ctx = DigestUtils.getDigest(algorithm); + + // b) for odd round numbers add the byte sequense P to digest C + // c) for even round numbers add digest A/C + /* + * Add key or last result. + */ + if ((i & 1) != 0) { + ctx.update(pBytes, 0, keyLen); + } else { + ctx.update(altResult, 0, blocksize); + } + + // d) for all round numbers not divisible by 3 add the byte sequence S + /* + * Add salt for numbers not divisible by 3. + */ + if (i % 3 != 0) { + ctx.update(sBytes, 0, saltLen); + } + + // e) for all round numbers not divisible by 7 add the byte sequence P + /* + * Add key for numbers not divisible by 7. + */ + if (i % 7 != 0) { + ctx.update(pBytes, 0, keyLen); + } + + // f) for odd round numbers add digest A/C + // g) for even round numbers add the byte sequence P + /* + * Add key or last result. + */ + if ((i & 1) != 0) { + ctx.update(altResult, 0, blocksize); + } else { + ctx.update(pBytes, 0, keyLen); + } + + // h) finish digest C. + /* + * Create intermediate result. + */ + altResult = ctx.digest(); + } + + // 22. Produce the output string. This is an ASCII string of the maximum + // size specified above, consisting of multiple pieces: + // + // a) the salt salt_prefix, $5$ or $6$ respectively + // + // b) the rounds= specification, if one was present in the input + // salt string. A trailing '$' is added in this case to separate + // the rounds specification from the following text. + // + // c) the salt string truncated to 16 characters + // + // d) a '$' character + /* + * Now we can construct the result string. It consists of three parts. + */ + final StringBuilder buffer = new StringBuilder(saltPrefix); + if (roundsCustom) { + buffer.append(ROUNDS_PREFIX); + buffer.append(rounds); + buffer.append("$"); + } + buffer.append(saltString); + buffer.append("$"); + + // e) the base-64 encoded final C digest. The encoding used is as + // follows: + // [...] + // + // Each group of three bytes from the digest produces four + // characters as output: + // + // 1. character: the six low bits of the first byte + // 2. character: the two high bits of the first byte and the + // four low bytes from the second byte + // 3. character: the four high bytes from the second byte and + // the two low bits from the third byte + // 4. character: the six high bits from the third byte + // + // The groups of three bytes are as follows (in this sequence). + // These are the indices into the byte array containing the + // digest, starting with index 0. For the last group there are + // not enough bytes left in the digest and the value zero is used + // in its place. This group also produces only three or two + // characters as output for SHA-512 and SHA-512 respectively. + + // This was just a safeguard in the C implementation: + // int buflen = salt_prefix.length() - 1 + ROUNDS_PREFIX.length() + 9 + 1 + salt_string.length() + 1 + 86 + 1; + + if (blocksize == 32) { + B64.b64from24bit(altResult[0], altResult[10], altResult[20], 4, buffer); + B64.b64from24bit(altResult[21], altResult[1], altResult[11], 4, buffer); + B64.b64from24bit(altResult[12], altResult[22], altResult[2], 4, buffer); + B64.b64from24bit(altResult[3], altResult[13], altResult[23], 4, buffer); + B64.b64from24bit(altResult[24], altResult[4], altResult[14], 4, buffer); + B64.b64from24bit(altResult[15], altResult[25], altResult[5], 4, buffer); + B64.b64from24bit(altResult[6], altResult[16], altResult[26], 4, buffer); + B64.b64from24bit(altResult[27], altResult[7], altResult[17], 4, buffer); + B64.b64from24bit(altResult[18], altResult[28], altResult[8], 4, buffer); + B64.b64from24bit(altResult[9], altResult[19], altResult[29], 4, buffer); + B64.b64from24bit((byte) 0, altResult[31], altResult[30], 3, buffer); + } else { + B64.b64from24bit(altResult[0], altResult[21], altResult[42], 4, buffer); + B64.b64from24bit(altResult[22], altResult[43], altResult[1], 4, buffer); + B64.b64from24bit(altResult[44], altResult[2], altResult[23], 4, buffer); + B64.b64from24bit(altResult[3], altResult[24], altResult[45], 4, buffer); + B64.b64from24bit(altResult[25], altResult[46], altResult[4], 4, buffer); + B64.b64from24bit(altResult[47], altResult[5], altResult[26], 4, buffer); + B64.b64from24bit(altResult[6], altResult[27], altResult[48], 4, buffer); + B64.b64from24bit(altResult[28], altResult[49], altResult[7], 4, buffer); + B64.b64from24bit(altResult[50], altResult[8], altResult[29], 4, buffer); + B64.b64from24bit(altResult[9], altResult[30], altResult[51], 4, buffer); + B64.b64from24bit(altResult[31], altResult[52], altResult[10], 4, buffer); + B64.b64from24bit(altResult[53], altResult[11], altResult[32], 4, buffer); + B64.b64from24bit(altResult[12], altResult[33], altResult[54], 4, buffer); + B64.b64from24bit(altResult[34], altResult[55], altResult[13], 4, buffer); + B64.b64from24bit(altResult[56], altResult[14], altResult[35], 4, buffer); + B64.b64from24bit(altResult[15], altResult[36], altResult[57], 4, buffer); + B64.b64from24bit(altResult[37], altResult[58], altResult[16], 4, buffer); + B64.b64from24bit(altResult[59], altResult[17], altResult[38], 4, buffer); + B64.b64from24bit(altResult[18], altResult[39], altResult[60], 4, buffer); + B64.b64from24bit(altResult[40], altResult[61], altResult[19], 4, buffer); + B64.b64from24bit(altResult[62], altResult[20], altResult[41], 4, buffer); + B64.b64from24bit((byte) 0, (byte) 0, altResult[63], 2, buffer); + } + + /* + * Clear the buffer for the intermediate result so that people attaching to processes or reading core dumps + * cannot get any information. + */ + // Is there a better way to do this with the JVM? + Arrays.fill(tempResult, (byte) 0); + Arrays.fill(pBytes, (byte) 0); + Arrays.fill(sBytes, (byte) 0); + ctx.reset(); + altCtx.reset(); + Arrays.fill(keyBytes, (byte) 0); + Arrays.fill(saltBytes, (byte) 0); + + return buffer.toString(); + } + + /** + * Generates a libc crypt() compatible "$6$" hash value with random salt. + *

+ * See {@link Crypt#crypt(String, String)} for details. + * + * @param keyBytes + * plaintext to hash + * @return complete hash value + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String sha512Crypt(final byte[] keyBytes) { + return sha512Crypt(keyBytes, null); + } + + /** + * Generates a libc6 crypt() compatible "$6$" hash value. + *

+ * See {@link Crypt#crypt(String, String)} for details. + * + * @param keyBytes + * plaintext to hash + * @param salt + * real salt value without prefix or "rounds=" + * @return complete hash value including salt + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + * @throws RuntimeException + * when a {@link java.security.NoSuchAlgorithmException} is caught. + */ + public static String sha512Crypt(final byte[] keyBytes, String salt) { + if (salt == null) { + salt = SHA512_PREFIX + B64.getRandomSalt(8); + } + return sha2Crypt(keyBytes, salt, SHA512_PREFIX, SHA512_BLOCKSIZE, MessageDigestAlgorithms.SHA_512); + } +} diff --git a/src/org/apache/commons/codec/digest/UnixCrypt.java b/src/org/apache/commons/codec/digest/UnixCrypt.java new file mode 100644 index 00000000..151d9c89 --- /dev/null +++ b/src/org/apache/commons/codec/digest/UnixCrypt.java @@ -0,0 +1,413 @@ +/* + * 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 org.apache.commons.codec.digest; + +import java.util.Random; + +import org.apache.commons.codec.Charsets; + +/** + * Unix crypt(3) algorithm implementation. + *

+ * This class only implements the traditional 56 bit DES based algorithm. Please use DigestUtils.crypt() for a method + * that distinguishes between all the algorithms supported in the current glibc's crypt(). + *

+ * The Java implementation was taken from the JetSpeed Portal project (see + * org.apache.jetspeed.services.security.ldap.UnixCrypt). + *

+ * This class is slightly incompatible if the given salt contains characters that are not part of the allowed range + * [a-zA-Z0-9./]. + *

+ * This class is immutable and thread-safe. + * + * @version $Id$ + * @since 1.7 + */ +public class UnixCrypt { + + private static final int CON_SALT[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, + 34, 35, 36, 37, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, + 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 0, 0, 0, 0, 0 }; + + private static final int COV2CHAR[] = { 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, + 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, + 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122 }; + + private static final char SALT_CHARS[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./" + .toCharArray(); + + private static final boolean SHIFT2[] = { false, false, true, true, true, true, true, true, false, true, true, + true, true, true, true, false }; + + private static final int SKB[][] = { + { 0, 16, 0x20000000, 0x20000010, 0x10000, 0x10010, 0x20010000, 0x20010010, 2048, 2064, 0x20000800, + 0x20000810, 0x10800, 0x10810, 0x20010800, 0x20010810, 32, 48, 0x20000020, 0x20000030, 0x10020, + 0x10030, 0x20010020, 0x20010030, 2080, 2096, 0x20000820, 0x20000830, 0x10820, 0x10830, 0x20010820, + 0x20010830, 0x80000, 0x80010, 0x20080000, 0x20080010, 0x90000, 0x90010, 0x20090000, 0x20090010, + 0x80800, 0x80810, 0x20080800, 0x20080810, 0x90800, 0x90810, 0x20090800, 0x20090810, 0x80020, + 0x80030, 0x20080020, 0x20080030, 0x90020, 0x90030, 0x20090020, 0x20090030, 0x80820, 0x80830, + 0x20080820, 0x20080830, 0x90820, 0x90830, 0x20090820, 0x20090830 }, + { 0, 0x2000000, 8192, 0x2002000, 0x200000, 0x2200000, 0x202000, 0x2202000, 4, 0x2000004, 8196, 0x2002004, + 0x200004, 0x2200004, 0x202004, 0x2202004, 1024, 0x2000400, 9216, 0x2002400, 0x200400, 0x2200400, + 0x202400, 0x2202400, 1028, 0x2000404, 9220, 0x2002404, 0x200404, 0x2200404, 0x202404, 0x2202404, + 0x10000000, 0x12000000, 0x10002000, 0x12002000, 0x10200000, 0x12200000, 0x10202000, 0x12202000, + 0x10000004, 0x12000004, 0x10002004, 0x12002004, 0x10200004, 0x12200004, 0x10202004, 0x12202004, + 0x10000400, 0x12000400, 0x10002400, 0x12002400, 0x10200400, 0x12200400, 0x10202400, 0x12202400, + 0x10000404, 0x12000404, 0x10002404, 0x12002404, 0x10200404, 0x12200404, 0x10202404, 0x12202404 }, + { 0, 1, 0x40000, 0x40001, 0x1000000, 0x1000001, 0x1040000, 0x1040001, 2, 3, 0x40002, 0x40003, 0x1000002, + 0x1000003, 0x1040002, 0x1040003, 512, 513, 0x40200, 0x40201, 0x1000200, 0x1000201, 0x1040200, + 0x1040201, 514, 515, 0x40202, 0x40203, 0x1000202, 0x1000203, 0x1040202, 0x1040203, 0x8000000, + 0x8000001, 0x8040000, 0x8040001, 0x9000000, 0x9000001, 0x9040000, 0x9040001, 0x8000002, 0x8000003, + 0x8040002, 0x8040003, 0x9000002, 0x9000003, 0x9040002, 0x9040003, 0x8000200, 0x8000201, 0x8040200, + 0x8040201, 0x9000200, 0x9000201, 0x9040200, 0x9040201, 0x8000202, 0x8000203, 0x8040202, 0x8040203, + 0x9000202, 0x9000203, 0x9040202, 0x9040203 }, + { 0, 0x100000, 256, 0x100100, 8, 0x100008, 264, 0x100108, 4096, 0x101000, 4352, 0x101100, 4104, 0x101008, + 4360, 0x101108, 0x4000000, 0x4100000, 0x4000100, 0x4100100, 0x4000008, 0x4100008, 0x4000108, + 0x4100108, 0x4001000, 0x4101000, 0x4001100, 0x4101100, 0x4001008, 0x4101008, 0x4001108, 0x4101108, + 0x20000, 0x120000, 0x20100, 0x120100, 0x20008, 0x120008, 0x20108, 0x120108, 0x21000, 0x121000, + 0x21100, 0x121100, 0x21008, 0x121008, 0x21108, 0x121108, 0x4020000, 0x4120000, 0x4020100, + 0x4120100, 0x4020008, 0x4120008, 0x4020108, 0x4120108, 0x4021000, 0x4121000, 0x4021100, 0x4121100, + 0x4021008, 0x4121008, 0x4021108, 0x4121108 }, + { 0, 0x10000000, 0x10000, 0x10010000, 4, 0x10000004, 0x10004, 0x10010004, 0x20000000, 0x30000000, + 0x20010000, 0x30010000, 0x20000004, 0x30000004, 0x20010004, 0x30010004, 0x100000, 0x10100000, + 0x110000, 0x10110000, 0x100004, 0x10100004, 0x110004, 0x10110004, 0x20100000, 0x30100000, + 0x20110000, 0x30110000, 0x20100004, 0x30100004, 0x20110004, 0x30110004, 4096, 0x10001000, 0x11000, + 0x10011000, 4100, 0x10001004, 0x11004, 0x10011004, 0x20001000, 0x30001000, 0x20011000, 0x30011000, + 0x20001004, 0x30001004, 0x20011004, 0x30011004, 0x101000, 0x10101000, 0x111000, 0x10111000, + 0x101004, 0x10101004, 0x111004, 0x10111004, 0x20101000, 0x30101000, 0x20111000, 0x30111000, + 0x20101004, 0x30101004, 0x20111004, 0x30111004 }, + { 0, 0x8000000, 8, 0x8000008, 1024, 0x8000400, 1032, 0x8000408, 0x20000, 0x8020000, 0x20008, 0x8020008, + 0x20400, 0x8020400, 0x20408, 0x8020408, 1, 0x8000001, 9, 0x8000009, 1025, 0x8000401, 1033, + 0x8000409, 0x20001, 0x8020001, 0x20009, 0x8020009, 0x20401, 0x8020401, 0x20409, 0x8020409, + 0x2000000, 0xa000000, 0x2000008, 0xa000008, 0x2000400, 0xa000400, 0x2000408, 0xa000408, 0x2020000, + 0xa020000, 0x2020008, 0xa020008, 0x2020400, 0xa020400, 0x2020408, 0xa020408, 0x2000001, 0xa000001, + 0x2000009, 0xa000009, 0x2000401, 0xa000401, 0x2000409, 0xa000409, 0x2020001, 0xa020001, 0x2020009, + 0xa020009, 0x2020401, 0xa020401, 0x2020409, 0xa020409 }, + { 0, 256, 0x80000, 0x80100, 0x1000000, 0x1000100, 0x1080000, 0x1080100, 16, 272, 0x80010, 0x80110, + 0x1000010, 0x1000110, 0x1080010, 0x1080110, 0x200000, 0x200100, 0x280000, 0x280100, 0x1200000, + 0x1200100, 0x1280000, 0x1280100, 0x200010, 0x200110, 0x280010, 0x280110, 0x1200010, 0x1200110, + 0x1280010, 0x1280110, 512, 768, 0x80200, 0x80300, 0x1000200, 0x1000300, 0x1080200, 0x1080300, 528, + 784, 0x80210, 0x80310, 0x1000210, 0x1000310, 0x1080210, 0x1080310, 0x200200, 0x200300, 0x280200, + 0x280300, 0x1200200, 0x1200300, 0x1280200, 0x1280300, 0x200210, 0x200310, 0x280210, 0x280310, + 0x1200210, 0x1200310, 0x1280210, 0x1280310 }, + { 0, 0x4000000, 0x40000, 0x4040000, 2, 0x4000002, 0x40002, 0x4040002, 8192, 0x4002000, 0x42000, 0x4042000, + 8194, 0x4002002, 0x42002, 0x4042002, 32, 0x4000020, 0x40020, 0x4040020, 34, 0x4000022, 0x40022, + 0x4040022, 8224, 0x4002020, 0x42020, 0x4042020, 8226, 0x4002022, 0x42022, 0x4042022, 2048, + 0x4000800, 0x40800, 0x4040800, 2050, 0x4000802, 0x40802, 0x4040802, 10240, 0x4002800, 0x42800, + 0x4042800, 10242, 0x4002802, 0x42802, 0x4042802, 2080, 0x4000820, 0x40820, 0x4040820, 2082, + 0x4000822, 0x40822, 0x4040822, 10272, 0x4002820, 0x42820, 0x4042820, 10274, 0x4002822, 0x42822, + 0x4042822 } }; + + private static final int SPTRANS[][] = { + { 0x820200, 0x20000, 0x80800000, 0x80820200, 0x800000, 0x80020200, 0x80020000, 0x80800000, 0x80020200, + 0x820200, 0x820000, 0x80000200, 0x80800200, 0x800000, 0, 0x80020000, 0x20000, 0x80000000, + 0x800200, 0x20200, 0x80820200, 0x820000, 0x80000200, 0x800200, 0x80000000, 512, 0x20200, + 0x80820000, 512, 0x80800200, 0x80820000, 0, 0, 0x80820200, 0x800200, 0x80020000, 0x820200, + 0x20000, 0x80000200, 0x800200, 0x80820000, 512, 0x20200, 0x80800000, 0x80020200, 0x80000000, + 0x80800000, 0x820000, 0x80820200, 0x20200, 0x820000, 0x80800200, 0x800000, 0x80000200, 0x80020000, + 0, 0x20000, 0x800000, 0x80800200, 0x820200, 0x80000000, 0x80820000, 512, 0x80020200 }, + { 0x10042004, 0, 0x42000, 0x10040000, 0x10000004, 8196, 0x10002000, 0x42000, 8192, 0x10040004, 4, + 0x10002000, 0x40004, 0x10042000, 0x10040000, 4, 0x40000, 0x10002004, 0x10040004, 8192, 0x42004, + 0x10000000, 0, 0x40004, 0x10002004, 0x42004, 0x10042000, 0x10000004, 0x10000000, 0x40000, 8196, + 0x10042004, 0x40004, 0x10042000, 0x10002000, 0x42004, 0x10042004, 0x40004, 0x10000004, 0, + 0x10000000, 8196, 0x40000, 0x10040004, 8192, 0x10000000, 0x42004, 0x10002004, 0x10042000, 8192, 0, + 0x10000004, 4, 0x10042004, 0x42000, 0x10040000, 0x10040004, 0x40000, 8196, 0x10002000, 0x10002004, + 4, 0x10040000, 0x42000 }, + { 0x41000000, 0x1010040, 64, 0x41000040, 0x40010000, 0x1000000, 0x41000040, 0x10040, 0x1000040, 0x10000, + 0x1010000, 0x40000000, 0x41010040, 0x40000040, 0x40000000, 0x41010000, 0, 0x40010000, 0x1010040, + 64, 0x40000040, 0x41010040, 0x10000, 0x41000000, 0x41010000, 0x1000040, 0x40010040, 0x1010000, + 0x10040, 0, 0x1000000, 0x40010040, 0x1010040, 64, 0x40000000, 0x10000, 0x40000040, 0x40010000, + 0x1010000, 0x41000040, 0, 0x1010040, 0x10040, 0x41010000, 0x40010000, 0x1000000, 0x41010040, + 0x40000000, 0x40010040, 0x41000000, 0x1000000, 0x41010040, 0x10000, 0x1000040, 0x41000040, + 0x10040, 0x1000040, 0, 0x41010000, 0x40000040, 0x41000000, 0x40010040, 64, 0x1010000 }, + { 0x100402, 0x4000400, 2, 0x4100402, 0, 0x4100000, 0x4000402, 0x100002, 0x4100400, 0x4000002, 0x4000000, + 1026, 0x4000002, 0x100402, 0x100000, 0x4000000, 0x4100002, 0x100400, 1024, 2, 0x100400, 0x4000402, + 0x4100000, 1024, 1026, 0, 0x100002, 0x4100400, 0x4000400, 0x4100002, 0x4100402, 0x100000, + 0x4100002, 1026, 0x100000, 0x4000002, 0x100400, 0x4000400, 2, 0x4100000, 0x4000402, 0, 1024, + 0x100002, 0, 0x4100002, 0x4100400, 1024, 0x4000000, 0x4100402, 0x100402, 0x100000, 0x4100402, 2, + 0x4000400, 0x100402, 0x100002, 0x100400, 0x4100000, 0x4000402, 1026, 0x4000000, 0x4000002, + 0x4100400 }, + { 0x2000000, 16384, 256, 0x2004108, 0x2004008, 0x2000100, 16648, 0x2004000, 16384, 8, 0x2000008, 16640, + 0x2000108, 0x2004008, 0x2004100, 0, 16640, 0x2000000, 16392, 264, 0x2000100, 16648, 0, 0x2000008, + 8, 0x2000108, 0x2004108, 16392, 0x2004000, 256, 264, 0x2004100, 0x2004100, 0x2000108, 16392, + 0x2004000, 16384, 8, 0x2000008, 0x2000100, 0x2000000, 16640, 0x2004108, 0, 16648, 0x2000000, 256, + 16392, 0x2000108, 256, 0, 0x2004108, 0x2004008, 0x2004100, 264, 16384, 16640, 0x2004008, + 0x2000100, 264, 8, 16648, 0x2004000, 0x2000008 }, + { 0x20000010, 0x80010, 0, 0x20080800, 0x80010, 2048, 0x20000810, 0x80000, 2064, 0x20080810, 0x80800, + 0x20000000, 0x20000800, 0x20000010, 0x20080000, 0x80810, 0x80000, 0x20000810, 0x20080010, 0, 2048, + 16, 0x20080800, 0x20080010, 0x20080810, 0x20080000, 0x20000000, 2064, 16, 0x80800, 0x80810, + 0x20000800, 2064, 0x20000000, 0x20000800, 0x80810, 0x20080800, 0x80010, 0, 0x20000800, 0x20000000, + 2048, 0x20080010, 0x80000, 0x80010, 0x20080810, 0x80800, 16, 0x20080810, 0x80800, 0x80000, + 0x20000810, 0x20000010, 0x20080000, 0x80810, 0, 2048, 0x20000010, 0x20000810, 0x20080800, + 0x20080000, 2064, 16, 0x20080010 }, + { 4096, 128, 0x400080, 0x400001, 0x401081, 4097, 4224, 0, 0x400000, 0x400081, 129, 0x401000, 1, 0x401080, + 0x401000, 129, 0x400081, 4096, 4097, 0x401081, 0, 0x400080, 0x400001, 4224, 0x401001, 4225, + 0x401080, 1, 4225, 0x401001, 128, 0x400000, 4225, 0x401000, 0x401001, 129, 4096, 128, 0x400000, + 0x401001, 0x400081, 4225, 4224, 0, 128, 0x400001, 1, 0x400080, 0, 0x400081, 0x400080, 4224, 129, + 4096, 0x401081, 0x400000, 0x401080, 1, 4097, 0x401081, 0x400001, 0x401080, 0x401000, 4097 }, + { 0x8200020, 0x8208000, 32800, 0, 0x8008000, 0x200020, 0x8200000, 0x8208020, 32, 0x8000000, 0x208000, + 32800, 0x208020, 0x8008020, 0x8000020, 0x8200000, 32768, 0x208020, 0x200020, 0x8008000, 0x8208020, + 0x8000020, 0, 0x208000, 0x8000000, 0x200000, 0x8008020, 0x8200020, 0x200000, 32768, 0x8208000, 32, + 0x200000, 32768, 0x8000020, 0x8208020, 32800, 0x8000000, 0, 0x208000, 0x8200020, 0x8008020, + 0x8008000, 0x200020, 0x8208000, 32, 0x200020, 0x8008000, 0x8208020, 0x200000, 0x8200000, + 0x8000020, 0x208000, 32800, 0x8008020, 0x8200000, 32, 0x8208000, 0x208020, 0, 0x8000000, + 0x8200020, 32768, 0x208020 } }; + + /** + * Generates a crypt(3) compatible hash using the DES algorithm. + *

+ * As no salt is given, a random one will be used. + * + * @param original + * plaintext password + * @return a 13 character string starting with the salt string + */ + public static String crypt(final byte[] original) { + return crypt(original, null); + } + + /** + * Generates a crypt(3) compatible hash using the DES algorithm. + *

+ * Using unspecified characters as salt results incompatible hash values. + * + * @param original + * plaintext password + * @param salt + * a two character string drawn from [a-zA-Z0-9./] or null for a random one + * @return a 13 character string starting with the salt string + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + */ + public static String crypt(final byte[] original, String salt) { + if (salt == null) { + final Random randomGenerator = new Random(); + final int numSaltChars = SALT_CHARS.length; + salt = "" + SALT_CHARS[randomGenerator.nextInt(numSaltChars)] + + SALT_CHARS[randomGenerator.nextInt(numSaltChars)]; + } else if (!salt.matches("^[" + B64.B64T + "]{2,}$")) { + throw new IllegalArgumentException("Invalid salt value: " + salt); + } + + final StringBuilder buffer = new StringBuilder(" "); + final char charZero = salt.charAt(0); + final char charOne = salt.charAt(1); + buffer.setCharAt(0, charZero); + buffer.setCharAt(1, charOne); + final int eSwap0 = CON_SALT[charZero]; + final int eSwap1 = CON_SALT[charOne] << 4; + final byte key[] = new byte[8]; + for (int i = 0; i < key.length; i++) { + key[i] = 0; + } + + for (int i = 0; i < key.length && i < original.length; i++) { + final int iChar = original[i]; + key[i] = (byte) (iChar << 1); + } + + final int schedule[] = desSetKey(key); + final int out[] = body(schedule, eSwap0, eSwap1); + final byte b[] = new byte[9]; + intToFourBytes(out[0], b, 0); + intToFourBytes(out[1], b, 4); + b[8] = 0; + int i = 2; + int y = 0; + int u = 128; + for (; i < 13; i++) { + int j = 0; + int c = 0; + for (; j < 6; j++) { + c <<= 1; + if ((b[y] & u) != 0) { + c |= 0x1; + } + u >>>= 1; + if (u == 0) { + y++; + u = 128; + } + buffer.setCharAt(i, (char) COV2CHAR[c]); + } + } + return buffer.toString(); + } + + /** + * Generates a crypt(3) compatible hash using the DES algorithm. + *

+ * As no salt is given, a random one is used. + * + * @param original + * plaintext password + * @return a 13 character string starting with the salt string + */ + public static String crypt(final String original) { + return crypt(original.getBytes(Charsets.UTF_8)); + } + + /** + * Generates a crypt(3) compatible hash using the DES algorithm. + * + * @param original + * plaintext password + * @param salt + * a two character string drawn from [a-zA-Z0-9./] or null for a random one + * @return a 13 character string starting with the salt string + * @throws IllegalArgumentException + * if the salt does not match the allowed pattern + */ + public static String crypt(final String original, final String salt) { + return crypt(original.getBytes(Charsets.UTF_8), salt); + } + + private static int[] body(final int schedule[], final int eSwap0, final int eSwap1) { + int left = 0; + int right = 0; + int t = 0; + for (int j = 0; j < 25; j++) { + for (int i = 0; i < 32; i += 4) { + left = dEncrypt(left, right, i, eSwap0, eSwap1, schedule); + right = dEncrypt(right, left, i + 2, eSwap0, eSwap1, schedule); + } + t = left; + left = right; + right = t; + } + + t = right; + right = left >>> 1 | left << 31; + left = t >>> 1 | t << 31; + final int results[] = new int[2]; + permOp(right, left, 1, 0x55555555, results); + right = results[0]; + left = results[1]; + permOp(left, right, 8, 0xff00ff, results); + left = results[0]; + right = results[1]; + permOp(right, left, 2, 0x33333333, results); + right = results[0]; + left = results[1]; + permOp(left, right, 16, 65535, results); + left = results[0]; + right = results[1]; + permOp(right, left, 4, 0xf0f0f0f, results); + right = results[0]; + left = results[1]; + final int out[] = new int[2]; + out[0] = left; + out[1] = right; + return out; + } + + private static int byteToUnsigned(final byte b) { + final int value = b; + return value < 0 ? value + 256 : value; + } + + private static int dEncrypt(int el, final int r, final int s, final int e0, final int e1, final int sArr[]) { + int v = r ^ r >>> 16; + int u = v & e0; + v &= e1; + u = u ^ u << 16 ^ r ^ sArr[s]; + int t = v ^ v << 16 ^ r ^ sArr[s + 1]; + t = t >>> 4 | t << 28; + el ^= SPTRANS[1][t & 0x3f] | SPTRANS[3][t >>> 8 & 0x3f] | SPTRANS[5][t >>> 16 & 0x3f] | + SPTRANS[7][t >>> 24 & 0x3f] | SPTRANS[0][u & 0x3f] | SPTRANS[2][u >>> 8 & 0x3f] | + SPTRANS[4][u >>> 16 & 0x3f] | SPTRANS[6][u >>> 24 & 0x3f]; + return el; + } + + private static int[] desSetKey(final byte key[]) { + final int schedule[] = new int[32]; + int c = fourBytesToInt(key, 0); + int d = fourBytesToInt(key, 4); + final int results[] = new int[2]; + permOp(d, c, 4, 0xf0f0f0f, results); + d = results[0]; + c = results[1]; + c = hPermOp(c, -2, 0xcccc0000); + d = hPermOp(d, -2, 0xcccc0000); + permOp(d, c, 1, 0x55555555, results); + d = results[0]; + c = results[1]; + permOp(c, d, 8, 0xff00ff, results); + c = results[0]; + d = results[1]; + permOp(d, c, 1, 0x55555555, results); + d = results[0]; + c = results[1]; + d = (d & 0xff) << 16 | d & 0xff00 | (d & 0xff0000) >>> 16 | (c & 0xf0000000) >>> 4; + c &= 0xfffffff; + int j = 0; + for (int i = 0; i < 16; i++) { + if (SHIFT2[i]) { + c = c >>> 2 | c << 26; + d = d >>> 2 | d << 26; + } else { + c = c >>> 1 | c << 27; + d = d >>> 1 | d << 27; + } + c &= 0xfffffff; + d &= 0xfffffff; + int s = SKB[0][c & 0x3f] | SKB[1][c >>> 6 & 0x3 | c >>> 7 & 0x3c] | + SKB[2][c >>> 13 & 0xf | c >>> 14 & 0x30] | + SKB[3][c >>> 20 & 0x1 | c >>> 21 & 0x6 | c >>> 22 & 0x38]; + final int t = SKB[4][d & 0x3f] | SKB[5][d >>> 7 & 0x3 | d >>> 8 & 0x3c] | SKB[6][d >>> 15 & 0x3f] | + SKB[7][d >>> 21 & 0xf | d >>> 22 & 0x30]; + schedule[j++] = (t << 16 | s & 0xffff); + s = s >>> 16 | t & 0xffff0000; + s = s << 4 | s >>> 28; + schedule[j++] = s; + } + + return schedule; + } + + private static int fourBytesToInt(final byte b[], int offset) { + int value = byteToUnsigned(b[offset++]); + value |= byteToUnsigned(b[offset++]) << 8; + value |= byteToUnsigned(b[offset++]) << 16; + value |= byteToUnsigned(b[offset++]) << 24; + return value; + } + + private static int hPermOp(int a, final int n, final int m) { + final int t = (a << 16 - n ^ a) & m; + a = a ^ t ^ t >>> 16 - n; + return a; + } + + private static void intToFourBytes(final int iValue, final byte b[], int offset) { + b[offset++] = (byte) (iValue & 0xff); + b[offset++] = (byte) (iValue >>> 8 & 0xff); + b[offset++] = (byte) (iValue >>> 16 & 0xff); + b[offset++] = (byte) (iValue >>> 24 & 0xff); + } + + private static void permOp(int a, int b, final int n, final int m, final int results[]) { + final int t = (a >>> n ^ b) & m; + a ^= t << n; + b ^= t; + results[0] = a; + results[1] = b; + } + +} diff --git a/src/org/apache/commons/codec/digest/package.html b/src/org/apache/commons/codec/digest/package.html new file mode 100644 index 00000000..22cceb4c --- /dev/null +++ b/src/org/apache/commons/codec/digest/package.html @@ -0,0 +1,24 @@ + + + + Simplifies common {@link java.security.MessageDigest} tasks and + includes a libc crypt(3) compatible crypt method that supports DES, + MD5, SHA-256 and SHA-512 based algorithms as well as the Apache + specific "$apr1$" variant. + + diff --git a/src/org/apache/commons/codec/language/AbstractCaverphone.java b/src/org/apache/commons/codec/language/AbstractCaverphone.java new file mode 100644 index 00000000..6a38d630 --- /dev/null +++ b/src/org/apache/commons/codec/language/AbstractCaverphone.java @@ -0,0 +1,80 @@ +/* + * 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 org.apache.commons.codec.language; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Encodes a string into a Caverphone value. + * + * This is an algorithm created by the Caversham Project at the University of Otago. It implements the Caverphone 2.0 + * algorithm: + * + *

This class is immutable and thread-safe.

+ * + * @version $Id: Caverphone.java 1075947 2011-03-01 17:56:14Z ggregory $ + * @see Wikipedia - Caverphone + * @since 1.5 + */ +public abstract class AbstractCaverphone implements StringEncoder { + + /** + * Creates an instance of the Caverphone encoder + */ + public AbstractCaverphone() { + super(); + } + + /** + * Encodes an Object using the caverphone algorithm. This method is provided in order to satisfy the requirements of + * the Encoder interface, and will throw an EncoderException if the supplied object is not of type java.lang.String. + * + * @param source + * Object to encode + * @return An object (or type java.lang.String) containing the caverphone code which corresponds to the String + * supplied. + * @throws EncoderException + * if the parameter supplied is not of type java.lang.String + */ + @Override + public Object encode(final Object source) throws EncoderException { + if (!(source instanceof String)) { + throw new EncoderException("Parameter supplied to Caverphone encode is not of type java.lang.String"); + } + return this.encode((String) source); + } + + /** + * Tests if the encodings of two strings are equal. + * + * This method might be promoted to a new AbstractStringEncoder superclass. + * + * @param str1 + * First of two strings to compare + * @param str2 + * Second of two strings to compare + * @return true if the encodings of these strings are identical, false otherwise. + * @throws EncoderException + * thrown if there is an error condition during the encoding process. + */ + public boolean isEncodeEqual(final String str1, final String str2) throws EncoderException { + return this.encode(str1).equals(this.encode(str2)); + } + +} diff --git a/src/org/apache/commons/codec/language/Caverphone.java b/src/org/apache/commons/codec/language/Caverphone.java new file mode 100644 index 00000000..42c96730 --- /dev/null +++ b/src/org/apache/commons/codec/language/Caverphone.java @@ -0,0 +1,105 @@ +/* + * 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 org.apache.commons.codec.language; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Encodes a string into a Caverphone 2.0 value. Delegate to a {@link Caverphone2} instance. + * + * This is an algorithm created by the Caversham Project at the University of Otago. It implements the Caverphone 2.0 + * algorithm: + * + * @version $Id: Caverphone.java 1079535 2011-03-08 20:54:37Z ggregory $ + * @see Wikipedia - Caverphone + * @see Caverphone 2.0 specification + * @since 1.4 + * @deprecated 1.5 Replaced by {@link Caverphone2}, will be removed in 2.0. + */ +@Deprecated +public class Caverphone implements StringEncoder { + + /** + * Delegate to a {@link Caverphone2} instance to avoid code duplication. + */ + final private Caverphone2 encoder = new Caverphone2(); + + /** + * Creates an instance of the Caverphone encoder + */ + public Caverphone() { + super(); + } + + /** + * Encodes the given String into a Caverphone value. + * + * @param source + * String the source string + * @return A caverphone code for the given String + */ + public String caverphone(final String source) { + return this.encoder.encode(source); + } + + /** + * Encodes an Object using the caverphone algorithm. This method is provided in order to satisfy the requirements of + * the Encoder interface, and will throw an EncoderException if the supplied object is not of type java.lang.String. + * + * @param obj + * Object to encode + * @return An object (or type java.lang.String) containing the caverphone code which corresponds to the String + * supplied. + * @throws EncoderException + * if the parameter supplied is not of type java.lang.String + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (!(obj instanceof String)) { + throw new EncoderException("Parameter supplied to Caverphone encode is not of type java.lang.String"); + } + return this.caverphone((String) obj); + } + + /** + * Encodes a String using the Caverphone algorithm. + * + * @param str + * String object to encode + * @return The caverphone code corresponding to the String supplied + */ + @Override + public String encode(final String str) { + return this.caverphone(str); + } + + /** + * Tests if the caverphones of two strings are identical. + * + * @param str1 + * First of two strings to compare + * @param str2 + * Second of two strings to compare + * @return true if the caverphones of these strings are identical, false otherwise. + */ + public boolean isCaverphoneEqual(final String str1, final String str2) { + return this.caverphone(str1).equals(this.caverphone(str2)); + } + +} diff --git a/src/org/apache/commons/codec/language/Caverphone1.java b/src/org/apache/commons/codec/language/Caverphone1.java new file mode 100644 index 00000000..6b8a312a --- /dev/null +++ b/src/org/apache/commons/codec/language/Caverphone1.java @@ -0,0 +1,127 @@ +/* + * 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 org.apache.commons.codec.language; + +/** + * Encodes a string into a Caverphone 1.0 value. + * + * This is an algorithm created by the Caversham Project at the University of Otago. It implements the Caverphone 1.0 + * algorithm: + * + * @version $Id: Caverphone.java 1075947 2011-03-01 17:56:14Z ggregory $ + * @see Wikipedia - Caverphone + * @see Caverphone 1.0 specification + * @since 1.5 + * + *

This class is immutable and thread-safe.

+ */ +public class Caverphone1 extends AbstractCaverphone { + + private static final String SIX_1 = "111111"; + + /** + * Encodes the given String into a Caverphone value. + * + * @param source + * String the source string + * @return A caverphone code for the given String + */ + @Override + public String encode(final String source) { + String txt = source; + if (txt == null || txt.length() == 0) { + return SIX_1; + } + + // 1. Convert to lowercase + txt = txt.toLowerCase(java.util.Locale.ENGLISH); + + // 2. Remove anything not A-Z + txt = txt.replaceAll("[^a-z]", ""); + + // 3. Handle various start options + // 2 is a temporary placeholder to indicate a consonant which we are no longer interested in. + txt = txt.replaceAll("^cough", "cou2f"); + txt = txt.replaceAll("^rough", "rou2f"); + txt = txt.replaceAll("^tough", "tou2f"); + txt = txt.replaceAll("^enough", "enou2f"); + txt = txt.replaceAll("^gn", "2n"); + + // End + txt = txt.replaceAll("mb$", "m2"); + + // 4. Handle replacements + txt = txt.replaceAll("cq", "2q"); + txt = txt.replaceAll("ci", "si"); + txt = txt.replaceAll("ce", "se"); + txt = txt.replaceAll("cy", "sy"); + txt = txt.replaceAll("tch", "2ch"); + txt = txt.replaceAll("c", "k"); + txt = txt.replaceAll("q", "k"); + txt = txt.replaceAll("x", "k"); + txt = txt.replaceAll("v", "f"); + txt = txt.replaceAll("dg", "2g"); + txt = txt.replaceAll("tio", "sio"); + txt = txt.replaceAll("tia", "sia"); + txt = txt.replaceAll("d", "t"); + txt = txt.replaceAll("ph", "fh"); + txt = txt.replaceAll("b", "p"); + txt = txt.replaceAll("sh", "s2"); + txt = txt.replaceAll("z", "s"); + txt = txt.replaceAll("^[aeiou]", "A"); + // 3 is a temporary placeholder marking a vowel + txt = txt.replaceAll("[aeiou]", "3"); + txt = txt.replaceAll("3gh3", "3kh3"); + txt = txt.replaceAll("gh", "22"); + txt = txt.replaceAll("g", "k"); + txt = txt.replaceAll("s+", "S"); + txt = txt.replaceAll("t+", "T"); + txt = txt.replaceAll("p+", "P"); + txt = txt.replaceAll("k+", "K"); + txt = txt.replaceAll("f+", "F"); + txt = txt.replaceAll("m+", "M"); + txt = txt.replaceAll("n+", "N"); + txt = txt.replaceAll("w3", "W3"); + txt = txt.replaceAll("wy", "Wy"); // 1.0 only + txt = txt.replaceAll("wh3", "Wh3"); + txt = txt.replaceAll("why", "Why"); // 1.0 only + txt = txt.replaceAll("w", "2"); + txt = txt.replaceAll("^h", "A"); + txt = txt.replaceAll("h", "2"); + txt = txt.replaceAll("r3", "R3"); + txt = txt.replaceAll("ry", "Ry"); // 1.0 only + txt = txt.replaceAll("r", "2"); + txt = txt.replaceAll("l3", "L3"); + txt = txt.replaceAll("ly", "Ly"); // 1.0 only + txt = txt.replaceAll("l", "2"); + txt = txt.replaceAll("j", "y"); // 1.0 only + txt = txt.replaceAll("y3", "Y3"); // 1.0 only + txt = txt.replaceAll("y", "2"); // 1.0 only + + // 5. Handle removals + txt = txt.replaceAll("2", ""); + txt = txt.replaceAll("3", ""); + + // 6. put ten 1s on the end + txt = txt + SIX_1; + + // 7. take the first six characters as the code + return txt.substring(0, SIX_1.length()); + } + +} diff --git a/src/org/apache/commons/codec/language/Caverphone2.java b/src/org/apache/commons/codec/language/Caverphone2.java new file mode 100644 index 00000000..624e3958 --- /dev/null +++ b/src/org/apache/commons/codec/language/Caverphone2.java @@ -0,0 +1,131 @@ +/* + * 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 org.apache.commons.codec.language; + +/** + * Encodes a string into a Caverphone 2.0 value. + * + * This is an algorithm created by the Caversham Project at the University of Otago. It implements the Caverphone 2.0 + * algorithm: + * + * @version $Id: Caverphone.java 1075947 2011-03-01 17:56:14Z ggregory $ + * @see Wikipedia - Caverphone + * @see Caverphone 2.0 specification + * @since 1.5 + * + *

This class is immutable and thread-safe.

+ */ +public class Caverphone2 extends AbstractCaverphone { + + private static final String TEN_1 = "1111111111"; + + /** + * Encodes the given String into a Caverphone 2.0 value. + * + * @param source + * String the source string + * @return A caverphone code for the given String + */ + @Override + public String encode(final String source) { + String txt = source; + if (txt == null || txt.length() == 0) { + return TEN_1; + } + + // 1. Convert to lowercase + txt = txt.toLowerCase(java.util.Locale.ENGLISH); + + // 2. Remove anything not A-Z + txt = txt.replaceAll("[^a-z]", ""); + + // 2.5. Remove final e + txt = txt.replaceAll("e$", ""); // 2.0 only + + // 3. Handle various start options + txt = txt.replaceAll("^cough", "cou2f"); + txt = txt.replaceAll("^rough", "rou2f"); + txt = txt.replaceAll("^tough", "tou2f"); + txt = txt.replaceAll("^enough", "enou2f"); // 2.0 only + txt = txt.replaceAll("^trough", "trou2f"); // 2.0 only + // note the spec says ^enough here again, c+p error I assume + txt = txt.replaceAll("^gn", "2n"); + + // End + txt = txt.replaceAll("mb$", "m2"); + + // 4. Handle replacements + txt = txt.replaceAll("cq", "2q"); + txt = txt.replaceAll("ci", "si"); + txt = txt.replaceAll("ce", "se"); + txt = txt.replaceAll("cy", "sy"); + txt = txt.replaceAll("tch", "2ch"); + txt = txt.replaceAll("c", "k"); + txt = txt.replaceAll("q", "k"); + txt = txt.replaceAll("x", "k"); + txt = txt.replaceAll("v", "f"); + txt = txt.replaceAll("dg", "2g"); + txt = txt.replaceAll("tio", "sio"); + txt = txt.replaceAll("tia", "sia"); + txt = txt.replaceAll("d", "t"); + txt = txt.replaceAll("ph", "fh"); + txt = txt.replaceAll("b", "p"); + txt = txt.replaceAll("sh", "s2"); + txt = txt.replaceAll("z", "s"); + txt = txt.replaceAll("^[aeiou]", "A"); + txt = txt.replaceAll("[aeiou]", "3"); + txt = txt.replaceAll("j", "y"); // 2.0 only + txt = txt.replaceAll("^y3", "Y3"); // 2.0 only + txt = txt.replaceAll("^y", "A"); // 2.0 only + txt = txt.replaceAll("y", "3"); // 2.0 only + txt = txt.replaceAll("3gh3", "3kh3"); + txt = txt.replaceAll("gh", "22"); + txt = txt.replaceAll("g", "k"); + txt = txt.replaceAll("s+", "S"); + txt = txt.replaceAll("t+", "T"); + txt = txt.replaceAll("p+", "P"); + txt = txt.replaceAll("k+", "K"); + txt = txt.replaceAll("f+", "F"); + txt = txt.replaceAll("m+", "M"); + txt = txt.replaceAll("n+", "N"); + txt = txt.replaceAll("w3", "W3"); + txt = txt.replaceAll("wh3", "Wh3"); + txt = txt.replaceAll("w$", "3"); // 2.0 only + txt = txt.replaceAll("w", "2"); + txt = txt.replaceAll("^h", "A"); + txt = txt.replaceAll("h", "2"); + txt = txt.replaceAll("r3", "R3"); + txt = txt.replaceAll("r$", "3"); // 2.0 only + txt = txt.replaceAll("r", "2"); + txt = txt.replaceAll("l3", "L3"); + txt = txt.replaceAll("l$", "3"); // 2.0 only + txt = txt.replaceAll("l", "2"); + + // 5. Handle removals + txt = txt.replaceAll("2", ""); + txt = txt.replaceAll("3$", "A"); // 2.0 only + txt = txt.replaceAll("3", ""); + + // 6. put ten 1s on the end + txt = txt + TEN_1; + + // 7. take the first ten characters as the code + return txt.substring(0, TEN_1.length()); + } + +} diff --git a/src/org/apache/commons/codec/language/ColognePhonetic.java b/src/org/apache/commons/codec/language/ColognePhonetic.java new file mode 100644 index 00000000..01f395c3 --- /dev/null +++ b/src/org/apache/commons/codec/language/ColognePhonetic.java @@ -0,0 +1,445 @@ +/* + * 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 org.apache.commons.codec.language; + +import java.util.Locale; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Encodes a string into a Cologne Phonetic value. + *

+ * Implements the Kölner Phonetik (Cologne + * Phonetic) algorithm issued by Hans Joachim Postel in 1969. + *

+ *

+ * The Kölner Phonetik is a phonetic algorithm which is optimized for the German language. It is related to + * the well-known soundex algorithm. + *

+ * + *

Algorithm

+ * + *
    + * + *
  • + *

    Step 1:

    + * After preprocessing (conversion to upper case, transcription of germanic umlauts, removal of non alphabetical characters) the + * letters of the supplied text are replaced by their phonetic code according to the following table. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    (Source: Wikipedia (de): Kölner Phonetik -- + * Buchstabencodes)
    LetterContextCode
    A, E, I, J, O, U, Y0
    H-
    B1
    Pnot before H
    D, Tnot before C, S, Z2
    F, V, W3
    Pbefore H
    G, K, Q4
    Cat onset before A, H, K, L, O, Q, R, U, X
    before A, H, K, O, Q, U, X except after S, Z
    Xnot after C, K, Q48
    L5
    M, N6
    R7
    S, Z8
    Cafter S, Z
    at onset except before A, H, K, L, O, Q, R, U, X
    not before A, H, K, O, Q, U, X
    D, Tbefore C, S, Z
    Xafter C, K, Q
    + * + *

    Example:

    + * + * "Müller-Lü + * denscheidt" => "MULLERLUDENSCHEIDT" => "6005507500206880022" + * + *
  • + * + *
  • + *

    Step 2:

    + * Collapse of all multiple consecutive code digits. + *

    Example:

    + * "6005507500206880022" => "6050750206802"
  • + * + *
  • + *

    Step 3:

    + * Removal of all codes "0" except at the beginning. This means that two or more identical consecutive digits can occur + * if they occur after removing the "0" digits. + * + *

    Example:

    + * "6050750206802" => "65752682"
  • + * + *
+ * + *

+ * This class is thread-safe. + *

+ * + * @see Wikipedia (de): Kölner Phonetik (in German) + * @since 1.5 + */ +public class ColognePhonetic implements StringEncoder { + + // Predefined char arrays for better performance and less GC load + private static final char[] AEIJOUY = new char[] { 'A', 'E', 'I', 'J', 'O', 'U', 'Y' }; + private static final char[] SCZ = new char[] { 'S', 'C', 'Z' }; + private static final char[] WFPV = new char[] { 'W', 'F', 'P', 'V' }; + private static final char[] GKQ = new char[] { 'G', 'K', 'Q' }; + private static final char[] CKQ = new char[] { 'C', 'K', 'Q' }; + private static final char[] AHKLOQRUX = new char[] { 'A', 'H', 'K', 'L', 'O', 'Q', 'R', 'U', 'X' }; + private static final char[] SZ = new char[] { 'S', 'Z' }; + private static final char[] AHOUKQX = new char[] { 'A', 'H', 'O', 'U', 'K', 'Q', 'X' }; + private static final char[] TDX = new char[] { 'T', 'D', 'X' }; + + /** + * This class is not thread-safe; the field {@link #length} is mutable. + * However, it is not shared between threads, as it is constructed on demand + * by the method {@link ColognePhonetic#colognePhonetic(String)} + */ + private abstract class CologneBuffer { + + protected final char[] data; + + protected int length = 0; + + public CologneBuffer(final char[] data) { + this.data = data; + this.length = data.length; + } + + public CologneBuffer(final int buffSize) { + this.data = new char[buffSize]; + this.length = 0; + } + + protected abstract char[] copyData(int start, final int length); + + public int length() { + return length; + } + + @Override + public String toString() { + return new String(copyData(0, length)); + } + } + + private class CologneOutputBuffer extends CologneBuffer { + + public CologneOutputBuffer(final int buffSize) { + super(buffSize); + } + + public void addRight(final char chr) { + data[length] = chr; + length++; + } + + @Override + protected char[] copyData(final int start, final int length) { + final char[] newData = new char[length]; + System.arraycopy(data, start, newData, 0, length); + return newData; + } + } + + private class CologneInputBuffer extends CologneBuffer { + + public CologneInputBuffer(final char[] data) { + super(data); + } + + public void addLeft(final char ch) { + length++; + data[getNextPos()] = ch; + } + + @Override + protected char[] copyData(final int start, final int length) { + final char[] newData = new char[length]; + System.arraycopy(data, data.length - this.length + start, newData, 0, length); + return newData; + } + + public char getNextChar() { + return data[getNextPos()]; + } + + protected int getNextPos() { + return data.length - length; + } + + public char removeNext() { + final char ch = getNextChar(); + length--; + return ch; + } + } + + /** + * Maps some Germanic characters to plain for internal processing. The following characters are mapped: + *
    + *
  • capital a, umlaut mark
  • + *
  • capital u, umlaut mark
  • + *
  • capital o, umlaut mark
  • + *
  • small sharp s, German
  • + *
+ */ + private static final char[][] PREPROCESS_MAP = new char[][]{ + {'\u00C4', 'A'}, // capital a, umlaut mark + {'\u00DC', 'U'}, // capital u, umlaut mark + {'\u00D6', 'O'}, // capital o, umlaut mark + {'\u00DF', 'S'} // small sharp s, German + }; + + /* + * Returns whether the array contains the key, or not. + */ + private static boolean arrayContains(final char[] arr, final char key) { + for (final char element : arr) { + if (element == key) { + return true; + } + } + return false; + } + + /** + *

+ * Implements the Kölner Phonetik algorithm. + *

+ *

+ * In contrast to the initial description of the algorithm, this implementation does the encoding in one pass. + *

+ * + * @param text The source text to encode + * @return the corresponding encoding according to the Kölner Phonetik algorithm + */ + public String colognePhonetic(String text) { + if (text == null) { + return null; + } + + text = preprocess(text); + + final CologneOutputBuffer output = new CologneOutputBuffer(text.length() * 2); + final CologneInputBuffer input = new CologneInputBuffer(text.toCharArray()); + + char nextChar; + + char lastChar = '-'; + char lastCode = '/'; + char code; + char chr; + + int rightLength = input.length(); + + while (rightLength > 0) { + chr = input.removeNext(); + + if ((rightLength = input.length()) > 0) { + nextChar = input.getNextChar(); + } else { + nextChar = '-'; + } + + if (arrayContains(AEIJOUY, chr)) { + code = '0'; + } else if (chr == 'H' || chr < 'A' || chr > 'Z') { + if (lastCode == '/') { + continue; + } + code = '-'; + } else if (chr == 'B' || (chr == 'P' && nextChar != 'H')) { + code = '1'; + } else if ((chr == 'D' || chr == 'T') && !arrayContains(SCZ, nextChar)) { + code = '2'; + } else if (arrayContains(WFPV, chr)) { + code = '3'; + } else if (arrayContains(GKQ, chr)) { + code = '4'; + } else if (chr == 'X' && !arrayContains(CKQ, lastChar)) { + code = '4'; + input.addLeft('S'); + rightLength++; + } else if (chr == 'S' || chr == 'Z') { + code = '8'; + } else if (chr == 'C') { + if (lastCode == '/') { + if (arrayContains(AHKLOQRUX, nextChar)) { + code = '4'; + } else { + code = '8'; + } + } else { + if (arrayContains(SZ, lastChar) || !arrayContains(AHOUKQX, nextChar)) { + code = '8'; + } else { + code = '4'; + } + } + } else if (arrayContains(TDX, chr)) { + code = '8'; + } else if (chr == 'R') { + code = '7'; + } else if (chr == 'L') { + code = '5'; + } else if (chr == 'M' || chr == 'N') { + code = '6'; + } else { + code = chr; + } + + if (code != '-' && (lastCode != code && (code != '0' || lastCode == '/') || code < '0' || code > '8')) { + output.addRight(code); + } + + lastChar = chr; + lastCode = code; + } + return output.toString(); + } + + @Override + public Object encode(final Object object) throws EncoderException { + if (!(object instanceof String)) { + throw new EncoderException("This method's parameter was expected to be of the type " + + String.class.getName() + + ". But actually it was of the type " + + object.getClass().getName() + + "."); + } + return encode((String) object); + } + + @Override + public String encode(final String text) { + return colognePhonetic(text); + } + + public boolean isEncodeEqual(final String text1, final String text2) { + return colognePhonetic(text1).equals(colognePhonetic(text2)); + } + + /** + * Converts the string to upper case and replaces germanic characters as defined in {@link #PREPROCESS_MAP}. + */ + private String preprocess(String text) { + text = text.toUpperCase(Locale.GERMAN); + + final char[] chrs = text.toCharArray(); + + for (int index = 0; index < chrs.length; index++) { + if (chrs[index] > 'Z') { + for (final char[] element : PREPROCESS_MAP) { + if (chrs[index] == element[0]) { + chrs[index] = element[1]; + break; + } + } + } + } + return new String(chrs); + } +} diff --git a/src/org/apache/commons/codec/language/DaitchMokotoffSoundex.java b/src/org/apache/commons/codec/language/DaitchMokotoffSoundex.java new file mode 100644 index 00000000..b33dbe83 --- /dev/null +++ b/src/org/apache/commons/codec/language/DaitchMokotoffSoundex.java @@ -0,0 +1,561 @@ +/* + * 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 org.apache.commons.codec.language; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.Set; + +import org.apache.commons.codec.CharEncoding; +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Encodes a string into a Daitch-Mokotoff Soundex value. + *

+ * The Daitch-Mokotoff Soundex algorithm is a refinement of the Russel and American Soundex algorithms, yielding greater + * accuracy in matching especially Slavish and Yiddish surnames with similar pronunciation but differences in spelling. + *

+ *

+ * The main differences compared to the other soundex variants are: + *

+ *
    + *
  • coded names are 6 digits long + *
  • the initial character of the name is coded + *
  • rules to encoded multi-character n-grams + *
  • multiple possible encodings for the same name (branching) + *
+ *

+ * This implementation supports branching, depending on the used method: + *

    + *
  • {@link #encode(String)} - branching disabled, only the first code will be returned + *
  • {@link #soundex(String)} - branching enabled, all codes will be returned, separated by '|' + *
+ *

+ * Note: this implementation has additional branching rules compared to the original description of the algorithm. The + * rules can be customized by overriding the default rules contained in the resource file + * {@code org/apache/commons/codec/language/dmrules.txt}. + *

+ *

+ * This class is thread-safe. + *

+ * + * @see Soundex + * @see Wikipedia - Daitch-Mokotoff Soundex + * @see Avotaynu - Soundexing and Genealogy + * + * @version $Id$ + * @since 1.10 + */ +public class DaitchMokotoffSoundex implements StringEncoder { + + /** + * Inner class representing a branch during DM soundex encoding. + */ + private static final class Branch { + private final StringBuilder builder; + private String cachedString; + private String lastReplacement; + + private Branch() { + builder = new StringBuilder(); + lastReplacement = null; + cachedString = null; + } + + /** + * Creates a new branch, identical to this branch. + * + * @return a new, identical branch + */ + public Branch createBranch() { + final Branch branch = new Branch(); + branch.builder.append(toString()); + branch.lastReplacement = this.lastReplacement; + return branch; + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Branch)) { + return false; + } + + return toString().equals(((Branch) other).toString()); + } + + /** + * Finish this branch by appending '0's until the maximum code length has been reached. + */ + public void finish() { + while (builder.length() < MAX_LENGTH) { + builder.append('0'); + cachedString = null; + } + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + /** + * Process the next replacement to be added to this branch. + * + * @param replacement + * the next replacement to append + * @param forceAppend + * indicates if the default processing shall be overridden + */ + public void processNextReplacement(final String replacement, final boolean forceAppend) { + final boolean append = lastReplacement == null || !lastReplacement.endsWith(replacement) || forceAppend; + + if (append && builder.length() < MAX_LENGTH) { + builder.append(replacement); + // remove all characters after the maximum length + if (builder.length() > MAX_LENGTH) { + builder.delete(MAX_LENGTH, builder.length()); + } + cachedString = null; + } + + lastReplacement = replacement; + } + + @Override + public String toString() { + if (cachedString == null) { + cachedString = builder.toString(); + } + return cachedString; + } + } + + /** + * Inner class for storing rules. + */ + private static final class Rule { + private final String pattern; + private final String[] replacementAtStart; + private final String[] replacementBeforeVowel; + private final String[] replacementDefault; + + protected Rule(final String pattern, final String replacementAtStart, final String replacementBeforeVowel, + final String replacementDefault) { + this.pattern = pattern; + this.replacementAtStart = replacementAtStart.split("\\|"); + this.replacementBeforeVowel = replacementBeforeVowel.split("\\|"); + this.replacementDefault = replacementDefault.split("\\|"); + } + + public int getPatternLength() { + return pattern.length(); + } + + public String[] getReplacements(final String context, final boolean atStart) { + if (atStart) { + return replacementAtStart; + } + + final int nextIndex = getPatternLength(); + final boolean nextCharIsVowel = nextIndex < context.length() ? isVowel(context.charAt(nextIndex)) : false; + if (nextCharIsVowel) { + return replacementBeforeVowel; + } + + return replacementDefault; + } + + private boolean isVowel(final char ch) { + return ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u'; + } + + public boolean matches(final String context) { + return context.startsWith(pattern); + } + + @Override + public String toString() { + return String.format("%s=(%s,%s,%s)", pattern, Arrays.asList(replacementAtStart), + Arrays.asList(replacementBeforeVowel), Arrays.asList(replacementDefault)); + } + } + + private static final String COMMENT = "//"; + private static final String DOUBLE_QUOTE = "\""; + + private static final String MULTILINE_COMMENT_END = "*/"; + + private static final String MULTILINE_COMMENT_START = "/*"; + + /** The resource file containing the replacement and folding rules */ + private static final String RESOURCE_FILE = "org/apache/commons/codec/language/dmrules.txt"; + + /** The code length of a DM soundex value. */ + private static final int MAX_LENGTH = 6; + + /** Transformation rules indexed by the first character of their pattern. */ + private static final Map> RULES = new HashMap>(); + + /** Folding rules. */ + private static final Map FOLDINGS = new HashMap(); + + static { + final InputStream rulesIS = DaitchMokotoffSoundex.class.getClassLoader().getResourceAsStream(RESOURCE_FILE); + if (rulesIS == null) { + throw new IllegalArgumentException("Unable to load resource: " + RESOURCE_FILE); + } + + final Scanner scanner = new Scanner(rulesIS, CharEncoding.UTF_8); + parseRules(scanner, RESOURCE_FILE, RULES, FOLDINGS); + scanner.close(); + + // sort RULES by pattern length in descending order + for (final Map.Entry> rule : RULES.entrySet()) { + final List ruleList = rule.getValue(); + Collections.sort(ruleList, new Comparator() { + @Override + public int compare(final Rule rule1, final Rule rule2) { + return rule2.getPatternLength() - rule1.getPatternLength(); + } + }); + } + } + + private static void parseRules(final Scanner scanner, final String location, + final Map> ruleMapping, final Map asciiFoldings) { + int currentLine = 0; + boolean inMultilineComment = false; + + while (scanner.hasNextLine()) { + currentLine++; + final String rawLine = scanner.nextLine(); + String line = rawLine; + + if (inMultilineComment) { + if (line.endsWith(MULTILINE_COMMENT_END)) { + inMultilineComment = false; + } + continue; + } + + if (line.startsWith(MULTILINE_COMMENT_START)) { + inMultilineComment = true; + } else { + // discard comments + final int cmtI = line.indexOf(COMMENT); + if (cmtI >= 0) { + line = line.substring(0, cmtI); + } + + // trim leading-trailing whitespace + line = line.trim(); + + if (line.length() == 0) { + continue; // empty lines can be safely skipped + } + + if (line.contains("=")) { + // folding + final String[] parts = line.split("="); + if (parts.length != 2) { + throw new IllegalArgumentException("Malformed folding statement split into " + parts.length + + " parts: " + rawLine + " in " + location); + } else { + final String leftCharacter = parts[0]; + final String rightCharacter = parts[1]; + + if (leftCharacter.length() != 1 || rightCharacter.length() != 1) { + throw new IllegalArgumentException("Malformed folding statement - " + + "patterns are not single characters: " + rawLine + " in " + location); + } + + asciiFoldings.put(leftCharacter.charAt(0), rightCharacter.charAt(0)); + } + } else { + // rule + final String[] parts = line.split("\\s+"); + if (parts.length != 4) { + throw new IllegalArgumentException("Malformed rule statement split into " + parts.length + + " parts: " + rawLine + " in " + location); + } else { + try { + final String pattern = stripQuotes(parts[0]); + final String replacement1 = stripQuotes(parts[1]); + final String replacement2 = stripQuotes(parts[2]); + final String replacement3 = stripQuotes(parts[3]); + + final Rule r = new Rule(pattern, replacement1, replacement2, replacement3); + final char patternKey = r.pattern.charAt(0); + List rules = ruleMapping.get(patternKey); + if (rules == null) { + rules = new ArrayList(); + ruleMapping.put(patternKey, rules); + } + rules.add(r); + } catch (final IllegalArgumentException e) { + throw new IllegalStateException( + "Problem parsing line '" + currentLine + "' in " + location, e); + } + } + } + } + } + } + + private static String stripQuotes(String str) { + if (str.startsWith(DOUBLE_QUOTE)) { + str = str.substring(1); + } + + if (str.endsWith(DOUBLE_QUOTE)) { + str = str.substring(0, str.length() - 1); + } + + return str; + } + + /** Whether to use ASCII folding prior to encoding. */ + private final boolean folding; + + /** + * Creates a new instance with ASCII-folding enabled. + */ + public DaitchMokotoffSoundex() { + this(true); + } + + /** + * Creates a new instance. + *

+ * With ASCII-folding enabled, certain accented characters will be transformed to equivalent ASCII characters, e.g. + * è -> e. + *

+ * + * @param folding + * if ASCII-folding shall be performed before encoding + */ + public DaitchMokotoffSoundex(final boolean folding) { + this.folding = folding; + } + + /** + * Performs a cleanup of the input string before the actual soundex transformation. + *

+ * Removes all whitespace characters and performs ASCII folding if enabled. + *

+ * + * @param input + * the input string to cleanup + * @return a cleaned up string + */ + private String cleanup(final String input) { + final StringBuilder sb = new StringBuilder(); + for (char ch : input.toCharArray()) { + if (Character.isWhitespace(ch)) { + continue; + } + + ch = Character.toLowerCase(ch); + if (folding && FOLDINGS.containsKey(ch)) { + ch = FOLDINGS.get(ch); + } + sb.append(ch); + } + return sb.toString(); + } + + /** + * Encodes an Object using the Daitch-Mokotoff soundex algorithm without branching. + *

+ * This method is provided in order to satisfy the requirements of the Encoder interface, and will throw an + * EncoderException if the supplied object is not of type java.lang.String. + *

+ * + * @see #soundex(String) + * + * @param obj + * Object to encode + * @return An object (of type java.lang.String) containing the DM soundex code, which corresponds to the String + * supplied. + * @throws EncoderException + * if the parameter supplied is not of type java.lang.String + * @throws IllegalArgumentException + * if a character is not mapped + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (!(obj instanceof String)) { + throw new EncoderException( + "Parameter supplied to DaitchMokotoffSoundex encode is not of type java.lang.String"); + } + return encode((String) obj); + } + + /** + * Encodes a String using the Daitch-Mokotoff soundex algorithm without branching. + * + * @see #soundex(String) + * + * @param source + * A String object to encode + * @return A DM Soundex code corresponding to the String supplied + * @throws IllegalArgumentException + * if a character is not mapped + */ + @Override + public String encode(final String source) { + if (source == null) { + return null; + } + return soundex(source, false)[0]; + } + + /** + * Encodes a String using the Daitch-Mokotoff soundex algorithm with branching. + *

+ * In case a string is encoded into multiple codes (see branching rules), the result will contain all codes, + * separated by '|'. + *

+ *

+ * Example: the name "AUERBACH" is encoded as both + *

+ *
    + *
  • 097400
  • + *
  • 097500
  • + *
+ *

+ * Thus the result will be "097400|097500". + *

+ * + * @param source + * A String object to encode + * @return A string containing a set of DM Soundex codes corresponding to the String supplied + * @throws IllegalArgumentException + * if a character is not mapped + */ + public String soundex(final String source) { + final String[] branches = soundex(source, true); + final StringBuilder sb = new StringBuilder(); + int index = 0; + for (final String branch : branches) { + sb.append(branch); + if (++index < branches.length) { + sb.append('|'); + } + } + return sb.toString(); + } + + /** + * Perform the actual DM Soundex algorithm on the input string. + * + * @param source + * A String object to encode + * @param branching + * If branching shall be performed + * @return A string array containing all DM Soundex codes corresponding to the String supplied depending on the + * selected branching mode + */ + private String[] soundex(final String source, final boolean branching) { + if (source == null) { + return null; + } + + final String input = cleanup(source); + + final Set currentBranches = new LinkedHashSet(); + currentBranches.add(new Branch()); + + char lastChar = '\0'; + for (int index = 0; index < input.length(); index++) { + final char ch = input.charAt(index); + + // ignore whitespace inside a name + if (Character.isWhitespace(ch)) { + continue; + } + + final String inputContext = input.substring(index); + final List rules = RULES.get(ch); + if (rules == null) { + continue; + } + + // use an EMPTY_LIST to avoid false positive warnings wrt potential null pointer access + @SuppressWarnings("unchecked") + final List nextBranches = branching ? new ArrayList() : Collections.EMPTY_LIST; + + for (final Rule rule : rules) { + if (rule.matches(inputContext)) { + if (branching) { + nextBranches.clear(); + } + final String[] replacements = rule.getReplacements(inputContext, lastChar == '\0'); + final boolean branchingRequired = replacements.length > 1 && branching; + + for (final Branch branch : currentBranches) { + for (final String nextReplacement : replacements) { + // if we have multiple replacements, always create a new branch + final Branch nextBranch = branchingRequired ? branch.createBranch() : branch; + + // special rule: occurrences of mn or nm are treated differently + final boolean force = (lastChar == 'm' && ch == 'n') || (lastChar == 'n' && ch == 'm'); + + nextBranch.processNextReplacement(nextReplacement, force); + + if (branching) { + nextBranches.add(nextBranch); + } else { + break; + } + } + } + + if (branching) { + currentBranches.clear(); + currentBranches.addAll(nextBranches); + } + index += rule.getPatternLength() - 1; + break; + } + } + + lastChar = ch; + } + + final String[] result = new String[currentBranches.size()]; + int index = 0; + for (final Branch branch : currentBranches) { + branch.finish(); + result[index++] = branch.toString(); + } + + return result; + } +} diff --git a/src/org/apache/commons/codec/language/DoubleMetaphone.java b/src/org/apache/commons/codec/language/DoubleMetaphone.java new file mode 100644 index 00000000..211e57fb --- /dev/null +++ b/src/org/apache/commons/codec/language/DoubleMetaphone.java @@ -0,0 +1,1009 @@ +/* + * 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 org.apache.commons.codec.language; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; +import org.apache.commons.codec.binary.StringUtils; + +/** + * Encodes a string into a double metaphone value. This Implementation is based on the algorithm by Lawrence + * Philips. + *

+ * This class is conditionally thread-safe. The instance field {@link #maxCodeLen} is mutable + * {@link #setMaxCodeLen(int)} but is not volatile, and accesses are not synchronized. If an instance of the class is + * shared between threads, the caller needs to ensure that suitable synchronization is used to ensure safe publication + * of the value between threads, and must not invoke {@link #setMaxCodeLen(int)} after initial setup. + * + * @see Original Article + * @see http://en.wikipedia.org/wiki/Metaphone + * + * @version $Id$ + */ +public class DoubleMetaphone implements StringEncoder { + + /** + * "Vowels" to test for + */ + private static final String VOWELS = "AEIOUY"; + + /** + * Prefixes when present which are not pronounced + */ + private static final String[] SILENT_START = + { "GN", "KN", "PN", "WR", "PS" }; + private static final String[] L_R_N_M_B_H_F_V_W_SPACE = + { "L", "R", "N", "M", "B", "H", "F", "V", "W", " " }; + private static final String[] ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER = + { "ES", "EP", "EB", "EL", "EY", "IB", "IL", "IN", "IE", "EI", "ER" }; + private static final String[] L_T_K_S_N_M_B_Z = + { "L", "T", "K", "S", "N", "M", "B", "Z" }; + + /** + * Maximum length of an encoding, default is 4 + */ + private int maxCodeLen = 4; + + /** + * Creates an instance of this DoubleMetaphone encoder + */ + public DoubleMetaphone() { + super(); + } + + /** + * Encode a value with Double Metaphone. + * + * @param value String to encode + * @return an encoded string + */ + public String doubleMetaphone(final String value) { + return doubleMetaphone(value, false); + } + + /** + * Encode a value with Double Metaphone, optionally using the alternate encoding. + * + * @param value String to encode + * @param alternate use alternate encode + * @return an encoded string + */ + public String doubleMetaphone(String value, final boolean alternate) { + value = cleanInput(value); + if (value == null) { + return null; + } + + final boolean slavoGermanic = isSlavoGermanic(value); + int index = isSilentStart(value) ? 1 : 0; + + final DoubleMetaphoneResult result = new DoubleMetaphoneResult(this.getMaxCodeLen()); + + while (!result.isComplete() && index <= value.length() - 1) { + switch (value.charAt(index)) { + case 'A': + case 'E': + case 'I': + case 'O': + case 'U': + case 'Y': + index = handleAEIOUY(result, index); + break; + case 'B': + result.append('P'); + index = charAt(value, index + 1) == 'B' ? index + 2 : index + 1; + break; + case '\u00C7': + // A C with a Cedilla + result.append('S'); + index++; + break; + case 'C': + index = handleC(value, result, index); + break; + case 'D': + index = handleD(value, result, index); + break; + case 'F': + result.append('F'); + index = charAt(value, index + 1) == 'F' ? index + 2 : index + 1; + break; + case 'G': + index = handleG(value, result, index, slavoGermanic); + break; + case 'H': + index = handleH(value, result, index); + break; + case 'J': + index = handleJ(value, result, index, slavoGermanic); + break; + case 'K': + result.append('K'); + index = charAt(value, index + 1) == 'K' ? index + 2 : index + 1; + break; + case 'L': + index = handleL(value, result, index); + break; + case 'M': + result.append('M'); + index = conditionM0(value, index) ? index + 2 : index + 1; + break; + case 'N': + result.append('N'); + index = charAt(value, index + 1) == 'N' ? index + 2 : index + 1; + break; + case '\u00D1': + // N with a tilde (spanish ene) + result.append('N'); + index++; + break; + case 'P': + index = handleP(value, result, index); + break; + case 'Q': + result.append('K'); + index = charAt(value, index + 1) == 'Q' ? index + 2 : index + 1; + break; + case 'R': + index = handleR(value, result, index, slavoGermanic); + break; + case 'S': + index = handleS(value, result, index, slavoGermanic); + break; + case 'T': + index = handleT(value, result, index); + break; + case 'V': + result.append('F'); + index = charAt(value, index + 1) == 'V' ? index + 2 : index + 1; + break; + case 'W': + index = handleW(value, result, index); + break; + case 'X': + index = handleX(value, result, index); + break; + case 'Z': + index = handleZ(value, result, index, slavoGermanic); + break; + default: + index++; + break; + } + } + + return alternate ? result.getAlternate() : result.getPrimary(); + } + + /** + * Encode the value using DoubleMetaphone. It will only work if + * obj is a String (like Metaphone). + * + * @param obj Object to encode (should be of type String) + * @return An encoded Object (will be of type String) + * @throws EncoderException encode parameter is not of type String + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (!(obj instanceof String)) { + throw new EncoderException("DoubleMetaphone encode parameter is not of type String"); + } + return doubleMetaphone((String) obj); + } + + /** + * Encode the value using DoubleMetaphone. + * + * @param value String to encode + * @return An encoded String + */ + @Override + public String encode(final String value) { + return doubleMetaphone(value); + } + + /** + * Check if the Double Metaphone values of two String values + * are equal. + * + * @param value1 The left-hand side of the encoded {@link String#equals(Object)}. + * @param value2 The right-hand side of the encoded {@link String#equals(Object)}. + * @return true if the encoded Strings are equal; + * false otherwise. + * @see #isDoubleMetaphoneEqual(String,String,boolean) + */ + public boolean isDoubleMetaphoneEqual(final String value1, final String value2) { + return isDoubleMetaphoneEqual(value1, value2, false); + } + + /** + * Check if the Double Metaphone values of two String values + * are equal, optionally using the alternate value. + * + * @param value1 The left-hand side of the encoded {@link String#equals(Object)}. + * @param value2 The right-hand side of the encoded {@link String#equals(Object)}. + * @param alternate use the alternate value if true. + * @return true if the encoded Strings are equal; + * false otherwise. + */ + public boolean isDoubleMetaphoneEqual(final String value1, final String value2, final boolean alternate) { + return StringUtils.equals(doubleMetaphone(value1, alternate), doubleMetaphone(value2, alternate)); + } + + /** + * Returns the maxCodeLen. + * @return int + */ + public int getMaxCodeLen() { + return this.maxCodeLen; + } + + /** + * Sets the maxCodeLen. + * @param maxCodeLen The maxCodeLen to set + */ + public void setMaxCodeLen(final int maxCodeLen) { + this.maxCodeLen = maxCodeLen; + } + + //-- BEGIN HANDLERS --// + + /** + * Handles 'A', 'E', 'I', 'O', 'U', and 'Y' cases. + */ + private int handleAEIOUY(final DoubleMetaphoneResult result, final int index) { + if (index == 0) { + result.append('A'); + } + return index + 1; + } + + /** + * Handles 'C' cases. + */ + private int handleC(final String value, final DoubleMetaphoneResult result, int index) { + if (conditionC0(value, index)) { // very confusing, moved out + result.append('K'); + index += 2; + } else if (index == 0 && contains(value, index, 6, "CAESAR")) { + result.append('S'); + index += 2; + } else if (contains(value, index, 2, "CH")) { + index = handleCH(value, result, index); + } else if (contains(value, index, 2, "CZ") && + !contains(value, index - 2, 4, "WICZ")) { + //-- "Czerny" --// + result.append('S', 'X'); + index += 2; + } else if (contains(value, index + 1, 3, "CIA")) { + //-- "focaccia" --// + result.append('X'); + index += 3; + } else if (contains(value, index, 2, "CC") && + !(index == 1 && charAt(value, 0) == 'M')) { + //-- double "cc" but not "McClelland" --// + return handleCC(value, result, index); + } else if (contains(value, index, 2, "CK", "CG", "CQ")) { + result.append('K'); + index += 2; + } else if (contains(value, index, 2, "CI", "CE", "CY")) { + //-- Italian vs. English --// + if (contains(value, index, 3, "CIO", "CIE", "CIA")) { + result.append('S', 'X'); + } else { + result.append('S'); + } + index += 2; + } else { + result.append('K'); + if (contains(value, index + 1, 2, " C", " Q", " G")) { + //-- Mac Caffrey, Mac Gregor --// + index += 3; + } else if (contains(value, index + 1, 1, "C", "K", "Q") && + !contains(value, index + 1, 2, "CE", "CI")) { + index += 2; + } else { + index++; + } + } + + return index; + } + + /** + * Handles 'CC' cases. + */ + private int handleCC(final String value, final DoubleMetaphoneResult result, int index) { + if (contains(value, index + 2, 1, "I", "E", "H") && + !contains(value, index + 2, 2, "HU")) { + //-- "bellocchio" but not "bacchus" --// + if ((index == 1 && charAt(value, index - 1) == 'A') || + contains(value, index - 1, 5, "UCCEE", "UCCES")) { + //-- "accident", "accede", "succeed" --// + result.append("KS"); + } else { + //-- "bacci", "bertucci", other Italian --// + result.append('X'); + } + index += 3; + } else { // Pierce's rule + result.append('K'); + index += 2; + } + + return index; + } + + /** + * Handles 'CH' cases. + */ + private int handleCH(final String value, final DoubleMetaphoneResult result, final int index) { + if (index > 0 && contains(value, index, 4, "CHAE")) { // Michael + result.append('K', 'X'); + return index + 2; + } else if (conditionCH0(value, index)) { + //-- Greek roots ("chemistry", "chorus", etc.) --// + result.append('K'); + return index + 2; + } else if (conditionCH1(value, index)) { + //-- Germanic, Greek, or otherwise 'ch' for 'kh' sound --// + result.append('K'); + return index + 2; + } else { + if (index > 0) { + if (contains(value, 0, 2, "MC")) { + result.append('K'); + } else { + result.append('X', 'K'); + } + } else { + result.append('X'); + } + return index + 2; + } + } + + /** + * Handles 'D' cases. + */ + private int handleD(final String value, final DoubleMetaphoneResult result, int index) { + if (contains(value, index, 2, "DG")) { + //-- "Edge" --// + if (contains(value, index + 2, 1, "I", "E", "Y")) { + result.append('J'); + index += 3; + //-- "Edgar" --// + } else { + result.append("TK"); + index += 2; + } + } else if (contains(value, index, 2, "DT", "DD")) { + result.append('T'); + index += 2; + } else { + result.append('T'); + index++; + } + return index; + } + + /** + * Handles 'G' cases. + */ + private int handleG(final String value, final DoubleMetaphoneResult result, int index, + final boolean slavoGermanic) { + if (charAt(value, index + 1) == 'H') { + index = handleGH(value, result, index); + } else if (charAt(value, index + 1) == 'N') { + if (index == 1 && isVowel(charAt(value, 0)) && !slavoGermanic) { + result.append("KN", "N"); + } else if (!contains(value, index + 2, 2, "EY") && + charAt(value, index + 1) != 'Y' && !slavoGermanic) { + result.append("N", "KN"); + } else { + result.append("KN"); + } + index = index + 2; + } else if (contains(value, index + 1, 2, "LI") && !slavoGermanic) { + result.append("KL", "L"); + index += 2; + } else if (index == 0 && + (charAt(value, index + 1) == 'Y' || + contains(value, index + 1, 2, ES_EP_EB_EL_EY_IB_IL_IN_IE_EI_ER))) { + //-- -ges-, -gep-, -gel-, -gie- at beginning --// + result.append('K', 'J'); + index += 2; + } else if ((contains(value, index + 1, 2, "ER") || + charAt(value, index + 1) == 'Y') && + !contains(value, 0, 6, "DANGER", "RANGER", "MANGER") && + !contains(value, index - 1, 1, "E", "I") && + !contains(value, index - 1, 3, "RGY", "OGY")) { + //-- -ger-, -gy- --// + result.append('K', 'J'); + index += 2; + } else if (contains(value, index + 1, 1, "E", "I", "Y") || + contains(value, index - 1, 4, "AGGI", "OGGI")) { + //-- Italian "biaggi" --// + if (contains(value, 0 ,4, "VAN ", "VON ") || + contains(value, 0, 3, "SCH") || + contains(value, index + 1, 2, "ET")) { + //-- obvious germanic --// + result.append('K'); + } else if (contains(value, index + 1, 3, "IER")) { + result.append('J'); + } else { + result.append('J', 'K'); + } + index += 2; + } else if (charAt(value, index + 1) == 'G') { + index += 2; + result.append('K'); + } else { + index++; + result.append('K'); + } + return index; + } + + /** + * Handles 'GH' cases. + */ + private int handleGH(final String value, final DoubleMetaphoneResult result, int index) { + if (index > 0 && !isVowel(charAt(value, index - 1))) { + result.append('K'); + index += 2; + } else if (index == 0) { + if (charAt(value, index + 2) == 'I') { + result.append('J'); + } else { + result.append('K'); + } + index += 2; + } else if ((index > 1 && contains(value, index - 2, 1, "B", "H", "D")) || + (index > 2 && contains(value, index - 3, 1, "B", "H", "D")) || + (index > 3 && contains(value, index - 4, 1, "B", "H"))) { + //-- Parker's rule (with some further refinements) - "hugh" + index += 2; + } else { + if (index > 2 && charAt(value, index - 1) == 'U' && + contains(value, index - 3, 1, "C", "G", "L", "R", "T")) { + //-- "laugh", "McLaughlin", "cough", "gough", "rough", "tough" + result.append('F'); + } else if (index > 0 && charAt(value, index - 1) != 'I') { + result.append('K'); + } + index += 2; + } + return index; + } + + /** + * Handles 'H' cases. + */ + private int handleH(final String value, final DoubleMetaphoneResult result, int index) { + //-- only keep if first & before vowel or between 2 vowels --// + if ((index == 0 || isVowel(charAt(value, index - 1))) && + isVowel(charAt(value, index + 1))) { + result.append('H'); + index += 2; + //-- also takes car of "HH" --// + } else { + index++; + } + return index; + } + + /** + * Handles 'J' cases. + */ + private int handleJ(final String value, final DoubleMetaphoneResult result, int index, + final boolean slavoGermanic) { + if (contains(value, index, 4, "JOSE") || contains(value, 0, 4, "SAN ")) { + //-- obvious Spanish, "Jose", "San Jacinto" --// + if ((index == 0 && (charAt(value, index + 4) == ' ') || + value.length() == 4) || contains(value, 0, 4, "SAN ")) { + result.append('H'); + } else { + result.append('J', 'H'); + } + index++; + } else { + if (index == 0 && !contains(value, index, 4, "JOSE")) { + result.append('J', 'A'); + } else if (isVowel(charAt(value, index - 1)) && !slavoGermanic && + (charAt(value, index + 1) == 'A' || charAt(value, index + 1) == 'O')) { + result.append('J', 'H'); + } else if (index == value.length() - 1) { + result.append('J', ' '); + } else if (!contains(value, index + 1, 1, L_T_K_S_N_M_B_Z) && + !contains(value, index - 1, 1, "S", "K", "L")) { + result.append('J'); + } + + if (charAt(value, index + 1) == 'J') { + index += 2; + } else { + index++; + } + } + return index; + } + + /** + * Handles 'L' cases. + */ + private int handleL(final String value, final DoubleMetaphoneResult result, int index) { + if (charAt(value, index + 1) == 'L') { + if (conditionL0(value, index)) { + result.appendPrimary('L'); + } else { + result.append('L'); + } + index += 2; + } else { + index++; + result.append('L'); + } + return index; + } + + /** + * Handles 'P' cases. + */ + private int handleP(final String value, final DoubleMetaphoneResult result, int index) { + if (charAt(value, index + 1) == 'H') { + result.append('F'); + index += 2; + } else { + result.append('P'); + index = contains(value, index + 1, 1, "P", "B") ? index + 2 : index + 1; + } + return index; + } + + /** + * Handles 'R' cases. + */ + private int handleR(final String value, final DoubleMetaphoneResult result, final int index, + final boolean slavoGermanic) { + if (index == value.length() - 1 && !slavoGermanic && + contains(value, index - 2, 2, "IE") && + !contains(value, index - 4, 2, "ME", "MA")) { + result.appendAlternate('R'); + } else { + result.append('R'); + } + return charAt(value, index + 1) == 'R' ? index + 2 : index + 1; + } + + /** + * Handles 'S' cases. + */ + private int handleS(final String value, final DoubleMetaphoneResult result, int index, + final boolean slavoGermanic) { + if (contains(value, index - 1, 3, "ISL", "YSL")) { + //-- special cases "island", "isle", "carlisle", "carlysle" --// + index++; + } else if (index == 0 && contains(value, index, 5, "SUGAR")) { + //-- special case "sugar-" --// + result.append('X', 'S'); + index++; + } else if (contains(value, index, 2, "SH")) { + if (contains(value, index + 1, 4, "HEIM", "HOEK", "HOLM", "HOLZ")) { + //-- germanic --// + result.append('S'); + } else { + result.append('X'); + } + index += 2; + } else if (contains(value, index, 3, "SIO", "SIA") || contains(value, index, 4, "SIAN")) { + //-- Italian and Armenian --// + if (slavoGermanic) { + result.append('S'); + } else { + result.append('S', 'X'); + } + index += 3; + } else if ((index == 0 && contains(value, index + 1, 1, "M", "N", "L", "W")) || + contains(value, index + 1, 1, "Z")) { + //-- german & anglicisations, e.g. "smith" match "schmidt" // + // "snider" match "schneider" --// + //-- also, -sz- in slavic language although in hungarian it // + // is pronounced "s" --// + result.append('S', 'X'); + index = contains(value, index + 1, 1, "Z") ? index + 2 : index + 1; + } else if (contains(value, index, 2, "SC")) { + index = handleSC(value, result, index); + } else { + if (index == value.length() - 1 && contains(value, index - 2, 2, "AI", "OI")) { + //-- french e.g. "resnais", "artois" --// + result.appendAlternate('S'); + } else { + result.append('S'); + } + index = contains(value, index + 1, 1, "S", "Z") ? index + 2 : index + 1; + } + return index; + } + + /** + * Handles 'SC' cases. + */ + private int handleSC(final String value, final DoubleMetaphoneResult result, final int index) { + if (charAt(value, index + 2) == 'H') { + //-- Schlesinger's rule --// + if (contains(value, index + 3, 2, "OO", "ER", "EN", "UY", "ED", "EM")) { + //-- Dutch origin, e.g. "school", "schooner" --// + if (contains(value, index + 3, 2, "ER", "EN")) { + //-- "schermerhorn", "schenker" --// + result.append("X", "SK"); + } else { + result.append("SK"); + } + } else { + if (index == 0 && !isVowel(charAt(value, 3)) && charAt(value, 3) != 'W') { + result.append('X', 'S'); + } else { + result.append('X'); + } + } + } else if (contains(value, index + 2, 1, "I", "E", "Y")) { + result.append('S'); + } else { + result.append("SK"); + } + return index + 3; + } + + /** + * Handles 'T' cases. + */ + private int handleT(final String value, final DoubleMetaphoneResult result, int index) { + if (contains(value, index, 4, "TION")) { + result.append('X'); + index += 3; + } else if (contains(value, index, 3, "TIA", "TCH")) { + result.append('X'); + index += 3; + } else if (contains(value, index, 2, "TH") || contains(value, index, 3, "TTH")) { + if (contains(value, index + 2, 2, "OM", "AM") || + //-- special case "thomas", "thames" or germanic --// + contains(value, 0, 4, "VAN ", "VON ") || + contains(value, 0, 3, "SCH")) { + result.append('T'); + } else { + result.append('0', 'T'); + } + index += 2; + } else { + result.append('T'); + index = contains(value, index + 1, 1, "T", "D") ? index + 2 : index + 1; + } + return index; + } + + /** + * Handles 'W' cases. + */ + private int handleW(final String value, final DoubleMetaphoneResult result, int index) { + if (contains(value, index, 2, "WR")) { + //-- can also be in middle of word --// + result.append('R'); + index += 2; + } else { + if (index == 0 && (isVowel(charAt(value, index + 1)) || + contains(value, index, 2, "WH"))) { + if (isVowel(charAt(value, index + 1))) { + //-- Wasserman should match Vasserman --// + result.append('A', 'F'); + } else { + //-- need Uomo to match Womo --// + result.append('A'); + } + index++; + } else if ((index == value.length() - 1 && isVowel(charAt(value, index - 1))) || + contains(value, index - 1, 5, "EWSKI", "EWSKY", "OWSKI", "OWSKY") || + contains(value, 0, 3, "SCH")) { + //-- Arnow should match Arnoff --// + result.appendAlternate('F'); + index++; + } else if (contains(value, index, 4, "WICZ", "WITZ")) { + //-- Polish e.g. "filipowicz" --// + result.append("TS", "FX"); + index += 4; + } else { + index++; + } + } + return index; + } + + /** + * Handles 'X' cases. + */ + private int handleX(final String value, final DoubleMetaphoneResult result, int index) { + if (index == 0) { + result.append('S'); + index++; + } else { + if (!((index == value.length() - 1) && + (contains(value, index - 3, 3, "IAU", "EAU") || + contains(value, index - 2, 2, "AU", "OU")))) { + //-- French e.g. breaux --// + result.append("KS"); + } + index = contains(value, index + 1, 1, "C", "X") ? index + 2 : index + 1; + } + return index; + } + + /** + * Handles 'Z' cases. + */ + private int handleZ(final String value, final DoubleMetaphoneResult result, int index, + final boolean slavoGermanic) { + if (charAt(value, index + 1) == 'H') { + //-- Chinese pinyin e.g. "zhao" or Angelina "Zhang" --// + result.append('J'); + index += 2; + } else { + if (contains(value, index + 1, 2, "ZO", "ZI", "ZA") || + (slavoGermanic && (index > 0 && charAt(value, index - 1) != 'T'))) { + result.append("S", "TS"); + } else { + result.append('S'); + } + index = charAt(value, index + 1) == 'Z' ? index + 2 : index + 1; + } + return index; + } + + //-- BEGIN CONDITIONS --// + + /** + * Complex condition 0 for 'C'. + */ + private boolean conditionC0(final String value, final int index) { + if (contains(value, index, 4, "CHIA")) { + return true; + } else if (index <= 1) { + return false; + } else if (isVowel(charAt(value, index - 2))) { + return false; + } else if (!contains(value, index - 1, 3, "ACH")) { + return false; + } else { + final char c = charAt(value, index + 2); + return (c != 'I' && c != 'E') || + contains(value, index - 2, 6, "BACHER", "MACHER"); + } + } + + /** + * Complex condition 0 for 'CH'. + */ + private boolean conditionCH0(final String value, final int index) { + if (index != 0) { + return false; + } else if (!contains(value, index + 1, 5, "HARAC", "HARIS") && + !contains(value, index + 1, 3, "HOR", "HYM", "HIA", "HEM")) { + return false; + } else if (contains(value, 0, 5, "CHORE")) { + return false; + } else { + return true; + } + } + + /** + * Complex condition 1 for 'CH'. + */ + private boolean conditionCH1(final String value, final int index) { + return ((contains(value, 0, 4, "VAN ", "VON ") || contains(value, 0, 3, "SCH")) || + contains(value, index - 2, 6, "ORCHES", "ARCHIT", "ORCHID") || + contains(value, index + 2, 1, "T", "S") || + ((contains(value, index - 1, 1, "A", "O", "U", "E") || index == 0) && + (contains(value, index + 2, 1, L_R_N_M_B_H_F_V_W_SPACE) || index + 1 == value.length() - 1))); + } + + /** + * Complex condition 0 for 'L'. + */ + private boolean conditionL0(final String value, final int index) { + if (index == value.length() - 3 && + contains(value, index - 1, 4, "ILLO", "ILLA", "ALLE")) { + return true; + } else if ((contains(value, value.length() - 2, 2, "AS", "OS") || + contains(value, value.length() - 1, 1, "A", "O")) && + contains(value, index - 1, 4, "ALLE")) { + return true; + } else { + return false; + } + } + + /** + * Complex condition 0 for 'M'. + */ + private boolean conditionM0(final String value, final int index) { + if (charAt(value, index + 1) == 'M') { + return true; + } + return contains(value, index - 1, 3, "UMB") && + ((index + 1) == value.length() - 1 || contains(value, index + 2, 2, "ER")); + } + + //-- BEGIN HELPER FUNCTIONS --// + + /** + * Determines whether or not a value is of slavo-germanic origin. A value is + * of slavo-germanic origin if it contians any of 'W', 'K', 'CZ', or 'WITZ'. + */ + private boolean isSlavoGermanic(final String value) { + return value.indexOf('W') > -1 || value.indexOf('K') > -1 || + value.indexOf("CZ") > -1 || value.indexOf("WITZ") > -1; + } + + /** + * Determines whether or not a character is a vowel or not + */ + private boolean isVowel(final char ch) { + return VOWELS.indexOf(ch) != -1; + } + + /** + * Determines whether or not the value starts with a silent letter. It will + * return true if the value starts with any of 'GN', 'KN', + * 'PN', 'WR' or 'PS'. + */ + private boolean isSilentStart(final String value) { + boolean result = false; + for (final String element : SILENT_START) { + if (value.startsWith(element)) { + result = true; + break; + } + } + return result; + } + + /** + * Cleans the input. + */ + private String cleanInput(String input) { + if (input == null) { + return null; + } + input = input.trim(); + if (input.length() == 0) { + return null; + } + return input.toUpperCase(java.util.Locale.ENGLISH); + } + + /* + * Gets the character at index index if available, otherwise + * it returns Character.MIN_VALUE so that there is some sort + * of a default. + */ + protected char charAt(final String value, final int index) { + if (index < 0 || index >= value.length()) { + return Character.MIN_VALUE; + } + return value.charAt(index); + } + + /* + * Determines whether value contains any of the criteria starting at index start and + * matching up to length length. + */ + protected static boolean contains(final String value, final int start, final int length, + final String... criteria) { + boolean result = false; + if (start >= 0 && start + length <= value.length()) { + final String target = value.substring(start, start + length); + + for (final String element : criteria) { + if (target.equals(element)) { + result = true; + break; + } + } + } + return result; + } + + //-- BEGIN INNER CLASSES --// + + /** + * Inner class for storing results, since there is the optional alternate encoding. + */ + public class DoubleMetaphoneResult { + + private final StringBuilder primary = new StringBuilder(getMaxCodeLen()); + private final StringBuilder alternate = new StringBuilder(getMaxCodeLen()); + private final int maxLength; + + public DoubleMetaphoneResult(final int maxLength) { + this.maxLength = maxLength; + } + + public void append(final char value) { + appendPrimary(value); + appendAlternate(value); + } + + public void append(final char primary, final char alternate) { + appendPrimary(primary); + appendAlternate(alternate); + } + + public void appendPrimary(final char value) { + if (this.primary.length() < this.maxLength) { + this.primary.append(value); + } + } + + public void appendAlternate(final char value) { + if (this.alternate.length() < this.maxLength) { + this.alternate.append(value); + } + } + + public void append(final String value) { + appendPrimary(value); + appendAlternate(value); + } + + public void append(final String primary, final String alternate) { + appendPrimary(primary); + appendAlternate(alternate); + } + + public void appendPrimary(final String value) { + final int addChars = this.maxLength - this.primary.length(); + if (value.length() <= addChars) { + this.primary.append(value); + } else { + this.primary.append(value.substring(0, addChars)); + } + } + + public void appendAlternate(final String value) { + final int addChars = this.maxLength - this.alternate.length(); + if (value.length() <= addChars) { + this.alternate.append(value); + } else { + this.alternate.append(value.substring(0, addChars)); + } + } + + public String getPrimary() { + return this.primary.toString(); + } + + public String getAlternate() { + return this.alternate.toString(); + } + + public boolean isComplete() { + return this.primary.length() >= this.maxLength && + this.alternate.length() >= this.maxLength; + } + } +} diff --git a/src/org/apache/commons/codec/language/MatchRatingApproachEncoder.java b/src/org/apache/commons/codec/language/MatchRatingApproachEncoder.java new file mode 100644 index 00000000..95858b43 --- /dev/null +++ b/src/org/apache/commons/codec/language/MatchRatingApproachEncoder.java @@ -0,0 +1,426 @@ +/* + * 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 org.apache.commons.codec.language; + +import java.util.Locale; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Match Rating Approach Phonetic Algorithm Developed by Western Airlines in 1977. + * + * This class is immutable and thread-safe. + * + * @see Wikipedia - Match Rating Approach + * @since 1.8 + */ +public class MatchRatingApproachEncoder implements StringEncoder { + + private static final String SPACE = " "; + + private static final String EMPTY = ""; + + /** + * Constants used mainly for the min rating value. + */ + private static final int ONE = 1, TWO = 2, THREE = 3, FOUR = 4, FIVE = 5, SIX = 6, SEVEN = 7, EIGHT = 8, + ELEVEN = 11, TWELVE = 12; + + /** + * The plain letter equivalent of the accented letters. + */ + private static final String PLAIN_ASCII = "AaEeIiOoUu" + // grave + "AaEeIiOoUuYy" + // acute + "AaEeIiOoUuYy" + // circumflex + "AaOoNn" + // tilde + "AaEeIiOoUuYy" + // umlaut + "Aa" + // ring + "Cc" + // cedilla + "OoUu"; // double acute + + /** + * Unicode characters corresponding to various accented letters. For example: \u00DA is U acute etc... + */ + private static final String UNICODE = "\u00C0\u00E0\u00C8\u00E8\u00CC\u00EC\u00D2\u00F2\u00D9\u00F9" + + "\u00C1\u00E1\u00C9\u00E9\u00CD\u00ED\u00D3\u00F3\u00DA\u00FA\u00DD\u00FD" + + "\u00C2\u00E2\u00CA\u00EA\u00CE\u00EE\u00D4\u00F4\u00DB\u00FB\u0176\u0177" + + "\u00C3\u00E3\u00D5\u00F5\u00D1\u00F1" + + "\u00C4\u00E4\u00CB\u00EB\u00CF\u00EF\u00D6\u00F6\u00DC\u00FC\u0178\u00FF" + + "\u00C5\u00E5" + "\u00C7\u00E7" + "\u0150\u0151\u0170\u0171"; + + private static final String[] DOUBLE_CONSONANT = + new String[] { "BB", "CC", "DD", "FF", "GG", "HH", "JJ", "KK", "LL", "MM", "NN", "PP", "QQ", "RR", "SS", + "TT", "VV", "WW", "XX", "YY", "ZZ" }; + + /** + * Cleans up a name: 1. Upper-cases everything 2. Removes some common punctuation 3. Removes accents 4. Removes any + * spaces. + * + *

API Usage

+ *

+ * Consider this method private, it is package protected for unit testing only. + *

+ * + * @param name + * The name to be cleaned + * @return The cleaned name + */ + String cleanName(final String name) { + String upperName = name.toUpperCase(Locale.ENGLISH); + + final String[] charsToTrim = { "\\-", "[&]", "\\'", "\\.", "[\\,]" }; + for (final String str : charsToTrim) { + upperName = upperName.replaceAll(str, EMPTY); + } + + upperName = removeAccents(upperName); + upperName = upperName.replaceAll("\\s+", EMPTY); + + return upperName; + } + + /** + * Encodes an Object using the Match Rating Approach algorithm. Method is here to satisfy the requirements of the + * Encoder interface Throws an EncoderException if input object is not of type java.lang.String. + * + * @param pObject + * Object to encode + * @return An object (or type java.lang.String) containing the Match Rating Approach code which corresponds to the + * String supplied. + * @throws EncoderException + * if the parameter supplied is not of type java.lang.String + */ + @Override + public final Object encode(final Object pObject) throws EncoderException { + if (!(pObject instanceof String)) { + throw new EncoderException( + "Parameter supplied to Match Rating Approach encoder is not of type java.lang.String"); + } + return encode((String) pObject); + } + + /** + * Encodes a String using the Match Rating Approach (MRA) algorithm. + * + * @param name + * String object to encode + * @return The MRA code corresponding to the String supplied + */ + @Override + public final String encode(String name) { + // Bulletproof for trivial input - NINO + if (name == null || EMPTY.equalsIgnoreCase(name) || SPACE.equalsIgnoreCase(name) || name.length() == 1) { + return EMPTY; + } + + // Preprocessing + name = cleanName(name); + + // BEGIN: Actual encoding part of the algorithm... + // 1. Delete all vowels unless the vowel begins the word + name = removeVowels(name); + + // 2. Remove second consonant from any double consonant + name = removeDoubleConsonants(name); + + // 3. Reduce codex to 6 letters by joining the first 3 and last 3 letters + name = getFirst3Last3(name); + + return name; + } + + /** + * Gets the first and last 3 letters of a name (if > 6 characters) Else just returns the name. + * + *

API Usage

+ *

+ * Consider this method private, it is package protected for unit testing only. + *

+ * + * @param name + * The string to get the substrings from + * @return Annexed first and last 3 letters of input word. + */ + String getFirst3Last3(final String name) { + final int nameLength = name.length(); + + if (nameLength > SIX) { + final String firstThree = name.substring(0, THREE); + final String lastThree = name.substring(nameLength - THREE, nameLength); + return firstThree + lastThree; + } else { + return name; + } + } + + /** + * Obtains the min rating of the length sum of the 2 names. In essence the larger the sum length the smaller the + * min rating. Values strictly from documentation. + * + *

API Usage

+ *

+ * Consider this method private, it is package protected for unit testing only. + *

+ * + * @param sumLength + * The length of 2 strings sent down + * @return The min rating value + */ + int getMinRating(final int sumLength) { + int minRating = 0; + + if (sumLength <= FOUR) { + minRating = FIVE; + } else if (sumLength >= FIVE && sumLength <= SEVEN) { + minRating = FOUR; + } else if (sumLength >= EIGHT && sumLength <= ELEVEN) { + minRating = THREE; + } else if (sumLength == TWELVE) { + minRating = TWO; + } else { + minRating = ONE; // docs said little here. + } + + return minRating; + } + + /** + * Determines if two names are homophonous via Match Rating Approach (MRA) algorithm. It should be noted that the + * strings are cleaned in the same way as {@link #encode(String)}. + * + * @param name1 + * First of the 2 strings (names) to compare + * @param name2 + * Second of the 2 names to compare + * @return true if the encodings are identical false otherwise. + */ + public boolean isEncodeEquals(String name1, String name2) { + // Bulletproof for trivial input - NINO + if (name1 == null || EMPTY.equalsIgnoreCase(name1) || SPACE.equalsIgnoreCase(name1)) { + return false; + } else if (name2 == null || EMPTY.equalsIgnoreCase(name2) || SPACE.equalsIgnoreCase(name2)) { + return false; + } else if (name1.length() == 1 || name2.length() == 1) { + return false; + } else if (name1.equalsIgnoreCase(name2)) { + return true; + } + + // Preprocessing + name1 = cleanName(name1); + name2 = cleanName(name2); + + // Actual MRA Algorithm + + // 1. Remove vowels + name1 = removeVowels(name1); + name2 = removeVowels(name2); + + // 2. Remove double consonants + name1 = removeDoubleConsonants(name1); + name2 = removeDoubleConsonants(name2); + + // 3. Reduce down to 3 letters + name1 = getFirst3Last3(name1); + name2 = getFirst3Last3(name2); + + // 4. Check for length difference - if 3 or greater then no similarity + // comparison is done + if (Math.abs(name1.length() - name2.length()) >= THREE) { + return false; + } + + // 5. Obtain the minimum rating value by calculating the length sum of the + // encoded Strings and sending it down. + final int sumLength = Math.abs(name1.length() + name2.length()); + int minRating = 0; + minRating = getMinRating(sumLength); + + // 6. Process the encoded Strings from left to right and remove any + // identical characters found from both Strings respectively. + final int count = leftToRightThenRightToLeftProcessing(name1, name2); + + // 7. Each PNI item that has a similarity rating equal to or greater than + // the min is considered to be a good candidate match + return count >= minRating; + + } + + /** + * Determines if a letter is a vowel. + * + *

API Usage

+ *

+ * Consider this method private, it is package protected for unit testing only. + *

+ * + * @param letter + * The letter under investiagtion + * @return True if a vowel, else false + */ + boolean isVowel(final String letter) { + return letter.equalsIgnoreCase("E") || letter.equalsIgnoreCase("A") || letter.equalsIgnoreCase("O") || + letter.equalsIgnoreCase("I") || letter.equalsIgnoreCase("U"); + } + + /** + * Processes the names from left to right (first) then right to left removing identical letters in same positions. + * Then subtracts the longer string that remains from 6 and returns this. + * + *

API Usage

+ *

+ * Consider this method private, it is package protected for unit testing only. + *

+ * + * @param name1 + * name2 + * @return + */ + int leftToRightThenRightToLeftProcessing(final String name1, final String name2) { + final char[] name1Char = name1.toCharArray(); + final char[] name2Char = name2.toCharArray(); + + final int name1Size = name1.length() - 1; + final int name2Size = name2.length() - 1; + + String name1LtRStart = EMPTY; + String name1LtREnd = EMPTY; + + String name2RtLStart = EMPTY; + String name2RtLEnd = EMPTY; + + for (int i = 0; i < name1Char.length; i++) { + if (i > name2Size) { + break; + } + + name1LtRStart = name1.substring(i, i + 1); + name1LtREnd = name1.substring(name1Size - i, name1Size - i + 1); + + name2RtLStart = name2.substring(i, i + 1); + name2RtLEnd = name2.substring(name2Size - i, name2Size - i + 1); + + // Left to right... + if (name1LtRStart.equals(name2RtLStart)) { + name1Char[i] = ' '; + name2Char[i] = ' '; + } + + // Right to left... + if (name1LtREnd.equals(name2RtLEnd)) { + name1Char[name1Size - i] = ' '; + name2Char[name2Size - i] = ' '; + } + } + + // Char arrays -> string & remove extraneous space + final String strA = new String(name1Char).replaceAll("\\s+", EMPTY); + final String strB = new String(name2Char).replaceAll("\\s+", EMPTY); + + // Final bit - subtract longest string from 6 and return this int value + if (strA.length() > strB.length()) { + return Math.abs(SIX - strA.length()); + } else { + return Math.abs(SIX - strB.length()); + } + } + + /** + * Removes accented letters and replaces with non-accented ascii equivalent Case is preserved. + * http://www.codecodex.com/wiki/Remove_accent_from_letters_%28ex_.%C3%A9_to_e%29 + * + * @param accentedWord + * The word that may have accents in it. + * @return De-accented word + */ + String removeAccents(final String accentedWord) { + if (accentedWord == null) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + final int n = accentedWord.length(); + + for (int i = 0; i < n; i++) { + final char c = accentedWord.charAt(i); + final int pos = UNICODE.indexOf(c); + if (pos > -1) { + sb.append(PLAIN_ASCII.charAt(pos)); + } else { + sb.append(c); + } + } + + return sb.toString(); + } + + /** + * Replaces any double consonant pair with the single letter equivalent. + * + *

API Usage

+ *

+ * Consider this method private, it is package protected for unit testing only. + *

+ * + * @param name + * String to have double consonants removed + * @return Single consonant word + */ + String removeDoubleConsonants(final String name) { + String replacedName = name.toUpperCase(); + for (final String dc : DOUBLE_CONSONANT) { + if (replacedName.contains(dc)) { + final String singleLetter = dc.substring(0, 1); + replacedName = replacedName.replace(dc, singleLetter); + } + } + return replacedName; + } + + /** + * Deletes all vowels unless the vowel begins the word. + * + *

API Usage

+ *

+ * Consider this method private, it is package protected for unit testing only. + *

+ * + * @param name + * The name to have vowels removed + * @return De-voweled word + */ + String removeVowels(String name) { + // Extract first letter + final String firstLetter = name.substring(0, 1); + + name = name.replaceAll("A", EMPTY); + name = name.replaceAll("E", EMPTY); + name = name.replaceAll("I", EMPTY); + name = name.replaceAll("O", EMPTY); + name = name.replaceAll("U", EMPTY); + + name = name.replaceAll("\\s{2,}\\b", SPACE); + + // return isVowel(firstLetter) ? (firstLetter + name) : name; + if (isVowel(firstLetter)) { + return firstLetter + name; + } else { + return name; + } + } +} diff --git a/src/org/apache/commons/codec/language/Metaphone.java b/src/org/apache/commons/codec/language/Metaphone.java new file mode 100644 index 00000000..766eda8c --- /dev/null +++ b/src/org/apache/commons/codec/language/Metaphone.java @@ -0,0 +1,430 @@ +/* + * 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 org.apache.commons.codec.language; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Encodes a string into a Metaphone value. + *

+ * Initial Java implementation by William B. Brogden. December, 1997. + * Permission given by wbrogden for code to be used anywhere. + *

+ * Hanging on the Metaphone by Lawrence Philips in Computer Language of Dec. 1990, + * p 39. + *

+ * Note, that this does not match the algorithm that ships with PHP, or the algorithm found in the Perl implementations: + *

+ * + *

+ * They have had undocumented changes from the originally published algorithm. + * For more information, see CODEC-57. + *

+ * This class is conditionally thread-safe. + * The instance field {@link #maxCodeLen} is mutable {@link #setMaxCodeLen(int)} + * but is not volatile, and accesses are not synchronized. + * If an instance of the class is shared between threads, the caller needs to ensure that suitable synchronization + * is used to ensure safe publication of the value between threads, and must not invoke {@link #setMaxCodeLen(int)} + * after initial setup. + * + * @version $Id$ + */ +public class Metaphone implements StringEncoder { + + /** + * Five values in the English language + */ + private static final String VOWELS = "AEIOU"; + + /** + * Variable used in Metaphone algorithm + */ + private static final String FRONTV = "EIY"; + + /** + * Variable used in Metaphone algorithm + */ + private static final String VARSON = "CSPTG"; + + /** + * The max code length for metaphone is 4 + */ + private int maxCodeLen = 4; + + /** + * Creates an instance of the Metaphone encoder + */ + public Metaphone() { + super(); + } + + /** + * Find the metaphone value of a String. This is similar to the + * soundex algorithm, but better at finding similar sounding words. + * All input is converted to upper case. + * Limitations: Input format is expected to be a single ASCII word + * with only characters in the A - Z range, no punctuation or numbers. + * + * @param txt String to find the metaphone code for + * @return A metaphone code corresponding to the String supplied + */ + public String metaphone(final String txt) { + boolean hard = false; + int txtLength; + if (txt == null || (txtLength = txt.length()) == 0) { + return ""; + } + // single character is itself + if (txtLength == 1) { + return txt.toUpperCase(java.util.Locale.ENGLISH); + } + + final char[] inwd = txt.toUpperCase(java.util.Locale.ENGLISH).toCharArray(); + + final StringBuilder local = new StringBuilder(40); // manipulate + final StringBuilder code = new StringBuilder(10); // output + // handle initial 2 characters exceptions + switch(inwd[0]) { + case 'K': + case 'G': + case 'P': /* looking for KN, etc*/ + if (inwd[1] == 'N') { + local.append(inwd, 1, inwd.length - 1); + } else { + local.append(inwd); + } + break; + case 'A': /* looking for AE */ + if (inwd[1] == 'E') { + local.append(inwd, 1, inwd.length - 1); + } else { + local.append(inwd); + } + break; + case 'W': /* looking for WR or WH */ + if (inwd[1] == 'R') { // WR -> R + local.append(inwd, 1, inwd.length - 1); + break; + } + if (inwd[1] == 'H') { + local.append(inwd, 1, inwd.length - 1); + local.setCharAt(0, 'W'); // WH -> W + } else { + local.append(inwd); + } + break; + case 'X': /* initial X becomes S */ + inwd[0] = 'S'; + local.append(inwd); + break; + default: + local.append(inwd); + } // now local has working string with initials fixed + + final int wdsz = local.length(); + int n = 0; + + while (code.length() < this.getMaxCodeLen() && + n < wdsz ) { // max code size of 4 works well + final char symb = local.charAt(n); + // remove duplicate letters except C + if (symb != 'C' && isPreviousChar( local, n, symb ) ) { + n++; + } else { // not dup + switch(symb) { + case 'A': + case 'E': + case 'I': + case 'O': + case 'U': + if (n == 0) { + code.append(symb); + } + break; // only use vowel if leading char + case 'B': + if ( isPreviousChar(local, n, 'M') && + isLastChar(wdsz, n) ) { // B is silent if word ends in MB + break; + } + code.append(symb); + break; + case 'C': // lots of C special cases + /* discard if SCI, SCE or SCY */ + if ( isPreviousChar(local, n, 'S') && + !isLastChar(wdsz, n) && + FRONTV.indexOf(local.charAt(n + 1)) >= 0 ) { + break; + } + if (regionMatch(local, n, "CIA")) { // "CIA" -> X + code.append('X'); + break; + } + if (!isLastChar(wdsz, n) && + FRONTV.indexOf(local.charAt(n + 1)) >= 0) { + code.append('S'); + break; // CI,CE,CY -> S + } + if (isPreviousChar(local, n, 'S') && + isNextChar(local, n, 'H') ) { // SCH->sk + code.append('K'); + break; + } + if (isNextChar(local, n, 'H')) { // detect CH + if (n == 0 && + wdsz >= 3 && + isVowel(local,2) ) { // CH consonant -> K consonant + code.append('K'); + } else { + code.append('X'); // CHvowel -> X + } + } else { + code.append('K'); + } + break; + case 'D': + if (!isLastChar(wdsz, n + 1) && + isNextChar(local, n, 'G') && + FRONTV.indexOf(local.charAt(n + 2)) >= 0) { // DGE DGI DGY -> J + code.append('J'); n += 2; + } else { + code.append('T'); + } + break; + case 'G': // GH silent at end or before consonant + if (isLastChar(wdsz, n + 1) && + isNextChar(local, n, 'H')) { + break; + } + if (!isLastChar(wdsz, n + 1) && + isNextChar(local,n,'H') && + !isVowel(local,n+2)) { + break; + } + if (n > 0 && + ( regionMatch(local, n, "GN") || + regionMatch(local, n, "GNED") ) ) { + break; // silent G + } + if (isPreviousChar(local, n, 'G')) { + // NOTE: Given that duplicated chars are removed, I don't see how this can ever be true + hard = true; + } else { + hard = false; + } + if (!isLastChar(wdsz, n) && + FRONTV.indexOf(local.charAt(n + 1)) >= 0 && + !hard) { + code.append('J'); + } else { + code.append('K'); + } + break; + case 'H': + if (isLastChar(wdsz, n)) { + break; // terminal H + } + if (n > 0 && + VARSON.indexOf(local.charAt(n - 1)) >= 0) { + break; + } + if (isVowel(local,n+1)) { + code.append('H'); // Hvowel + } + break; + case 'F': + case 'J': + case 'L': + case 'M': + case 'N': + case 'R': + code.append(symb); + break; + case 'K': + if (n > 0) { // not initial + if (!isPreviousChar(local, n, 'C')) { + code.append(symb); + } + } else { + code.append(symb); // initial K + } + break; + case 'P': + if (isNextChar(local,n,'H')) { + // PH -> F + code.append('F'); + } else { + code.append(symb); + } + break; + case 'Q': + code.append('K'); + break; + case 'S': + if (regionMatch(local,n,"SH") || + regionMatch(local,n,"SIO") || + regionMatch(local,n,"SIA")) { + code.append('X'); + } else { + code.append('S'); + } + break; + case 'T': + if (regionMatch(local,n,"TIA") || + regionMatch(local,n,"TIO")) { + code.append('X'); + break; + } + if (regionMatch(local,n,"TCH")) { + // Silent if in "TCH" + break; + } + // substitute numeral 0 for TH (resembles theta after all) + if (regionMatch(local,n,"TH")) { + code.append('0'); + } else { + code.append('T'); + } + break; + case 'V': + code.append('F'); break; + case 'W': + case 'Y': // silent if not followed by vowel + if (!isLastChar(wdsz,n) && + isVowel(local,n+1)) { + code.append(symb); + } + break; + case 'X': + code.append('K'); + code.append('S'); + break; + case 'Z': + code.append('S'); + break; + default: + // do nothing + break; + } // end switch + n++; + } // end else from symb != 'C' + if (code.length() > this.getMaxCodeLen()) { + code.setLength(this.getMaxCodeLen()); + } + } + return code.toString(); + } + + private boolean isVowel(final StringBuilder string, final int index) { + return VOWELS.indexOf(string.charAt(index)) >= 0; + } + + private boolean isPreviousChar(final StringBuilder string, final int index, final char c) { + boolean matches = false; + if( index > 0 && + index < string.length() ) { + matches = string.charAt(index - 1) == c; + } + return matches; + } + + private boolean isNextChar(final StringBuilder string, final int index, final char c) { + boolean matches = false; + if( index >= 0 && + index < string.length() - 1 ) { + matches = string.charAt(index + 1) == c; + } + return matches; + } + + private boolean regionMatch(final StringBuilder string, final int index, final String test) { + boolean matches = false; + if( index >= 0 && + index + test.length() - 1 < string.length() ) { + final String substring = string.substring( index, index + test.length()); + matches = substring.equals( test ); + } + return matches; + } + + private boolean isLastChar(final int wdsz, final int n) { + return n + 1 == wdsz; + } + + + /** + * Encodes an Object using the metaphone algorithm. This method + * is provided in order to satisfy the requirements of the + * Encoder interface, and will throw an EncoderException if the + * supplied object is not of type java.lang.String. + * + * @param obj Object to encode + * @return An object (or type java.lang.String) containing the + * metaphone code which corresponds to the String supplied. + * @throws EncoderException if the parameter supplied is not + * of type java.lang.String + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (!(obj instanceof String)) { + throw new EncoderException("Parameter supplied to Metaphone encode is not of type java.lang.String"); + } + return metaphone((String) obj); + } + + /** + * Encodes a String using the Metaphone algorithm. + * + * @param str String object to encode + * @return The metaphone code corresponding to the String supplied + */ + @Override + public String encode(final String str) { + return metaphone(str); + } + + /** + * Tests is the metaphones of two strings are identical. + * + * @param str1 First of two strings to compare + * @param str2 Second of two strings to compare + * @return true if the metaphones of these strings are identical, + * false otherwise. + */ + public boolean isMetaphoneEqual(final String str1, final String str2) { + return metaphone(str1).equals(metaphone(str2)); + } + + /** + * Returns the maxCodeLen. + * @return int + */ + public int getMaxCodeLen() { return this.maxCodeLen; } + + /** + * Sets the maxCodeLen. + * @param maxCodeLen The maxCodeLen to set + */ + public void setMaxCodeLen(final int maxCodeLen) { this.maxCodeLen = maxCodeLen; } + +} diff --git a/src/org/apache/commons/codec/language/Nysiis.java b/src/org/apache/commons/codec/language/Nysiis.java new file mode 100644 index 00000000..14c1505c --- /dev/null +++ b/src/org/apache/commons/codec/language/Nysiis.java @@ -0,0 +1,319 @@ +/* + * 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 org.apache.commons.codec.language; + +import java.util.regex.Pattern; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Encodes a string into a NYSIIS value. NYSIIS is an encoding used to relate similar names, but can also be used as a + * general purpose scheme to find word with similar phonemes. + *

+ * NYSIIS features an accuracy increase of 2.7% over the traditional Soundex algorithm. + *

+ * Algorithm description: + *

+ * 1. Transcode first characters of name
+ *   1a. MAC ->   MCC
+ *   1b. KN  ->   NN
+ *   1c. K   ->   C
+ *   1d. PH  ->   FF
+ *   1e. PF  ->   FF
+ *   1f. SCH ->   SSS
+ * 2. Transcode last characters of name
+ *   2a. EE, IE          ->   Y
+ *   2b. DT,RT,RD,NT,ND  ->   D
+ * 3. First character of key = first character of name
+ * 4. Transcode remaining characters by following these rules, incrementing by one character each time
+ *   4a. EV  ->   AF  else A,E,I,O,U -> A
+ *   4b. Q   ->   G
+ *   4c. Z   ->   S
+ *   4d. M   ->   N
+ *   4e. KN  ->   N   else K -> C
+ *   4f. SCH ->   SSS
+ *   4g. PH  ->   FF
+ *   4h. H   ->   If previous or next is nonvowel, previous
+ *   4i. W   ->   If previous is vowel, previous
+ *   4j. Add current to key if current != last key character
+ * 5. If last character is S, remove it
+ * 6. If last characters are AY, replace with Y
+ * 7. If last character is A, remove it
+ * 8. Collapse all strings of repeated characters
+ * 9. Add original first character of name as first character of key
+ * 
+ *

+ * This class is immutable and thread-safe. + * + * @see NYSIIS on Wikipedia + * @see NYSIIS on dropby.com + * @see Soundex + * @since 1.7 + * @version $Id$ + */ +public class Nysiis implements StringEncoder { + + private static final char[] CHARS_A = new char[] { 'A' }; + private static final char[] CHARS_AF = new char[] { 'A', 'F' }; + private static final char[] CHARS_C = new char[] { 'C' }; + private static final char[] CHARS_FF = new char[] { 'F', 'F' }; + private static final char[] CHARS_G = new char[] { 'G' }; + private static final char[] CHARS_N = new char[] { 'N' }; + private static final char[] CHARS_NN = new char[] { 'N', 'N' }; + private static final char[] CHARS_S = new char[] { 'S' }; + private static final char[] CHARS_SSS = new char[] { 'S', 'S', 'S' }; + + private static final Pattern PAT_MAC = Pattern.compile("^MAC"); + private static final Pattern PAT_KN = Pattern.compile("^KN"); + private static final Pattern PAT_K = Pattern.compile("^K"); + private static final Pattern PAT_PH_PF = Pattern.compile("^(PH|PF)"); + private static final Pattern PAT_SCH = Pattern.compile("^SCH"); + private static final Pattern PAT_EE_IE = Pattern.compile("(EE|IE)$"); + private static final Pattern PAT_DT_ETC = Pattern.compile("(DT|RT|RD|NT|ND)$"); + + private static final char SPACE = ' '; + private static final int TRUE_LENGTH = 6; + + /** + * Tests if the given character is a vowel. + * + * @param c + * the character to test + * @return true if the character is a vowel, false otherwise + */ + private static boolean isVowel(final char c) { + return c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U'; + } + + /** + * Transcodes the remaining parts of the String. The method operates on a sliding window, looking at 4 characters at + * a time: [i-1, i, i+1, i+2]. + * + * @param prev + * the previous character + * @param curr + * the current character + * @param next + * the next character + * @param aNext + * the after next character + * @return a transcoded array of characters, starting from the current position + */ + private static char[] transcodeRemaining(final char prev, final char curr, final char next, final char aNext) { + // 1. EV -> AF + if (curr == 'E' && next == 'V') { + return CHARS_AF; + } + + // A, E, I, O, U -> A + if (isVowel(curr)) { + return CHARS_A; + } + + // 2. Q -> G, Z -> S, M -> N + if (curr == 'Q') { + return CHARS_G; + } else if (curr == 'Z') { + return CHARS_S; + } else if (curr == 'M') { + return CHARS_N; + } + + // 3. KN -> NN else K -> C + if (curr == 'K') { + if (next == 'N') { + return CHARS_NN; + } else { + return CHARS_C; + } + } + + // 4. SCH -> SSS + if (curr == 'S' && next == 'C' && aNext == 'H') { + return CHARS_SSS; + } + + // PH -> FF + if (curr == 'P' && next == 'H') { + return CHARS_FF; + } + + // 5. H -> If previous or next is a non vowel, previous. + if (curr == 'H' && (!isVowel(prev) || !isVowel(next))) { + return new char[] { prev }; + } + + // 6. W -> If previous is vowel, previous. + if (curr == 'W' && isVowel(prev)) { + return new char[] { prev }; + } + + return new char[] { curr }; + } + + /** Indicates the strict mode. */ + private final boolean strict; + + /** + * Creates an instance of the {@link Nysiis} encoder with strict mode (original form), + * i.e. encoded strings have a maximum length of 6. + */ + public Nysiis() { + this(true); + } + + /** + * Create an instance of the {@link Nysiis} encoder with the specified strict mode: + * + *

    + *
  • true: encoded strings have a maximum length of 6
  • + *
  • false: encoded strings may have arbitrary length
  • + *
+ * + * @param strict + * the strict mode + */ + public Nysiis(final boolean strict) { + this.strict = strict; + } + + /** + * Encodes an Object using the NYSIIS algorithm. This method is provided in order to satisfy the requirements of the + * Encoder interface, and will throw an {@link EncoderException} if the supplied object is not of type + * {@link String}. + * + * @param obj + * Object to encode + * @return An object (or a {@link String}) containing the NYSIIS code which corresponds to the given String. + * @throws EncoderException + * if the parameter supplied is not of a {@link String} + * @throws IllegalArgumentException + * if a character is not mapped + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (!(obj instanceof String)) { + throw new EncoderException("Parameter supplied to Nysiis encode is not of type java.lang.String"); + } + return this.nysiis((String) obj); + } + + /** + * Encodes a String using the NYSIIS algorithm. + * + * @param str + * A String object to encode + * @return A Nysiis code corresponding to the String supplied + * @throws IllegalArgumentException + * if a character is not mapped + */ + @Override + public String encode(final String str) { + return this.nysiis(str); + } + + /** + * Indicates the strict mode for this {@link Nysiis} encoder. + * + * @return true if the encoder is configured for strict mode, false otherwise + */ + public boolean isStrict() { + return this.strict; + } + + /** + * Retrieves the NYSIIS code for a given String object. + * + * @param str + * String to encode using the NYSIIS algorithm + * @return A NYSIIS code for the String supplied + */ + public String nysiis(String str) { + if (str == null) { + return null; + } + + // Use the same clean rules as Soundex + str = SoundexUtils.clean(str); + + if (str.length() == 0) { + return str; + } + + // Translate first characters of name: + // MAC -> MCC, KN -> NN, K -> C, PH | PF -> FF, SCH -> SSS + str = PAT_MAC.matcher(str).replaceFirst("MCC"); + str = PAT_KN.matcher(str).replaceFirst("NN"); + str = PAT_K.matcher(str).replaceFirst("C"); + str = PAT_PH_PF.matcher(str).replaceFirst("FF"); + str = PAT_SCH.matcher(str).replaceFirst("SSS"); + + // Translate last characters of name: + // EE -> Y, IE -> Y, DT | RT | RD | NT | ND -> D + str = PAT_EE_IE.matcher(str).replaceFirst("Y"); + str = PAT_DT_ETC.matcher(str).replaceFirst("D"); + + // First character of key = first character of name. + final StringBuilder key = new StringBuilder(str.length()); + key.append(str.charAt(0)); + + // Transcode remaining characters, incrementing by one character each time + final char[] chars = str.toCharArray(); + final int len = chars.length; + + for (int i = 1; i < len; i++) { + final char next = i < len - 1 ? chars[i + 1] : SPACE; + final char aNext = i < len - 2 ? chars[i + 2] : SPACE; + final char[] transcoded = transcodeRemaining(chars[i - 1], chars[i], next, aNext); + System.arraycopy(transcoded, 0, chars, i, transcoded.length); + + // only append the current char to the key if it is different from the last one + if (chars[i] != chars[i - 1]) { + key.append(chars[i]); + } + } + + if (key.length() > 1) { + char lastChar = key.charAt(key.length() - 1); + + // If last character is S, remove it. + if (lastChar == 'S') { + key.deleteCharAt(key.length() - 1); + lastChar = key.charAt(key.length() - 1); + } + + if (key.length() > 2) { + final char last2Char = key.charAt(key.length() - 2); + // If last characters are AY, replace with Y. + if (last2Char == 'A' && lastChar == 'Y') { + key.deleteCharAt(key.length() - 2); + } + } + + // If last character is A, remove it. + if (lastChar == 'A') { + key.deleteCharAt(key.length() - 1); + } + } + + final String string = key.toString(); + return this.isStrict() ? string.substring(0, Math.min(TRUE_LENGTH, string.length())) : string; + } + +} diff --git a/src/org/apache/commons/codec/language/RefinedSoundex.java b/src/org/apache/commons/codec/language/RefinedSoundex.java new file mode 100644 index 00000000..22d6d9eb --- /dev/null +++ b/src/org/apache/commons/codec/language/RefinedSoundex.java @@ -0,0 +1,205 @@ +/* + * 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 org.apache.commons.codec.language; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Encodes a string into a Refined Soundex value. A refined soundex code is + * optimized for spell checking words. Soundex method originally developed by + * Margaret Odell and Robert Russell. + * + *

This class is immutable and thread-safe.

+ * + * @version $Id$ + */ +public class RefinedSoundex implements StringEncoder { + + /** + * @since 1.4 + */ + public static final String US_ENGLISH_MAPPING_STRING = "01360240043788015936020505"; + + /** + * RefinedSoundex is *refined* for a number of reasons one being that the + * mappings have been altered. This implementation contains default + * mappings for US English. + */ + private static final char[] US_ENGLISH_MAPPING = US_ENGLISH_MAPPING_STRING.toCharArray(); + + /** + * Every letter of the alphabet is "mapped" to a numerical value. This char + * array holds the values to which each letter is mapped. This + * implementation contains a default map for US_ENGLISH + */ + private final char[] soundexMapping; + + /** + * This static variable contains an instance of the RefinedSoundex using + * the US_ENGLISH mapping. + */ + public static final RefinedSoundex US_ENGLISH = new RefinedSoundex(); + + /** + * Creates an instance of the RefinedSoundex object using the default US + * English mapping. + */ + public RefinedSoundex() { + this.soundexMapping = US_ENGLISH_MAPPING; + } + + /** + * Creates a refined soundex instance using a custom mapping. This + * constructor can be used to customize the mapping, and/or possibly + * provide an internationalized mapping for a non-Western character set. + * + * @param mapping + * Mapping array to use when finding the corresponding code for + * a given character + */ + public RefinedSoundex(final char[] mapping) { + this.soundexMapping = new char[mapping.length]; + System.arraycopy(mapping, 0, this.soundexMapping, 0, mapping.length); + } + + /** + * Creates a refined Soundex instance using a custom mapping. This constructor can be used to customize the mapping, + * and/or possibly provide an internationalized mapping for a non-Western character set. + * + * @param mapping + * Mapping string to use when finding the corresponding code for a given character + * @since 1.4 + */ + public RefinedSoundex(final String mapping) { + this.soundexMapping = mapping.toCharArray(); + } + + /** + * Returns the number of characters in the two encoded Strings that are the + * same. This return value ranges from 0 to the length of the shortest + * encoded String: 0 indicates little or no similarity, and 4 out of 4 (for + * example) indicates strong similarity or identical values. For refined + * Soundex, the return value can be greater than 4. + * + * @param s1 + * A String that will be encoded and compared. + * @param s2 + * A String that will be encoded and compared. + * @return The number of characters in the two encoded Strings that are the + * same from 0 to to the length of the shortest encoded String. + * + * @see SoundexUtils#difference(StringEncoder,String,String) + * @see + * MS T-SQL DIFFERENCE + * + * @throws EncoderException + * if an error occurs encoding one of the strings + * @since 1.3 + */ + public int difference(final String s1, final String s2) throws EncoderException { + return SoundexUtils.difference(this, s1, s2); + } + + /** + * Encodes an Object using the refined soundex algorithm. This method is + * provided in order to satisfy the requirements of the Encoder interface, + * and will throw an EncoderException if the supplied object is not of type + * java.lang.String. + * + * @param obj + * Object to encode + * @return An object (or type java.lang.String) containing the refined + * soundex code which corresponds to the String supplied. + * @throws EncoderException + * if the parameter supplied is not of type java.lang.String + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (!(obj instanceof String)) { + throw new EncoderException("Parameter supplied to RefinedSoundex encode is not of type java.lang.String"); + } + return soundex((String) obj); + } + + /** + * Encodes a String using the refined soundex algorithm. + * + * @param str + * A String object to encode + * @return A Soundex code corresponding to the String supplied + */ + @Override + public String encode(final String str) { + return soundex(str); + } + + /** + * Returns the mapping code for a given character. The mapping codes are + * maintained in an internal char array named soundexMapping, and the + * default values of these mappings are US English. + * + * @param c + * char to get mapping for + * @return A character (really a numeral) to return for the given char + */ + char getMappingCode(final char c) { + if (!Character.isLetter(c)) { + return 0; + } + return this.soundexMapping[Character.toUpperCase(c) - 'A']; + } + + /** + * Retrieves the Refined Soundex code for a given String object. + * + * @param str + * String to encode using the Refined Soundex algorithm + * @return A soundex code for the String supplied + */ + public String soundex(String str) { + if (str == null) { + return null; + } + str = SoundexUtils.clean(str); + if (str.length() == 0) { + return str; + } + + final StringBuilder sBuf = new StringBuilder(); + sBuf.append(str.charAt(0)); + + char last, current; + last = '*'; + + for (int i = 0; i < str.length(); i++) { + + current = getMappingCode(str.charAt(i)); + if (current == last) { + continue; + } else if (current != 0) { + sBuf.append(current); + } + + last = current; + + } + + return sBuf.toString(); + } +} diff --git a/src/org/apache/commons/codec/language/Soundex.java b/src/org/apache/commons/codec/language/Soundex.java new file mode 100644 index 00000000..2d4bbc1d --- /dev/null +++ b/src/org/apache/commons/codec/language/Soundex.java @@ -0,0 +1,254 @@ +/* + * 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 org.apache.commons.codec.language; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Encodes a string into a Soundex value. Soundex is an encoding used to relate similar names, but can also be used as a + * general purpose scheme to find word with similar phonemes. + * + * This class is thread-safe. + * Although not strictly immutable, the {@link #maxLength} field is not actually used. + * + * @version $Id$ + */ +public class Soundex implements StringEncoder { + + /** + * This is a default mapping of the 26 letters used in US English. A value of 0 for a letter position + * means do not encode. + *

+ * (This constant is provided as both an implementation convenience and to allow Javadoc to pick + * up the value for the constant values page.) + *

+ * + * @see #US_ENGLISH_MAPPING + */ + public static final String US_ENGLISH_MAPPING_STRING = "0123012#02245501262301#202"; + + /** + * This is a default mapping of the 26 letters used in US English. A value of 0 for a letter position + * means do not encode. + * + * @see Soundex#Soundex(char[]) + */ + private static final char[] US_ENGLISH_MAPPING = US_ENGLISH_MAPPING_STRING.toCharArray(); + + /** + * An instance of Soundex using the US_ENGLISH_MAPPING mapping. + * + * @see #US_ENGLISH_MAPPING + */ + public static final Soundex US_ENGLISH = new Soundex(); + + /** + * The maximum length of a Soundex code - Soundex codes are only four characters by definition. + * + * @deprecated This feature is not needed since the encoding size must be constant. Will be removed in 2.0. + */ + @Deprecated + private int maxLength = 4; + + /** + * Every letter of the alphabet is "mapped" to a numerical value. This char array holds the values to which each + * letter is mapped. This implementation contains a default map for US_ENGLISH + */ + private final char[] soundexMapping; + + /** + * Creates an instance using US_ENGLISH_MAPPING + * + * @see Soundex#Soundex(char[]) + * @see Soundex#US_ENGLISH_MAPPING + */ + public Soundex() { + this.soundexMapping = US_ENGLISH_MAPPING; + } + + /** + * Creates a soundex instance using the given mapping. This constructor can be used to provide an internationalized + * mapping for a non-Western character set. + * + * Every letter of the alphabet is "mapped" to a numerical value. This char array holds the values to which each + * letter is mapped. This implementation contains a default map for US_ENGLISH + * + * @param mapping + * Mapping array to use when finding the corresponding code for a given character + */ + public Soundex(final char[] mapping) { + this.soundexMapping = new char[mapping.length]; + System.arraycopy(mapping, 0, this.soundexMapping, 0, mapping.length); + } + + /** + * Creates a refined soundex instance using a custom mapping. This constructor can be used to customize the mapping, + * and/or possibly provide an internationalized mapping for a non-Western character set. + * + * @param mapping + * Mapping string to use when finding the corresponding code for a given character + * @since 1.4 + */ + public Soundex(final String mapping) { + this.soundexMapping = mapping.toCharArray(); + } + + /** + * Encodes the Strings and returns the number of characters in the two encoded Strings that are the same. This + * return value ranges from 0 through 4: 0 indicates little or no similarity, and 4 indicates strong similarity or + * identical values. + * + * @param s1 + * A String that will be encoded and compared. + * @param s2 + * A String that will be encoded and compared. + * @return The number of characters in the two encoded Strings that are the same from 0 to 4. + * + * @see SoundexUtils#difference(StringEncoder,String,String) + * @see MS + * T-SQL DIFFERENCE + * + * @throws EncoderException + * if an error occurs encoding one of the strings + * @since 1.3 + */ + public int difference(final String s1, final String s2) throws EncoderException { + return SoundexUtils.difference(this, s1, s2); + } + + /** + * Encodes an Object using the soundex algorithm. This method is provided in order to satisfy the requirements of + * the Encoder interface, and will throw an EncoderException if the supplied object is not of type java.lang.String. + * + * @param obj + * Object to encode + * @return An object (or type java.lang.String) containing the soundex code which corresponds to the String + * supplied. + * @throws EncoderException + * if the parameter supplied is not of type java.lang.String + * @throws IllegalArgumentException + * if a character is not mapped + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (!(obj instanceof String)) { + throw new EncoderException("Parameter supplied to Soundex encode is not of type java.lang.String"); + } + return soundex((String) obj); + } + + /** + * Encodes a String using the soundex algorithm. + * + * @param str + * A String object to encode + * @return A Soundex code corresponding to the String supplied + * @throws IllegalArgumentException + * if a character is not mapped + */ + @Override + public String encode(final String str) { + return soundex(str); + } + + /** + * Returns the maxLength. Standard Soundex + * + * @deprecated This feature is not needed since the encoding size must be constant. Will be removed in 2.0. + * @return int + */ + @Deprecated + public int getMaxLength() { + return this.maxLength; + } + + /** + * Returns the soundex mapping. + * + * @return soundexMapping. + */ + private char[] getSoundexMapping() { + return this.soundexMapping; + } + + /** + * Maps the given upper-case character to its Soundex code. + * + * @param ch + * An upper-case character. + * @return A Soundex code. + * @throws IllegalArgumentException + * Thrown if ch is not mapped. + */ + private char map(final char ch) { + final int index = ch - 'A'; + if (index < 0 || index >= this.getSoundexMapping().length) { + throw new IllegalArgumentException("The character is not mapped: " + ch); + } + return this.getSoundexMapping()[index]; + } + + /** + * Sets the maxLength. + * + * @deprecated This feature is not needed since the encoding size must be constant. Will be removed in 2.0. + * @param maxLength + * The maxLength to set + */ + @Deprecated + public void setMaxLength(final int maxLength) { + this.maxLength = maxLength; + } + + /** + * Retrieves the Soundex code for a given String object. + * + * @param str + * String to encode using the Soundex algorithm + * @return A soundex code for the String supplied + * @throws IllegalArgumentException + * if a character is not mapped + */ + public String soundex(String str) { + if (str == null) { + return null; + } + str = SoundexUtils.clean(str); + if (str.length() == 0) { + return str; + } + final char out[] = {'0', '0', '0', '0'}; + char last, mapped; + int incount = 1, count = 1; + out[0] = str.charAt(0); + // map() throws IllegalArgumentException + last = this.map(str.charAt(0)); + while (incount < str.length() && count < out.length) { + mapped = this.map(str.charAt(incount++)); + if (mapped == '0') { + last = mapped; + } else if (mapped != '#' && mapped != last) { + out[count++] = mapped; + last = mapped; + } + } + return new String(out); + } + +} diff --git a/src/org/apache/commons/codec/language/SoundexUtils.java b/src/org/apache/commons/codec/language/SoundexUtils.java new file mode 100644 index 00000000..6409eb24 --- /dev/null +++ b/src/org/apache/commons/codec/language/SoundexUtils.java @@ -0,0 +1,124 @@ +/* + * 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 org.apache.commons.codec.language; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Utility methods for {@link Soundex} and {@link RefinedSoundex} classes. + * + *

This class is immutable and thread-safe.

+ * + * @version $Id$ + * @since 1.3 + */ +final class SoundexUtils { + + /** + * Cleans up the input string before Soundex processing by only returning + * upper case letters. + * + * @param str + * The String to clean. + * @return A clean String. + */ + static String clean(final String str) { + if (str == null || str.length() == 0) { + return str; + } + final int len = str.length(); + final char[] chars = new char[len]; + int count = 0; + for (int i = 0; i < len; i++) { + if (Character.isLetter(str.charAt(i))) { + chars[count++] = str.charAt(i); + } + } + if (count == len) { + return str.toUpperCase(java.util.Locale.ENGLISH); + } + return new String(chars, 0, count).toUpperCase(java.util.Locale.ENGLISH); + } + + /** + * Encodes the Strings and returns the number of characters in the two + * encoded Strings that are the same. + *
    + *
  • For Soundex, this return value ranges from 0 through 4: 0 indicates + * little or no similarity, and 4 indicates strong similarity or identical + * values.
  • + *
  • For refined Soundex, the return value can be greater than 4.
  • + *
+ * + * @param encoder + * The encoder to use to encode the Strings. + * @param s1 + * A String that will be encoded and compared. + * @param s2 + * A String that will be encoded and compared. + * @return The number of characters in the two Soundex encoded Strings that + * are the same. + * + * @see #differenceEncoded(String,String) + * @see + * MS T-SQL DIFFERENCE + * + * @throws EncoderException + * if an error occurs encoding one of the strings + */ + static int difference(final StringEncoder encoder, final String s1, final String s2) throws EncoderException { + return differenceEncoded(encoder.encode(s1), encoder.encode(s2)); + } + + /** + * Returns the number of characters in the two Soundex encoded Strings that + * are the same. + *
    + *
  • For Soundex, this return value ranges from 0 through 4: 0 indicates + * little or no similarity, and 4 indicates strong similarity or identical + * values.
  • + *
  • For refined Soundex, the return value can be greater than 4.
  • + *
+ * + * @param es1 + * An encoded String. + * @param es2 + * An encoded String. + * @return The number of characters in the two Soundex encoded Strings that + * are the same. + * + * @see + * MS T-SQL DIFFERENCE + */ + static int differenceEncoded(final String es1, final String es2) { + + if (es1 == null || es2 == null) { + return 0; + } + final int lengthToMatch = Math.min(es1.length(), es2.length()); + int diff = 0; + for (int i = 0; i < lengthToMatch; i++) { + if (es1.charAt(i) == es2.charAt(i)) { + diff++; + } + } + return diff; + } + +} diff --git a/src/org/apache/commons/codec/language/bm/BeiderMorseEncoder.java b/src/org/apache/commons/codec/language/bm/BeiderMorseEncoder.java new file mode 100644 index 00000000..7339e7b4 --- /dev/null +++ b/src/org/apache/commons/codec/language/bm/BeiderMorseEncoder.java @@ -0,0 +1,181 @@ +/* + * 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 org.apache.commons.codec.language.bm; + +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringEncoder; + +/** + * Encodes strings into their Beider-Morse phonetic encoding. + *

+ * Beider-Morse phonetic encodings are optimised for family names. However, they may be useful for a wide range of + * words. + *

+ * This encoder is intentionally mutable to allow dynamic configuration through bean properties. As such, it is mutable, + * and may not be thread-safe. If you require a guaranteed thread-safe encoding then use {@link PhoneticEngine} + * directly. + *

+ * Encoding overview + *

+ * Beider-Morse phonetic encodings is a multi-step process. Firstly, a table of rules is consulted to guess what + * language the word comes from. For example, if it ends in "ault" then it infers that the word is French. + * Next, the word is translated into a phonetic representation using a language-specific phonetics table. Some runs of + * letters can be pronounced in multiple ways, and a single run of letters may be potentially broken up into phonemes at + * different places, so this stage results in a set of possible language-specific phonetic representations. Lastly, this + * language-specific phonetic representation is processed by a table of rules that re-writes it phonetically taking into + * account systematic pronunciation differences between languages, to move it towards a pan-indo-european phonetic + * representation. Again, sometimes there are multiple ways this could be done and sometimes things that can be + * pronounced in several ways in the source language have only one way to represent them in this average phonetic + * language, so the result is again a set of phonetic spellings. + *

+ * Some names are treated as having multiple parts. This can be due to two things. Firstly, they may be hyphenated. In + * this case, each individual hyphenated word is encoded, and then these are combined end-to-end for the final encoding. + * Secondly, some names have standard prefixes, for example, "Mac/Mc" in Scottish (English) names. As + * sometimes it is ambiguous whether the prefix is intended or is an accident of the spelling, the word is encoded once + * with the prefix and once without it. The resulting encoding contains one and then the other result. + *

+ * Encoding format + *

+ * Individual phonetic spellings of an input word are represented in upper- and lower-case roman characters. Where there + * are multiple possible phonetic representations, these are joined with a pipe (|) character. If multiple + * hyphenated words where found, or if the word may contain a name prefix, each encoded word is placed in elipses and + * these blocks are then joined with hyphens. For example, "d'ortley" has a possible prefix. The form + * without prefix encodes to "ortlaj|ortlej", while the form with prefix encodes to " + * dortlaj|dortlej". Thus, the full, combined encoding is "(ortlaj|ortlej)-(dortlaj|dortlej)". + *

+ * The encoded forms are often quite a bit longer than the input strings. This is because a single input may have many + * potential phonetic interpretations. For example, "Renault" encodes to " + * rYnDlt|rYnalt|rYnult|rinDlt|rinalt|rinult". The APPROX rules will tend to produce larger + * encodings as they consider a wider range of possible, approximate phonetic interpretations of the original word. + * Down-stream applications may wish to further process the encoding for indexing or lookup purposes, for example, by + * splitting on pipe (|) and indexing under each of these alternatives. + *

+ * Note: this version of the Beider-Morse encoding is equivalent with v3.4 of the reference implementation. + * + * @see Beider-Morse Phonetic Matching + * @see Reference implementation + * + * @since 1.6 + * @version $Id$ + */ +public class BeiderMorseEncoder implements StringEncoder { + // Implementation note: This class is a spring-friendly facade to PhoneticEngine. It allows read/write configuration + // of an immutable PhoneticEngine instance that will be delegated to for the actual encoding. + + // a cached object + private PhoneticEngine engine = new PhoneticEngine(NameType.GENERIC, RuleType.APPROX, true); + + @Override + public Object encode(final Object source) throws EncoderException { + if (!(source instanceof String)) { + throw new EncoderException("BeiderMorseEncoder encode parameter is not of type String"); + } + return encode((String) source); + } + + @Override + public String encode(final String source) throws EncoderException { + if (source == null) { + return null; + } + return this.engine.encode(source); + } + + /** + * Gets the name type currently in operation. + * + * @return the NameType currently being used + */ + public NameType getNameType() { + return this.engine.getNameType(); + } + + /** + * Gets the rule type currently in operation. + * + * @return the RuleType currently being used + */ + public RuleType getRuleType() { + return this.engine.getRuleType(); + } + + /** + * Discovers if multiple possible encodings are concatenated. + * + * @return true if multiple encodings are concatenated, false if just the first one is returned + */ + public boolean isConcat() { + return this.engine.isConcat(); + } + + /** + * Sets how multiple possible phonetic encodings are combined. + * + * @param concat + * true if multiple encodings are to be combined with a '|', false if just the first one is + * to be considered + */ + public void setConcat(final boolean concat) { + this.engine = new PhoneticEngine(this.engine.getNameType(), + this.engine.getRuleType(), + concat, + this.engine.getMaxPhonemes()); + } + + /** + * Sets the type of name. Use {@link NameType#GENERIC} unless you specifically want phonetic encodings + * optimized for Ashkenazi or Sephardic Jewish family names. + * + * @param nameType + * the NameType in use + */ + public void setNameType(final NameType nameType) { + this.engine = new PhoneticEngine(nameType, + this.engine.getRuleType(), + this.engine.isConcat(), + this.engine.getMaxPhonemes()); + } + + /** + * Sets the rule type to apply. This will widen or narrow the range of phonetic encodings considered. + * + * @param ruleType + * {@link RuleType#APPROX} or {@link RuleType#EXACT} for approximate or exact phonetic matches + */ + public void setRuleType(final RuleType ruleType) { + this.engine = new PhoneticEngine(this.engine.getNameType(), + ruleType, + this.engine.isConcat(), + this.engine.getMaxPhonemes()); + } + + /** + * Sets the number of maximum of phonemes that shall be considered by the engine. + * + * @param maxPhonemes + * the maximum number of phonemes returned by the engine + * @since 1.7 + */ + public void setMaxPhonemes(final int maxPhonemes) { + this.engine = new PhoneticEngine(this.engine.getNameType(), + this.engine.getRuleType(), + this.engine.isConcat(), + maxPhonemes); + } + +} diff --git a/src/org/apache/commons/codec/language/bm/Lang.java b/src/org/apache/commons/codec/language/bm/Lang.java new file mode 100644 index 00000000..a7ebba6e --- /dev/null +++ b/src/org/apache/commons/codec/language/bm/Lang.java @@ -0,0 +1,231 @@ +/* + * 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 org.apache.commons.codec.language.bm; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Scanner; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Language guessing utility. + *

+ * This class encapsulates rules used to guess the possible languages that a word originates from. This is + * done by reference to a whole series of rules distributed in resource files. + *

+ * Instances of this class are typically managed through the static factory method instance(). + * Unless you are developing your own language guessing rules, you will not need to interact with this class directly. + *

+ * This class is intended to be immutable and thread-safe. + *

+ * Lang resources + *

+ * Language guessing rules are typically loaded from resource files. These are UTF-8 encoded text files. + * They are systematically named following the pattern: + *

org/apache/commons/codec/language/bm/lang.txt
+ * The format of these resources is the following: + *
    + *
  • Rules: whitespace separated strings. + * There should be 3 columns to each row, and these will be interpreted as: + *
      + *
    1. pattern: a regular expression.
    2. + *
    3. languages: a '+'-separated list of languages.
    4. + *
    5. acceptOnMatch: 'true' or 'false' indicating if a match rules in or rules out the language.
    6. + *
    + *
  • + *
  • End-of-line comments: Any occurrence of '//' will cause all text following on that line to be + * discarded as a comment.
  • + *
  • Multi-line comments: Any line starting with '/*' will start multi-line commenting mode. + * This will skip all content until a line ending in '*' and '/' is found.
  • + *
  • Blank lines: All blank lines will be skipped.
  • + *
+ *

+ * Port of lang.php + * + * @since 1.6 + * @version $Id$ + */ +public class Lang { + // Implementation note: This class is divided into two sections. The first part is a static factory interface that + // exposes the LANGUAGE_RULES_RN resource as a Lang instance. The second part is the Lang instance methods that + // encapsulate a particular language-guessing rule table and the language guessing itself. + // + // It may make sense in the future to expose the private constructor to allow power users to build custom language- + // guessing rules, perhaps by marking it protected and allowing sub-classing. However, the vast majority of users + // should be strongly encouraged to use the static factory instance method to get their Lang instances. + + private static final class LangRule { + private final boolean acceptOnMatch; + private final Set languages; + private final Pattern pattern; + + private LangRule(final Pattern pattern, final Set languages, final boolean acceptOnMatch) { + this.pattern = pattern; + this.languages = languages; + this.acceptOnMatch = acceptOnMatch; + } + + public boolean matches(final String txt) { + return this.pattern.matcher(txt).find(); + } + } + + private static final Map Langs = new EnumMap(NameType.class); + + private static final String LANGUAGE_RULES_RN = "org/apache/commons/codec/language/bm/%s_lang.txt"; + + static { + for (final NameType s : NameType.values()) { + Langs.put(s, loadFromResource(String.format(LANGUAGE_RULES_RN, s.getName()), Languages.getInstance(s))); + } + } + + /** + * Gets a Lang instance for one of the supported NameTypes. + * + * @param nameType + * the NameType to look up + * @return a Lang encapsulating the language guessing rules for that name type + */ + public static Lang instance(final NameType nameType) { + return Langs.get(nameType); + } + + /** + * Loads language rules from a resource. + *

+ * In normal use, you will obtain instances of Lang through the {@link #instance(NameType)} method. + * You will only need to call this yourself if you are developing custom language mapping rules. + * + * @param languageRulesResourceName + * the fully-qualified resource name to load + * @param languages + * the languages that these rules will support + * @return a Lang encapsulating the loaded language-guessing rules. + */ + public static Lang loadFromResource(final String languageRulesResourceName, final Languages languages) { + final List rules = new ArrayList(); + final InputStream lRulesIS = Lang.class.getClassLoader().getResourceAsStream(languageRulesResourceName); + + if (lRulesIS == null) { + throw new IllegalStateException("Unable to resolve required resource:" + LANGUAGE_RULES_RN); + } + + final Scanner scanner = new Scanner(lRulesIS, ResourceConstants.ENCODING); + try { + boolean inExtendedComment = false; + while (scanner.hasNextLine()) { + final String rawLine = scanner.nextLine(); + String line = rawLine; + if (inExtendedComment) { + // check for closing comment marker, otherwise discard doc comment line + if (line.endsWith(ResourceConstants.EXT_CMT_END)) { + inExtendedComment = false; + } + } else { + if (line.startsWith(ResourceConstants.EXT_CMT_START)) { + inExtendedComment = true; + } else { + // discard comments + final int cmtI = line.indexOf(ResourceConstants.CMT); + if (cmtI >= 0) { + line = line.substring(0, cmtI); + } + + // trim leading-trailing whitespace + line = line.trim(); + + if (line.length() == 0) { + continue; // empty lines can be safely skipped + } + + // split it up + final String[] parts = line.split("\\s+"); + + if (parts.length != 3) { + throw new IllegalArgumentException("Malformed line '" + rawLine + + "' in language resource '" + languageRulesResourceName + "'"); + } + + final Pattern pattern = Pattern.compile(parts[0]); + final String[] langs = parts[1].split("\\+"); + final boolean accept = parts[2].equals("true"); + + rules.add(new LangRule(pattern, new HashSet(Arrays.asList(langs)), accept)); + } + } + } + } finally { + scanner.close(); + } + return new Lang(rules, languages); + } + + private final Languages languages; + private final List rules; + + private Lang(final List rules, final Languages languages) { + this.rules = Collections.unmodifiableList(rules); + this.languages = languages; + } + + /** + * Guesses the language of a word. + * + * @param text + * the word + * @return the language that the word originates from or {@link Languages#ANY} if there was no unique match + */ + public String guessLanguage(final String text) { + final Languages.LanguageSet ls = guessLanguages(text); + return ls.isSingleton() ? ls.getAny() : Languages.ANY; + } + + /** + * Guesses the languages of a word. + * + * @param input + * the word + * @return a Set of Strings of language names that are potential matches for the input word + */ + public Languages.LanguageSet guessLanguages(final String input) { + final String text = input.toLowerCase(Locale.ENGLISH); + + final Set langs = new HashSet(this.languages.getLanguages()); + for (final LangRule rule : this.rules) { + if (rule.matches(text)) { + if (rule.acceptOnMatch) { + langs.retainAll(rule.languages); + } else { + langs.removeAll(rule.languages); + } + } + } + + final Languages.LanguageSet ls = Languages.LanguageSet.from(langs); + return ls.equals(Languages.NO_LANGUAGES) ? Languages.ANY_LANGUAGE : ls; + } +} diff --git a/src/org/apache/commons/codec/language/bm/Languages.java b/src/org/apache/commons/codec/language/bm/Languages.java new file mode 100644 index 00000000..e092c15c --- /dev/null +++ b/src/org/apache/commons/codec/language/bm/Languages.java @@ -0,0 +1,295 @@ +/* + * 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 org.apache.commons.codec.language.bm; + +import java.io.InputStream; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Scanner; +import java.util.Set; + +/** + * Language codes. + *

+ * Language codes are typically loaded from resource files. These are UTF-8 encoded text files. They are + * systematically named following the pattern: + *

org/apache/commons/codec/language/bm/${{@link NameType#getName()} languages.txt
+ *

+ * The format of these resources is the following: + *

    + *
  • Language: a single string containing no whitespace
  • + *
  • End-of-line comments: Any occurrence of '//' will cause all text following on that line to be + * discarded as a comment.
  • + *
  • Multi-line comments: Any line starting with '/*' will start multi-line commenting mode. + * This will skip all content until a line ending in '*' and '/' is found.
  • + *
  • Blank lines: All blank lines will be skipped.
  • + *
+ *

+ * Ported from language.php + *

+ * This class is immutable and thread-safe. + * + * @since 1.6 + * @version $Id$ + */ +public class Languages { + // Implementation note: This class is divided into two sections. The first part is a static factory interface that + // exposes org/apache/commons/codec/language/bm/%s_languages.txt for %s in NameType.* as a list of supported + // languages, and a second part that provides instance methods for accessing this set for supported languages. + + /** + * A set of languages. + */ + public static abstract class LanguageSet { + + public static LanguageSet from(final Set langs) { + return langs.isEmpty() ? NO_LANGUAGES : new SomeLanguages(langs); + } + + public abstract boolean contains(String language); + + public abstract String getAny(); + + public abstract boolean isEmpty(); + + public abstract boolean isSingleton(); + + public abstract LanguageSet restrictTo(LanguageSet other); + + abstract LanguageSet merge(LanguageSet other); + } + + /** + * Some languages, explicitly enumerated. + */ + public static final class SomeLanguages extends LanguageSet { + private final Set languages; + + private SomeLanguages(final Set languages) { + this.languages = Collections.unmodifiableSet(languages); + } + + @Override + public boolean contains(final String language) { + return this.languages.contains(language); + } + + @Override + public String getAny() { + return this.languages.iterator().next(); + } + + public Set getLanguages() { + return this.languages; + } + + @Override + public boolean isEmpty() { + return this.languages.isEmpty(); + } + + @Override + public boolean isSingleton() { + return this.languages.size() == 1; + } + + @Override + public LanguageSet restrictTo(final LanguageSet other) { + if (other == NO_LANGUAGES) { + return other; + } else if (other == ANY_LANGUAGE) { + return this; + } else { + final SomeLanguages sl = (SomeLanguages) other; + final Set ls = new HashSet(Math.min(languages.size(), sl.languages.size())); + for (String lang : languages) { + if (sl.languages.contains(lang)) { + ls.add(lang); + } + } + return from(ls); + } + } + + @Override + public LanguageSet merge(final LanguageSet other) { + if (other == NO_LANGUAGES) { + return this; + } else if (other == ANY_LANGUAGE) { + return other; + } else { + final SomeLanguages sl = (SomeLanguages) other; + final Set ls = new HashSet(languages); + for (String lang : sl.languages) { + ls.add(lang); + } + return from(ls); + } + } + + @Override + public String toString() { + return "Languages(" + languages.toString() + ")"; + } + + } + + public static final String ANY = "any"; + + private static final Map LANGUAGES = new EnumMap(NameType.class); + + static { + for (final NameType s : NameType.values()) { + LANGUAGES.put(s, getInstance(langResourceName(s))); + } + } + + public static Languages getInstance(final NameType nameType) { + return LANGUAGES.get(nameType); + } + + public static Languages getInstance(final String languagesResourceName) { + // read languages list + final Set ls = new HashSet(); + final InputStream langIS = Languages.class.getClassLoader().getResourceAsStream(languagesResourceName); + + if (langIS == null) { + throw new IllegalArgumentException("Unable to resolve required resource: " + languagesResourceName); + } + + final Scanner lsScanner = new Scanner(langIS, ResourceConstants.ENCODING); + try { + boolean inExtendedComment = false; + while (lsScanner.hasNextLine()) { + final String line = lsScanner.nextLine().trim(); + if (inExtendedComment) { + if (line.endsWith(ResourceConstants.EXT_CMT_END)) { + inExtendedComment = false; + } + } else { + if (line.startsWith(ResourceConstants.EXT_CMT_START)) { + inExtendedComment = true; + } else if (line.length() > 0) { + ls.add(line); + } + } + } + } finally { + lsScanner.close(); + } + + return new Languages(Collections.unmodifiableSet(ls)); + } + + private static String langResourceName(final NameType nameType) { + return String.format("org/apache/commons/codec/language/bm/%s_languages.txt", nameType.getName()); + } + + private final Set languages; + + /** + * No languages at all. + */ + public static final LanguageSet NO_LANGUAGES = new LanguageSet() { + @Override + public boolean contains(final String language) { + return false; + } + + @Override + public String getAny() { + throw new NoSuchElementException("Can't fetch any language from the empty language set."); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean isSingleton() { + return false; + } + + @Override + public LanguageSet restrictTo(final LanguageSet other) { + return this; + } + + @Override + public LanguageSet merge(final LanguageSet other) { + return other; + } + + @Override + public String toString() { + return "NO_LANGUAGES"; + } + }; + + /** + * Any/all languages. + */ + public static final LanguageSet ANY_LANGUAGE = new LanguageSet() { + @Override + public boolean contains(final String language) { + return true; + } + + @Override + public String getAny() { + throw new NoSuchElementException("Can't fetch any language from the any language set."); + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean isSingleton() { + return false; + } + + @Override + public LanguageSet restrictTo(final LanguageSet other) { + return other; + } + + @Override + public LanguageSet merge(final LanguageSet other) { + return other; + } + + @Override + public String toString() { + return "ANY_LANGUAGE"; + } + }; + + private Languages(final Set languages) { + this.languages = languages; + } + + public Set getLanguages() { + return this.languages; + } +} diff --git a/src/org/apache/commons/codec/language/bm/NameType.java b/src/org/apache/commons/codec/language/bm/NameType.java new file mode 100644 index 00000000..004b2cf2 --- /dev/null +++ b/src/org/apache/commons/codec/language/bm/NameType.java @@ -0,0 +1,53 @@ +/* + * 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 org.apache.commons.codec.language.bm; + +/** + * Supported types of names. Unless you are matching particular family names, use {@link #GENERIC}. The + * GENERIC NameType should work reasonably well for non-name words. The other encodings are + * specifically tuned to family names, and may not work well at all for general text. + * + * @since 1.6 + * @version $Id$ + */ +public enum NameType { + + /** Ashkenazi family names */ + ASHKENAZI("ash"), + + /** Generic names and words */ + GENERIC("gen"), + + /** Sephardic family names */ + SEPHARDIC("sep"); + + private final String name; + + NameType(final String name) { + this.name = name; + } + + /** + * Gets the short version of the name type. + * + * @return the NameType short string + */ + public String getName() { + return this.name; + } +} diff --git a/src/org/apache/commons/codec/language/bm/PhoneticEngine.java b/src/org/apache/commons/codec/language/bm/PhoneticEngine.java new file mode 100644 index 00000000..490757be --- /dev/null +++ b/src/org/apache/commons/codec/language/bm/PhoneticEngine.java @@ -0,0 +1,529 @@ +/* + * 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 org.apache.commons.codec.language.bm; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.apache.commons.codec.language.bm.Languages.LanguageSet; +import org.apache.commons.codec.language.bm.Rule.Phoneme; + +/** + * Converts words into potential phonetic representations. + *

+ * This is a two-stage process. Firstly, the word is converted into a phonetic representation that takes + * into account the likely source language. Next, this phonetic representation is converted into a + * pan-European 'average' representation, allowing comparison between different versions of essentially + * the same word from different languages. + *

+ * This class is intentionally immutable and thread-safe. + * If you wish to alter the settings for a PhoneticEngine, you + * must make a new one with the updated settings. + *

+ * Ported from phoneticengine.php + * + * @since 1.6 + * @version $Id$ + */ +public class PhoneticEngine { + + /** + * Utility for manipulating a set of phonemes as they are being built up. Not intended for use outside + * this package, and probably not outside the {@link PhoneticEngine} class. + * + * @since 1.6 + */ + static final class PhonemeBuilder { + + /** + * An empty builder where all phonemes must come from some set of languages. This will contain a single + * phoneme of zero characters. This can then be appended to. This should be the only way to create a new + * phoneme from scratch. + * + * @param languages the set of languages + * @return a new, empty phoneme builder + */ + public static PhonemeBuilder empty(final Languages.LanguageSet languages) { + return new PhonemeBuilder(new Rule.Phoneme("", languages)); + } + + private final Set phonemes; + + private PhonemeBuilder(final Rule.Phoneme phoneme) { + this.phonemes = new LinkedHashSet(); + this.phonemes.add(phoneme); + } + + private PhonemeBuilder(final Set phonemes) { + this.phonemes = phonemes; + } + + /** + * Creates a new phoneme builder containing all phonemes in this one extended by str. + * + * @param str the characters to append to the phonemes + */ + public void append(final CharSequence str) { + for (final Rule.Phoneme ph : this.phonemes) { + ph.append(str); + } + } + + /** + * Applies the given phoneme expression to all phonemes in this phoneme builder. + *

+ * This will lengthen phonemes that have compatible language sets to the expression, and drop those that are + * incompatible. + * + * @param phonemeExpr the expression to apply + * @param maxPhonemes the maximum number of phonemes to build up + */ + public void apply(final Rule.PhonemeExpr phonemeExpr, final int maxPhonemes) { + final Set newPhonemes = new LinkedHashSet(maxPhonemes); + + EXPR: for (final Rule.Phoneme left : this.phonemes) { + for (final Rule.Phoneme right : phonemeExpr.getPhonemes()) { + final LanguageSet languages = left.getLanguages().restrictTo(right.getLanguages()); + if (!languages.isEmpty()) { + final Rule.Phoneme join = new Phoneme(left, right, languages); + if (newPhonemes.size() < maxPhonemes) { + newPhonemes.add(join); + if (newPhonemes.size() >= maxPhonemes) { + break EXPR; + } + } + } + } + } + + this.phonemes.clear(); + this.phonemes.addAll(newPhonemes); + } + + /** + * Gets underlying phoneme set. Please don't mutate. + * + * @return the phoneme set + */ + public Set getPhonemes() { + return this.phonemes; + } + + /** + * Stringifies the phoneme set. This produces a single string of the strings of each phoneme, + * joined with a pipe. This is explicitly provided in place of toString as it is a potentially + * expensive operation, which should be avoided when debugging. + * + * @return the stringified phoneme set + */ + public String makeString() { + final StringBuilder sb = new StringBuilder(); + + for (final Rule.Phoneme ph : this.phonemes) { + if (sb.length() > 0) { + sb.append("|"); + } + sb.append(ph.getPhonemeText()); + } + + return sb.toString(); + } + } + + /** + * A function closure capturing the application of a list of rules to an input sequence at a particular offset. + * After invocation, the values i and found are updated. i points to the + * index of the next char in input that must be processed next (the input up to that index having been + * processed already), and found indicates if a matching rule was found or not. In the case where a + * matching rule was found, phonemeBuilder is replaced with a new builder containing the phonemes + * updated by the matching rule. + * + * Although this class is not thread-safe (it has mutable unprotected fields), it is not shared between threads + * as it is constructed as needed by the calling methods. + * @since 1.6 + */ + private static final class RulesApplication { + private final Map> finalRules; + private final CharSequence input; + + private PhonemeBuilder phonemeBuilder; + private int i; + private final int maxPhonemes; + private boolean found; + + public RulesApplication(final Map> finalRules, final CharSequence input, + final PhonemeBuilder phonemeBuilder, final int i, final int maxPhonemes) { + if (finalRules == null) { + throw new NullPointerException("The finalRules argument must not be null"); + } + this.finalRules = finalRules; + this.phonemeBuilder = phonemeBuilder; + this.input = input; + this.i = i; + this.maxPhonemes = maxPhonemes; + } + + public int getI() { + return this.i; + } + + public PhonemeBuilder getPhonemeBuilder() { + return this.phonemeBuilder; + } + + /** + * Invokes the rules. Loops over the rules list, stopping at the first one that has a matching context + * and pattern. Then applies this rule to the phoneme builder to produce updated phonemes. If there was no + * match, i is advanced one and the character is silently dropped from the phonetic spelling. + * + * @return this + */ + public RulesApplication invoke() { + this.found = false; + int patternLength = 1; + final List rules = this.finalRules.get(input.subSequence(i, i+patternLength)); + if (rules != null) { + for (final Rule rule : rules) { + final String pattern = rule.getPattern(); + patternLength = pattern.length(); + if (rule.patternAndContextMatches(this.input, this.i)) { + this.phonemeBuilder.apply(rule.getPhoneme(), maxPhonemes); + this.found = true; + break; + } + } + } + + if (!this.found) { + patternLength = 1; + } + + this.i += patternLength; + return this; + } + + public boolean isFound() { + return this.found; + } + } + + private static final Map> NAME_PREFIXES = new EnumMap>(NameType.class); + + static { + NAME_PREFIXES.put(NameType.ASHKENAZI, + Collections.unmodifiableSet( + new HashSet(Arrays.asList("bar", "ben", "da", "de", "van", "von")))); + NAME_PREFIXES.put(NameType.SEPHARDIC, + Collections.unmodifiableSet( + new HashSet(Arrays.asList("al", "el", "da", "dal", "de", "del", "dela", "de la", + "della", "des", "di", "do", "dos", "du", "van", "von")))); + NAME_PREFIXES.put(NameType.GENERIC, + Collections.unmodifiableSet( + new HashSet(Arrays.asList("da", "dal", "de", "del", "dela", "de la", "della", + "des", "di", "do", "dos", "du", "van", "von")))); + } + + /** + * Joins some strings with an internal separator. + * @param strings Strings to join + * @param sep String to separate them with + * @return a single String consisting of each element of strings interleaved by sep + */ + private static String join(final Iterable strings, final String sep) { + final StringBuilder sb = new StringBuilder(); + final Iterator si = strings.iterator(); + if (si.hasNext()) { + sb.append(si.next()); + } + while (si.hasNext()) { + sb.append(sep).append(si.next()); + } + + return sb.toString(); + } + + private static final int DEFAULT_MAX_PHONEMES = 20; + + private final Lang lang; + + private final NameType nameType; + + private final RuleType ruleType; + + private final boolean concat; + + private final int maxPhonemes; + + /** + * Generates a new, fully-configured phonetic engine. + * + * @param nameType + * the type of names it will use + * @param ruleType + * the type of rules it will apply + * @param concat + * if it will concatenate multiple encodings + */ + public PhoneticEngine(final NameType nameType, final RuleType ruleType, final boolean concat) { + this(nameType, ruleType, concat, DEFAULT_MAX_PHONEMES); + } + + /** + * Generates a new, fully-configured phonetic engine. + * + * @param nameType + * the type of names it will use + * @param ruleType + * the type of rules it will apply + * @param concat + * if it will concatenate multiple encodings + * @param maxPhonemes + * the maximum number of phonemes that will be handled + * @since 1.7 + */ + public PhoneticEngine(final NameType nameType, final RuleType ruleType, final boolean concat, + final int maxPhonemes) { + if (ruleType == RuleType.RULES) { + throw new IllegalArgumentException("ruleType must not be " + RuleType.RULES); + } + this.nameType = nameType; + this.ruleType = ruleType; + this.concat = concat; + this.lang = Lang.instance(nameType); + this.maxPhonemes = maxPhonemes; + } + + /** + * Applies the final rules to convert from a language-specific phonetic representation to a + * language-independent representation. + * + * @param phonemeBuilder the current phonemes + * @param finalRules the final rules to apply + * @return the resulting phonemes + */ + private PhonemeBuilder applyFinalRules(final PhonemeBuilder phonemeBuilder, + final Map> finalRules) { + if (finalRules == null) { + throw new NullPointerException("finalRules can not be null"); + } + if (finalRules.isEmpty()) { + return phonemeBuilder; + } + + final Map phonemes = + new TreeMap(Rule.Phoneme.COMPARATOR); + + for (final Rule.Phoneme phoneme : phonemeBuilder.getPhonemes()) { + PhonemeBuilder subBuilder = PhonemeBuilder.empty(phoneme.getLanguages()); + final String phonemeText = phoneme.getPhonemeText().toString(); + + for (int i = 0; i < phonemeText.length();) { + final RulesApplication rulesApplication = + new RulesApplication(finalRules, phonemeText, subBuilder, i, maxPhonemes).invoke(); + final boolean found = rulesApplication.isFound(); + subBuilder = rulesApplication.getPhonemeBuilder(); + + if (!found) { + // not found, appending as-is + subBuilder.append(phonemeText.subSequence(i, i + 1)); + } + + i = rulesApplication.getI(); + } + + // the phonemes map orders the phonemes only based on their text, but ignores the language set + // when adding new phonemes, check for equal phonemes and merge their language set, otherwise + // phonemes with the same text but different language set get lost + for (final Rule.Phoneme newPhoneme : subBuilder.getPhonemes()) { + if (phonemes.containsKey(newPhoneme)) { + final Rule.Phoneme oldPhoneme = phonemes.remove(newPhoneme); + final Rule.Phoneme mergedPhoneme = oldPhoneme.mergeWithLanguage(newPhoneme.getLanguages()); + phonemes.put(mergedPhoneme, mergedPhoneme); + } else { + phonemes.put(newPhoneme, newPhoneme); + } + } + } + + return new PhonemeBuilder(phonemes.keySet()); + } + + /** + * Encodes a string to its phonetic representation. + * + * @param input + * the String to encode + * @return the encoding of the input + */ + public String encode(final String input) { + final Languages.LanguageSet languageSet = this.lang.guessLanguages(input); + return encode(input, languageSet); + } + + /** + * Encodes an input string into an output phonetic representation, given a set of possible origin languages. + * + * @param input + * String to phoneticise; a String with dashes or spaces separating each word + * @param languageSet + * set of possible origin languages + * @return a phonetic representation of the input; a String containing '-'-separated phonetic representations of the + * input + */ + public String encode(String input, final Languages.LanguageSet languageSet) { + final Map> rules = Rule.getInstanceMap(this.nameType, RuleType.RULES, languageSet); + // rules common across many (all) languages + final Map> finalRules1 = Rule.getInstanceMap(this.nameType, this.ruleType, "common"); + // rules that apply to a specific language that may be ambiguous or wrong if applied to other languages + final Map> finalRules2 = Rule.getInstanceMap(this.nameType, this.ruleType, languageSet); + + // tidy the input + // lower case is a locale-dependent operation + input = input.toLowerCase(Locale.ENGLISH).replace('-', ' ').trim(); + + if (this.nameType == NameType.GENERIC) { + if (input.length() >= 2 && input.substring(0, 2).equals("d'")) { // check for d' + final String remainder = input.substring(2); + final String combined = "d" + remainder; + return "(" + encode(remainder) + ")-(" + encode(combined) + ")"; + } + for (final String l : NAME_PREFIXES.get(this.nameType)) { + // handle generic prefixes + if (input.startsWith(l + " ")) { + // check for any prefix in the words list + final String remainder = input.substring(l.length() + 1); // input without the prefix + final String combined = l + remainder; // input with prefix without space + return "(" + encode(remainder) + ")-(" + encode(combined) + ")"; + } + } + } + + final List words = Arrays.asList(input.split("\\s+")); + final List words2 = new ArrayList(); + + // special-case handling of word prefixes based upon the name type + switch (this.nameType) { + case SEPHARDIC: + for (final String aWord : words) { + final String[] parts = aWord.split("'"); + final String lastPart = parts[parts.length - 1]; + words2.add(lastPart); + } + words2.removeAll(NAME_PREFIXES.get(this.nameType)); + break; + case ASHKENAZI: + words2.addAll(words); + words2.removeAll(NAME_PREFIXES.get(this.nameType)); + break; + case GENERIC: + words2.addAll(words); + break; + default: + throw new IllegalStateException("Unreachable case: " + this.nameType); + } + + if (this.concat) { + // concat mode enabled + input = join(words2, " "); + } else if (words2.size() == 1) { + // not a multi-word name + input = words.iterator().next(); + } else { + // encode each word in a multi-word name separately (normally used for approx matches) + final StringBuilder result = new StringBuilder(); + for (final String word : words2) { + result.append("-").append(encode(word)); + } + // return the result without the leading "-" + return result.substring(1); + } + + PhonemeBuilder phonemeBuilder = PhonemeBuilder.empty(languageSet); + + // loop over each char in the input - we will handle the increment manually + for (int i = 0; i < input.length();) { + final RulesApplication rulesApplication = + new RulesApplication(rules, input, phonemeBuilder, i, maxPhonemes).invoke(); + i = rulesApplication.getI(); + phonemeBuilder = rulesApplication.getPhonemeBuilder(); + } + + // Apply the general rules + phonemeBuilder = applyFinalRules(phonemeBuilder, finalRules1); + // Apply the language-specific rules + phonemeBuilder = applyFinalRules(phonemeBuilder, finalRules2); + + return phonemeBuilder.makeString(); + } + + /** + * Gets the Lang language guessing rules being used. + * + * @return the Lang in use + */ + public Lang getLang() { + return this.lang; + } + + /** + * Gets the NameType being used. + * + * @return the NameType in use + */ + public NameType getNameType() { + return this.nameType; + } + + /** + * Gets the RuleType being used. + * + * @return the RuleType in use + */ + public RuleType getRuleType() { + return this.ruleType; + } + + /** + * Gets if multiple phonetic encodings are concatenated or if just the first one is kept. + * + * @return true if multiple phonetic encodings are returned, false if just the first is + */ + public boolean isConcat() { + return this.concat; + } + + /** + * Gets the maximum number of phonemes the engine will calculate for a given input. + * + * @return the maximum number of phonemes + * @since 1.7 + */ + public int getMaxPhonemes() { + return this.maxPhonemes; + } +} diff --git a/src/org/apache/commons/codec/language/bm/ResourceConstants.java b/src/org/apache/commons/codec/language/bm/ResourceConstants.java new file mode 100644 index 00000000..6558f0da --- /dev/null +++ b/src/org/apache/commons/codec/language/bm/ResourceConstants.java @@ -0,0 +1,37 @@ +/* + * 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 org.apache.commons.codec.language.bm; + +import org.apache.commons.codec.CharEncoding; + +/** + * Constants used to process resource files. + * + *

This class is immutable and thread-safe.

+ * + * @since 1.6 + * @version $Id$ + */ +class ResourceConstants { + + static final String CMT = "//"; + static final String ENCODING = CharEncoding.UTF_8; + static final String EXT_CMT_END = "*/"; + static final String EXT_CMT_START = "/*"; + +} diff --git a/src/org/apache/commons/codec/language/bm/Rule.java b/src/org/apache/commons/codec/language/bm/Rule.java new file mode 100644 index 00000000..eacbae85 --- /dev/null +++ b/src/org/apache/commons/codec/language/bm/Rule.java @@ -0,0 +1,720 @@ +/* + * 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 org.apache.commons.codec.language.bm; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Scanner; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.codec.language.bm.Languages.LanguageSet; + +/** + * A phoneme rule. + *

+ * Rules have a pattern, left context, right context, output phoneme, set of languages for which they apply + * and a logical flag indicating if all languages must be in play. A rule matches if: + *

    + *
  • the pattern matches at the current position
  • + *
  • the string up until the beginning of the pattern matches the left context
  • + *
  • the string from the end of the pattern matches the right context
  • + *
  • logical is ALL and all languages are in scope; or
  • + *
  • logical is any other value and at least one language is in scope
  • + *
+ *

+ * Rules are typically generated by parsing rules resources. In normal use, there will be no need for the user + * to explicitly construct their own. + *

+ * Rules are immutable and thread-safe. + *

+ * Rules resources + *

+ * Rules are typically loaded from resource files. These are UTF-8 encoded text files. They are systematically + * named following the pattern: + *

org/apache/commons/codec/language/bm/${NameType#getName}_${RuleType#getName}_${language}.txt
+ *

+ * The format of these resources is the following: + *

    + *
  • Rules: whitespace separated, double-quoted strings. There should be 4 columns to each row, and these + * will be interpreted as: + *
      + *
    1. pattern
    2. + *
    3. left context
    4. + *
    5. right context
    6. + *
    7. phoneme
    8. + *
    + *
  • + *
  • End-of-line comments: Any occurrence of '//' will cause all text following on that line to be discarded + * as a comment.
  • + *
  • Multi-line comments: Any line starting with '/*' will start multi-line commenting mode. This will skip + * all content until a line ending in '*' and '/' is found.
  • + *
  • Blank lines: All blank lines will be skipped.
  • + *
+ * + * @since 1.6 + * @version $Id$ + */ +public class Rule { + + public static final class Phoneme implements PhonemeExpr { + public static final Comparator COMPARATOR = new Comparator() { + @Override + public int compare(final Phoneme o1, final Phoneme o2) { + for (int i = 0; i < o1.phonemeText.length(); i++) { + if (i >= o2.phonemeText.length()) { + return +1; + } + final int c = o1.phonemeText.charAt(i) - o2.phonemeText.charAt(i); + if (c != 0) { + return c; + } + } + + if (o1.phonemeText.length() < o2.phonemeText.length()) { + return -1; + } + + return 0; + } + }; + + private final StringBuilder phonemeText; + private final Languages.LanguageSet languages; + + public Phoneme(final CharSequence phonemeText, final Languages.LanguageSet languages) { + this.phonemeText = new StringBuilder(phonemeText); + this.languages = languages; + } + + public Phoneme(final Phoneme phonemeLeft, final Phoneme phonemeRight) { + this(phonemeLeft.phonemeText, phonemeLeft.languages); + this.phonemeText.append(phonemeRight.phonemeText); + } + + public Phoneme(final Phoneme phonemeLeft, final Phoneme phonemeRight, final Languages.LanguageSet languages) { + this(phonemeLeft.phonemeText, languages); + this.phonemeText.append(phonemeRight.phonemeText); + } + + public Phoneme append(final CharSequence str) { + this.phonemeText.append(str); + return this; + } + + public Languages.LanguageSet getLanguages() { + return this.languages; + } + + @Override + public Iterable getPhonemes() { + return Collections.singleton(this); + } + + public CharSequence getPhonemeText() { + return this.phonemeText; + } + + /** + * Deprecated since 1.9. + * + * @param right the Phoneme to join + * @return a new Phoneme + * @deprecated since 1.9 + */ + @Deprecated + public Phoneme join(final Phoneme right) { + return new Phoneme(this.phonemeText.toString() + right.phonemeText.toString(), + this.languages.restrictTo(right.languages)); + } + + /** + * Returns a new Phoneme with the same text but a union of its + * current language set and the given one. + * + * @param lang the language set to merge + * @return a new Phoneme + */ + public Phoneme mergeWithLanguage(final LanguageSet lang) { + return new Phoneme(this.phonemeText.toString(), this.languages.merge(lang)); + } + + @Override + public String toString() { + return phonemeText.toString() + "[" + languages + "]"; + } + } + + public interface PhonemeExpr { + Iterable getPhonemes(); + } + + public static final class PhonemeList implements PhonemeExpr { + private final List phonemes; + + public PhonemeList(final List phonemes) { + this.phonemes = phonemes; + } + + @Override + public List getPhonemes() { + return this.phonemes; + } + } + + /** + * A minimal wrapper around the functionality of Pattern that we use, to allow for alternate implementations. + */ + public interface RPattern { + boolean isMatch(CharSequence input); + } + + public static final RPattern ALL_STRINGS_RMATCHER = new RPattern() { + @Override + public boolean isMatch(final CharSequence input) { + return true; + } + }; + + public static final String ALL = "ALL"; + + private static final String DOUBLE_QUOTE = "\""; + + private static final String HASH_INCLUDE = "#include"; + + private static final Map>>>> RULES = + new EnumMap>>>>(NameType.class); + + static { + for (final NameType s : NameType.values()) { + final Map>>> rts = + new EnumMap>>>(RuleType.class); + + for (final RuleType rt : RuleType.values()) { + final Map>> rs = new HashMap>>(); + + final Languages ls = Languages.getInstance(s); + for (final String l : ls.getLanguages()) { + try { + rs.put(l, parseRules(createScanner(s, rt, l), createResourceName(s, rt, l))); + } catch (final IllegalStateException e) { + throw new IllegalStateException("Problem processing " + createResourceName(s, rt, l), e); + } + } + if (!rt.equals(RuleType.RULES)) { + rs.put("common", parseRules(createScanner(s, rt, "common"), createResourceName(s, rt, "common"))); + } + + rts.put(rt, Collections.unmodifiableMap(rs)); + } + + RULES.put(s, Collections.unmodifiableMap(rts)); + } + } + + private static boolean contains(final CharSequence chars, final char input) { + for (int i = 0; i < chars.length(); i++) { + if (chars.charAt(i) == input) { + return true; + } + } + return false; + } + + private static String createResourceName(final NameType nameType, final RuleType rt, final String lang) { + return String.format("org/apache/commons/codec/language/bm/%s_%s_%s.txt", + nameType.getName(), rt.getName(), lang); + } + + private static Scanner createScanner(final NameType nameType, final RuleType rt, final String lang) { + final String resName = createResourceName(nameType, rt, lang); + final InputStream rulesIS = Languages.class.getClassLoader().getResourceAsStream(resName); + + if (rulesIS == null) { + throw new IllegalArgumentException("Unable to load resource: " + resName); + } + + return new Scanner(rulesIS, ResourceConstants.ENCODING); + } + + private static Scanner createScanner(final String lang) { + final String resName = String.format("org/apache/commons/codec/language/bm/%s.txt", lang); + final InputStream rulesIS = Languages.class.getClassLoader().getResourceAsStream(resName); + + if (rulesIS == null) { + throw new IllegalArgumentException("Unable to load resource: " + resName); + } + + return new Scanner(rulesIS, ResourceConstants.ENCODING); + } + + private static boolean endsWith(final CharSequence input, final CharSequence suffix) { + if (suffix.length() > input.length()) { + return false; + } + for (int i = input.length() - 1, j = suffix.length() - 1; j >= 0; i--, j--) { + if (input.charAt(i) != suffix.charAt(j)) { + return false; + } + } + return true; + } + + /** + * Gets rules for a combination of name type, rule type and languages. + * + * @param nameType + * the NameType to consider + * @param rt + * the RuleType to consider + * @param langs + * the set of languages to consider + * @return a list of Rules that apply + */ + public static List getInstance(final NameType nameType, final RuleType rt, + final Languages.LanguageSet langs) { + final Map> ruleMap = getInstanceMap(nameType, rt, langs); + final List allRules = new ArrayList(); + for (final List rules : ruleMap.values()) { + allRules.addAll(rules); + } + return allRules; + } + + /** + * Gets rules for a combination of name type, rule type and a single language. + * + * @param nameType + * the NameType to consider + * @param rt + * the RuleType to consider + * @param lang + * the language to consider + * @return a list of Rules that apply + */ + public static List getInstance(final NameType nameType, final RuleType rt, final String lang) { + return getInstance(nameType, rt, LanguageSet.from(new HashSet(Arrays.asList(lang)))); + } + + /** + * Gets rules for a combination of name type, rule type and languages. + * + * @param nameType + * the NameType to consider + * @param rt + * the RuleType to consider + * @param langs + * the set of languages to consider + * @return a map containing all Rules that apply, grouped by the first character of the rule pattern + * @since 1.9 + */ + public static Map> getInstanceMap(final NameType nameType, final RuleType rt, + final Languages.LanguageSet langs) { + return langs.isSingleton() ? getInstanceMap(nameType, rt, langs.getAny()) : + getInstanceMap(nameType, rt, Languages.ANY); + } + + /** + * Gets rules for a combination of name type, rule type and a single language. + * + * @param nameType + * the NameType to consider + * @param rt + * the RuleType to consider + * @param lang + * the language to consider + * @return a map containing all Rules that apply, grouped by the first character of the rule pattern + * @since 1.9 + */ + public static Map> getInstanceMap(final NameType nameType, final RuleType rt, + final String lang) { + final Map> rules = RULES.get(nameType).get(rt).get(lang); + + if (rules == null) { + throw new IllegalArgumentException(String.format("No rules found for %s, %s, %s.", + nameType.getName(), rt.getName(), lang)); + } + + return rules; + } + + private static Phoneme parsePhoneme(final String ph) { + final int open = ph.indexOf("["); + if (open >= 0) { + if (!ph.endsWith("]")) { + throw new IllegalArgumentException("Phoneme expression contains a '[' but does not end in ']'"); + } + final String before = ph.substring(0, open); + final String in = ph.substring(open + 1, ph.length() - 1); + final Set langs = new HashSet(Arrays.asList(in.split("[+]"))); + + return new Phoneme(before, Languages.LanguageSet.from(langs)); + } else { + return new Phoneme(ph, Languages.ANY_LANGUAGE); + } + } + + private static PhonemeExpr parsePhonemeExpr(final String ph) { + if (ph.startsWith("(")) { // we have a bracketed list of options + if (!ph.endsWith(")")) { + throw new IllegalArgumentException("Phoneme starts with '(' so must end with ')'"); + } + + final List phs = new ArrayList(); + final String body = ph.substring(1, ph.length() - 1); + for (final String part : body.split("[|]")) { + phs.add(parsePhoneme(part)); + } + if (body.startsWith("|") || body.endsWith("|")) { + phs.add(new Phoneme("", Languages.ANY_LANGUAGE)); + } + + return new PhonemeList(phs); + } else { + return parsePhoneme(ph); + } + } + + private static Map> parseRules(final Scanner scanner, final String location) { + final Map> lines = new HashMap>(); + int currentLine = 0; + + boolean inMultilineComment = false; + while (scanner.hasNextLine()) { + currentLine++; + final String rawLine = scanner.nextLine(); + String line = rawLine; + + if (inMultilineComment) { + if (line.endsWith(ResourceConstants.EXT_CMT_END)) { + inMultilineComment = false; + } + } else { + if (line.startsWith(ResourceConstants.EXT_CMT_START)) { + inMultilineComment = true; + } else { + // discard comments + final int cmtI = line.indexOf(ResourceConstants.CMT); + if (cmtI >= 0) { + line = line.substring(0, cmtI); + } + + // trim leading-trailing whitespace + line = line.trim(); + + if (line.length() == 0) { + continue; // empty lines can be safely skipped + } + + if (line.startsWith(HASH_INCLUDE)) { + // include statement + final String incl = line.substring(HASH_INCLUDE.length()).trim(); + if (incl.contains(" ")) { + throw new IllegalArgumentException("Malformed import statement '" + rawLine + "' in " + + location); + } else { + lines.putAll(parseRules(createScanner(incl), location + "->" + incl)); + } + } else { + // rule + final String[] parts = line.split("\\s+"); + if (parts.length != 4) { + throw new IllegalArgumentException("Malformed rule statement split into " + parts.length + + " parts: " + rawLine + " in " + location); + } else { + try { + final String pat = stripQuotes(parts[0]); + final String lCon = stripQuotes(parts[1]); + final String rCon = stripQuotes(parts[2]); + final PhonemeExpr ph = parsePhonemeExpr(stripQuotes(parts[3])); + final int cLine = currentLine; + final Rule r = new Rule(pat, lCon, rCon, ph) { + private final int myLine = cLine; + private final String loc = location; + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(); + sb.append("Rule"); + sb.append("{line=").append(myLine); + sb.append(", loc='").append(loc).append('\''); + sb.append(", pat='").append(pat).append('\''); + sb.append(", lcon='").append(lCon).append('\''); + sb.append(", rcon='").append(rCon).append('\''); + sb.append('}'); + return sb.toString(); + } + }; + final String patternKey = r.pattern.substring(0,1); + List rules = lines.get(patternKey); + if (rules == null) { + rules = new ArrayList(); + lines.put(patternKey, rules); + } + rules.add(r); + } catch (final IllegalArgumentException e) { + throw new IllegalStateException("Problem parsing line '" + currentLine + "' in " + + location, e); + } + } + } + } + } + } + + return lines; + } + + /** + * Attempts to compile the regex into direct string ops, falling back to Pattern and Matcher in the worst case. + * + * @param regex + * the regular expression to compile + * @return an RPattern that will match this regex + */ + private static RPattern pattern(final String regex) { + final boolean startsWith = regex.startsWith("^"); + final boolean endsWith = regex.endsWith("$"); + final String content = regex.substring(startsWith ? 1 : 0, endsWith ? regex.length() - 1 : regex.length()); + final boolean boxes = content.contains("["); + + if (!boxes) { + if (startsWith && endsWith) { + // exact match + if (content.length() == 0) { + // empty + return new RPattern() { + @Override + public boolean isMatch(final CharSequence input) { + return input.length() == 0; + } + }; + } else { + return new RPattern() { + @Override + public boolean isMatch(final CharSequence input) { + return input.equals(content); + } + }; + } + } else if ((startsWith || endsWith) && content.length() == 0) { + // matches every string + return ALL_STRINGS_RMATCHER; + } else if (startsWith) { + // matches from start + return new RPattern() { + @Override + public boolean isMatch(final CharSequence input) { + return startsWith(input, content); + } + }; + } else if (endsWith) { + // matches from start + return new RPattern() { + @Override + public boolean isMatch(final CharSequence input) { + return endsWith(input, content); + } + }; + } + } else { + final boolean startsWithBox = content.startsWith("["); + final boolean endsWithBox = content.endsWith("]"); + + if (startsWithBox && endsWithBox) { + String boxContent = content.substring(1, content.length() - 1); + if (!boxContent.contains("[")) { + // box containing alternatives + final boolean negate = boxContent.startsWith("^"); + if (negate) { + boxContent = boxContent.substring(1); + } + final String bContent = boxContent; + final boolean shouldMatch = !negate; + + if (startsWith && endsWith) { + // exact match + return new RPattern() { + @Override + public boolean isMatch(final CharSequence input) { + return input.length() == 1 && contains(bContent, input.charAt(0)) == shouldMatch; + } + }; + } else if (startsWith) { + // first char + return new RPattern() { + @Override + public boolean isMatch(final CharSequence input) { + return input.length() > 0 && contains(bContent, input.charAt(0)) == shouldMatch; + } + }; + } else if (endsWith) { + // last char + return new RPattern() { + @Override + public boolean isMatch(final CharSequence input) { + return input.length() > 0 && + contains(bContent, input.charAt(input.length() - 1)) == shouldMatch; + } + }; + } + } + } + } + + return new RPattern() { + Pattern pattern = Pattern.compile(regex); + + @Override + public boolean isMatch(final CharSequence input) { + final Matcher matcher = pattern.matcher(input); + return matcher.find(); + } + }; + } + + private static boolean startsWith(final CharSequence input, final CharSequence prefix) { + if (prefix.length() > input.length()) { + return false; + } + for (int i = 0; i < prefix.length(); i++) { + if (input.charAt(i) != prefix.charAt(i)) { + return false; + } + } + return true; + } + + private static String stripQuotes(String str) { + if (str.startsWith(DOUBLE_QUOTE)) { + str = str.substring(1); + } + + if (str.endsWith(DOUBLE_QUOTE)) { + str = str.substring(0, str.length() - 1); + } + + return str; + } + + private final RPattern lContext; + + private final String pattern; + + private final PhonemeExpr phoneme; + + private final RPattern rContext; + + /** + * Creates a new rule. + * + * @param pattern + * the pattern + * @param lContext + * the left context + * @param rContext + * the right context + * @param phoneme + * the resulting phoneme + */ + public Rule(final String pattern, final String lContext, final String rContext, final PhonemeExpr phoneme) { + this.pattern = pattern; + this.lContext = pattern(lContext + "$"); + this.rContext = pattern("^" + rContext); + this.phoneme = phoneme; + } + + /** + * Gets the left context. This is a regular expression that must match to the left of the pattern. + * + * @return the left context Pattern + */ + public RPattern getLContext() { + return this.lContext; + } + + /** + * Gets the pattern. This is a string-literal that must exactly match. + * + * @return the pattern + */ + public String getPattern() { + return this.pattern; + } + + /** + * Gets the phoneme. If the rule matches, this is the phoneme associated with the pattern match. + * + * @return the phoneme + */ + public PhonemeExpr getPhoneme() { + return this.phoneme; + } + + /** + * Gets the right context. This is a regular expression that must match to the right of the pattern. + * + * @return the right context Pattern + */ + public RPattern getRContext() { + return this.rContext; + } + + /** + * Decides if the pattern and context match the input starting at a position. It is a match if the + * lContext matches input up to i, pattern matches at i and + * rContext matches from the end of the match of pattern to the end of input. + * + * @param input + * the input String + * @param i + * the int position within the input + * @return true if the pattern and left/right context match, false otherwise + */ + public boolean patternAndContextMatches(final CharSequence input, final int i) { + if (i < 0) { + throw new IndexOutOfBoundsException("Can not match pattern at negative indexes"); + } + + final int patternLength = this.pattern.length(); + final int ipl = i + patternLength; + + if (ipl > input.length()) { + // not enough room for the pattern to match + return false; + } + + // evaluate the pattern, left context and right context + // fail early if any of the evaluations is not successful + if (!input.subSequence(i, ipl).equals(this.pattern)) { + return false; + } else if (!this.rContext.isMatch(input.subSequence(ipl, input.length()))) { + return false; + } + return this.lContext.isMatch(input.subSequence(0, i)); + } +} diff --git a/src/org/apache/commons/codec/language/bm/RuleType.java b/src/org/apache/commons/codec/language/bm/RuleType.java new file mode 100644 index 00000000..0e7608b5 --- /dev/null +++ b/src/org/apache/commons/codec/language/bm/RuleType.java @@ -0,0 +1,50 @@ +/* + * 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 org.apache.commons.codec.language.bm; + +/** + * Types of rule. + * + * @since 1.6 + * @version $Id$ + */ +public enum RuleType { + + /** Approximate rules, which will lead to the largest number of phonetic interpretations. */ + APPROX("approx"), + /** Exact rules, which will lead to a minimum number of phonetic interpretations. */ + EXACT("exact"), + /** For internal use only. Please use {@link #APPROX} or {@link #EXACT}. */ + RULES("rules"); + + private final String name; + + RuleType(final String name) { + this.name = name; + } + + /** + * Gets the rule name. + * + * @return the rule name. + */ + public String getName() { + return this.name; + } + +} diff --git a/src/org/apache/commons/codec/language/bm/package.html b/src/org/apache/commons/codec/language/bm/package.html new file mode 100644 index 00000000..95a02ebb --- /dev/null +++ b/src/org/apache/commons/codec/language/bm/package.html @@ -0,0 +1,21 @@ + + + + Implementation details of the Beider-Morse codec. + + diff --git a/src/org/apache/commons/codec/language/package.html b/src/org/apache/commons/codec/language/package.html new file mode 100644 index 00000000..6e337668 --- /dev/null +++ b/src/org/apache/commons/codec/language/package.html @@ -0,0 +1,21 @@ + + + + Language and phonetic encoders. + + diff --git a/src/org/apache/commons/codec/net/BCodec.java b/src/org/apache/commons/codec/net/BCodec.java new file mode 100644 index 00000000..651ed979 --- /dev/null +++ b/src/org/apache/commons/codec/net/BCodec.java @@ -0,0 +1,251 @@ +/* + * 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 org.apache.commons.codec.net; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +import org.apache.commons.codec.Charsets; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringDecoder; +import org.apache.commons.codec.StringEncoder; +import org.apache.commons.codec.binary.Base64; + +/** + * Identical to the Base64 encoding defined by RFC 1521 + * and allows a character set to be specified. + *

+ * RFC 1522 describes techniques to allow the encoding of non-ASCII + * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message + * handling software. + *

+ * This class is immutable and thread-safe. + * + * @see MIME (Multipurpose Internet Mail Extensions) Part Two: Message + * Header Extensions for Non-ASCII Text + * + * @since 1.3 + * @version $Id$ + */ +public class BCodec extends RFC1522Codec implements StringEncoder, StringDecoder { + /** + * The default charset used for string decoding and encoding. + */ + private final Charset charset; + + /** + * Default constructor. + */ + public BCodec() { + this(Charsets.UTF_8); + } + + /** + * Constructor which allows for the selection of a default charset + * + * @param charset + * the default string charset to use. + * + * @see Standard charsets + * @since 1.7 + */ + public BCodec(final Charset charset) { + this.charset = charset; + } + + /** + * Constructor which allows for the selection of a default charset + * + * @param charsetName + * the default charset to use. + * @throws java.nio.charset.UnsupportedCharsetException + * If the named charset is unavailable + * @since 1.7 throws UnsupportedCharsetException if the named charset is unavailable + * @see Standard charsets + */ + public BCodec(final String charsetName) { + this(Charset.forName(charsetName)); + } + + @Override + protected String getEncoding() { + return "B"; + } + + @Override + protected byte[] doEncoding(final byte[] bytes) { + if (bytes == null) { + return null; + } + return Base64.encodeBase64(bytes); + } + + @Override + protected byte[] doDecoding(final byte[] bytes) { + if (bytes == null) { + return null; + } + return Base64.decodeBase64(bytes); + } + + /** + * Encodes a string into its Base64 form using the specified charset. Unsafe characters are escaped. + * + * @param value + * string to convert to Base64 form + * @param charset + * the charset for value + * @return Base64 string + * @throws EncoderException + * thrown if a failure condition is encountered during the encoding process. + * @since 1.7 + */ + public String encode(final String value, final Charset charset) throws EncoderException { + if (value == null) { + return null; + } + return encodeText(value, charset); + } + + /** + * Encodes a string into its Base64 form using the specified charset. Unsafe characters are escaped. + * + * @param value + * string to convert to Base64 form + * @param charset + * the charset for value + * @return Base64 string + * @throws EncoderException + * thrown if a failure condition is encountered during the encoding process. + */ + public String encode(final String value, final String charset) throws EncoderException { + if (value == null) { + return null; + } + try { + return this.encodeText(value, charset); + } catch (final UnsupportedEncodingException e) { + throw new EncoderException(e.getMessage(), e); + } + } + + /** + * Encodes a string into its Base64 form using the default charset. Unsafe characters are escaped. + * + * @param value + * string to convert to Base64 form + * @return Base64 string + * @throws EncoderException + * thrown if a failure condition is encountered during the encoding process. + */ + @Override + public String encode(final String value) throws EncoderException { + if (value == null) { + return null; + } + return encode(value, this.getCharset()); + } + + /** + * Decodes a Base64 string into its original form. Escaped characters are converted back to their original + * representation. + * + * @param value + * Base64 string to convert into its original form + * @return original string + * @throws DecoderException + * A decoder exception is thrown if a failure condition is encountered during the decode process. + */ + @Override + public String decode(final String value) throws DecoderException { + if (value == null) { + return null; + } + try { + return this.decodeText(value); + } catch (final UnsupportedEncodingException e) { + throw new DecoderException(e.getMessage(), e); + } + } + + /** + * Encodes an object into its Base64 form using the default charset. Unsafe characters are escaped. + * + * @param value + * object to convert to Base64 form + * @return Base64 object + * @throws EncoderException + * thrown if a failure condition is encountered during the encoding process. + */ + @Override + public Object encode(final Object value) throws EncoderException { + if (value == null) { + return null; + } else if (value instanceof String) { + return encode((String) value); + } else { + throw new EncoderException("Objects of type " + + value.getClass().getName() + + " cannot be encoded using BCodec"); + } + } + + /** + * Decodes a Base64 object into its original form. Escaped characters are converted back to their original + * representation. + * + * @param value + * Base64 object to convert into its original form + * @return original object + * @throws DecoderException + * Thrown if the argument is not a String. Thrown if a failure condition is encountered + * during the decode process. + */ + @Override + public Object decode(final Object value) throws DecoderException { + if (value == null) { + return null; + } else if (value instanceof String) { + return decode((String) value); + } else { + throw new DecoderException("Objects of type " + + value.getClass().getName() + + " cannot be decoded using BCodec"); + } + } + + /** + * Gets the default charset name used for string decoding and encoding. + * + * @return the default charset name + * @since 1.7 + */ + public Charset getCharset() { + return this.charset; + } + + /** + * Gets the default charset name used for string decoding and encoding. + * + * @return the default charset name + */ + public String getDefaultCharset() { + return this.charset.name(); + } +} diff --git a/src/org/apache/commons/codec/net/QCodec.java b/src/org/apache/commons/codec/net/QCodec.java new file mode 100644 index 00000000..584b3ff0 --- /dev/null +++ b/src/org/apache/commons/codec/net/QCodec.java @@ -0,0 +1,358 @@ +/* + * 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 org.apache.commons.codec.net; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.BitSet; + +import org.apache.commons.codec.Charsets; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringDecoder; +import org.apache.commons.codec.StringEncoder; + +/** + * Similar to the Quoted-Printable content-transfer-encoding defined in + * RFC 1521 and designed to allow text containing mostly ASCII + * characters to be decipherable on an ASCII terminal without decoding. + *

+ * RFC 1522 describes techniques to allow the encoding of non-ASCII + * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message + * handling software. + *

+ * This class is conditionally thread-safe. + * The instance field {@link #encodeBlanks} is mutable {@link #setEncodeBlanks(boolean)} + * but is not volatile, and accesses are not synchronised. + * If an instance of the class is shared between threads, the caller needs to ensure that suitable synchronisation + * is used to ensure safe publication of the value between threads, and must not invoke + * {@link #setEncodeBlanks(boolean)} after initial setup. + * + * @see MIME (Multipurpose Internet Mail Extensions) Part Two: Message + * Header Extensions for Non-ASCII Text + * + * @since 1.3 + * @version $Id$ + */ +public class QCodec extends RFC1522Codec implements StringEncoder, StringDecoder { + /** + * The default charset used for string decoding and encoding. + */ + private final Charset charset; + + /** + * BitSet of printable characters as defined in RFC 1522. + */ + private static final BitSet PRINTABLE_CHARS = new BitSet(256); + // Static initializer for printable chars collection + static { + // alpha characters + PRINTABLE_CHARS.set(' '); + PRINTABLE_CHARS.set('!'); + PRINTABLE_CHARS.set('"'); + PRINTABLE_CHARS.set('#'); + PRINTABLE_CHARS.set('$'); + PRINTABLE_CHARS.set('%'); + PRINTABLE_CHARS.set('&'); + PRINTABLE_CHARS.set('\''); + PRINTABLE_CHARS.set('('); + PRINTABLE_CHARS.set(')'); + PRINTABLE_CHARS.set('*'); + PRINTABLE_CHARS.set('+'); + PRINTABLE_CHARS.set(','); + PRINTABLE_CHARS.set('-'); + PRINTABLE_CHARS.set('.'); + PRINTABLE_CHARS.set('/'); + for (int i = '0'; i <= '9'; i++) { + PRINTABLE_CHARS.set(i); + } + PRINTABLE_CHARS.set(':'); + PRINTABLE_CHARS.set(';'); + PRINTABLE_CHARS.set('<'); + PRINTABLE_CHARS.set('>'); + PRINTABLE_CHARS.set('@'); + for (int i = 'A'; i <= 'Z'; i++) { + PRINTABLE_CHARS.set(i); + } + PRINTABLE_CHARS.set('['); + PRINTABLE_CHARS.set('\\'); + PRINTABLE_CHARS.set(']'); + PRINTABLE_CHARS.set('^'); + PRINTABLE_CHARS.set('`'); + for (int i = 'a'; i <= 'z'; i++) { + PRINTABLE_CHARS.set(i); + } + PRINTABLE_CHARS.set('{'); + PRINTABLE_CHARS.set('|'); + PRINTABLE_CHARS.set('}'); + PRINTABLE_CHARS.set('~'); + } + + private static final byte BLANK = 32; + + private static final byte UNDERSCORE = 95; + + private boolean encodeBlanks = false; + + /** + * Default constructor. + */ + public QCodec() { + this(Charsets.UTF_8); + } + + /** + * Constructor which allows for the selection of a default charset. + * + * @param charset + * the default string charset to use. + * + * @see Standard charsets + * @since 1.7 + */ + public QCodec(final Charset charset) { + super(); + this.charset = charset; + } + + /** + * Constructor which allows for the selection of a default charset. + * + * @param charsetName + * the charset to use. + * @throws java.nio.charset.UnsupportedCharsetException + * If the named charset is unavailable + * @since 1.7 throws UnsupportedCharsetException if the named charset is unavailable + * @see Standard charsets + */ + public QCodec(final String charsetName) { + this(Charset.forName(charsetName)); + } + + @Override + protected String getEncoding() { + return "Q"; + } + + @Override + protected byte[] doEncoding(final byte[] bytes) { + if (bytes == null) { + return null; + } + final byte[] data = QuotedPrintableCodec.encodeQuotedPrintable(PRINTABLE_CHARS, bytes); + if (this.encodeBlanks) { + for (int i = 0; i < data.length; i++) { + if (data[i] == BLANK) { + data[i] = UNDERSCORE; + } + } + } + return data; + } + + @Override + protected byte[] doDecoding(final byte[] bytes) throws DecoderException { + if (bytes == null) { + return null; + } + boolean hasUnderscores = false; + for (final byte b : bytes) { + if (b == UNDERSCORE) { + hasUnderscores = true; + break; + } + } + if (hasUnderscores) { + final byte[] tmp = new byte[bytes.length]; + for (int i = 0; i < bytes.length; i++) { + final byte b = bytes[i]; + if (b != UNDERSCORE) { + tmp[i] = b; + } else { + tmp[i] = BLANK; + } + } + return QuotedPrintableCodec.decodeQuotedPrintable(tmp); + } + return QuotedPrintableCodec.decodeQuotedPrintable(bytes); + } + + /** + * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped. + * + * @param str + * string to convert to quoted-printable form + * @param charset + * the charset for str + * @return quoted-printable string + * @throws EncoderException + * thrown if a failure condition is encountered during the encoding process. + * @since 1.7 + */ + public String encode(final String str, final Charset charset) throws EncoderException { + if (str == null) { + return null; + } + return encodeText(str, charset); + } + + /** + * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped. + * + * @param str + * string to convert to quoted-printable form + * @param charset + * the charset for str + * @return quoted-printable string + * @throws EncoderException + * thrown if a failure condition is encountered during the encoding process. + */ + public String encode(final String str, final String charset) throws EncoderException { + if (str == null) { + return null; + } + try { + return encodeText(str, charset); + } catch (final UnsupportedEncodingException e) { + throw new EncoderException(e.getMessage(), e); + } + } + + /** + * Encodes a string into its quoted-printable form using the default charset. Unsafe characters are escaped. + * + * @param str + * string to convert to quoted-printable form + * @return quoted-printable string + * @throws EncoderException + * thrown if a failure condition is encountered during the encoding process. + */ + @Override + public String encode(final String str) throws EncoderException { + if (str == null) { + return null; + } + return encode(str, getCharset()); + } + + /** + * Decodes a quoted-printable string into its original form. Escaped characters are converted back to their original + * representation. + * + * @param str + * quoted-printable string to convert into its original form + * @return original string + * @throws DecoderException + * A decoder exception is thrown if a failure condition is encountered during the decode process. + */ + @Override + public String decode(final String str) throws DecoderException { + if (str == null) { + return null; + } + try { + return decodeText(str); + } catch (final UnsupportedEncodingException e) { + throw new DecoderException(e.getMessage(), e); + } + } + + /** + * Encodes an object into its quoted-printable form using the default charset. Unsafe characters are escaped. + * + * @param obj + * object to convert to quoted-printable form + * @return quoted-printable object + * @throws EncoderException + * thrown if a failure condition is encountered during the encoding process. + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (obj == null) { + return null; + } else if (obj instanceof String) { + return encode((String) obj); + } else { + throw new EncoderException("Objects of type " + + obj.getClass().getName() + + " cannot be encoded using Q codec"); + } + } + + /** + * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original + * representation. + * + * @param obj + * quoted-printable object to convert into its original form + * @return original object + * @throws DecoderException + * Thrown if the argument is not a String. Thrown if a failure condition is encountered + * during the decode process. + */ + @Override + public Object decode(final Object obj) throws DecoderException { + if (obj == null) { + return null; + } else if (obj instanceof String) { + return decode((String) obj); + } else { + throw new DecoderException("Objects of type " + + obj.getClass().getName() + + " cannot be decoded using Q codec"); + } + } + + /** + * Gets the default charset name used for string decoding and encoding. + * + * @return the default charset name + * @since 1.7 + */ + public Charset getCharset() { + return this.charset; + } + + /** + * Gets the default charset name used for string decoding and encoding. + * + * @return the default charset name + */ + public String getDefaultCharset() { + return this.charset.name(); + } + + /** + * Tests if optional transformation of SPACE characters is to be used + * + * @return true if SPACE characters are to be transformed, false otherwise + */ + public boolean isEncodeBlanks() { + return this.encodeBlanks; + } + + /** + * Defines whether optional transformation of SPACE characters is to be used + * + * @param b + * true if SPACE characters are to be transformed, false otherwise + */ + public void setEncodeBlanks(final boolean b) { + this.encodeBlanks = b; + } +} diff --git a/src/org/apache/commons/codec/net/QuotedPrintableCodec.java b/src/org/apache/commons/codec/net/QuotedPrintableCodec.java new file mode 100644 index 00000000..82b88617 --- /dev/null +++ b/src/org/apache/commons/codec/net/QuotedPrintableCodec.java @@ -0,0 +1,602 @@ +/* + * 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 org.apache.commons.codec.net; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; +import java.util.BitSet; + +import org.apache.commons.codec.BinaryDecoder; +import org.apache.commons.codec.BinaryEncoder; +import org.apache.commons.codec.Charsets; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringDecoder; +import org.apache.commons.codec.StringEncoder; +import org.apache.commons.codec.binary.StringUtils; + +/** + * Codec for the Quoted-Printable section of RFC 1521. + *

+ * The Quoted-Printable encoding is intended to represent data that largely consists of octets that correspond to + * printable characters in the ASCII character set. It encodes the data in such a way that the resulting octets are + * unlikely to be modified by mail transport. If the data being encoded are mostly ASCII text, the encoded form of the + * data remains largely recognizable by humans. A body which is entirely ASCII may also be encoded in Quoted-Printable + * to ensure the integrity of the data should the message pass through a character- translating, and/or line-wrapping + * gateway. + *

+ * Note: + *

+ * Depending on the selected {@code strict} parameter, this class will implement a different set of rules of the + * quoted-printable spec: + *

    + *
  • {@code strict=false}: only rules #1 and #2 are implemented + *
  • {@code strict=true}: all rules #1 through #5 are implemented + *
+ * Originally, this class only supported the non-strict mode, but the codec in this partial form could already be used + * for certain applications that do not require quoted-printable line formatting (rules #3, #4, #5), for instance + * Q codec. The strict mode has been added in 1.10. + *

+ * This class is immutable and thread-safe. + * + * @see RFC 1521 MIME (Multipurpose Internet Mail Extensions) Part One: + * Mechanisms for Specifying and Describing the Format of Internet Message Bodies + * + * @since 1.3 + * @version $Id$ + */ +public class QuotedPrintableCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder { + /** + * The default charset used for string decoding and encoding. + */ + private final Charset charset; + + /** + * Indicates whether soft line breaks shall be used during encoding (rule #3-5). + */ + private final boolean strict; + + /** + * BitSet of printable characters as defined in RFC 1521. + */ + private static final BitSet PRINTABLE_CHARS = new BitSet(256); + + private static final byte ESCAPE_CHAR = '='; + + private static final byte TAB = 9; + + private static final byte SPACE = 32; + + private static final byte CR = 13; + + private static final byte LF = 10; + + /** + * Safe line length for quoted printable encoded text. + */ + private static final int SAFE_LENGTH = 73; + + // Static initializer for printable chars collection + static { + // alpha characters + for (int i = 33; i <= 60; i++) { + PRINTABLE_CHARS.set(i); + } + for (int i = 62; i <= 126; i++) { + PRINTABLE_CHARS.set(i); + } + PRINTABLE_CHARS.set(TAB); + PRINTABLE_CHARS.set(SPACE); + } + + /** + * Default constructor, assumes default charset of {@link Charsets#UTF_8} + */ + public QuotedPrintableCodec() { + this(Charsets.UTF_8, false); + } + + /** + * Constructor which allows for the selection of the strict mode. + * + * @param strict + * if {@code true}, soft line breaks will be used + * @since 1.10 + */ + public QuotedPrintableCodec(final boolean strict) { + this(Charsets.UTF_8, strict); + } + + /** + * Constructor which allows for the selection of a default charset. + * + * @param charset + * the default string charset to use. + * @since 1.7 + */ + public QuotedPrintableCodec(final Charset charset) { + this(charset, false); + } + + /** + * Constructor which allows for the selection of a default charset and strict mode. + * + * @param charset + * the default string charset to use. + * @param strict + * if {@code true}, soft line breaks will be used + * @since 1.10 + */ + public QuotedPrintableCodec(final Charset charset, final boolean strict) { + this.charset = charset; + this.strict = strict; + } + + /** + * Constructor which allows for the selection of a default charset. + * + * @param charsetName + * the default string charset to use. + * @throws UnsupportedCharsetException + * If no support for the named charset is available + * in this instance of the Java virtual machine + * @throws IllegalArgumentException + * If the given charsetName is null + * @throws IllegalCharsetNameException + * If the given charset name is illegal + * + * @since 1.7 throws UnsupportedCharsetException if the named charset is unavailable + */ + public QuotedPrintableCodec(final String charsetName) + throws IllegalCharsetNameException, IllegalArgumentException, UnsupportedCharsetException { + this(Charset.forName(charsetName), false); + } + + /** + * Encodes byte into its quoted-printable representation. + * + * @param b + * byte to encode + * @param buffer + * the buffer to write to + * @return The number of bytes written to the buffer + */ + private static final int encodeQuotedPrintable(final int b, final ByteArrayOutputStream buffer) { + buffer.write(ESCAPE_CHAR); + final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, 16)); + final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, 16)); + buffer.write(hex1); + buffer.write(hex2); + return 3; + } + + /** + * Return the byte at position index of the byte array and + * make sure it is unsigned. + * + * @param index + * position in the array + * @param bytes + * the byte array + * @return the unsigned octet at position index from the array + */ + private static int getUnsignedOctet(final int index, final byte[] bytes) { + int b = bytes[index]; + if (b < 0) { + b = 256 + b; + } + return b; + } + + /** + * Write a byte to the buffer. + * + * @param b + * byte to write + * @param encode + * indicates whether the octet shall be encoded + * @param buffer + * the buffer to write to + * @return the number of bytes that have been written to the buffer + */ + private static int encodeByte(final int b, final boolean encode, + final ByteArrayOutputStream buffer) { + if (encode) { + return encodeQuotedPrintable(b, buffer); + } else { + buffer.write(b); + return 1; + } + } + + /** + * Checks whether the given byte is whitespace. + * + * @param b + * byte to be checked + * @return true if the byte is either a space or tab character + */ + private static boolean isWhitespace(final int b) { + return b == SPACE || b == TAB; + } + + /** + * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped. + *

+ * This function implements a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in + * RFC 1521 and is suitable for encoding binary data and unformatted text. + * + * @param printable + * bitset of characters deemed quoted-printable + * @param bytes + * array of bytes to be encoded + * @return array of bytes containing quoted-printable data + */ + public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[] bytes) { + return encodeQuotedPrintable(printable, bytes, false); + } + + /** + * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped. + *

+ * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset + * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in + * RFC 1521 and is suitable for encoding binary data and unformatted text. + * + * @param printable + * bitset of characters deemed quoted-printable + * @param bytes + * array of bytes to be encoded + * @param strict + * if {@code true} the full ruleset is used, otherwise only rule #1 and rule #2 + * @return array of bytes containing quoted-printable data + * @since 1.10 + */ + public static final byte[] encodeQuotedPrintable(BitSet printable, final byte[] bytes, boolean strict) { + if (bytes == null) { + return null; + } + if (printable == null) { + printable = PRINTABLE_CHARS; + } + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + if (strict) { + int pos = 1; + // encode up to buffer.length - 3, the last three octets will be treated + // separately for simplification of note #3 + for (int i = 0; i < bytes.length - 3; i++) { + int b = getUnsignedOctet(i, bytes); + if (pos < SAFE_LENGTH) { + // up to this length it is safe to add any byte, encoded or not + pos += encodeByte(b, !printable.get(b), buffer); + } else { + // rule #3: whitespace at the end of a line *must* be encoded + encodeByte(b, !printable.get(b) || isWhitespace(b), buffer); + + // rule #5: soft line break + buffer.write(ESCAPE_CHAR); + buffer.write(CR); + buffer.write(LF); + pos = 1; + } + } + + // rule #3: whitespace at the end of a line *must* be encoded + // if we would do a soft break line after this octet, encode whitespace + int b = getUnsignedOctet(bytes.length - 3, bytes); + boolean encode = !printable.get(b) || (isWhitespace(b) && pos > SAFE_LENGTH - 5); + pos += encodeByte(b, encode, buffer); + + // note #3: '=' *must not* be the ultimate or penultimate character + // simplification: if < 6 bytes left, do a soft line break as we may need + // exactly 6 bytes space for the last 2 bytes + if (pos > SAFE_LENGTH - 2) { + buffer.write(ESCAPE_CHAR); + buffer.write(CR); + buffer.write(LF); + } + for (int i = bytes.length - 2; i < bytes.length; i++) { + b = getUnsignedOctet(i, bytes); + // rule #3: trailing whitespace shall be encoded + encode = !printable.get(b) || (i > bytes.length - 2 && isWhitespace(b)); + encodeByte(b, encode, buffer); + } + } else { + for (final byte c : bytes) { + int b = c; + if (b < 0) { + b = 256 + b; + } + if (printable.get(b)) { + buffer.write(b); + } else { + encodeQuotedPrintable(b, buffer); + } + } + } + return buffer.toByteArray(); + } + + /** + * Decodes an array quoted-printable characters into an array of original bytes. Escaped characters are converted + * back to their original representation. + *

+ * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as + * defined in RFC 1521. + * + * @param bytes + * array of quoted-printable characters + * @return array of original bytes + * @throws DecoderException + * Thrown if quoted-printable decoding is unsuccessful + */ + public static final byte[] decodeQuotedPrintable(final byte[] bytes) throws DecoderException { + if (bytes == null) { + return null; + } + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + for (int i = 0; i < bytes.length; i++) { + final int b = bytes[i]; + if (b == ESCAPE_CHAR) { + try { + // if the next octet is a CR we have found a soft line break + if (bytes[++i] == CR) { + continue; + } + final int u = Utils.digit16(bytes[i]); + final int l = Utils.digit16(bytes[++i]); + buffer.write((char) ((u << 4) + l)); + } catch (final ArrayIndexOutOfBoundsException e) { + throw new DecoderException("Invalid quoted-printable encoding", e); + } + } else if (b != CR && b != LF) { + // every other octet is appended except for CR & LF + buffer.write(b); + } + } + return buffer.toByteArray(); + } + + /** + * Encodes an array of bytes into an array of quoted-printable 7-bit characters. Unsafe characters are escaped. + *

+ * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset + * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in + * RFC 1521 and is suitable for encoding binary data and unformatted text. + * + * @param bytes + * array of bytes to be encoded + * @return array of bytes containing quoted-printable data + */ + @Override + public byte[] encode(final byte[] bytes) { + return encodeQuotedPrintable(PRINTABLE_CHARS, bytes, strict); + } + + /** + * Decodes an array of quoted-printable characters into an array of original bytes. Escaped characters are converted + * back to their original representation. + *

+ * This function fully implements the quoted-printable encoding specification (rule #1 through rule #5) as + * defined in RFC 1521. + * + * @param bytes + * array of quoted-printable characters + * @return array of original bytes + * @throws DecoderException + * Thrown if quoted-printable decoding is unsuccessful + */ + @Override + public byte[] decode(final byte[] bytes) throws DecoderException { + return decodeQuotedPrintable(bytes); + } + + /** + * Encodes a string into its quoted-printable form using the default string charset. Unsafe characters are escaped. + *

+ * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset + * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in + * RFC 1521 and is suitable for encoding binary data and unformatted text. + * + * @param str + * string to convert to quoted-printable form + * @return quoted-printable string + * @throws EncoderException + * Thrown if quoted-printable encoding is unsuccessful + * + * @see #getCharset() + */ + @Override + public String encode(final String str) throws EncoderException { + return this.encode(str, getCharset()); + } + + /** + * Decodes a quoted-printable string into its original form using the specified string charset. Escaped characters + * are converted back to their original representation. + * + * @param str + * quoted-printable string to convert into its original form + * @param charset + * the original string charset + * @return original string + * @throws DecoderException + * Thrown if quoted-printable decoding is unsuccessful + * @since 1.7 + */ + public String decode(final String str, final Charset charset) throws DecoderException { + if (str == null) { + return null; + } + return new String(this.decode(StringUtils.getBytesUsAscii(str)), charset); + } + + /** + * Decodes a quoted-printable string into its original form using the specified string charset. Escaped characters + * are converted back to their original representation. + * + * @param str + * quoted-printable string to convert into its original form + * @param charset + * the original string charset + * @return original string + * @throws DecoderException + * Thrown if quoted-printable decoding is unsuccessful + * @throws UnsupportedEncodingException + * Thrown if charset is not supported + */ + public String decode(final String str, final String charset) throws DecoderException, UnsupportedEncodingException { + if (str == null) { + return null; + } + return new String(decode(StringUtils.getBytesUsAscii(str)), charset); + } + + /** + * Decodes a quoted-printable string into its original form using the default string charset. Escaped characters are + * converted back to their original representation. + * + * @param str + * quoted-printable string to convert into its original form + * @return original string + * @throws DecoderException + * Thrown if quoted-printable decoding is unsuccessful. Thrown if charset is not supported. + * @see #getCharset() + */ + @Override + public String decode(final String str) throws DecoderException { + return this.decode(str, this.getCharset()); + } + + /** + * Encodes an object into its quoted-printable safe form. Unsafe characters are escaped. + * + * @param obj + * string to convert to a quoted-printable form + * @return quoted-printable object + * @throws EncoderException + * Thrown if quoted-printable encoding is not applicable to objects of this type or if encoding is + * unsuccessful + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (obj == null) { + return null; + } else if (obj instanceof byte[]) { + return encode((byte[]) obj); + } else if (obj instanceof String) { + return encode((String) obj); + } else { + throw new EncoderException("Objects of type " + + obj.getClass().getName() + + " cannot be quoted-printable encoded"); + } + } + + /** + * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original + * representation. + * + * @param obj + * quoted-printable object to convert into its original form + * @return original object + * @throws DecoderException + * Thrown if the argument is not a String or byte[]. Thrown if a failure + * condition is encountered during the decode process. + */ + @Override + public Object decode(final Object obj) throws DecoderException { + if (obj == null) { + return null; + } else if (obj instanceof byte[]) { + return decode((byte[]) obj); + } else if (obj instanceof String) { + return decode((String) obj); + } else { + throw new DecoderException("Objects of type " + + obj.getClass().getName() + + " cannot be quoted-printable decoded"); + } + } + + /** + * Gets the default charset name used for string decoding and encoding. + * + * @return the default charset name + * @since 1.7 + */ + public Charset getCharset() { + return this.charset; + } + + /** + * Gets the default charset name used for string decoding and encoding. + * + * @return the default charset name + */ + public String getDefaultCharset() { + return this.charset.name(); + } + + /** + * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped. + *

+ * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset + * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in + * RFC 1521 and is suitable for encoding binary data and unformatted text. + * + * @param str + * string to convert to quoted-printable form + * @param charset + * the charset for str + * @return quoted-printable string + * @since 1.7 + */ + public String encode(final String str, final Charset charset) { + if (str == null) { + return null; + } + return StringUtils.newStringUsAscii(this.encode(str.getBytes(charset))); + } + + /** + * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped. + *

+ * Depending on the selection of the {@code strict} parameter, this function either implements the full ruleset + * or only a subset of quoted-printable encoding specification (rule #1 and rule #2) as defined in + * RFC 1521 and is suitable for encoding binary data and unformatted text. + * + * @param str + * string to convert to quoted-printable form + * @param charset + * the charset for str + * @return quoted-printable string + * @throws UnsupportedEncodingException + * Thrown if the charset is not supported + */ + public String encode(final String str, final String charset) throws UnsupportedEncodingException { + if (str == null) { + return null; + } + return StringUtils.newStringUsAscii(encode(str.getBytes(charset))); + } +} diff --git a/src/org/apache/commons/codec/net/RFC1522Codec.java b/src/org/apache/commons/codec/net/RFC1522Codec.java new file mode 100644 index 00000000..6cad34f9 --- /dev/null +++ b/src/org/apache/commons/codec/net/RFC1522Codec.java @@ -0,0 +1,186 @@ +/* + * 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 org.apache.commons.codec.net; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.binary.StringUtils; + +/** + * Implements methods common to all codecs defined in RFC 1522. + *

+ * RFC 1522 describes techniques to allow the + * encoding of non-ASCII text in various portions of a RFC 822 [2] message header, in a manner which + * is unlikely to confuse existing message handling software. + *

+ * This class is immutable and thread-safe. + * + * @see MIME (Multipurpose Internet Mail Extensions) Part Two: + * Message Header Extensions for Non-ASCII Text + * + * @since 1.3 + * @version $Id$ + */ +abstract class RFC1522Codec { + + /** Separator. */ + protected static final char SEP = '?'; + + /** Prefix. */ + protected static final String POSTFIX = "?="; + + /** Postfix. */ + protected static final String PREFIX = "=?"; + + /** + * Applies an RFC 1522 compliant encoding scheme to the given string of text with the given charset. + *

+ * This method constructs the "encoded-word" header common to all the RFC 1522 codecs and then invokes + * {@link #doEncoding(byte [])} method of a concrete class to perform the specific encoding. + * + * @param text + * a string to encode + * @param charset + * a charset to be used + * @return RFC 1522 compliant "encoded-word" + * @throws EncoderException + * thrown if there is an error condition during the Encoding process. + * @see Standard charsets + */ + protected String encodeText(final String text, final Charset charset) throws EncoderException { + if (text == null) { + return null; + } + final StringBuilder buffer = new StringBuilder(); + buffer.append(PREFIX); + buffer.append(charset); + buffer.append(SEP); + buffer.append(this.getEncoding()); + buffer.append(SEP); + final byte [] rawData = this.doEncoding(text.getBytes(charset)); + buffer.append(StringUtils.newStringUsAscii(rawData)); + buffer.append(POSTFIX); + return buffer.toString(); + } + + /** + * Applies an RFC 1522 compliant encoding scheme to the given string of text with the given charset. + *

+ * This method constructs the "encoded-word" header common to all the RFC 1522 codecs and then invokes + * {@link #doEncoding(byte [])} method of a concrete class to perform the specific encoding. + * + * @param text + * a string to encode + * @param charsetName + * the charset to use + * @return RFC 1522 compliant "encoded-word" + * @throws EncoderException + * thrown if there is an error condition during the Encoding process. + * @throws UnsupportedEncodingException + * if charset is not available + * + * @see Standard charsets + */ + protected String encodeText(final String text, final String charsetName) + throws EncoderException, UnsupportedEncodingException { + if (text == null) { + return null; + } + return this.encodeText(text, Charset.forName(charsetName)); + } + + /** + * Applies an RFC 1522 compliant decoding scheme to the given string of text. + *

+ * This method processes the "encoded-word" header common to all the RFC 1522 codecs and then invokes + * {@link #doEncoding(byte [])} method of a concrete class to perform the specific decoding. + * + * @param text + * a string to decode + * @return A new decoded String or null if the input is null. + * @throws DecoderException + * thrown if there is an error condition during the decoding process. + * @throws UnsupportedEncodingException + * thrown if charset specified in the "encoded-word" header is not supported + */ + protected String decodeText(final String text) + throws DecoderException, UnsupportedEncodingException { + if (text == null) { + return null; + } + if (!text.startsWith(PREFIX) || !text.endsWith(POSTFIX)) { + throw new DecoderException("RFC 1522 violation: malformed encoded content"); + } + final int terminator = text.length() - 2; + int from = 2; + int to = text.indexOf(SEP, from); + if (to == terminator) { + throw new DecoderException("RFC 1522 violation: charset token not found"); + } + final String charset = text.substring(from, to); + if (charset.equals("")) { + throw new DecoderException("RFC 1522 violation: charset not specified"); + } + from = to + 1; + to = text.indexOf(SEP, from); + if (to == terminator) { + throw new DecoderException("RFC 1522 violation: encoding token not found"); + } + final String encoding = text.substring(from, to); + if (!getEncoding().equalsIgnoreCase(encoding)) { + throw new DecoderException("This codec cannot decode " + encoding + " encoded content"); + } + from = to + 1; + to = text.indexOf(SEP, from); + byte[] data = StringUtils.getBytesUsAscii(text.substring(from, to)); + data = doDecoding(data); + return new String(data, charset); + } + + /** + * Returns the codec name (referred to as encoding in the RFC 1522). + * + * @return name of the codec + */ + protected abstract String getEncoding(); + + /** + * Encodes an array of bytes using the defined encoding scheme. + * + * @param bytes + * Data to be encoded + * @return A byte array containing the encoded data + * @throws EncoderException + * thrown if the Encoder encounters a failure condition during the encoding process. + */ + protected abstract byte[] doEncoding(byte[] bytes) throws EncoderException; + + /** + * Decodes an array of bytes using the defined encoding scheme. + * + * @param bytes + * Data to be decoded + * @return a byte array that contains decoded data + * @throws DecoderException + * A decoder exception is thrown if a Decoder encounters a failure condition during the decode process. + */ + protected abstract byte[] doDecoding(byte[] bytes) throws DecoderException; +} diff --git a/src/org/apache/commons/codec/net/URLCodec.java b/src/org/apache/commons/codec/net/URLCodec.java new file mode 100644 index 00000000..e53cd635 --- /dev/null +++ b/src/org/apache/commons/codec/net/URLCodec.java @@ -0,0 +1,368 @@ +/* + * 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 org.apache.commons.codec.net; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.util.BitSet; + +import org.apache.commons.codec.BinaryDecoder; +import org.apache.commons.codec.BinaryEncoder; +import org.apache.commons.codec.CharEncoding; +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.EncoderException; +import org.apache.commons.codec.StringDecoder; +import org.apache.commons.codec.StringEncoder; +import org.apache.commons.codec.binary.StringUtils; + +/** + * Implements the 'www-form-urlencoded' encoding scheme, also misleadingly known as URL encoding. + *

+ * This codec is meant to be a replacement for standard Java classes {@link java.net.URLEncoder} and + * {@link java.net.URLDecoder} on older Java platforms, as these classes in Java versions below + * 1.4 rely on the platform's default charset encoding. + *

+ * This class is immutable and thread-safe. + * + * @see Chapter 17.13.4 Form content types + * of the HTML 4.01 Specification + * + * @since 1.2 + * @version $Id$ + */ +public class URLCodec implements BinaryEncoder, BinaryDecoder, StringEncoder, StringDecoder { + + /** + * Radix used in encoding and decoding. + */ + static final int RADIX = 16; + + /** + * The default charset used for string decoding and encoding. + * + * @deprecated TODO: This field will be changed to a private final Charset in 2.0. + */ + @Deprecated + protected String charset; + + /** + * Release 1.5 made this field final. + */ + protected static final byte ESCAPE_CHAR = '%'; + /** + * BitSet of www-form-url safe characters. + */ + protected static final BitSet WWW_FORM_URL = new BitSet(256); + + // Static initializer for www_form_url + static { + // alpha characters + for (int i = 'a'; i <= 'z'; i++) { + WWW_FORM_URL.set(i); + } + for (int i = 'A'; i <= 'Z'; i++) { + WWW_FORM_URL.set(i); + } + // numeric characters + for (int i = '0'; i <= '9'; i++) { + WWW_FORM_URL.set(i); + } + // special chars + WWW_FORM_URL.set('-'); + WWW_FORM_URL.set('_'); + WWW_FORM_URL.set('.'); + WWW_FORM_URL.set('*'); + // blank to be replaced with + + WWW_FORM_URL.set(' '); + } + + + /** + * Default constructor. + */ + public URLCodec() { + this(CharEncoding.UTF_8); + } + + /** + * Constructor which allows for the selection of a default charset. + * + * @param charset the default string charset to use. + */ + public URLCodec(final String charset) { + super(); + this.charset = charset; + } + + /** + * Encodes an array of bytes into an array of URL safe 7-bit characters. Unsafe characters are escaped. + * + * @param urlsafe + * bitset of characters deemed URL safe + * @param bytes + * array of bytes to convert to URL safe characters + * @return array of bytes containing URL safe characters + */ + public static final byte[] encodeUrl(BitSet urlsafe, final byte[] bytes) { + if (bytes == null) { + return null; + } + if (urlsafe == null) { + urlsafe = WWW_FORM_URL; + } + + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + for (final byte c : bytes) { + int b = c; + if (b < 0) { + b = 256 + b; + } + if (urlsafe.get(b)) { + if (b == ' ') { + b = '+'; + } + buffer.write(b); + } else { + buffer.write(ESCAPE_CHAR); + final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, RADIX)); + final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, RADIX)); + buffer.write(hex1); + buffer.write(hex2); + } + } + return buffer.toByteArray(); + } + + /** + * Decodes an array of URL safe 7-bit characters into an array of original bytes. Escaped characters are converted + * back to their original representation. + * + * @param bytes + * array of URL safe characters + * @return array of original bytes + * @throws DecoderException + * Thrown if URL decoding is unsuccessful + */ + public static final byte[] decodeUrl(final byte[] bytes) throws DecoderException { + if (bytes == null) { + return null; + } + final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + for (int i = 0; i < bytes.length; i++) { + final int b = bytes[i]; + if (b == '+') { + buffer.write(' '); + } else if (b == ESCAPE_CHAR) { + try { + final int u = Utils.digit16(bytes[++i]); + final int l = Utils.digit16(bytes[++i]); + buffer.write((char) ((u << 4) + l)); + } catch (final ArrayIndexOutOfBoundsException e) { + throw new DecoderException("Invalid URL encoding: ", e); + } + } else { + buffer.write(b); + } + } + return buffer.toByteArray(); + } + + /** + * Encodes an array of bytes into an array of URL safe 7-bit characters. Unsafe characters are escaped. + * + * @param bytes + * array of bytes to convert to URL safe characters + * @return array of bytes containing URL safe characters + */ + @Override + public byte[] encode(final byte[] bytes) { + return encodeUrl(WWW_FORM_URL, bytes); + } + + + /** + * Decodes an array of URL safe 7-bit characters into an array of original bytes. Escaped characters are converted + * back to their original representation. + * + * @param bytes + * array of URL safe characters + * @return array of original bytes + * @throws DecoderException + * Thrown if URL decoding is unsuccessful + */ + @Override + public byte[] decode(final byte[] bytes) throws DecoderException { + return decodeUrl(bytes); + } + + /** + * Encodes a string into its URL safe form using the specified string charset. Unsafe characters are escaped. + * + * @param str + * string to convert to a URL safe form + * @param charset + * the charset for str + * @return URL safe string + * @throws UnsupportedEncodingException + * Thrown if charset is not supported + */ + public String encode(final String str, final String charset) throws UnsupportedEncodingException { + if (str == null) { + return null; + } + return StringUtils.newStringUsAscii(encode(str.getBytes(charset))); + } + + /** + * Encodes a string into its URL safe form using the default string charset. Unsafe characters are escaped. + * + * @param str + * string to convert to a URL safe form + * @return URL safe string + * @throws EncoderException + * Thrown if URL encoding is unsuccessful + * + * @see #getDefaultCharset() + */ + @Override + public String encode(final String str) throws EncoderException { + if (str == null) { + return null; + } + try { + return encode(str, getDefaultCharset()); + } catch (final UnsupportedEncodingException e) { + throw new EncoderException(e.getMessage(), e); + } + } + + + /** + * Decodes a URL safe string into its original form using the specified encoding. Escaped characters are converted + * back to their original representation. + * + * @param str + * URL safe string to convert into its original form + * @param charset + * the original string charset + * @return original string + * @throws DecoderException + * Thrown if URL decoding is unsuccessful + * @throws UnsupportedEncodingException + * Thrown if charset is not supported + */ + public String decode(final String str, final String charset) throws DecoderException, UnsupportedEncodingException { + if (str == null) { + return null; + } + return new String(decode(StringUtils.getBytesUsAscii(str)), charset); + } + + /** + * Decodes a URL safe string into its original form using the default string charset. Escaped characters are + * converted back to their original representation. + * + * @param str + * URL safe string to convert into its original form + * @return original string + * @throws DecoderException + * Thrown if URL decoding is unsuccessful + * @see #getDefaultCharset() + */ + @Override + public String decode(final String str) throws DecoderException { + if (str == null) { + return null; + } + try { + return decode(str, getDefaultCharset()); + } catch (final UnsupportedEncodingException e) { + throw new DecoderException(e.getMessage(), e); + } + } + + /** + * Encodes an object into its URL safe form. Unsafe characters are escaped. + * + * @param obj + * string to convert to a URL safe form + * @return URL safe object + * @throws EncoderException + * Thrown if URL encoding is not applicable to objects of this type or if encoding is unsuccessful + */ + @Override + public Object encode(final Object obj) throws EncoderException { + if (obj == null) { + return null; + } else if (obj instanceof byte[]) { + return encode((byte[])obj); + } else if (obj instanceof String) { + return encode((String)obj); + } else { + throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be URL encoded"); + + } + } + + /** + * Decodes a URL safe object into its original form. Escaped characters are converted back to their original + * representation. + * + * @param obj + * URL safe object to convert into its original form + * @return original object + * @throws DecoderException + * Thrown if the argument is not a String or byte[]. Thrown if a failure + * condition is encountered during the decode process. + */ + @Override + public Object decode(final Object obj) throws DecoderException { + if (obj == null) { + return null; + } else if (obj instanceof byte[]) { + return decode((byte[]) obj); + } else if (obj instanceof String) { + return decode((String) obj); + } else { + throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be URL decoded"); + + } + } + + /** + * The default charset used for string decoding and encoding. + * + * @return the default string charset. + */ + public String getDefaultCharset() { + return this.charset; + } + + /** + * The String encoding used for decoding and encoding. + * + * @return Returns the encoding. + * + * @deprecated Use {@link #getDefaultCharset()}, will be removed in 2.0. + */ + @Deprecated + public String getEncoding() { + return this.charset; + } + +} diff --git a/src/org/apache/commons/codec/net/Utils.java b/src/org/apache/commons/codec/net/Utils.java new file mode 100644 index 00000000..c482c0ab --- /dev/null +++ b/src/org/apache/commons/codec/net/Utils.java @@ -0,0 +1,50 @@ +/* + * 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 org.apache.commons.codec.net; + +import org.apache.commons.codec.DecoderException; + +/** + * Utility methods for this package. + * + *

This class is immutable and thread-safe.

+ * + * @version $Id$ + * @since 1.4 + */ +class Utils { + + /** + * Returns the numeric value of the character b in radix 16. + * + * @param b + * The byte to be converted. + * @return The numeric value represented by the character in radix 16. + * + * @throws DecoderException + * Thrown when the byte is not valid per {@link Character#digit(char,int)} + */ + static int digit16(final byte b) throws DecoderException { + final int i = Character.digit((char) b, URLCodec.RADIX); + if (i == -1) { + throw new DecoderException("Invalid URL encoding: not a valid digit (radix " + URLCodec.RADIX + "): " + b); + } + return i; + } + +} diff --git a/src/org/apache/commons/codec/net/package.html b/src/org/apache/commons/codec/net/package.html new file mode 100644 index 00000000..2b8ceab2 --- /dev/null +++ b/src/org/apache/commons/codec/net/package.html @@ -0,0 +1,23 @@ + + + +

+ Network related encoding and decoding. +

+ + diff --git a/src/org/apache/commons/codec/overview.html b/src/org/apache/commons/codec/overview.html new file mode 100644 index 00000000..76b0a621 --- /dev/null +++ b/src/org/apache/commons/codec/overview.html @@ -0,0 +1,29 @@ + + + + +

+This document is the API specification for the Apache Commons Codec Library, version 1.3. +

+

+This library requires a JRE version of 1.2.2 or greater. +The hypertext links originating from this document point to Sun's version 1.3 API as the 1.2.2 API documentation +is no longer on-line. +

+ + diff --git a/src/org/apache/commons/codec/package.html b/src/org/apache/commons/codec/package.html new file mode 100644 index 00000000..fc1fac13 --- /dev/null +++ b/src/org/apache/commons/codec/package.html @@ -0,0 +1,100 @@ + + + + + + +

Interfaces and classes used by + the various implementations in the sub-packages.

+ +

Definitive implementations of commonly used encoders and decoders.

+ +

Codec is currently comprised of a modest set of utilities and a + simple framework for String encoding and decoding in three categories: + Binary Encoders, Language Encoders, and Network Encoders.

+ +

Binary Encoders

+ + + + + + + + + + + + + + +
+ + org.apache.commons.codec.binary.Base64 + + Provides Base64 content-transfer-encoding as defined in + RFC 2045 + Production
+ + org.apache.commons.codec.binary.Hex + + Converts an array of bytes into an array of characters + representing the hexadecimal values of each byte in order + Production
+

+ Language Encoders +

+

+ Codec contains a number of commonly used language and phonetic + encoders +

+ + + + + + + + + + + + + +
+ org.apache.commons.codec.language.Soundex + Implementation of the Soundex algorithm.Production
+ org.apache.commons.codec.language.Metaphone + Implementation of the Metaphone algorithm.Production
+

Network Encoders

+

+

Codec contains network related encoders

+ + + + + + + + +
+ org.apache.commons.codec.net.URLCodec + Implements the 'www-form-urlencoded' encoding scheme.Production
+
+ + diff --git a/src/org/imgscalr/AsyncScalr.java b/src/org/imgscalr/AsyncScalr.java new file mode 100644 index 00000000..3be52752 --- /dev/null +++ b/src/org/imgscalr/AsyncScalr.java @@ -0,0 +1,594 @@ +/** + * Copyright 2011 The Buzz Media, LLC + * + * 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 org.imgscalr; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ImagingOpException; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.imgscalr.Scalr.Method; +import org.imgscalr.Scalr.Mode; +import org.imgscalr.Scalr.Rotation; + +/** + * Class used to provide the asynchronous versions of all the methods defined in + * {@link Scalr} for the purpose of efficiently handling large amounts of image + * operations via a select number of processing threads asynchronously. + *

+ * Given that image-scaling operations, especially when working with large + * images, can be very hardware-intensive (both CPU and memory), in large-scale + * deployments (e.g. a busy web application) it becomes increasingly important + * that the scale operations performed by imgscalr be manageable so as not to + * fire off too many simultaneous operations that the JVM's heap explodes and + * runs out of memory or pegs the CPU on the host machine, staving all other + * running processes. + *

+ * Up until now it was left to the caller to implement their own serialization + * or limiting logic to handle these use-cases. Given imgscalr's popularity in + * web applications it was determined that this requirement be common enough + * that it should be integrated directly into the imgscalr library for everyone + * to benefit from. + *

+ * Every method in this class wraps the matching methods in the {@link Scalr} + * class in new {@link Callable} instances that are submitted to an internal + * {@link ExecutorService} for execution at a later date. A {@link Future} is + * returned to the caller representing the task that is either currently + * performing the scale operation or will at a future date depending on where it + * is in the {@link ExecutorService}'s queue. {@link Future#get()} or + * {@link Future#get(long, TimeUnit)} can be used to block on the + * Future, waiting for the scale operation to complete and return + * the resultant {@link BufferedImage} to the caller. + *

+ * This design provides the following features: + *

    + *
  • Non-blocking, asynchronous scale operations that can continue execution + * while waiting on the scaled result.
  • + *
  • Serialize all scale requests down into a maximum number of + * simultaneous scale operations with no additional/complex logic. The + * number of simultaneous scale operations is caller-configurable (see + * {@link #THREAD_COUNT}) so as best to optimize the host system (e.g. 1 scale + * thread per core).
  • + *
  • No need to worry about overloading the host system with too many scale + * operations, they will simply queue up in this class and execute in-order.
  • + *
  • Synchronous/blocking behavior can still be achieved (if desired) by + * calling get() or get(long, TimeUnit) immediately on + * the returned {@link Future} from any of the methods below.
  • + *
+ *

Performance

+ * When tuning this class for optimal performance, benchmarking your particular + * hardware is the best approach. For some rough guidelines though, there are + * two resources you want to watch closely: + *
    + *
  1. JVM Heap Memory (Assume physical machine memory is always sufficiently + * large)
  2. + *
  3. # of CPU Cores
  4. + *
+ * You never want to allocate more scaling threads than you have CPU cores and + * on a sufficiently busy host where some of the cores may be busy running a + * database or a web server, you will want to allocate even less scaling + * threads. + *

+ * So as a maximum you would never want more scaling threads than CPU cores in + * any situation and less so on a busy server. + *

+ * If you allocate more threads than you have available CPU cores, your scaling + * operations will slow down as the CPU will spend a considerable amount of time + * context-switching between threads on the same core trying to finish all the + * tasks in parallel. You might still be tempted to do this because of the I/O + * delay some threads will encounter reading images off disk, but when you do + * your own benchmarking you'll likely find (as I did) that the actual disk I/O + * necessary to pull the image data off disk is a much smaller portion of the + * execution time than the actual scaling operations. + *

+ * If you are executing on a storage medium that is unexpectedly slow and I/O is + * a considerable portion of the scaling operation (e.g. S3 or EBS volumes), + * feel free to try using more threads than CPU cores to see if that helps; but + * in most normal cases, it will only slow down all other parallel scaling + * operations. + *

+ * As for memory, every time an image is scaled it is decoded into a + * {@link BufferedImage} and stored in the JVM Heap space (decoded image + * instances are always larger than the source images on-disk). For larger + * images, that can use up quite a bit of memory. You will need to benchmark + * your particular use-cases on your hardware to get an idea of where the sweet + * spot is for this; if you are operating within tight memory bounds, you may + * want to limit simultaneous scaling operations to 1 or 2 regardless of the + * number of cores just to avoid having too many {@link BufferedImage} instances + * in JVM Heap space at the same time. + *

+ * These are rough metrics and behaviors to give you an idea of how best to tune + * this class for your deployment, but nothing can replacement writing a small + * Java class that scales a handful of images in a number of different ways and + * testing that directly on your deployment hardware. + *

Resource Overhead

+ * The {@link ExecutorService} utilized by this class won't be initialized until + * one of the operation methods are called, at which point the + * service will be instantiated for the first time and operation + * queued up. + *

+ * More specifically, if you have no need for asynchronous image processing + * offered by this class, you don't need to worry about wasted resources or + * hanging/idle threads as they will never be created if you never use this + * class. + *

Cleaning up Service Threads

+ * By default the {@link Thread}s created by the internal + * {@link ThreadPoolExecutor} do not run in daemon mode; which + * means they will block the host VM from exiting until they are explicitly shut + * down in a client application; in a server application the container will shut + * down the pool forcibly. + *

+ * If you have used the {@link AsyncScalr} class and are trying to shut down a + * client application, you will need to call {@link #getService()} then + * {@link ExecutorService#shutdown()} or {@link ExecutorService#shutdownNow()} + * to have the threads terminated; you may also want to look at the + * {@link ExecutorService#awaitTermination(long, TimeUnit)} method if you'd like + * to more closely monitor the shutting down process (and finalization of + * pending scale operations). + *

Reusing Shutdown AsyncScalr

+ * If you have previously called shutdown on the underlying service + * utilized by this class, subsequent calls to any of the operations this class + * provides will invoke the internal {@link #checkService()} method which will + * replace the terminated underlying {@link ExecutorService} with a new one via + * the {@link #createService()} method. + *

Custom Implementations

+ * If a subclass wants to customize the {@link ExecutorService} or + * {@link ThreadFactory} used under the covers, this can be done by overriding + * the {@link #createService()} method which is invoked by this class anytime a + * new {@link ExecutorService} is needed. + *

+ * By default the {@link #createService()} method delegates to the + * {@link #createService(ThreadFactory)} method with a new instance of + * {@link DefaultThreadFactory}. Either of these methods can be overridden and + * customized easily if desired. + *

+ * TIP: A common customization to this class is to make the + * {@link Thread}s generated by the underlying factory more server-friendly, in + * which case the caller would want to use an instance of the + * {@link ServerThreadFactory} when creating the new {@link ExecutorService}. + *

+ * This can be done in one line by overriding {@link #createService()} and + * returning the result of: + * return createService(new ServerThreadFactory()); + *

+ * By default this class uses an {@link ThreadPoolExecutor} internally to handle + * execution of queued image operations. If a different type of + * {@link ExecutorService} is desired, again, simply overriding the + * {@link #createService()} method of choice is the right way to do that. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 3.2 + */ +@SuppressWarnings("javadoc") +public class AsyncScalr { + /** + * System property name used to set the number of threads the default + * underlying {@link ExecutorService} will use to process async image + * operations. + *

+ * Value is "imgscalr.async.threadCount". + */ + public static final String THREAD_COUNT_PROPERTY_NAME = "imgscalr.async.threadCount"; + + /** + * Number of threads the internal {@link ExecutorService} will use to + * simultaneously execute scale requests. + *

+ * This value can be changed by setting the + * imgscalr.async.threadCount system property (see + * {@link #THREAD_COUNT_PROPERTY_NAME}) to a valid integer value > 0. + *

+ * Default value is 2. + */ + public static final int THREAD_COUNT = Integer.getInteger( + THREAD_COUNT_PROPERTY_NAME, 2); + + /** + * Initializer used to verify the THREAD_COUNT system property. + */ + static { + if (THREAD_COUNT < 1) + throw new RuntimeException("System property '" + + THREAD_COUNT_PROPERTY_NAME + "' set THREAD_COUNT to " + + THREAD_COUNT + ", but THREAD_COUNT must be > 0."); + } + + protected static ExecutorService service; + + /** + * Used to get access to the internal {@link ExecutorService} used by this + * class to process scale operations. + *

+ * NOTE: You will need to explicitly shutdown any service + * currently set on this class before the host JVM exits. + *

+ * You can call {@link ExecutorService#shutdown()} to wait for all scaling + * operations to complete first or call + * {@link ExecutorService#shutdownNow()} to kill any in-process operations + * and purge all pending operations before exiting. + *

+ * Additionally you can use + * {@link ExecutorService#awaitTermination(long, TimeUnit)} after issuing a + * shutdown command to try and wait until the service has finished all + * tasks. + * + * @return the current {@link ExecutorService} used by this class to process + * scale operations. + */ + public static ExecutorService getService() { + return service; + } + + /** + * @see Scalr#apply(BufferedImage, BufferedImageOp...) + */ + public static Future apply(final BufferedImage src, + final BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.apply(src, ops); + } + }); + } + + /** + * @see Scalr#crop(BufferedImage, int, int, BufferedImageOp...) + */ + public static Future crop(final BufferedImage src, + final int width, final int height, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.crop(src, width, height, ops); + } + }); + } + + /** + * @see Scalr#crop(BufferedImage, int, int, int, int, BufferedImageOp...) + */ + public static Future crop(final BufferedImage src, + final int x, final int y, final int width, final int height, + final BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.crop(src, x, y, width, height, ops); + } + }); + } + + /** + * @see Scalr#pad(BufferedImage, int, BufferedImageOp...) + */ + public static Future pad(final BufferedImage src, + final int padding, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.pad(src, padding, ops); + } + }); + } + + /** + * @see Scalr#pad(BufferedImage, int, Color, BufferedImageOp...) + */ + public static Future pad(final BufferedImage src, + final int padding, final Color color, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.pad(src, padding, color, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, + final int targetSize, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, targetSize, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Method, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, + final Method scalingMethod, final int targetSize, + final BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, scalingMethod, targetSize, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Mode, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, + final Mode resizeMode, final int targetSize, + final BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, resizeMode, targetSize, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Method, Mode, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, + final Method scalingMethod, final Mode resizeMode, + final int targetSize, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, scalingMethod, resizeMode, targetSize, + ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, int, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, + final int targetWidth, final int targetHeight, + final BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, targetWidth, targetHeight, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Method, int, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, + final Method scalingMethod, final int targetWidth, + final int targetHeight, final BufferedImageOp... ops) { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, scalingMethod, targetWidth, + targetHeight, ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Mode, int, int, BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, + final Mode resizeMode, final int targetWidth, + final int targetHeight, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, resizeMode, targetWidth, targetHeight, + ops); + } + }); + } + + /** + * @see Scalr#resize(BufferedImage, Method, Mode, int, int, + * BufferedImageOp...) + */ + public static Future resize(final BufferedImage src, + final Method scalingMethod, final Mode resizeMode, + final int targetWidth, final int targetHeight, + final BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.resize(src, scalingMethod, resizeMode, + targetWidth, targetHeight, ops); + } + }); + } + + /** + * @see Scalr#rotate(BufferedImage, Rotation, BufferedImageOp...) + */ + public static Future rotate(final BufferedImage src, + final Rotation rotation, final BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + checkService(); + + return service.submit(new Callable() { + public BufferedImage call() throws Exception { + return Scalr.rotate(src, rotation, ops); + } + }); + } + + protected static ExecutorService createService() { + return createService(new DefaultThreadFactory()); + } + + protected static ExecutorService createService(ThreadFactory factory) + throws IllegalArgumentException { + if (factory == null) + throw new IllegalArgumentException("factory cannot be null"); + + return Executors.newFixedThreadPool(THREAD_COUNT, factory); + } + + /** + * Used to verify that the underlying service points at an + * active {@link ExecutorService} instance that can be used by this class. + *

+ * If service is null, has been shutdown or + * terminated then this method will replace it with a new + * {@link ExecutorService} by calling the {@link #createService()} method + * and assigning the returned value to service. + *

+ * Any subclass that wants to customize the {@link ExecutorService} or + * {@link ThreadFactory} used internally by this class should override the + * {@link #createService()}. + */ + protected static void checkService() { + if (service == null || service.isShutdown() || service.isTerminated()) { + /* + * If service was shutdown or terminated, assigning a new value will + * free the reference to the instance, allowing it to be GC'ed when + * it is done shutting down (assuming it hadn't already). + */ + service = createService(); + } + } + + /** + * Default {@link ThreadFactory} used by the internal + * {@link ExecutorService} to creates execution {@link Thread}s for image + * scaling. + *

+ * More or less a copy of the hidden class backing the + * {@link Executors#defaultThreadFactory()} method, but exposed here to make + * it easier for implementors to extend and customize. + * + * @author Doug Lea + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 4.0 + */ + protected static class DefaultThreadFactory implements ThreadFactory { + protected static final AtomicInteger poolNumber = new AtomicInteger(1); + + protected final ThreadGroup group; + protected final AtomicInteger threadNumber = new AtomicInteger(1); + protected final String namePrefix; + + DefaultThreadFactory() { + SecurityManager manager = System.getSecurityManager(); + + /* + * Determine the group that threads created by this factory will be + * in. + */ + group = (manager == null ? Thread.currentThread().getThreadGroup() + : manager.getThreadGroup()); + + /* + * Define a common name prefix for the threads created by this + * factory. + */ + namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-"; + } + + /** + * Used to create a {@link Thread} capable of executing the given + * {@link Runnable}. + *

+ * Thread created by this factory are utilized by the parent + * {@link ExecutorService} when processing queued up scale operations. + */ + public Thread newThread(Runnable r) { + /* + * Create a new thread in our specified group with a meaningful + * thread name so it is easy to identify. + */ + Thread thread = new Thread(group, r, namePrefix + + threadNumber.getAndIncrement(), 0); + + // Configure thread according to class or subclass + thread.setDaemon(false); + thread.setPriority(Thread.NORM_PRIORITY); + + return thread; + } + } + + /** + * An extension of the {@link DefaultThreadFactory} class that makes two + * changes to the execution {@link Thread}s it generations: + *

    + *
  1. Threads are set to be daemon threads instead of user threads.
  2. + *
  3. Threads execute with a priority of {@link Thread#MIN_PRIORITY} to + * make them more compatible with server environment deployments.
  4. + *
+ * This class is provided as a convenience for subclasses to use if they + * want this (common) customization to the {@link Thread}s used internally + * by {@link AsyncScalr} to process images, but don't want to have to write + * the implementation. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 4.0 + */ + protected static class ServerThreadFactory extends DefaultThreadFactory { + /** + * Overridden to set daemon property to true + * and decrease the priority of the new thread to + * {@link Thread#MIN_PRIORITY} before returning it. + */ + @Override + public Thread newThread(Runnable r) { + Thread thread = super.newThread(r); + + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + + return thread; + } + } +} \ No newline at end of file diff --git a/src/org/imgscalr/Scalr.java b/src/org/imgscalr/Scalr.java new file mode 100644 index 00000000..d08911c1 --- /dev/null +++ b/src/org/imgscalr/Scalr.java @@ -0,0 +1,2349 @@ +/** + * Copyright 2011 The Buzz Media, LLC + * + * 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 org.imgscalr; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.RenderingHints; +import java.awt.Transparency; +import java.awt.color.ColorSpace; +import java.awt.geom.AffineTransform; +import java.awt.geom.Rectangle2D; +import java.awt.image.AreaAveragingScaleFilter; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ColorConvertOp; +import java.awt.image.ColorModel; +import java.awt.image.ConvolveOp; +import java.awt.image.ImagingOpException; +import java.awt.image.IndexColorModel; +import java.awt.image.Kernel; +import java.awt.image.RasterFormatException; +import java.awt.image.RescaleOp; + +import javax.imageio.ImageIO; + +/** + * Class used to implement performant, high-quality and intelligent image + * scaling and manipulation algorithms in native Java 2D. + *

+ * This class utilizes the Java2D "best practices" for image manipulation, + * ensuring that all operations (even most user-provided {@link BufferedImageOp} + * s) are hardware accelerated if provided by the platform and host-VM. + *

+ *

Image Quality

+ * This class implements a few different methods for scaling an image, providing + * either the best-looking result, the fastest result or a balanced result + * between the two depending on the scaling hint provided (see {@link Method}). + *

+ * This class also implements an optimized version of the incremental scaling + * algorithm presented by Chris Campbell in his Perils of + * Image.getScaledInstance() article in order to give the best-looking image + * resize results (e.g. generating thumbnails that aren't blurry or jagged). + *

+ * The results generated by imgscalr using this method, as compared to a single + * {@link RenderingHints#VALUE_INTERPOLATION_BICUBIC} scale operation look much + * better, especially when using the {@link Method#ULTRA_QUALITY} method. + *

+ * Only when scaling using the {@link Method#AUTOMATIC} method will this class + * look at the size of the image before selecting an approach to scaling the + * image. If {@link Method#QUALITY} is specified, the best-looking algorithm + * possible is always used. + *

+ * Minor modifications are made to Campbell's original implementation in the + * form of: + *

    + *
  1. Instead of accepting a user-supplied interpolation method, + * {@link RenderingHints#VALUE_INTERPOLATION_BICUBIC} interpolation is always + * used. This was done after A/B comparison testing with large images + * down-scaled to thumbnail sizes showed noticeable "blurring" when BILINEAR + * interpolation was used. Given that Campbell's algorithm is only used in + * QUALITY mode when down-scaling, it was determined that the user's expectation + * of a much less blurry picture would require that BICUBIC be the default + * interpolation in order to meet the QUALITY expectation.
  2. + *
  3. After each iteration of the do-while loop that incrementally scales the + * source image down, an explicit effort is made to call + * {@link BufferedImage#flush()} on the interim temporary {@link BufferedImage} + * instances created by the algorithm in an attempt to ensure a more complete GC + * cycle by the VM when cleaning up the temporary instances (this is in addition + * to disposing of the temporary {@link Graphics2D} references as well).
  4. + *
  5. Extensive comments have been added to increase readability of the code.
  6. + *
  7. Variable names have been expanded to increase readability of the code.
  8. + *
+ *

+ * NOTE: This class does not call {@link BufferedImage#flush()} + * on any of the source images passed in by calling code; it is up to + * the original caller to dispose of their source images when they are no longer + * needed so the VM can most efficiently GC them. + *

Image Proportions

+ * All scaling operations implemented by this class maintain the proportions of + * the original image unless a mode of {@link Mode#FIT_EXACT} is specified; in + * which case the orientation and proportion of the source image is ignored and + * the image is stretched (if necessary) to fit the exact dimensions given. + *

+ * When not using {@link Mode#FIT_EXACT}, in order to maintain the + * proportionality of the original images, this class implements the following + * behavior: + *

    + *
  1. If the image is LANDSCAPE-oriented or SQUARE, treat the + * targetWidth as the primary dimension and re-calculate the + * targetHeight regardless of what is passed in.
  2. + *
  3. If image is PORTRAIT-oriented, treat the targetHeight as the + * primary dimension and re-calculate the targetWidth regardless of + * what is passed in.
  4. + *
  5. If a {@link Mode} value of {@link Mode#FIT_TO_WIDTH} or + * {@link Mode#FIT_TO_HEIGHT} is passed in to the resize method, + * the image's orientation is ignored and the scaled image is fit to the + * preferred dimension by using the value passed in by the user for that + * dimension and recalculating the other (regardless of image orientation). This + * is useful, for example, when working with PORTRAIT oriented images that you + * need to all be the same width or visa-versa (e.g. showing user profile + * pictures in a directory listing).
  6. + *
+ *

Optimized Image Handling

+ * Java2D provides support for a number of different image types defined as + * BufferedImage.TYPE_* variables, unfortunately not all image + * types are supported equally in the Java2D rendering pipeline. + *

+ * Some more obscure image types either have poor or no support, leading to + * severely degraded quality and processing performance when an attempt is made + * by imgscalr to create a scaled instance of the same type as the + * source image. In many cases, especially when applying {@link BufferedImageOp} + * s, using poorly supported image types can even lead to exceptions or total + * corruption of the image (e.g. solid black image). + *

+ * imgscalr specifically accounts for and automatically hands + * ALL of these pain points for you internally by shuffling all + * images into one of two types: + *

    + *
  1. {@link BufferedImage#TYPE_INT_RGB}
  2. + *
  3. {@link BufferedImage#TYPE_INT_ARGB}
  4. + *
+ * depending on if the source image utilizes transparency or not. This is a + * recommended approach by the Java2D team for dealing with poorly (or non) + * supported image types. More can be read about this issue here. + *

+ * This is also the reason we recommend using + * {@link #apply(BufferedImage, BufferedImageOp...)} to apply your own ops to + * images even if you aren't using imgscalr for anything else. + *

GIF Transparency

+ * Unfortunately in Java 6 and earlier, support for GIF's + * {@link IndexColorModel} is sub-par, both in accurate color-selection and in + * maintaining transparency when moving to an image of type + * {@link BufferedImage#TYPE_INT_ARGB}; because of this issue when a GIF image + * is processed by imgscalr and the result saved as a GIF file (instead of PNG), + * it is possible to lose the alpha channel of a transparent image or in the + * case of applying an optional {@link BufferedImageOp}, lose the entire picture + * all together in the result (long standing JDK bugs are filed for all of these + * issues). + *

+ * imgscalr currently does nothing to work around this manually because it is a + * defect in the native platform code itself. Fortunately it looks like the + * issues are half-fixed in Java 7 and any manual workarounds we could attempt + * internally are relatively expensive, in the form of hand-creating and setting + * RGB values pixel-by-pixel with a custom {@link ColorModel} in the scaled + * image. This would lead to a very measurable negative impact on performance + * without the caller understanding why. + *

+ * Workaround: A workaround to this issue with all version of + * Java is to simply save a GIF as a PNG; no change to your code needs to be + * made except when the image is saved out, e.g. using {@link ImageIO}. + *

+ * When a file type of "PNG" is used, both the transparency and high color + * quality will be maintained as the PNG code path in Java2D is superior to the + * GIF implementation. + *

+ * If the issue with optional {@link BufferedImageOp}s destroying GIF image + * content is ever fixed in the platform, saving out resulting images as GIFs + * should suddenly start working. + *

+ * More can be read about the issue here and here. + *

Thread Safety

+ * The {@link Scalr} class is thread-safe (as all the methods + * are static); this class maintains no internal state while + * performing any of the provided operations and is safe to call simultaneously + * from multiple threads. + *

Logging

+ * This class implements all its debug logging via the + * {@link #log(int, String, Object...)} method. At this time logging is done + * directly to System.out via the printf method. This + * allows the logging to be light weight and easy to capture (every imgscalr log + * message is prefixed with the {@link #LOG_PREFIX} string) while adding no + * dependencies to the library. + *

+ * Implementation of logging in this class is as efficient as possible; avoiding + * any calls to the logger method or passing of arguments if logging is not + * enabled to avoid the (hidden) cost of constructing the Object[] argument for + * the varargs-based method call. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 1.1 + */ +public class Scalr { + /** + * System property name used to define the debug boolean flag. + *

+ * Value is "imgscalr.debug". + */ + public static final String DEBUG_PROPERTY_NAME = "imgscalr.debug"; + + /** + * System property name used to define a custom log prefix. + *

+ * Value is "imgscalr.logPrefix". + */ + public static final String LOG_PREFIX_PROPERTY_NAME = "imgscalr.logPrefix"; + + /** + * Flag used to indicate if debugging output has been enabled by setting the + * "imgscalr.debug" system property to true. This + * value will be false if the "imgscalr.debug" + * system property is undefined or set to false. + *

+ * This property can be set on startup with:
+ * + * -Dimgscalr.debug=true + * or by calling {@link System#setProperty(String, String)} to set a + * new property value for {@link #DEBUG_PROPERTY_NAME} before this class is + * loaded. + *

+ * Default value is false. + */ + public static final boolean DEBUG = Boolean.getBoolean(DEBUG_PROPERTY_NAME); + + /** + * Prefix to every log message this library logs. Using a well-defined + * prefix helps make it easier both visually and programmatically to scan + * log files for messages produced by this library. + *

+ * This property can be set on startup with:
+ * + * -Dimgscalr.logPrefix=<YOUR PREFIX HERE> + * or by calling {@link System#setProperty(String, String)} to set a + * new property value for {@link #LOG_PREFIX_PROPERTY_NAME} before this + * class is loaded. + *

+ * Default value is "[imgscalr] " (including the space). + */ + public static final String LOG_PREFIX = System.getProperty( + LOG_PREFIX_PROPERTY_NAME, "[imgscalr] "); + + /** + * A {@link ConvolveOp} using a very light "blur" kernel that acts like an + * anti-aliasing filter (softens the image a bit) when applied to an image. + *

+ * A common request by users of the library was that they wished to "soften" + * resulting images when scaling them down drastically. After quite a bit of + * A/B testing, the kernel used by this Op was selected as the closest match + * for the target which was the softer results from the deprecated + * {@link AreaAveragingScaleFilter} (which is used internally by the + * deprecated {@link Image#getScaledInstance(int, int, int)} method in the + * JDK that imgscalr is meant to replace). + *

+ * This ConvolveOp uses a 3x3 kernel with the values: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
.0f.08f.0f
.08f.68f.08f
.0f.08f.0f
+ *

+ * For those that have worked with ConvolveOps before, this Op uses the + * {@link ConvolveOp#EDGE_NO_OP} instruction to not process the pixels along + * the very edge of the image (otherwise EDGE_ZERO_FILL would create a + * black-border around the image). If you have not worked with a ConvolveOp + * before, it just means this default OP will "do the right thing" and not + * give you garbage results. + *

+ * This ConvolveOp uses no {@link RenderingHints} values as internally the + * {@link ConvolveOp} class only uses hints when doing a color conversion + * between the source and destination {@link BufferedImage} targets. + * imgscalr allows the {@link ConvolveOp} to create its own destination + * image every time, so no color conversion is ever needed and thus no + * hints. + *

Performance

+ * Use of this (and other) {@link ConvolveOp}s are hardware accelerated when + * possible. For more information on if your image op is hardware + * accelerated or not, check the source code of the underlying JDK class + * that actually executes the Op code, sun.awt.image.ImagingLib. + *

Known Issues

+ * In all versions of Java (tested up to Java 7 preview Build 131), running + * this op against a GIF with transparency and attempting to save the + * resulting image as a GIF results in a corrupted/empty file. The file must + * be saved out as a PNG to maintain the transparency. + * + * @since 3.0 + */ + public static final ConvolveOp OP_ANTIALIAS = new ConvolveOp( + new Kernel(3, 3, new float[] { .0f, .08f, .0f, .08f, .68f, .08f, + .0f, .08f, .0f }), ConvolveOp.EDGE_NO_OP, null); + + /** + * A {@link RescaleOp} used to make any input image 10% darker. + *

+ * This operation can be applied multiple times in a row if greater than 10% + * changes in brightness are desired. + * + * @since 4.0 + */ + public static final RescaleOp OP_DARKER = new RescaleOp(0.9f, 0, null); + + /** + * A {@link RescaleOp} used to make any input image 10% brighter. + *

+ * This operation can be applied multiple times in a row if greater than 10% + * changes in brightness are desired. + * + * @since 4.0 + */ + public static final RescaleOp OP_BRIGHTER = new RescaleOp(1.1f, 0, null); + + /** + * A {@link ColorConvertOp} used to convert any image to a grayscale color + * palette. + *

+ * Applying this op multiple times to the same image has no compounding + * effects. + * + * @since 4.0 + */ + public static final ColorConvertOp OP_GRAYSCALE = new ColorConvertOp( + ColorSpace.getInstance(ColorSpace.CS_GRAY), null); + + /** + * Static initializer used to prepare some of the variables used by this + * class. + */ + static { + log(0, "Debug output ENABLED"); + } + + /** + * Used to define the different scaling hints that the algorithm can use. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 1.1 + */ + public static enum Method { + /** + * Used to indicate that the scaling implementation should decide which + * method to use in order to get the best looking scaled image in the + * least amount of time. + *

+ * The scaling algorithm will use the + * {@link Scalr#THRESHOLD_QUALITY_BALANCED} or + * {@link Scalr#THRESHOLD_BALANCED_SPEED} thresholds as cut-offs to + * decide between selecting the QUALITY, + * BALANCED or SPEED scaling algorithms. + *

+ * By default the thresholds chosen will give nearly the best looking + * result in the fastest amount of time. We intend this method to work + * for 80% of people looking to scale an image quickly and get a good + * looking result. + */ + AUTOMATIC, + /** + * Used to indicate that the scaling implementation should scale as fast + * as possible and return a result. For smaller images (800px in size) + * this can result in noticeable aliasing but it can be a few magnitudes + * times faster than using the QUALITY method. + */ + SPEED, + /** + * Used to indicate that the scaling implementation should use a scaling + * operation balanced between SPEED and QUALITY. Sometimes SPEED looks + * too low quality to be useful (e.g. text can become unreadable when + * scaled using SPEED) but using QUALITY mode will increase the + * processing time too much. This mode provides a "better than SPEED" + * quality in a "less than QUALITY" amount of time. + */ + BALANCED, + /** + * Used to indicate that the scaling implementation should do everything + * it can to create as nice of a result as possible. This approach is + * most important for smaller pictures (800px or smaller) and less + * important for larger pictures as the difference between this method + * and the SPEED method become less and less noticeable as the + * source-image size increases. Using the AUTOMATIC method will + * automatically prefer the QUALITY method when scaling an image down + * below 800px in size. + */ + QUALITY, + /** + * Used to indicate that the scaling implementation should go above and + * beyond the work done by {@link Method#QUALITY} to make the image look + * exceptionally good at the cost of more processing time. This is + * especially evident when generating thumbnails of images that look + * jagged with some of the other {@link Method}s (even + * {@link Method#QUALITY}). + */ + ULTRA_QUALITY; + } + + /** + * Used to define the different modes of resizing that the algorithm can + * use. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 3.1 + */ + public static enum Mode { + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the resultant image by looking at the image's + * orientation and generating proportional dimensions that best fit into + * the target width and height given + * + * See "Image Proportions" in the {@link Scalr} class description for + * more detail. + */ + AUTOMATIC, + /** + * Used to fit the image to the exact dimensions given regardless of the + * image's proportions. If the dimensions are not proportionally + * correct, this will introduce vertical or horizontal stretching to the + * image. + *

+ * It is recommended that you use one of the other FIT_TO + * modes or {@link Mode#AUTOMATIC} if you want the image to look + * correct, but if dimension-fitting is the #1 priority regardless of + * how it makes the image look, that is what this mode is for. + */ + FIT_EXACT, + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the largest image that fit within the bounding box, + * without cropping or distortion, retaining the original proportions. + */ + BEST_FIT_BOTH, + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the resultant image that best-fit within the given + * width, regardless of the orientation of the image. + */ + FIT_TO_WIDTH, + /** + * Used to indicate that the scaling implementation should calculate + * dimensions for the resultant image that best-fit within the given + * height, regardless of the orientation of the image. + */ + FIT_TO_HEIGHT; + } + + /** + * Used to define the different types of rotations that can be applied to an + * image during a resize operation. + * + * @author Riyad Kalla (software@thebuzzmedia.com) + * @since 3.2 + */ + public static enum Rotation { + /** + * 90-degree, clockwise rotation (to the right). This is equivalent to a + * quarter-turn of the image to the right; moving the picture on to its + * right side. + */ + CW_90, + /** + * 180-degree, clockwise rotation (to the right). This is equivalent to + * 1 half-turn of the image to the right; rotating the picture around + * until it is upside down from the original position. + */ + CW_180, + /** + * 270-degree, clockwise rotation (to the right). This is equivalent to + * a quarter-turn of the image to the left; moving the picture on to its + * left side. + */ + CW_270, + /** + * Flip the image horizontally by reflecting it around the y axis. + *

+ * This is not a standard rotation around a center point, but instead + * creates the mirrored reflection of the image horizontally. + *

+ * More specifically, the vertical orientation of the image stays the + * same (the top stays on top, and the bottom on bottom), but the right + * and left sides flip. This is different than a standard rotation where + * the top and bottom would also have been flipped. + */ + FLIP_HORZ, + /** + * Flip the image vertically by reflecting it around the x axis. + *

+ * This is not a standard rotation around a center point, but instead + * creates the mirrored reflection of the image vertically. + *

+ * More specifically, the horizontal orientation of the image stays the + * same (the left stays on the left and the right stays on the right), + * but the top and bottom sides flip. This is different than a standard + * rotation where the left and right would also have been flipped. + */ + FLIP_VERT; + } + + /** + * Threshold (in pixels) at which point the scaling operation using the + * {@link Method#AUTOMATIC} method will decide if a {@link Method#BALANCED} + * method will be used (if smaller than or equal to threshold) or a + * {@link Method#SPEED} method will be used (if larger than threshold). + *

+ * The bigger the image is being scaled to, the less noticeable degradations + * in the image becomes and the faster algorithms can be selected. + *

+ * The value of this threshold (1600) was chosen after visual, by-hand, A/B + * testing between different types of images scaled with this library; both + * photographs and screenshots. It was determined that images below this + * size need to use a {@link Method#BALANCED} scale method to look decent in + * most all cases while using the faster {@link Method#SPEED} method for + * images bigger than this threshold showed no noticeable degradation over a + * BALANCED scale. + */ + public static final int THRESHOLD_BALANCED_SPEED = 1600; + + /** + * Threshold (in pixels) at which point the scaling operation using the + * {@link Method#AUTOMATIC} method will decide if a {@link Method#QUALITY} + * method will be used (if smaller than or equal to threshold) or a + * {@link Method#BALANCED} method will be used (if larger than threshold). + *

+ * The bigger the image is being scaled to, the less noticeable degradations + * in the image becomes and the faster algorithms can be selected. + *

+ * The value of this threshold (800) was chosen after visual, by-hand, A/B + * testing between different types of images scaled with this library; both + * photographs and screenshots. It was determined that images below this + * size need to use a {@link Method#QUALITY} scale method to look decent in + * most all cases while using the faster {@link Method#BALANCED} method for + * images bigger than this threshold showed no noticeable degradation over a + * QUALITY scale. + */ + public static final int THRESHOLD_QUALITY_BALANCED = 800; + + /** + * Used to apply, in the order given, 1 or more {@link BufferedImageOp}s to + * a given {@link BufferedImage} and return the result. + *

+ * Feature: This implementation works around a + * decade-old JDK bug that can cause a {@link RasterFormatException} + * when applying a perfectly valid {@link BufferedImageOp}s to images. + *

+ * Feature: This implementation also works around + * {@link BufferedImageOp}s failing to apply and throwing + * {@link ImagingOpException}s when run against a src image + * type that is poorly supported. Unfortunately using {@link ImageIO} and + * standard Java methods to load images provides no consistency in getting + * images in well-supported formats. This method automatically accounts and + * corrects for all those problems (if necessary). + *

+ * It is recommended you always use this method to apply any + * {@link BufferedImageOp}s instead of relying on directly using the + * {@link BufferedImageOp#filter(BufferedImage, BufferedImage)} method. + *

+ * Performance: Not all {@link BufferedImageOp}s are + * hardware accelerated operations, but many of the most popular (like + * {@link ConvolveOp}) are. For more information on if your image op is + * hardware accelerated or not, check the source code of the underlying JDK + * class that actually executes the Op code, sun.awt.image.ImagingLib. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will have the ops applied to it. + * @param ops + * 1 or more ops to apply to the image. + * + * @return a new {@link BufferedImage} that represents the src + * with all the given operations applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if ops is null or empty. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage apply(BufferedImage src, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + long t = -1; + if (DEBUG) + t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (ops == null || ops.length == 0) + throw new IllegalArgumentException("ops cannot be null or empty"); + + int type = src.getType(); + + /* + * Ensure the src image is in the best supported image type before we + * continue, otherwise it is possible our calls below to getBounds2D and + * certainly filter(...) may fail if not. + * + * Java2D makes an attempt at applying most BufferedImageOps using + * hardware acceleration via the ImagingLib internal library. + * + * Unfortunately may of the BufferedImageOp are written to simply fail + * with an ImagingOpException if the operation cannot be applied with no + * additional information about what went wrong or attempts at + * re-applying it in different ways. + * + * This is assuming the failing BufferedImageOp even returns a null + * image after failing to apply; some simply return a corrupted/black + * image that result in no exception and it is up to the user to + * discover this. + * + * In internal testing, EVERY failure I've ever seen was the result of + * the source image being in a poorly-supported BufferedImage Type like + * BGR or ABGR (even though it was loaded with ImageIO). + * + * To avoid this nasty/stupid surprise with BufferedImageOps, we always + * ensure that the src image starts in an optimally supported format + * before we try and apply the filter. + */ + if (!(type == BufferedImage.TYPE_INT_RGB || type == BufferedImage.TYPE_INT_ARGB)) + src = copyToOptimalImage(src); + + if (DEBUG) + log(0, "Applying %d BufferedImageOps...", ops.length); + + boolean hasReassignedSrc = false; + + for (int i = 0; i < ops.length; i++) { + long subT = -1; + if (DEBUG) + subT = System.currentTimeMillis(); + BufferedImageOp op = ops[i]; + + // Skip null ops instead of throwing an exception. + if (op == null) + continue; + + if (DEBUG) + log(1, "Applying BufferedImageOp [class=%s, toString=%s]...", + op.getClass(), op.toString()); + + /* + * Must use op.getBounds instead of src.getWidth and src.getHeight + * because we are trying to create an image big enough to hold the + * result of this operation (which may be to scale the image + * smaller), in that case the bounds reported by this op and the + * bounds reported by the source image will be different. + */ + Rectangle2D resultBounds = op.getBounds2D(src); + + // Watch out for flaky/misbehaving ops that fail to work right. + if (resultBounds == null) + throw new ImagingOpException( + "BufferedImageOp [" + + op.toString() + + "] getBounds2D(src) returned null bounds for the target image; this should not happen and indicates a problem with application of this type of op."); + + /* + * We must manually create the target image; we cannot rely on the + * null-destination filter() method to create a valid destination + * for us thanks to this JDK bug that has been filed for almost a + * decade: + * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4965606 + */ + BufferedImage dest = createOptimalImage(src, + (int) Math.round(resultBounds.getWidth()), + (int) Math.round(resultBounds.getHeight())); + + // Perform the operation, update our result to return. + BufferedImage result = op.filter(src, dest); + + /* + * Flush the 'src' image ONLY IF it is one of our interim temporary + * images being used when applying 2 or more operations back to + * back. We never want to flush the original image passed in. + */ + if (hasReassignedSrc) + src.flush(); + + /* + * Incase there are more operations to perform, update what we + * consider the 'src' reference to our last result so on the next + * iteration the next op is applied to this result and not back + * against the original src passed in. + */ + src = result; + + /* + * Keep track of when we re-assign 'src' to an interim temporary + * image, so we know when we can explicitly flush it and clean up + * references on future iterations. + */ + hasReassignedSrc = true; + + if (DEBUG) + log(1, + "Applied BufferedImageOp in %d ms, result [width=%d, height=%d]", + System.currentTimeMillis() - subT, result.getWidth(), + result.getHeight()); + } + + if (DEBUG) + log(0, "All %d BufferedImageOps applied in %d ms", ops.length, + System.currentTimeMillis() - t); + + return src; + } + + /** + * Used to crop the given src image from the top-left corner + * and applying any optional {@link BufferedImageOp}s to the result before + * returning it. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image to crop. + * @param width + * The width of the bounding cropping box. + * @param height + * The height of the bounding cropping box. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing the cropped region of + * the src image with any optional operations applied + * to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if any coordinates of the bounding crop box is invalid within + * the bounds of the src image (e.g. negative or + * too big). + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage crop(BufferedImage src, int width, int height, + BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + return crop(src, 0, 0, width, height, ops); + } + + /** + * Used to crop the given src image and apply any optional + * {@link BufferedImageOp}s to it before returning the result. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image to crop. + * @param x + * The x-coordinate of the top-left corner of the bounding box + * used for cropping. + * @param y + * The y-coordinate of the top-left corner of the bounding box + * used for cropping. + * @param width + * The width of the bounding cropping box. + * @param height + * The height of the bounding cropping box. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing the cropped region of + * the src image with any optional operations applied + * to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if any coordinates of the bounding crop box is invalid within + * the bounds of the src image (e.g. negative or + * too big). + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage crop(BufferedImage src, int x, int y, + int width, int height, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + long t = -1; + if (DEBUG) + t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (x < 0 || y < 0 || width < 0 || height < 0) + throw new IllegalArgumentException("Invalid crop bounds: x [" + x + + "], y [" + y + "], width [" + width + "] and height [" + + height + "] must all be >= 0"); + + int srcWidth = src.getWidth(); + int srcHeight = src.getHeight(); + + if ((x + width) > srcWidth) + throw new IllegalArgumentException( + "Invalid crop bounds: x + width [" + (x + width) + + "] must be <= src.getWidth() [" + srcWidth + "]"); + if ((y + height) > srcHeight) + throw new IllegalArgumentException( + "Invalid crop bounds: y + height [" + (y + height) + + "] must be <= src.getHeight() [" + srcHeight + + "]"); + + if (DEBUG) + log(0, + "Cropping Image [width=%d, height=%d] to [x=%d, y=%d, width=%d, height=%d]...", + srcWidth, srcHeight, x, y, width, height); + + // Create a target image of an optimal type to render into. + BufferedImage result = createOptimalImage(src, width, height); + Graphics g = result.getGraphics(); + + /* + * Render the region specified by our crop bounds from the src image + * directly into our result image (which is the exact size of the crop + * region). + */ + g.drawImage(src, 0, 0, width, height, x, y, (x + width), (y + height), + null); + g.dispose(); + + if (DEBUG) + log(0, "Cropped Image in %d ms", System.currentTimeMillis() - t); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Used to apply padding around the edges of an image using + * {@link Color#BLACK} to fill the extra padded space and then return the + * result. + *

+ * The amount of padding specified is applied to all sides; + * more specifically, a padding of 2 would add 2 + * extra pixels of space (filled by the given color) on the + * top, bottom, left and right sides of the resulting image causing the + * result to be 4 pixels wider and 4 pixels taller than the src + * image. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image the padding will be added to. + * @param padding + * The number of pixels of padding to add to each side in the + * resulting image. If this value is 0 then + * src is returned unmodified. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing src with + * the given padding applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if padding is < 1. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage pad(BufferedImage src, int padding, + BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + return pad(src, padding, Color.BLACK); + } + + /** + * Used to apply padding around the edges of an image using the given color + * to fill the extra padded space and then return the result. {@link Color}s + * using an alpha channel (i.e. transparency) are supported. + *

+ * The amount of padding specified is applied to all sides; + * more specifically, a padding of 2 would add 2 + * extra pixels of space (filled by the given color) on the + * top, bottom, left and right sides of the resulting image causing the + * result to be 4 pixels wider and 4 pixels taller than the src + * image. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image the padding will be added to. + * @param padding + * The number of pixels of padding to add to each side in the + * resulting image. If this value is 0 then + * src is returned unmodified. + * @param color + * The color to fill the padded space with. {@link Color}s using + * an alpha channel (i.e. transparency) are supported. + * @param ops + * 0 or more ops to apply to the image. If + * null or empty then src is return + * unmodified. + * + * @return a new {@link BufferedImage} representing src with + * the given padding applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if padding is < 1. + * @throws IllegalArgumentException + * if color is null. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage pad(BufferedImage src, int padding, + Color color, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + long t = -1; + if (DEBUG) + t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (padding < 1) + throw new IllegalArgumentException("padding [" + padding + + "] must be > 0"); + if (color == null) + throw new IllegalArgumentException("color cannot be null"); + + int srcWidth = src.getWidth(); + int srcHeight = src.getHeight(); + + /* + * Double the padding to account for all sides of the image. More + * specifically, if padding is "1" we add 2 pixels to width and 2 to + * height, so we have 1 new pixel of padding all the way around our + * image. + */ + int sizeDiff = (padding * 2); + int newWidth = srcWidth + sizeDiff; + int newHeight = srcHeight + sizeDiff; + + if (DEBUG) + log(0, + "Padding Image from [originalWidth=%d, originalHeight=%d, padding=%d] to [newWidth=%d, newHeight=%d]...", + srcWidth, srcHeight, padding, newWidth, newHeight); + + boolean colorHasAlpha = (color.getAlpha() != 255); + boolean imageHasAlpha = (src.getTransparency() != BufferedImage.OPAQUE); + + BufferedImage result; + + /* + * We need to make sure our resulting image that we render into contains + * alpha if either our original image OR the padding color we are using + * contain it. + */ + if (colorHasAlpha || imageHasAlpha) { + if (DEBUG) + log(1, + "Transparency FOUND in source image or color, using ARGB image type..."); + + result = new BufferedImage(newWidth, newHeight, + BufferedImage.TYPE_INT_ARGB); + } else { + if (DEBUG) + log(1, + "Transparency NOT FOUND in source image or color, using RGB image type..."); + + result = new BufferedImage(newWidth, newHeight, + BufferedImage.TYPE_INT_RGB); + } + + Graphics g = result.getGraphics(); + + // "Clear" the background of the new image with our padding color first. + g.setColor(color); + g.fillRect(0, 0, newWidth, newHeight); + + // Draw the image into the center of the new padded image. + g.drawImage(src, padding, padding, null); + g.dispose(); + + if (DEBUG) + log(0, "Padding Applied in %d ms", System.currentTimeMillis() - t); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} and mode of + * {@link Mode#AUTOMATIC} are used. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage resize(BufferedImage src, int targetSize, + BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + return resize(src, Method.AUTOMATIC, Mode.AUTOMATIC, targetSize, + targetSize, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize using the given scaling + * method and apply the given {@link BufferedImageOp}s (if any) to the + * result before returning it. + *

+ * A mode of {@link Mode#AUTOMATIC} is used. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, + int targetSize, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, scalingMethod, Mode.AUTOMATIC, targetSize, + targetSize, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize (or fitting the image to + * the given WIDTH or HEIGHT explicitly, depending on the {@link Mode} + * specified) and apply the given {@link BufferedImageOp}s (if any) to the + * result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} is used. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Mode resizeMode, + int targetSize, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, Method.AUTOMATIC, resizeMode, targetSize, + targetSize, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to a width and + * height no bigger than targetSize (or fitting the image to + * the given WIDTH or HEIGHT explicitly, depending on the {@link Mode} + * specified) using the given scaling method and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetSize + * The target width and height (square) that you wish the image + * to fit within. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetSize is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, + Mode resizeMode, int targetSize, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, scalingMethod, resizeMode, targetSize, targetSize, + ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height and apply the given {@link BufferedImageOp}s (if any) to + * the result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} and mode of + * {@link Mode#AUTOMATIC} are used. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + */ + public static BufferedImage resize(BufferedImage src, int targetWidth, + int targetHeight, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, Method.AUTOMATIC, Mode.AUTOMATIC, targetWidth, + targetHeight, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height using the given scaling method and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * A mode of {@link Mode#AUTOMATIC} is used. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, + int targetWidth, int targetHeight, BufferedImageOp... ops) { + return resize(src, scalingMethod, Mode.AUTOMATIC, targetWidth, + targetHeight, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height (or fitting the image to the given WIDTH or HEIGHT + * explicitly, depending on the {@link Mode} specified) and apply the given + * {@link BufferedImageOp}s (if any) to the result before returning it. + *

+ * A scaling method of {@link Method#AUTOMATIC} is used. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Mode resizeMode, + int targetWidth, int targetHeight, BufferedImageOp... ops) + throws IllegalArgumentException, ImagingOpException { + return resize(src, Method.AUTOMATIC, resizeMode, targetWidth, + targetHeight, ops); + } + + /** + * Resize a given image (maintaining its original proportion) to the target + * width and height (or fitting the image to the given WIDTH or HEIGHT + * explicitly, depending on the {@link Mode} specified) using the given + * scaling method and apply the given {@link BufferedImageOp}s (if any) to + * the result before returning it. + *

+ * TIP: See the class description to understand how this + * class handles recalculation of the targetWidth or + * targetHeight depending on the image's orientation in order + * to maintain the original proportion. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will be scaled. + * @param scalingMethod + * The method used for scaling the image; preferring speed to + * quality or a balance of both. + * @param resizeMode + * Used to indicate how imgscalr should calculate the final + * target size for the image, either fitting the image to the + * given width ({@link Mode#FIT_TO_WIDTH}) or fitting the image + * to the given height ({@link Mode#FIT_TO_HEIGHT}). If + * {@link Mode#AUTOMATIC} is passed in, imgscalr will calculate + * proportional dimensions for the scaled image based on its + * orientation (landscape, square or portrait). Unless you have + * very specific size requirements, most of the time you just + * want to use {@link Mode#AUTOMATIC} to "do the right thing". + * @param targetWidth + * The target width that you wish the image to have. + * @param targetHeight + * The target height that you wish the image to have. + * @param ops + * 0 or more optional image operations (e.g. + * sharpen, blur, etc.) that can be applied to the final result + * before returning the image. + * + * @return a new {@link BufferedImage} representing the scaled + * src image. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if scalingMethod is null. + * @throws IllegalArgumentException + * if resizeMode is null. + * @throws IllegalArgumentException + * if targetWidth is < 0 or if + * targetHeight is < 0. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Method + * @see Mode + */ + public static BufferedImage resize(BufferedImage src, Method scalingMethod, + Mode resizeMode, int targetWidth, int targetHeight, + BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + long t = -1; + if (DEBUG) + t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (targetWidth < 0) + throw new IllegalArgumentException("targetWidth must be >= 0"); + if (targetHeight < 0) + throw new IllegalArgumentException("targetHeight must be >= 0"); + if (scalingMethod == null) + throw new IllegalArgumentException( + "scalingMethod cannot be null. A good default value is Method.AUTOMATIC."); + if (resizeMode == null) + throw new IllegalArgumentException( + "resizeMode cannot be null. A good default value is Mode.AUTOMATIC."); + + BufferedImage result = null; + + int currentWidth = src.getWidth(); + int currentHeight = src.getHeight(); + + // <= 1 is a square or landscape-oriented image, > 1 is a portrait. + float ratio = ((float) currentHeight / (float) currentWidth); + + if (DEBUG) + log(0, + "Resizing Image [size=%dx%d, resizeMode=%s, orientation=%s, ratio(H/W)=%f] to [targetSize=%dx%d]", + currentWidth, currentHeight, resizeMode, + (ratio <= 1 ? "Landscape/Square" : "Portrait"), ratio, + targetWidth, targetHeight); + + /* + * First determine if ANY size calculation needs to be done, in the case + * of FIT_EXACT, ignore image proportions and orientation and just use + * what the user sent in, otherwise the proportion of the picture must + * be honored. + * + * The way that is done is to figure out if the image is in a + * LANDSCAPE/SQUARE or PORTRAIT orientation and depending on its + * orientation, use the primary dimension (width for LANDSCAPE/SQUARE + * and height for PORTRAIT) to recalculate the alternative (height and + * width respectively) value that adheres to the existing ratio. + * + * This helps make life easier for the caller as they don't need to + * pre-compute proportional dimensions before calling the API, they can + * just specify the dimensions they would like the image to roughly fit + * within and it will do the right thing without mangling the result. + */ + if (resizeMode == Mode.FIT_EXACT) { + if (DEBUG) + log(1, + "Resize Mode FIT_EXACT used, no width/height checking or re-calculation will be done."); + } else if (resizeMode == Mode.BEST_FIT_BOTH) { + float requestedHeightScaling = ((float) targetHeight / (float) currentHeight); + float requestedWidthScaling = ((float) targetWidth / (float) currentWidth); + float actualScaling = Math.min(requestedHeightScaling, requestedWidthScaling); + + targetHeight = Math.round((float) currentHeight * actualScaling); + targetWidth = Math.round((float) currentWidth * actualScaling); + + if (targetHeight == currentHeight && targetWidth == currentWidth) + return src; + + if (DEBUG) + log(1, "Auto-Corrected width and height based on scalingRatio %d.", actualScaling); + } else { + if ((ratio <= 1 && resizeMode == Mode.AUTOMATIC) + || (resizeMode == Mode.FIT_TO_WIDTH)) { + // First make sure we need to do any work in the first place + if (targetWidth == src.getWidth()) + return src; + + // Save for detailed logging (this is cheap). + int originalTargetHeight = targetHeight; + + /* + * Landscape or Square Orientation: Ignore the given height and + * re-calculate a proportionally correct value based on the + * targetWidth. + */ + targetHeight = Math.round((float) targetWidth * ratio); + + if (DEBUG && originalTargetHeight != targetHeight) + log(1, + "Auto-Corrected targetHeight [from=%d to=%d] to honor image proportions.", + originalTargetHeight, targetHeight); + } else { + // First make sure we need to do any work in the first place + if (targetHeight == src.getHeight()) + return src; + + // Save for detailed logging (this is cheap). + int originalTargetWidth = targetWidth; + + /* + * Portrait Orientation: Ignore the given width and re-calculate + * a proportionally correct value based on the targetHeight. + */ + targetWidth = Math.round((float) targetHeight / ratio); + + if (DEBUG && originalTargetWidth != targetWidth) + log(1, + "Auto-Corrected targetWidth [from=%d to=%d] to honor image proportions.", + originalTargetWidth, targetWidth); + } + } + + // If AUTOMATIC was specified, determine the real scaling method. + if (scalingMethod == Scalr.Method.AUTOMATIC) + scalingMethod = determineScalingMethod(targetWidth, targetHeight, + ratio); + + if (DEBUG) + log(1, "Using Scaling Method: %s", scalingMethod); + + // Now we scale the image + if (scalingMethod == Scalr.Method.SPEED) { + result = scaleImage(src, targetWidth, targetHeight, + RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + } else if (scalingMethod == Scalr.Method.BALANCED) { + result = scaleImage(src, targetWidth, targetHeight, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + } else if (scalingMethod == Scalr.Method.QUALITY + || scalingMethod == Scalr.Method.ULTRA_QUALITY) { + /* + * If we are scaling up (in either width or height - since we know + * the image will stay proportional we just check if either are + * being scaled up), directly using a single BICUBIC will give us + * better results then using Chris Campbell's incremental scaling + * operation (and take a lot less time). + * + * If we are scaling down, we must use the incremental scaling + * algorithm for the best result. + */ + if (targetWidth > currentWidth || targetHeight > currentHeight) { + if (DEBUG) + log(1, + "QUALITY scale-up, a single BICUBIC scale operation will be used..."); + + /* + * BILINEAR and BICUBIC look similar the smaller the scale jump + * upwards is, if the scale is larger BICUBIC looks sharper and + * less fuzzy. But most importantly we have to use BICUBIC to + * match the contract of the QUALITY rendering scalingMethod. + * This note is just here for anyone reading the code and + * wondering how they can speed their own calls up. + */ + result = scaleImage(src, targetWidth, targetHeight, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + } else { + if (DEBUG) + log(1, + "QUALITY scale-down, incremental scaling will be used..."); + + /* + * Originally we wanted to use BILINEAR interpolation here + * because it takes 1/3rd the time that the BICUBIC + * interpolation does, however, when scaling large images down + * to most sizes bigger than a thumbnail we witnessed noticeable + * "softening" in the resultant image with BILINEAR that would + * be unexpectedly annoying to a user expecting a "QUALITY" + * scale of their original image. Instead BICUBIC was chosen to + * honor the contract of a QUALITY scale of the original image. + */ + result = scaleImageIncrementally(src, targetWidth, + targetHeight, scalingMethod, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + } + } + + if (DEBUG) + log(0, "Resized Image in %d ms", System.currentTimeMillis() - t); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Used to apply a {@link Rotation} and then 0 or more + * {@link BufferedImageOp}s to a given image and return the result. + *

+ * TIP: This operation leaves the original src + * image unmodified. If the caller is done with the src image + * after getting the result of this operation, remember to call + * {@link BufferedImage#flush()} on the src to free up native + * resources and make it easier for the GC to collect the unused image. + * + * @param src + * The image that will have the rotation applied to it. + * @param rotation + * The rotation that will be applied to the image. + * @param ops + * Zero or more optional image operations (e.g. sharpen, blur, + * etc.) that can be applied to the final result before returning + * the image. + * + * @return a new {@link BufferedImage} representing src rotated + * by the given amount and any optional ops applied to it. + * + * @throws IllegalArgumentException + * if src is null. + * @throws IllegalArgumentException + * if rotation is null. + * @throws ImagingOpException + * if one of the given {@link BufferedImageOp}s fails to apply. + * These exceptions bubble up from the inside of most of the + * {@link BufferedImageOp} implementations and are explicitly + * defined on the imgscalr API to make it easier for callers to + * catch the exception (if they are passing along optional ops + * to be applied). imgscalr takes detailed steps to avoid the + * most common pitfalls that will cause {@link BufferedImageOp}s + * to fail, even when using straight forward JDK-image + * operations. + * + * @see Rotation + */ + public static BufferedImage rotate(BufferedImage src, Rotation rotation, + BufferedImageOp... ops) throws IllegalArgumentException, + ImagingOpException { + long t = -1; + if (DEBUG) + t = System.currentTimeMillis(); + + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + if (rotation == null) + throw new IllegalArgumentException("rotation cannot be null"); + + if (DEBUG) + log(0, "Rotating Image [%s]...", rotation); + + /* + * Setup the default width/height values from our image. + * + * In the case of a 90 or 270 (-90) degree rotation, these two values + * flip-flop and we will correct those cases down below in the switch + * statement. + */ + int newWidth = src.getWidth(); + int newHeight = src.getHeight(); + + /* + * We create a transform per operation request as (oddly enough) it ends + * up being faster for the VM to create, use and destroy these instances + * than it is to re-use a single AffineTransform per-thread via the + * AffineTransform.setTo(...) methods which was my first choice (less + * object creation); after benchmarking this explicit case and looking + * at just how much code gets run inside of setTo() I opted for a new AT + * for every rotation. + * + * Besides the performance win, trying to safely reuse AffineTransforms + * via setTo(...) would have required ThreadLocal instances to avoid + * race conditions where two or more resize threads are manipulating the + * same transform before applying it. + * + * Misusing ThreadLocals are one of the #1 reasons for memory leaks in + * server applications and since we have no nice way to hook into the + * init/destroy Servlet cycle or any other initialization cycle for this + * library to automatically call ThreadLocal.remove() to avoid the + * memory leak, it would have made using this library *safely* on the + * server side much harder. + * + * So we opt for creating individual transforms per rotation op and let + * the VM clean them up in a GC. I only clarify all this reasoning here + * for anyone else reading this code and being tempted to reuse the AT + * instances of performance gains; there aren't any AND you get a lot of + * pain along with it. + */ + AffineTransform tx = new AffineTransform(); + + switch (rotation) { + case CW_90: + /* + * A 90 or -90 degree rotation will cause the height and width to + * flip-flop from the original image to the rotated one. + */ + newWidth = src.getHeight(); + newHeight = src.getWidth(); + + // Reminder: newWidth == result.getHeight() at this point + tx.translate(newWidth, 0); + tx.rotate(Math.toRadians(90)); + + break; + + case CW_270: + /* + * A 90 or -90 degree rotation will cause the height and width to + * flip-flop from the original image to the rotated one. + */ + newWidth = src.getHeight(); + newHeight = src.getWidth(); + + // Reminder: newHeight == result.getWidth() at this point + tx.translate(0, newHeight); + tx.rotate(Math.toRadians(-90)); + break; + + case CW_180: + tx.translate(newWidth, newHeight); + tx.rotate(Math.toRadians(180)); + break; + + case FLIP_HORZ: + tx.translate(newWidth, 0); + tx.scale(-1.0, 1.0); + break; + + case FLIP_VERT: + tx.translate(0, newHeight); + tx.scale(1.0, -1.0); + break; + } + + // Create our target image we will render the rotated result to. + BufferedImage result = createOptimalImage(src, newWidth, newHeight); + Graphics2D g2d = (Graphics2D) result.createGraphics(); + + /* + * Render the resultant image to our new rotatedImage buffer, applying + * the AffineTransform that we calculated above during rendering so the + * pixels from the old position are transposed to the new positions in + * the resulting image correctly. + */ + g2d.drawImage(src, tx, null); + g2d.dispose(); + + if (DEBUG) + log(0, "Rotation Applied in %d ms, result [width=%d, height=%d]", + System.currentTimeMillis() - t, result.getWidth(), + result.getHeight()); + + // Apply any optional operations (if specified). + if (ops != null && ops.length > 0) + result = apply(result, ops); + + return result; + } + + /** + * Used to write out a useful and well-formatted log message by any piece of + * code inside of the imgscalr library. + *

+ * If a message cannot be logged (logging is disabled) then this method + * returns immediately. + *

+ * NOTE: Because Java will auto-box primitive arguments + * into Objects when building out the params array, care should + * be taken not to call this method with primitive values unless + * {@link Scalr#DEBUG} is true; otherwise the VM will be + * spending time performing unnecessary auto-boxing calculations. + * + * @param depth + * The indentation level of the log message. + * @param message + * The log message in format string syntax that will be logged. + * @param params + * The parameters that will be swapped into all the place holders + * in the original messages before being logged. + * + * @see Scalr#LOG_PREFIX + * @see Scalr#LOG_PREFIX_PROPERTY_NAME + */ + protected static void log(int depth, String message, Object... params) { + if (Scalr.DEBUG) { + System.out.print(Scalr.LOG_PREFIX); + + for (int i = 0; i < depth; i++) + System.out.print("\t"); + + System.out.printf(message, params); + System.out.println(); + } + } + + /** + * Used to create a {@link BufferedImage} with the most optimal RGB TYPE ( + * {@link BufferedImage#TYPE_INT_RGB} or {@link BufferedImage#TYPE_INT_ARGB} + * ) capable of being rendered into from the given src. The + * width and height of both images will be identical. + *

+ * This does not perform a copy of the image data from src into + * the result image; see {@link #copyToOptimalImage(BufferedImage)} for + * that. + *

+ * We force all rendering results into one of these two types, avoiding the + * case where a source image is of an unsupported (or poorly supported) + * format by Java2D causing the rendering result to end up looking terrible + * (common with GIFs) or be totally corrupt (e.g. solid black image). + *

+ * Originally reported by Magnus Kvalheim from Movellas when scaling certain + * GIF and PNG images. + * + * @param src + * The source image that will be analyzed to determine the most + * optimal image type it can be rendered into. + * + * @return a new {@link BufferedImage} representing the most optimal target + * image type that src can be rendered into. + * + * @see How + * Java2D handles poorly supported image types + * @see Thanks + * to Morten Nobel for implementation hint + */ + protected static BufferedImage createOptimalImage(BufferedImage src) { + return createOptimalImage(src, src.getWidth(), src.getHeight()); + } + + /** + * Used to create a {@link BufferedImage} with the given dimensions and the + * most optimal RGB TYPE ( {@link BufferedImage#TYPE_INT_RGB} or + * {@link BufferedImage#TYPE_INT_ARGB} ) capable of being rendered into from + * the given src. + *

+ * This does not perform a copy of the image data from src into + * the result image; see {@link #copyToOptimalImage(BufferedImage)} for + * that. + *

+ * We force all rendering results into one of these two types, avoiding the + * case where a source image is of an unsupported (or poorly supported) + * format by Java2D causing the rendering result to end up looking terrible + * (common with GIFs) or be totally corrupt (e.g. solid black image). + *

+ * Originally reported by Magnus Kvalheim from Movellas when scaling certain + * GIF and PNG images. + * + * @param src + * The source image that will be analyzed to determine the most + * optimal image type it can be rendered into. + * @param width + * The width of the newly created resulting image. + * @param height + * The height of the newly created resulting image. + * + * @return a new {@link BufferedImage} representing the most optimal target + * image type that src can be rendered into. + * + * @throws IllegalArgumentException + * if width or height are < 0. + * + * @see How + * Java2D handles poorly supported image types + * @see Thanks + * to Morten Nobel for implementation hint + */ + protected static BufferedImage createOptimalImage(BufferedImage src, + int width, int height) throws IllegalArgumentException { + if (width < 0 || height < 0) + throw new IllegalArgumentException("width [" + width + + "] and height [" + height + "] must be >= 0"); + + return new BufferedImage( + width, + height, + (src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB + : BufferedImage.TYPE_INT_ARGB)); + } + + /** + * Used to copy a {@link BufferedImage} from a non-optimal type into a new + * {@link BufferedImage} instance of an optimal type (RGB or ARGB). If + * src is already of an optimal type, then it is returned + * unmodified. + *

+ * This method is meant to be used by any calling code (imgscalr's or + * otherwise) to convert any inbound image from a poorly supported image + * type into the 2 most well-supported image types in Java2D ( + * {@link BufferedImage#TYPE_INT_RGB} or {@link BufferedImage#TYPE_INT_ARGB} + * ) in order to ensure all subsequent graphics operations are performed as + * efficiently and correctly as possible. + *

+ * When using Java2D to work with image types that are not well supported, + * the results can be anything from exceptions bubbling up from the depths + * of Java2D to images being completely corrupted and just returned as solid + * black. + * + * @param src + * The image to copy (if necessary) into an optimally typed + * {@link BufferedImage}. + * + * @return a representation of the src image in an optimally + * typed {@link BufferedImage}, otherwise src if it was + * already of an optimal type. + * + * @throws IllegalArgumentException + * if src is null. + */ + protected static BufferedImage copyToOptimalImage(BufferedImage src) + throws IllegalArgumentException { + if (src == null) + throw new IllegalArgumentException("src cannot be null"); + + // Calculate the type depending on the presence of alpha. + int type = (src.getTransparency() == Transparency.OPAQUE ? BufferedImage.TYPE_INT_RGB + : BufferedImage.TYPE_INT_ARGB); + BufferedImage result = new BufferedImage(src.getWidth(), + src.getHeight(), type); + + // Render the src image into our new optimal source. + Graphics g = result.getGraphics(); + g.drawImage(src, 0, 0, null); + g.dispose(); + + return result; + } + + /** + * Used to determine the scaling {@link Method} that is best suited for + * scaling the image to the targeted dimensions. + *

+ * This method is intended to be used to select a specific scaling + * {@link Method} when a {@link Method#AUTOMATIC} method is specified. This + * method utilizes the {@link Scalr#THRESHOLD_QUALITY_BALANCED} and + * {@link Scalr#THRESHOLD_BALANCED_SPEED} thresholds when selecting which + * method should be used by comparing the primary dimension (width or + * height) against the threshold and seeing where the image falls. The + * primary dimension is determined by looking at the orientation of the + * image: landscape or square images use their width and portrait-oriented + * images use their height. + * + * @param targetWidth + * The target width for the scaled image. + * @param targetHeight + * The target height for the scaled image. + * @param ratio + * A height/width ratio used to determine the orientation of the + * image so the primary dimension (width or height) can be + * selected to test if it is greater than or less than a + * particular threshold. + * + * @return the fastest {@link Method} suited for scaling the image to the + * specified dimensions while maintaining a good-looking result. + */ + protected static Method determineScalingMethod(int targetWidth, + int targetHeight, float ratio) { + // Get the primary dimension based on the orientation of the image + int length = (ratio <= 1 ? targetWidth : targetHeight); + + // Default to speed + Method result = Method.SPEED; + + // Figure out which scalingMethod should be used + if (length <= Scalr.THRESHOLD_QUALITY_BALANCED) + result = Method.QUALITY; + else if (length <= Scalr.THRESHOLD_BALANCED_SPEED) + result = Method.BALANCED; + + if (DEBUG) + log(2, "AUTOMATIC scaling method selected: %s", result.name()); + + return result; + } + + /** + * Used to implement a straight-forward image-scaling operation using Java + * 2D. + *

+ * This method uses the Oracle-encouraged method of + * Graphics2D.drawImage(...) to scale the given image with the + * given interpolation hint. + * + * @param src + * The image that will be scaled. + * @param targetWidth + * The target width for the scaled image. + * @param targetHeight + * The target height for the scaled image. + * @param interpolationHintValue + * The {@link RenderingHints} interpolation value used to + * indicate the method that {@link Graphics2D} should use when + * scaling the image. + * + * @return the result of scaling the original src to the given + * dimensions using the given interpolation method. + */ + protected static BufferedImage scaleImage(BufferedImage src, + int targetWidth, int targetHeight, Object interpolationHintValue) { + // Setup the rendering resources to match the source image's + BufferedImage result = createOptimalImage(src, targetWidth, + targetHeight); + Graphics2D resultGraphics = result.createGraphics(); + + // Scale the image to the new buffer using the specified rendering hint. + resultGraphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + interpolationHintValue); + resultGraphics.drawImage(src, 0, 0, targetWidth, targetHeight, null); + + // Just to be clean, explicitly dispose our temporary graphics object + resultGraphics.dispose(); + + // Return the scaled image to the caller. + return result; + } + + /** + * Used to implement Chris Campbell's incremental-scaling algorithm: http://today.java.net/pub/a/today/2007/04/03/perils + * -of-image-getscaledinstance.html. + *

+ * Modifications to the original algorithm are variable names and comments + * added for clarity and the hard-coding of using BICUBIC interpolation as + * well as the explicit "flush()" operation on the interim BufferedImage + * instances to avoid resource leaking. + * + * @param src + * The image that will be scaled. + * @param targetWidth + * The target width for the scaled image. + * @param targetHeight + * The target height for the scaled image. + * @param scalingMethod + * The scaling method specified by the user (or calculated by + * imgscalr) to use for this incremental scaling operation. + * @param interpolationHintValue + * The {@link RenderingHints} interpolation value used to + * indicate the method that {@link Graphics2D} should use when + * scaling the image. + * + * @return an image scaled to the given dimensions using the given rendering + * hint. + */ + protected static BufferedImage scaleImageIncrementally(BufferedImage src, + int targetWidth, int targetHeight, Method scalingMethod, + Object interpolationHintValue) { + boolean hasReassignedSrc = false; + int incrementCount = 0; + int currentWidth = src.getWidth(); + int currentHeight = src.getHeight(); + + /* + * The original QUALITY mode, representing Chris Campbell's algorithm, + * is to step down by 1/2s every time when scaling the image + * incrementally. Users pointed out that using this method to scale + * images with noticeable straight lines left them really jagged in + * smaller thumbnail format. + * + * After investigation it was discovered that scaling incrementally by + * smaller increments was the ONLY way to make the thumbnail sized + * images look less jagged and more accurate; almost matching the + * accuracy of Mac's built in thumbnail generation which is the highest + * quality resize I've come across (better than GIMP Lanczos3 and + * Windows 7). + * + * A divisor of 7 was chose as using 5 still left some jaggedness in the + * image while a divisor of 8 or higher made the resulting thumbnail too + * soft; like our OP_ANTIALIAS convolve op had been forcibly applied to + * the result even if the user didn't want it that soft. + * + * Using a divisor of 7 for the ULTRA_QUALITY seemed to be the sweet + * spot. + * + * NOTE: Below when the actual fraction is used to calculate the small + * portion to subtract from the current dimension, this is a + * progressively smaller and smaller chunk. When the code was changed to + * do a linear reduction of the image of equal steps for each + * incremental resize (e.g. say 50px each time) the result was + * significantly worse than the progressive approach used below; even + * when a very high number of incremental steps (13) was tested. + */ + int fraction = (scalingMethod == Method.ULTRA_QUALITY ? 7 : 2); + + do { + int prevCurrentWidth = currentWidth; + int prevCurrentHeight = currentHeight; + + /* + * If the current width is bigger than our target, cut it in half + * and sample again. + */ + if (currentWidth > targetWidth) { + currentWidth -= (currentWidth / fraction); + + /* + * If we cut the width too far it means we are on our last + * iteration. Just set it to the target width and finish up. + */ + if (currentWidth < targetWidth) + currentWidth = targetWidth; + } + + /* + * If the current height is bigger than our target, cut it in half + * and sample again. + */ + + if (currentHeight > targetHeight) { + currentHeight -= (currentHeight / fraction); + + /* + * If we cut the height too far it means we are on our last + * iteration. Just set it to the target height and finish up. + */ + + if (currentHeight < targetHeight) + currentHeight = targetHeight; + } + + /* + * Stop when we cannot incrementally step down anymore. + * + * This used to use a || condition, but that would cause problems + * when using FIT_EXACT such that sometimes the width OR height + * would not change between iterations, but the other dimension + * would (e.g. resizing 500x500 to 500x250). + * + * Now changing this to an && condition requires that both + * dimensions do not change between a resize iteration before we + * consider ourselves done. + */ + if (prevCurrentWidth == currentWidth + && prevCurrentHeight == currentHeight) + break; + + if (DEBUG) + log(2, "Scaling from [%d x %d] to [%d x %d]", prevCurrentWidth, + prevCurrentHeight, currentWidth, currentHeight); + + // Render the incremental scaled image. + BufferedImage incrementalImage = scaleImage(src, currentWidth, + currentHeight, interpolationHintValue); + + /* + * Before re-assigning our interim (partially scaled) + * incrementalImage to be the new src image before we iterate around + * again to process it down further, we want to flush() the previous + * src image IF (and only IF) it was one of our own temporary + * BufferedImages created during this incremental down-sampling + * cycle. If it wasn't one of ours, then it was the original + * caller-supplied BufferedImage in which case we don't want to + * flush() it and just leave it alone. + */ + if (hasReassignedSrc) + src.flush(); + + /* + * Now treat our incremental partially scaled image as the src image + * and cycle through our loop again to do another incremental + * scaling of it (if necessary). + */ + src = incrementalImage; + + /* + * Keep track of us re-assigning the original caller-supplied source + * image with one of our interim BufferedImages so we know when to + * explicitly flush the interim "src" on the next cycle through. + */ + hasReassignedSrc = true; + + // Track how many times we go through this cycle to scale the image. + incrementCount++; + } while (currentWidth != targetWidth || currentHeight != targetHeight); + + if (DEBUG) + log(2, "Incrementally Scaled Image in %d steps.", incrementCount); + + /* + * Once the loop has exited, the src image argument is now our scaled + * result image that we want to return. + */ + return src; + } +} \ No newline at end of file diff --git a/src/the/bytecode/club/bytecodeviewer/BytecodeViewer.java b/src/the/bytecode/club/bytecodeviewer/BytecodeViewer.java index b742c491..8286b7be 100644 --- a/src/the/bytecode/club/bytecodeviewer/BytecodeViewer.java +++ b/src/the/bytecode/club/bytecodeviewer/BytecodeViewer.java @@ -33,6 +33,7 @@ import org.objectweb.asm.tree.ClassNode; import the.bytecode.club.bytecodeviewer.api.ClassNodeLoader; import the.bytecode.club.bytecodeviewer.gui.ClassViewer; import the.bytecode.club.bytecodeviewer.gui.FileNavigationPane; +import the.bytecode.club.bytecodeviewer.gui.BootScreen; import the.bytecode.club.bytecodeviewer.gui.MainViewerGUI; import the.bytecode.club.bytecodeviewer.gui.RunOptions; import the.bytecode.club.bytecodeviewer.gui.SearchingPane; @@ -67,7 +68,6 @@ import the.bytecode.club.bytecodeviewer.plugin.PluginManager; * TODO: * * 3.0.0: (RETIREMENT PARTY, WOHOOO) - * maybe just do AMS5 then obfuscate the dex2jar shit. * Add obfuscation: * - Add integer boxing and other obfuscation methods contra implemented * - Insert unadded/debug opcodes to try to fuck up decompilers @@ -91,11 +91,13 @@ import the.bytecode.club.bytecodeviewer.plugin.PluginManager; * refresh appears under panes that are non refreshable * make ez-injection plugin console show all sys.out calls * edit then save issues? - * - * Search open doesnt append .class + * Search open doesnt append .class to tab name * * -----2.9.7-----: * 07/02/2015 - Added ajustable font size. + * 07/05/2015 - Started working on the new Boot Screen. + * 07/06/2015 - Moved the font size to be under the view menu. + * 07/06/2015 - Fixed a bug with plugins not being able to grab the currently viewed class. * * @author Konloch * @@ -123,6 +125,7 @@ public class BytecodeViewer { private static String pluginsName = getBCVDirectory() + fs + "recentplugins.bcv"; public static String settingsName = getBCVDirectory() + fs + "settings.bcv"; public static String tempDirectory = getBCVDirectory() + fs + "bcv_temp" + fs; + public static String libsDirectory = getBCVDirectory() + fs + "libs" + fs; public static String krakatauWorkingDirectory = getBCVDirectory() + fs + "krakatau_" + krakatauVersion + fs + "Krakatau-master"; private static ArrayList recentFiles = DiskReader.loadArrayList(filesName, false); private static ArrayList recentPlugins = DiskReader.loadArrayList(pluginsName, false); @@ -132,11 +135,12 @@ public class BytecodeViewer { public static ArrayList krakatau = new ArrayList(); public static Refactorer refactorer = new Refactorer(); public static boolean pingback = false; + public static boolean deleteForiegnLibraries = true; /** * The version checker thread */ - private static Thread versionChecker = new Thread() { + public static Thread versionChecker = new Thread() { @Override public void run() { try { @@ -294,6 +298,39 @@ public class BytecodeViewer { } }; + public static Thread PingBack = new Thread() { + public void run() { + try { + new HTTPRequest(new URL("https://bytecodeviewer.com/add.php")).read(); + } catch(Exception e) { + //ignore + } + } + }; + + public static void pingback() { + JOptionPane pane = new JOptionPane( + "Would you like to 'pingback' to https://bytecodeviewer.com to be counted in the global users for BCV?"); + Object[] options = new String[] { "Yes", "No" }; + pane.setOptions(options); + JDialog dialog = pane.createDialog(BytecodeViewer.viewer, + "Bytecode Viewer - Optional Pingback"); + dialog.setVisible(true); + Object obj = pane.getValue(); + int result = -1; + for (int k = 0; k < options.length; k++) + if (options[k].equals(obj)) + result = k; + + if (result == 0) { + try { + PingBack.start(); + } catch (Exception e) { + new the.bytecode.club.bytecodeviewer.api.ExceptionUI(e); + } + } + } + /** * Grab the byte array from the loaded Class object * @param clazz @@ -317,6 +354,15 @@ public class BytecodeViewer { */ public static void main(String[] args) { System.setSecurityManager(sm); + try { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception e) { + new the.bytecode.club.bytecodeviewer.api.ExceptionUI(e); + } + new BootScreen().DO_FIRST_BOOT(args); + } + + public static void BOOT(String[] args) { checkKrakatau(); System.out.println("https://the.bytecode.club - Created by @Konloch - Bytecode Viewer " + version); cleanup(); @@ -329,18 +375,13 @@ public class BytecodeViewer { cleanup(); } }); - try { - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (Exception e) { - new the.bytecode.club.bytecodeviewer.api.ExceptionUI(e); - } viewer = new MainViewerGUI(); Settings.loadGUI(); resetRecentFilesMenu(); - if (viewer.chckbxmntmNewCheckItem_12.isSelected()) // start only if selected - versionChecker.start(); + /*if (viewer.chckbxmntmNewCheckItem_12.isSelected()) // start only if selected + versionChecker.start();*/ viewer.setVisible(true); System.out.println("Start up took " + ((System.currentTimeMillis() - start) / 1000) + " seconds"); @@ -350,33 +391,10 @@ public class BytecodeViewer { openFiles(new File[] { new File(s) }, true); } - if(!pingback) { + /*if(!pingback) { pingback = true; pingback(); - } - } - - public static void pingback() { - JOptionPane pane = new JOptionPane( - "Would you like to 'pingback' to https://bytecodeviewer.com to be counted in the global users for BCV?"); - Object[] options = new String[] { "Yes", "No" }; - pane.setOptions(options); - JDialog dialog = pane.createDialog(BytecodeViewer.viewer, - "Bytecode Viewer - Optional Pingback"); - dialog.setVisible(true); - Object obj = pane.getValue(); - int result = -1; - for (int k = 0; k < options.length; k++) - if (options[k].equals(obj)) - result = k; - - if (result == 0) { - try { - new HTTPRequest(new URL("https://bytecodeviewer.com/add.php")).read(); - } catch (Exception e) { - new the.bytecode.club.bytecodeviewer.api.ExceptionUI(e); - } - } + }*/ } /** @@ -569,7 +587,8 @@ public class BytecodeViewer { public static void openFiles(final File[] files, boolean recentFiles) { if(recentFiles) for (File f : files) - BytecodeViewer.addRecentFile(f); + if(f.exists()) + BytecodeViewer.addRecentFile(f); BytecodeViewer.viewer.setIcon(true); update = true; diff --git a/src/the/bytecode/club/bytecodeviewer/JarUtils.java b/src/the/bytecode/club/bytecodeviewer/JarUtils.java index 8f63f801..79a1bcb7 100644 --- a/src/the/bytecode/club/bytecodeviewer/JarUtils.java +++ b/src/the/bytecode/club/bytecodeviewer/JarUtils.java @@ -77,6 +77,8 @@ public class JarUtils { * @throws IOException */ public static void loadResources(final File zipFile) throws IOException { + if(!zipFile.exists()) + return; //just ignore ZipInputStream jis = new ZipInputStream(new FileInputStream(zipFile)); ZipEntry entry; while ((entry = jis.getNextEntry()) != null) { diff --git a/src/the/bytecode/club/bytecodeviewer/Settings.java b/src/the/bytecode/club/bytecodeviewer/Settings.java index 8faeee0d..1cb2e544 100644 --- a/src/the/bytecode/club/bytecodeviewer/Settings.java +++ b/src/the/bytecode/club/bytecodeviewer/Settings.java @@ -189,6 +189,7 @@ public class Settings { DiskWriter.writeNewLine(BytecodeViewer.settingsName, String.valueOf(BytecodeViewer.viewer.panel2JDGUI_E.isSelected()), false); DiskWriter.writeNewLine(BytecodeViewer.settingsName, String.valueOf(BytecodeViewer.viewer.panel3JDGUI_E.isSelected()), false); DiskWriter.writeNewLine(BytecodeViewer.settingsName, String.valueOf(BytecodeViewer.viewer.fontSpinner.getValue()), false); + DiskWriter.writeNewLine(BytecodeViewer.settingsName, String.valueOf(BytecodeViewer.deleteForiegnLibraries), false); } catch(Exception e) { new the.bytecode.club.bytecodeviewer.api.ExceptionUI(e); } @@ -376,6 +377,7 @@ public class Settings { BytecodeViewer.viewer.panel2JDGUI_E.setSelected(Boolean.parseBoolean(DiskReader.loadString(BytecodeViewer.settingsName, 110, false))); BytecodeViewer.viewer.panel3JDGUI_E.setSelected(Boolean.parseBoolean(DiskReader.loadString(BytecodeViewer.settingsName, 111, false))); BytecodeViewer.viewer.fontSpinner.setValue(Integer.parseInt(DiskReader.loadString(BytecodeViewer.settingsName, 112, false))); + BytecodeViewer.deleteForiegnLibraries = Boolean.parseBoolean(DiskReader.loadString(BytecodeViewer.settingsName, 113, false)); } catch(Exception e) { //ignore because errors are expected, first start up and outdated settings. //e.printStackTrace(); diff --git a/src/the/bytecode/club/bytecodeviewer/gui/AboutWindow.java b/src/the/bytecode/club/bytecodeviewer/gui/AboutWindow.java index 647cb73a..f19c1f6f 100644 --- a/src/the/bytecode/club/bytecodeviewer/gui/AboutWindow.java +++ b/src/the/bytecode/club/bytecodeviewer/gui/AboutWindow.java @@ -34,7 +34,7 @@ public class AboutWindow extends JFrame { getContentPane().add(txtrBytecodeViewerIs, "name_140466526081695");txtrBytecodeViewerIs.setEnabled(false); this.setResizable(false); this.setLocationRelativeTo(null); - } + } @Override public void setVisible(boolean b) { diff --git a/src/the/bytecode/club/bytecodeviewer/gui/BootScreen.java b/src/the/bytecode/club/bytecodeviewer/gui/BootScreen.java new file mode 100644 index 00000000..a1f4a371 --- /dev/null +++ b/src/the/bytecode/club/bytecodeviewer/gui/BootScreen.java @@ -0,0 +1,281 @@ +package the.bytecode.club.bytecodeviewer.gui; + +import javax.swing.JEditorPane; +import javax.swing.JFrame; + +import java.awt.Dimension; +import java.awt.GridBagLayout; + +import javax.swing.JProgressBar; + +import java.awt.GridBagConstraints; + +import javax.swing.JScrollPane; + +import java.awt.Insets; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import javax.swing.text.html.HTMLEditorKit; + +import the.bytecode.club.bytecodeviewer.BytecodeViewer; +import the.bytecode.club.bytecodeviewer.Resources; +import me.konloch.kontainer.io.HTTPRequest; + +/** + * First boot, will automatically connect to BytecodeViewer for PingBack + * It'll Check BCV version + * then it'll download repos from the library + * After it's completed and compared MD5 hashes, it simply dynamically loads all jars in /libs/ folder of BCV + * While all of this is happening, it'll show the HOW-TO guide for BCV + * + * Download Failed? Corrupt Jar? Append -clean to BCV startup + * + * @author Konloch + * + */ + +public class BootScreen extends JFrame { + + private static final long serialVersionUID = -1098467609722393444L; + + private static boolean FIRST_BOOT = false; + + private JProgressBar progressBar = new JProgressBar(); + + public BootScreen() { + setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + this.setIconImages(Resources.iconList); + setSize(new Dimension(600, 800)); + setTitle("Bytecode Viewer Boot Screen - Starting Up"); + GridBagLayout gridBagLayout = new GridBagLayout(); + gridBagLayout.columnWidths = new int[]{0, 0}; + gridBagLayout.rowHeights = new int[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + gridBagLayout.columnWeights = new double[]{1.0, Double.MIN_VALUE}; + gridBagLayout.rowWeights = new double[]{1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE}; + getContentPane().setLayout(gridBagLayout); + + JScrollPane scrollPane = new JScrollPane(); + GridBagConstraints gbc_scrollPane = new GridBagConstraints(); + gbc_scrollPane.gridheight = 24; + gbc_scrollPane.insets = new Insets(0, 0, 5, 0); + gbc_scrollPane.fill = GridBagConstraints.BOTH; + gbc_scrollPane.gridx = 0; + gbc_scrollPane.gridy = 0; + getContentPane().add(scrollPane, gbc_scrollPane); + + JEditorPane editorPane = new JEditorPane(); + editorPane.setEditorKit(new HTMLEditorKit()); + + editorPane.setText("http://www.icesoft.org/java/home.jsf"); + + scrollPane.setViewportView(editorPane); + + GridBagConstraints gbc_progressBar = new GridBagConstraints(); + gbc_progressBar.fill = GridBagConstraints.HORIZONTAL; + gbc_progressBar.gridx = 0; + gbc_progressBar.gridy = 24; + getContentPane().add(progressBar, gbc_progressBar); + this.setLocationRelativeTo(null); + } + + public void DO_FIRST_BOOT(String args[]) { + this.setVisible(true); + if(FIRST_BOOT) + return; + + FIRST_BOOT = true; + boolean foundAtleastOne = false; + + + setTitle("Bytecode Viewer Boot Screen - Checking Libraries..."); + + try { + int completedCheck = 0; + List urlList = new ArrayList(); + HTTPRequest req = new HTTPRequest(new URL("https://github.com/Konloch/bytecode-viewer/tree/master/libs")); + for(String s : req.read()) + if(s.contains("href=\"/Konloch/bytecode-viewer/blob/master/libs/")) { + urlList.add("https://github.com"+s.split("= 1) + if(args[0].equalsIgnoreCase("-clean")) + libsDirectory.delete(); + + if(!libsDirectory.exists()) + libsDirectory.mkdir(); + + List libsList = new ArrayList(); + List libsFileList = new ArrayList(); + for(File f : libsDirectory.listFiles()) { + libsList.add(f.getName()); + libsFileList.add(f.getAbsolutePath()); + } + + progressBar.setMaximum(urlList.size()); + + for(String s : urlList) { + String fileName = s.substring("https://github.com/Konloch/bytecode-viewer/blob/master/libs/".length(), s.length()); + if(!libsList.contains(fileName)) { + setTitle("Bytecode Viewer Boot Screen - Downloading " + fileName); + boolean passed = false; + while(!passed) { + InputStream is = null; + FileOutputStream fos = null; + try { + is = new URL("https://github.com/Konloch/bytecode-viewer/raw/master/libs/" + fileName).openConnection().getInputStream(); + fos = new FileOutputStream(BytecodeViewer.libsDirectory + BytecodeViewer.fs + fileName); + System.out.println("Downloading from "+s); + byte[] buffer = new byte[8192]; + int len; + int downloaded = 0; + boolean flag = false; + while ((len = is.read(buffer)) > 0) { + fos.write(buffer, 0, len); + fos.flush(); + downloaded += 8192; + int mbs = downloaded / 1048576; + if(mbs % 5 == 0 && mbs != 0) { + if(!flag) + System.out.println("Downloaded " + mbs + "MBs so far"); + flag = true; + } else + flag = false; + } + libsFileList.add(BytecodeViewer.libsDirectory + BytecodeViewer.fs + fileName); + } finally { + try { + if (is != null) { + is.close(); + } + } finally { + if (fos != null) { + fos.flush(); + } + if (fos != null) { + fos.close(); + } + } + } + System.out.println("Download finished!"); + passed = true; + } + } + completedCheck++; + progressBar.setValue(completedCheck); + } + + if(BytecodeViewer.deleteForiegnLibraries) { + setTitle("Bytecode Viewer Boot Screen - Checking & Deleting Foriegn/Outdated Libraries..."); + for(String s : libsFileList) { + File f = new File(s); + boolean delete = true; + for(String urlS : urlList) { + String fileName = urlS.substring("https://github.com/Konloch/bytecode-viewer/blob/master/libs/".length(), urlS.length()); + if(fileName.equals(f.getName())) { + delete = false; + } + } + if(delete) { + f.delete(); + System.out.println("Detected & Deleted Foriegn/Outdated Jar/File: " + f.getName()); + } + } + } + + setTitle("Bytecode Viewer Boot Screen - Loading Libraries..."); + + for(String s : libsFileList ) { + if(s.endsWith(".jar")) { + File f = new File(s); + setTitle("Bytecode Viewer Boot Screen - Loading Library " + f.getName()); + System.out.println(f.getName()); + + JarFile jarFile = new JarFile(s); + Enumeration e = jarFile.entries(); + ClassPathHack.addFile(f); + while (e.hasMoreElements()) { + JarEntry je = (JarEntry) e.nextElement(); + if(je.isDirectory() || !je.getName().endsWith(".class")){ + continue; + } + try { + String className = je.getName().substring(0,je.getName().length()-6); + className = className.replace('/', '.'); + ClassLoader.getSystemClassLoader().loadClass(className); + } catch(java.lang.VerifyError | java.lang.ExceptionInInitializerError | java.lang.IncompatibleClassChangeError | java.lang.NoClassDefFoundError | Exception e2) { + //ignore + } + } + jarFile.close(); + } + } + setTitle("Bytecode Viewer Boot Screen - Booting!"); + + } catch(Exception e) { + StringWriter sw = new StringWriter(); + e.printStackTrace(new PrintWriter(sw)); + e.printStackTrace(); + new the.bytecode.club.bytecodeviewer.api.ExceptionUI("Bytecode Viewer ran into an error while booting, trying to force it anyways."+ BytecodeViewer.nl+ BytecodeViewer.nl+ + "Please ensure you have an active internet connection and restart BCV. If this presists please visit http://github.com/Konloch/Bytecode-Viewer or http://bytecodeviewer.com"+ BytecodeViewer.nl + BytecodeViewer.nl + sw.toString()); + } + + setTitle("Bytecode Viewer Boot Screen - Finished"); + + BytecodeViewer.BOOT(args); + + if(BytecodeViewer.pingback) { + BytecodeViewer.PingBack.start(); + BytecodeViewer.pingback = true; + } + + if(BytecodeViewer.viewer.chckbxmntmNewCheckItem_12.isSelected()) + BytecodeViewer.versionChecker.start(); + + this.setVisible(false); + } + + public static class ClassPathHack { + private static final Class[] parameters = new Class[] {URL.class}; + + public static void addFile(File f) throws IOException { + // f.toURL is deprecated + addURL(f.toURI().toURL()); + } + + protected static void addURL(URL u) throws IOException { + URLClassLoader sysloader = (URLClassLoader) ClassLoader.getSystemClassLoader(); + Class sysclass = URLClassLoader.class; + + try { + Method method = sysclass.getDeclaredMethod("addURL", parameters); + method.setAccessible(true); + method.invoke(sysloader, u); + } catch (Exception e) { + e.printStackTrace(); + } + + } + } + +} diff --git a/src/the/bytecode/club/bytecodeviewer/gui/ClassViewer.java b/src/the/bytecode/club/bytecodeviewer/gui/ClassViewer.java index 5224adb5..89b671c7 100644 --- a/src/the/bytecode/club/bytecodeviewer/gui/ClassViewer.java +++ b/src/the/bytecode/club/bytecodeviewer/gui/ClassViewer.java @@ -222,7 +222,6 @@ public class ClassViewer extends Viewer { private static final long serialVersionUID = -8650495368920680024L; ArrayList lnData = new ArrayList(); String name; - public ClassNode cn; JSplitPane sp; JSplitPane sp2; public JPanel panel1Search = new JPanel(new BorderLayout()); diff --git a/src/the/bytecode/club/bytecodeviewer/gui/MainViewerGUI.java b/src/the/bytecode/club/bytecodeviewer/gui/MainViewerGUI.java index 53b9cb25..dcb5ca50 100644 --- a/src/the/bytecode/club/bytecodeviewer/gui/MainViewerGUI.java +++ b/src/the/bytecode/club/bytecodeviewer/gui/MainViewerGUI.java @@ -1293,13 +1293,6 @@ public class MainViewerGUI extends JFrame implements FileChangeNotifier { mnSettings.add(decodeAPKResources); mnSettings.add(separator_36); - - mnSettings.add(mnFontSize); - fontSpinner.setModel(new SpinnerNumberModel(new Integer(12), new Integer(1), null, new Integer(1))); - - mnFontSize.add(fontSpinner); - - mnSettings.add(separator_13); mntmSetPythonDirectory.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { @@ -1758,6 +1751,13 @@ public class MainViewerGUI extends JFrame implements FileChangeNotifier { panelGroup3.add(panel3Smali); panelGroup3.add(panel3Bytecode); panelGroup3.add(panel3Hexcode); + mnNewMenu_6.add(separator_13); + fontSpinner.setPreferredSize(new Dimension(42, 20)); + fontSpinner.setSize(new Dimension(42, 20)); + fontSpinner.setModel(new SpinnerNumberModel(new Integer(12), new Integer(1), null, new Integer(1))); + mnNewMenu_6.add(mnFontSize); + + mnFontSize.add(fontSpinner); panelGroup1.setSelected(panel1Proc.getModel(), true);//my one true love diff --git a/src/the/bytecode/club/bytecodeviewer/plugin/strategies/CompiledJavaPluginLaunchStrategy.java b/src/the/bytecode/club/bytecodeviewer/plugin/strategies/CompiledJavaPluginLaunchStrategy.java index 7ef9e8db..6fb7270b 100644 --- a/src/the/bytecode/club/bytecodeviewer/plugin/strategies/CompiledJavaPluginLaunchStrategy.java +++ b/src/the/bytecode/club/bytecodeviewer/plugin/strategies/CompiledJavaPluginLaunchStrategy.java @@ -17,6 +17,7 @@ import the.bytecode.club.bytecodeviewer.api.Plugin; import the.bytecode.club.bytecodeviewer.plugin.PluginLaunchStrategy; /** + * @author Konloch * @author Bibl (don't ban me pls) * @created 1 Jun 2015 */