diff options
Diffstat (limited to 'src/org/jivesoftware/smack/util')
34 files changed, 9501 insertions, 0 deletions
diff --git a/src/org/jivesoftware/smack/util/Base32Encoder.java b/src/org/jivesoftware/smack/util/Base32Encoder.java new file mode 100644 index 0000000..0a4ea21 --- /dev/null +++ b/src/org/jivesoftware/smack/util/Base32Encoder.java @@ -0,0 +1,184 @@ +/** + * All rights reserved. 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.jivesoftware.smack.util; + + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Base32 string encoding is useful for when filenames case-insensitive filesystems are encoded. + * Base32 representation takes roughly 20% more space then Base64. + * + * @author Florian Schmaus + * Based on code by Brian Wellington (bwelling@xbill.org) + * @see <a href="http://en.wikipedia.org/wiki/Base32">Base32 Wikipedia entry<a> + * + */ +public class Base32Encoder implements StringEncoder { + + private static Base32Encoder instance = new Base32Encoder(); + private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ2345678"; + + private Base32Encoder() { + // Use getInstance() + } + + public static Base32Encoder getInstance() { + return instance; + } + + @Override + public String decode(String str) { + ByteArrayOutputStream bs = new ByteArrayOutputStream(); + byte[] raw = str.getBytes(); + for (int i = 0; i < raw.length; i++) { + char c = (char) raw[i]; + if (!Character.isWhitespace(c)) { + c = Character.toUpperCase(c); + bs.write((byte) c); + } + } + + while (bs.size() % 8 != 0) + bs.write('8'); + + byte[] in = bs.toByteArray(); + + bs.reset(); + DataOutputStream ds = new DataOutputStream(bs); + + for (int i = 0; i < in.length / 8; i++) { + short[] s = new short[8]; + int[] t = new int[5]; + + int padlen = 8; + for (int j = 0; j < 8; j++) { + char c = (char) in[i * 8 + j]; + if (c == '8') + break; + s[j] = (short) ALPHABET.indexOf(in[i * 8 + j]); + if (s[j] < 0) + return null; + padlen--; + } + int blocklen = paddingToLen(padlen); + if (blocklen < 0) + return null; + + // all 5 bits of 1st, high 3 (of 5) of 2nd + t[0] = (s[0] << 3) | s[1] >> 2; + // lower 2 of 2nd, all 5 of 3rd, high 1 of 4th + t[1] = ((s[1] & 0x03) << 6) | (s[2] << 1) | (s[3] >> 4); + // lower 4 of 4th, high 4 of 5th + t[2] = ((s[3] & 0x0F) << 4) | ((s[4] >> 1) & 0x0F); + // lower 1 of 5th, all 5 of 6th, high 2 of 7th + t[3] = (s[4] << 7) | (s[5] << 2) | (s[6] >> 3); + // lower 3 of 7th, all of 8th + t[4] = ((s[6] & 0x07) << 5) | s[7]; + + try { + for (int j = 0; j < blocklen; j++) + ds.writeByte((byte) (t[j] & 0xFF)); + } catch (IOException e) { + } + } + + return new String(bs.toByteArray()); + } + + @Override + public String encode(String str) { + byte[] b = str.getBytes(); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + for (int i = 0; i < (b.length + 4) / 5; i++) { + short s[] = new short[5]; + int t[] = new int[8]; + + int blocklen = 5; + for (int j = 0; j < 5; j++) { + if ((i * 5 + j) < b.length) + s[j] = (short) (b[i * 5 + j] & 0xFF); + else { + s[j] = 0; + blocklen--; + } + } + int padlen = lenToPadding(blocklen); + + // convert the 5 byte block into 8 characters (values 0-31). + + // upper 5 bits from first byte + t[0] = (byte) ((s[0] >> 3) & 0x1F); + // lower 3 bits from 1st byte, upper 2 bits from 2nd. + t[1] = (byte) (((s[0] & 0x07) << 2) | ((s[1] >> 6) & 0x03)); + // bits 5-1 from 2nd. + t[2] = (byte) ((s[1] >> 1) & 0x1F); + // lower 1 bit from 2nd, upper 4 from 3rd + t[3] = (byte) (((s[1] & 0x01) << 4) | ((s[2] >> 4) & 0x0F)); + // lower 4 from 3rd, upper 1 from 4th. + t[4] = (byte) (((s[2] & 0x0F) << 1) | ((s[3] >> 7) & 0x01)); + // bits 6-2 from 4th + t[5] = (byte) ((s[3] >> 2) & 0x1F); + // lower 2 from 4th, upper 3 from 5th; + t[6] = (byte) (((s[3] & 0x03) << 3) | ((s[4] >> 5) & 0x07)); + // lower 5 from 5th; + t[7] = (byte) (s[4] & 0x1F); + + // write out the actual characters. + for (int j = 0; j < t.length - padlen; j++) { + char c = ALPHABET.charAt(t[j]); + os.write(c); + } + } + return new String(os.toByteArray()); + } + + private static int lenToPadding(int blocklen) { + switch (blocklen) { + case 1: + return 6; + case 2: + return 4; + case 3: + return 3; + case 4: + return 1; + case 5: + return 0; + default: + return -1; + } + } + + private static int paddingToLen(int padlen) { + switch (padlen) { + case 6: + return 1; + case 4: + return 2; + case 3: + return 3; + case 1: + return 4; + case 0: + return 5; + default: + return -1; + } + } + +} diff --git a/src/org/jivesoftware/smack/util/Base64.java b/src/org/jivesoftware/smack/util/Base64.java new file mode 100644 index 0000000..ba6eb37 --- /dev/null +++ b/src/org/jivesoftware/smack/util/Base64.java @@ -0,0 +1,1689 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + */ +package org.jivesoftware.smack.util;
+
+/**
+ * <p>Encodes and decodes to and from Base64 notation.</p> + * This code was obtained from <a href="http://iharder.net/base64">http://iharder.net/base64</a></p>
+ *
+ *
+ * @author Robert Harder
+ * @author rob@iharder.net
+ * @version 2.2.1
+ */
+public class Base64
+{
+
+/* ******** P U B L I C F I E L D S ******** */
+
+
+ /** No options specified. Value is zero. */
+ public final static int NO_OPTIONS = 0;
+
+ /** Specify encoding. */
+ public final static int ENCODE = 1;
+
+
+ /** Specify decoding. */
+ public final static int DECODE = 0;
+
+
+ /** Specify that data should be gzip-compressed. */
+ public final static int GZIP = 2;
+
+
+ /** Don't break lines when encoding (violates strict Base64 specification) */
+ public final static int DONT_BREAK_LINES = 8;
+
+ /**
+ * Encode using Base64-like encoding that is URL- and Filename-safe as described
+ * in Section 4 of RFC3548:
+ * <a href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org/rfcs/rfc3548.html</a>.
+ * It is important to note that data encoded this way is <em>not</em> officially valid Base64,
+ * or at the very least should not be called Base64 without also specifying that is
+ * was encoded using the URL- and Filename-safe dialect.
+ */
+ public final static int URL_SAFE = 16;
+
+
+ /**
+ * Encode using the special "ordered" dialect of Base64 described here:
+ * <a href="http://www.faqs.org/qa/rfcc-1940.html">http://www.faqs.org/qa/rfcc-1940.html</a>.
+ */
+ public final static int ORDERED = 32;
+
+
+/* ******** P R I V A T E F I E L D S ******** */
+
+
+ /** Maximum line length (76) of Base64 output. */
+ private final static int MAX_LINE_LENGTH = 76;
+
+
+ /** The equals sign (=) as a byte. */
+ private final static byte EQUALS_SIGN = (byte)'=';
+
+
+ /** The new line character (\n) as a byte. */
+ private final static byte NEW_LINE = (byte)'\n';
+
+
+ /** Preferred encoding. */
+ private final static String PREFERRED_ENCODING = "UTF-8";
+
+
+ // I think I end up not using the BAD_ENCODING indicator.
+ //private final static byte BAD_ENCODING = -9; // Indicates error in encoding
+ private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
+ private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
+
+
+/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
+
+ /** The 64 valid Base64 values. */
+ //private final static byte[] ALPHABET;
+ /* Host platform me be something funny like EBCDIC, so we hardcode these values. */
+ private final static byte[] _STANDARD_ALPHABET =
+ {
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
+ (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/'
+ };
+
+
+ /**
+ * Translates a Base64 value to either its 6-bit reconstruction value
+ * or a negative number indicating some other meaning.
+ **/
+ private final static byte[] _STANDARD_DECODABET =
+ {
+ -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8
+ -5,-5, // Whitespace: Tab and Linefeed
+ -9,-9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26
+ -9,-9,-9,-9,-9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42
+ 62, // Plus sign at decimal 43
+ -9,-9,-9, // Decimal 44 - 46
+ 63, // Slash at decimal 47
+ 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine
+ -9,-9,-9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9,-9,-9, // Decimal 62 - 64
+ 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N'
+ 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z'
+ -9,-9,-9,-9,-9,-9, // Decimal 91 - 96
+ 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm'
+ 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z'
+ -9,-9,-9,-9 // Decimal 123 - 126
+ /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+
+/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */
+
+ /**
+ * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548:
+ * <a href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org/rfcs/rfc3548.html</a>.
+ * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash."
+ */
+ private final static byte[] _URL_SAFE_ALPHABET =
+ {
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
+ (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_'
+ };
+
+ /**
+ * Used in decoding URL- and Filename-safe dialects of Base64.
+ */
+ private final static byte[] _URL_SAFE_DECODABET =
+ {
+ -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8
+ -5,-5, // Whitespace: Tab and Linefeed
+ -9,-9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26
+ -9,-9,-9,-9,-9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42
+ -9, // Plus sign at decimal 43
+ -9, // Decimal 44
+ 62, // Minus sign at decimal 45
+ -9, // Decimal 46
+ -9, // Slash at decimal 47
+ 52,53,54,55,56,57,58,59,60,61, // Numbers zero through nine
+ -9,-9,-9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9,-9,-9, // Decimal 62 - 64
+ 0,1,2,3,4,5,6,7,8,9,10,11,12,13, // Letters 'A' through 'N'
+ 14,15,16,17,18,19,20,21,22,23,24,25, // Letters 'O' through 'Z'
+ -9,-9,-9,-9, // Decimal 91 - 94
+ 63, // Underscore at decimal 95
+ -9, // Decimal 96
+ 26,27,28,29,30,31,32,33,34,35,36,37,38, // Letters 'a' through 'm'
+ 39,40,41,42,43,44,45,46,47,48,49,50,51, // Letters 'n' through 'z'
+ -9,-9,-9,-9 // Decimal 123 - 126
+ /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+
+
+/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */
+
+ /**
+ * I don't get the point of this technique, but it is described here:
+ * <a href="http://www.faqs.org/qa/rfcc-1940.html">http://www.faqs.org/qa/rfcc-1940.html</a>.
+ */
+ private final static byte[] _ORDERED_ALPHABET =
+ {
+ (byte)'-',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4',
+ (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9',
+ (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
+ (byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
+ (byte)'_',
+ (byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
+ (byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z'
+ };
+
+ /**
+ * Used in decoding the "ordered" dialect of Base64.
+ */
+ private final static byte[] _ORDERED_DECODABET =
+ {
+ -9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 0 - 8
+ -5,-5, // Whitespace: Tab and Linefeed
+ -9,-9, // Decimal 11 - 12
+ -5, // Whitespace: Carriage Return
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 14 - 26
+ -9,-9,-9,-9,-9, // Decimal 27 - 31
+ -5, // Whitespace: Space
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 33 - 42
+ -9, // Plus sign at decimal 43
+ -9, // Decimal 44
+ 0, // Minus sign at decimal 45
+ -9, // Decimal 46
+ -9, // Slash at decimal 47
+ 1,2,3,4,5,6,7,8,9,10, // Numbers zero through nine
+ -9,-9,-9, // Decimal 58 - 60
+ -1, // Equals sign at decimal 61
+ -9,-9,-9, // Decimal 62 - 64
+ 11,12,13,14,15,16,17,18,19,20,21,22,23, // Letters 'A' through 'M'
+ 24,25,26,27,28,29,30,31,32,33,34,35,36, // Letters 'N' through 'Z'
+ -9,-9,-9,-9, // Decimal 91 - 94
+ 37, // Underscore at decimal 95
+ -9, // Decimal 96
+ 38,39,40,41,42,43,44,45,46,47,48,49,50, // Letters 'a' through 'm'
+ 51,52,53,54,55,56,57,58,59,60,61,62,63, // Letters 'n' through 'z'
+ -9,-9,-9,-9 // Decimal 123 - 126
+ /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */
+ };
+
+
+/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */
+
+
+ /**
+ * Returns one of the _SOMETHING_ALPHABET byte arrays depending on
+ * the options specified.
+ * It's possible, though silly, to specify ORDERED and URLSAFE
+ * in which case one of them will be picked, though there is
+ * no guarantee as to which one will be picked.
+ */
+ private final static byte[] getAlphabet( int options )
+ {
+ if( (options & URL_SAFE) == URL_SAFE ) return _URL_SAFE_ALPHABET;
+ else if( (options & ORDERED) == ORDERED ) return _ORDERED_ALPHABET;
+ else return _STANDARD_ALPHABET;
+
+ } // end getAlphabet
+
+
+ /**
+ * Returns one of the _SOMETHING_DECODABET byte arrays depending on
+ * the options specified.
+ * It's possible, though silly, to specify ORDERED and URL_SAFE
+ * in which case one of them will be picked, though there is
+ * no guarantee as to which one will be picked.
+ */
+ private final static byte[] getDecodabet( int options )
+ {
+ if( (options & URL_SAFE) == URL_SAFE ) return _URL_SAFE_DECODABET;
+ else if( (options & ORDERED) == ORDERED ) return _ORDERED_DECODABET;
+ else return _STANDARD_DECODABET;
+
+ } // end getAlphabet
+
+
+
+ /** Defeats instantiation. */
+ private Base64(){}
+
+ /**
+ * Prints command line usage.
+ *
+ * @param msg A message to include with usage info.
+ */
+ private final static void usage( String msg )
+ {
+ System.err.println( msg );
+ System.err.println( "Usage: java Base64 -e|-d inputfile outputfile" );
+ } // end usage
+
+
+/* ******** E N C O D I N G M E T H O D S ******** */
+
+
+ /**
+ * Encodes up to the first three bytes of array <var>threeBytes</var>
+ * and returns a four-byte array in Base64 notation.
+ * The actual number of significant bytes in your array is
+ * given by <var>numSigBytes</var>.
+ * The array <var>threeBytes</var> needs only be as big as
+ * <var>numSigBytes</var>.
+ * Code can reuse a byte array by passing a four-byte array as <var>b4</var>.
+ *
+ * @param b4 A reusable byte array to reduce array instantiation
+ * @param threeBytes the array to convert
+ * @param numSigBytes the number of significant bytes in your array
+ * @return four byte array in Base64 notation.
+ * @since 1.5.1
+ */
+ private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes, int options )
+ {
+ encode3to4( threeBytes, 0, numSigBytes, b4, 0, options );
+ return b4;
+ } // end encode3to4
+
+
+ /**
+ * <p>Encodes up to three bytes of the array <var>source</var>
+ * and writes the resulting four Base64 bytes to <var>destination</var>.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * <var>srcOffset</var> and <var>destOffset</var>.
+ * This method does not check to make sure your arrays
+ * are large enough to accomodate <var>srcOffset</var> + 3 for
+ * the <var>source</var> array or <var>destOffset</var> + 4 for
+ * the <var>destination</var> array.
+ * The actual number of significant bytes in your array is
+ * given by <var>numSigBytes</var>.</p>
+ * <p>This is the lowest level of the encoding methods with
+ * all possible parameters.</p>
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param numSigBytes the number of significant bytes in your array
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @return the <var>destination</var> array
+ * @since 1.3
+ */
+ private static byte[] encode3to4(
+ byte[] source, int srcOffset, int numSigBytes,
+ byte[] destination, int destOffset, int options )
+ {
+ byte[] ALPHABET = getAlphabet( options );
+
+ // 1 2 3
+ // 01234567890123456789012345678901 Bit position
+ // --------000000001111111122222222 Array position from threeBytes
+ // --------| || || || | Six bit groups to index ALPHABET
+ // >>18 >>12 >> 6 >> 0 Right shift necessary
+ // 0x3f 0x3f 0x3f Additional AND
+
+ // Create buffer with zero-padding if there are only one or two
+ // significant bytes passed in the array.
+ // We have to shift left 24 in order to flush out the 1's that appear
+ // when Java treats a value as negative that is cast from a byte to an int.
+ int inBuff = ( numSigBytes > 0 ? ((source[ srcOffset ] << 24) >>> 8) : 0 )
+ | ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 )
+ | ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 );
+
+ switch( numSigBytes )
+ {
+ case 3:
+ destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
+ destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+ destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ];
+ destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ];
+ return destination;
+
+ case 2:
+ destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
+ destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+ destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ];
+ destination[ destOffset + 3 ] = EQUALS_SIGN;
+ return destination;
+
+ case 1:
+ destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
+ destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
+ destination[ destOffset + 2 ] = EQUALS_SIGN;
+ destination[ destOffset + 3 ] = EQUALS_SIGN;
+ return destination;
+
+ default:
+ return destination;
+ } // end switch
+ } // end encode3to4
+
+
+
+ /**
+ * Serializes an object and returns the Base64-encoded
+ * version of that serialized object. If the object
+ * cannot be serialized or there is another error,
+ * the method will return <tt>null</tt>.
+ * The object is not GZip-compressed before being encoded.
+ *
+ * @param serializableObject The object to encode
+ * @return The Base64-encoded object
+ * @since 1.4
+ */
+ public static String encodeObject( java.io.Serializable serializableObject )
+ {
+ return encodeObject( serializableObject, NO_OPTIONS );
+ } // end encodeObject
+
+
+
+ /**
+ * Serializes an object and returns the Base64-encoded
+ * version of that serialized object. If the object
+ * cannot be serialized or there is another error,
+ * the method will return <tt>null</tt>.
+ * <p>
+ * Valid options:<pre>
+ * GZIP: gzip-compresses object before encoding it.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * <i>Note: Technically, this makes your encoding non-compliant.</i>
+ * </pre>
+ * <p>
+ * Example: <code>encodeObject( myObj, Base64.GZIP )</code> or
+ * <p>
+ * Example: <code>encodeObject( myObj, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
+ *
+ * @param serializableObject The object to encode
+ * @param options Specified options
+ * @return The Base64-encoded object
+ * @see Base64#GZIP
+ * @see Base64#DONT_BREAK_LINES
+ * @since 2.0
+ */
+ public static String encodeObject( java.io.Serializable serializableObject, int options )
+ {
+ // Streams
+ java.io.ByteArrayOutputStream baos = null;
+ java.io.OutputStream b64os = null;
+ java.io.ObjectOutputStream oos = null;
+ java.util.zip.GZIPOutputStream gzos = null;
+
+ // Isolate options
+ int gzip = (options & GZIP);
+ int dontBreakLines = (options & DONT_BREAK_LINES);
+
+ try
+ {
+ // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream
+ baos = new java.io.ByteArrayOutputStream();
+ b64os = new Base64.OutputStream( baos, ENCODE | options );
+
+ // GZip?
+ if( gzip == GZIP )
+ {
+ gzos = new java.util.zip.GZIPOutputStream( b64os );
+ oos = new java.io.ObjectOutputStream( gzos );
+ } // end if: gzip
+ else
+ oos = new java.io.ObjectOutputStream( b64os );
+
+ oos.writeObject( serializableObject );
+ } // end try
+ catch( java.io.IOException e )
+ {
+ e.printStackTrace();
+ return null;
+ } // end catch
+ finally
+ {
+ try{ oos.close(); } catch( Exception e ){}
+ try{ gzos.close(); } catch( Exception e ){}
+ try{ b64os.close(); } catch( Exception e ){}
+ try{ baos.close(); } catch( Exception e ){}
+ } // end finally
+
+ // Return value according to relevant encoding.
+ try
+ {
+ return new String( baos.toByteArray(), PREFERRED_ENCODING );
+ } // end try
+ catch (java.io.UnsupportedEncodingException uue)
+ {
+ return new String( baos.toByteArray() );
+ } // end catch
+
+ } // end encode
+
+
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Does not GZip-compress data.
+ *
+ * @param source The data to convert
+ * @since 1.4
+ */
+ public static String encodeBytes( byte[] source )
+ {
+ return encodeBytes( source, 0, source.length, NO_OPTIONS );
+ } // end encodeBytes
+
+
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * <p>
+ * Valid options:<pre>
+ * GZIP: gzip-compresses object before encoding it.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * <i>Note: Technically, this makes your encoding non-compliant.</i>
+ * </pre>
+ * <p>
+ * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or
+ * <p>
+ * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
+ *
+ *
+ * @param source The data to convert
+ * @param options Specified options
+ * @see Base64#GZIP
+ * @see Base64#DONT_BREAK_LINES
+ * @since 2.0
+ */
+ public static String encodeBytes( byte[] source, int options )
+ {
+ return encodeBytes( source, 0, source.length, options );
+ } // end encodeBytes
+
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * Does not GZip-compress data.
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @since 1.4
+ */
+ public static String encodeBytes( byte[] source, int off, int len )
+ {
+ return encodeBytes( source, off, len, NO_OPTIONS );
+ } // end encodeBytes
+
+
+
+ /**
+ * Encodes a byte array into Base64 notation.
+ * <p>
+ * Valid options:<pre>
+ * GZIP: gzip-compresses object before encoding it.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * <i>Note: Technically, this makes your encoding non-compliant.</i>
+ * </pre>
+ * <p>
+ * Example: <code>encodeBytes( myData, Base64.GZIP )</code> or
+ * <p>
+ * Example: <code>encodeBytes( myData, Base64.GZIP | Base64.DONT_BREAK_LINES )</code>
+ *
+ *
+ * @param source The data to convert
+ * @param off Offset in array where conversion should begin
+ * @param len Length of data to convert
+ * @param options Specified options; alphabet type is pulled from this (standard, url-safe, ordered)
+ * @see Base64#GZIP
+ * @see Base64#DONT_BREAK_LINES
+ * @since 2.0
+ */
+ public static String encodeBytes( byte[] source, int off, int len, int options )
+ {
+ // Isolate options
+ int dontBreakLines = ( options & DONT_BREAK_LINES );
+ int gzip = ( options & GZIP );
+
+ // Compress?
+ if( gzip == GZIP )
+ {
+ java.io.ByteArrayOutputStream baos = null;
+ java.util.zip.GZIPOutputStream gzos = null;
+ Base64.OutputStream b64os = null;
+
+
+ try
+ {
+ // GZip -> Base64 -> ByteArray
+ baos = new java.io.ByteArrayOutputStream();
+ b64os = new Base64.OutputStream( baos, ENCODE | options );
+ gzos = new java.util.zip.GZIPOutputStream( b64os );
+
+ gzos.write( source, off, len );
+ gzos.close();
+ } // end try
+ catch( java.io.IOException e )
+ {
+ e.printStackTrace();
+ return null;
+ } // end catch
+ finally
+ {
+ try{ gzos.close(); } catch( Exception e ){}
+ try{ b64os.close(); } catch( Exception e ){}
+ try{ baos.close(); } catch( Exception e ){}
+ } // end finally
+
+ // Return value according to relevant encoding.
+ try
+ {
+ return new String( baos.toByteArray(), PREFERRED_ENCODING );
+ } // end try
+ catch (java.io.UnsupportedEncodingException uue)
+ {
+ return new String( baos.toByteArray() );
+ } // end catch
+ } // end if: compress
+
+ // Else, don't compress. Better not to use streams at all then.
+ else
+ {
+ // Convert option to boolean in way that code likes it.
+ boolean breakLines = dontBreakLines == 0;
+
+ int len43 = len * 4 / 3;
+ byte[] outBuff = new byte[ ( len43 ) // Main 4:3
+ + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding
+ + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines
+ int d = 0;
+ int e = 0;
+ int len2 = len - 2;
+ int lineLength = 0;
+ for( ; d < len2; d+=3, e+=4 )
+ {
+ encode3to4( source, d+off, 3, outBuff, e, options );
+
+ lineLength += 4;
+ if( breakLines && lineLength == MAX_LINE_LENGTH )
+ {
+ outBuff[e+4] = NEW_LINE;
+ e++;
+ lineLength = 0;
+ } // end if: end of line
+ } // en dfor: each piece of array
+
+ if( d < len )
+ {
+ encode3to4( source, d+off, len - d, outBuff, e, options );
+ e += 4;
+ } // end if: some padding needed
+
+
+ // Return value according to relevant encoding.
+ try
+ {
+ return new String( outBuff, 0, e, PREFERRED_ENCODING );
+ } // end try
+ catch (java.io.UnsupportedEncodingException uue)
+ {
+ return new String( outBuff, 0, e );
+ } // end catch
+
+ } // end else: don't compress
+
+ } // end encodeBytes
+
+
+
+
+
+/* ******** D E C O D I N G M E T H O D S ******** */
+
+
+ /**
+ * Decodes four bytes from array <var>source</var>
+ * and writes the resulting bytes (up to three of them)
+ * to <var>destination</var>.
+ * The source and destination arrays can be manipulated
+ * anywhere along their length by specifying
+ * <var>srcOffset</var> and <var>destOffset</var>.
+ * This method does not check to make sure your arrays
+ * are large enough to accomodate <var>srcOffset</var> + 4 for
+ * the <var>source</var> array or <var>destOffset</var> + 3 for
+ * the <var>destination</var> array.
+ * This method returns the actual number of bytes that
+ * were converted from the Base64 encoding.
+ * <p>This is the lowest level of the decoding methods with
+ * all possible parameters.</p>
+ *
+ *
+ * @param source the array to convert
+ * @param srcOffset the index where conversion begins
+ * @param destination the array to hold the conversion
+ * @param destOffset the index where output will be put
+ * @param options alphabet type is pulled from this (standard, url-safe, ordered)
+ * @return the number of decoded bytes converted
+ * @since 1.3
+ */
+ private static int decode4to3( byte[] source, int srcOffset, byte[] destination, int destOffset, int options )
+ {
+ byte[] DECODABET = getDecodabet( options );
+
+ // Example: Dk==
+ if( source[ srcOffset + 2] == EQUALS_SIGN )
+ {
+ // Two ways to do the same thing. Don't know which way I like best.
+ //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
+ // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
+ int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 )
+ | ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 );
+
+ destination[ destOffset ] = (byte)( outBuff >>> 16 );
+ return 1;
+ }
+
+ // Example: DkL=
+ else if( source[ srcOffset + 3 ] == EQUALS_SIGN )
+ {
+ // Two ways to do the same thing. Don't know which way I like best.
+ //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
+ // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
+ // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 );
+ int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 )
+ | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
+ | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 );
+
+ destination[ destOffset ] = (byte)( outBuff >>> 16 );
+ destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 );
+ return 2;
+ }
+
+ // Example: DkLE
+ else
+ {
+ try{
+ // Two ways to do the same thing. Don't know which way I like best.
+ //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
+ // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
+ // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 )
+ // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 );
+ int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 )
+ | ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
+ | ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6)
+ | ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) );
+
+
+ destination[ destOffset ] = (byte)( outBuff >> 16 );
+ destination[ destOffset + 1 ] = (byte)( outBuff >> 8 );
+ destination[ destOffset + 2 ] = (byte)( outBuff );
+
+ return 3;
+ }catch( Exception e){
+ System.out.println(""+source[srcOffset]+ ": " + ( DECODABET[ source[ srcOffset ] ] ) );
+ System.out.println(""+source[srcOffset+1]+ ": " + ( DECODABET[ source[ srcOffset + 1 ] ] ) );
+ System.out.println(""+source[srcOffset+2]+ ": " + ( DECODABET[ source[ srcOffset + 2 ] ] ) );
+ System.out.println(""+source[srcOffset+3]+ ": " + ( DECODABET[ source[ srcOffset + 3 ] ] ) );
+ return -1;
+ } // end catch
+ }
+ } // end decodeToBytes
+
+
+
+
+ /**
+ * Very low-level access to decoding ASCII characters in
+ * the form of a byte array. Does not support automatically
+ * gunzipping or any other "fancy" features.
+ *
+ * @param source The Base64 encoded data
+ * @param off The offset of where to begin decoding
+ * @param len The length of characters to decode
+ * @return decoded data
+ * @since 1.3
+ */
+ public static byte[] decode( byte[] source, int off, int len, int options )
+ {
+ byte[] DECODABET = getDecodabet( options );
+
+ int len34 = len * 3 / 4;
+ byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output
+ int outBuffPosn = 0;
+
+ byte[] b4 = new byte[4];
+ int b4Posn = 0;
+ int i = 0;
+ byte sbiCrop = 0;
+ byte sbiDecode = 0;
+ for( i = off; i < off+len; i++ )
+ {
+ sbiCrop = (byte)(source[i] & 0x7f); // Only the low seven bits
+ sbiDecode = DECODABET[ sbiCrop ];
+
+ if( sbiDecode >= WHITE_SPACE_ENC ) // White space, Equals sign or better
+ {
+ if( sbiDecode >= EQUALS_SIGN_ENC )
+ {
+ b4[ b4Posn++ ] = sbiCrop;
+ if( b4Posn > 3 )
+ {
+ outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options );
+ b4Posn = 0;
+
+ // If that was the equals sign, break out of 'for' loop
+ if( sbiCrop == EQUALS_SIGN )
+ break;
+ } // end if: quartet built
+
+ } // end if: equals sign or better
+
+ } // end if: white space, equals sign or better
+ else
+ {
+ System.err.println( "Bad Base64 input character at " + i + ": " + source[i] + "(decimal)" );
+ return null;
+ } // end else:
+ } // each input character
+
+ byte[] out = new byte[ outBuffPosn ];
+ System.arraycopy( outBuff, 0, out, 0, outBuffPosn );
+ return out;
+ } // end decode
+
+
+
+
+ /**
+ * Decodes data from Base64 notation, automatically
+ * detecting gzip-compressed data and decompressing it.
+ *
+ * @param s the string to decode
+ * @return the decoded data
+ * @since 1.4
+ */
+ public static byte[] decode( String s )
+ {
+ return decode( s, NO_OPTIONS );
+ }
+
+
+ /**
+ * Decodes data from Base64 notation, automatically
+ * detecting gzip-compressed data and decompressing it.
+ *
+ * @param s the string to decode
+ * @param options encode options such as URL_SAFE
+ * @return the decoded data
+ * @since 1.4
+ */
+ public static byte[] decode( String s, int options )
+ {
+ byte[] bytes;
+ try
+ {
+ bytes = s.getBytes( PREFERRED_ENCODING );
+ } // end try
+ catch( java.io.UnsupportedEncodingException uee )
+ {
+ bytes = s.getBytes();
+ } // end catch
+ //</change>
+
+ // Decode
+ bytes = decode( bytes, 0, bytes.length, options );
+
+
+ // Check to see if it's gzip-compressed
+ // GZIP Magic Two-Byte Number: 0x8b1f (35615)
+ if( bytes != null && bytes.length >= 4 )
+ {
+
+ int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
+ if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head )
+ {
+ java.io.ByteArrayInputStream bais = null;
+ java.util.zip.GZIPInputStream gzis = null;
+ java.io.ByteArrayOutputStream baos = null;
+ byte[] buffer = new byte[2048];
+ int length = 0;
+
+ try
+ {
+ baos = new java.io.ByteArrayOutputStream();
+ bais = new java.io.ByteArrayInputStream( bytes );
+ gzis = new java.util.zip.GZIPInputStream( bais );
+
+ while( ( length = gzis.read( buffer ) ) >= 0 )
+ {
+ baos.write(buffer,0,length);
+ } // end while: reading input
+
+ // No error? Get new bytes.
+ bytes = baos.toByteArray();
+
+ } // end try
+ catch( java.io.IOException e )
+ {
+ // Just return originally-decoded bytes
+ } // end catch
+ finally
+ {
+ try{ baos.close(); } catch( Exception e ){}
+ try{ gzis.close(); } catch( Exception e ){}
+ try{ bais.close(); } catch( Exception e ){}
+ } // end finally
+
+ } // end if: gzipped
+ } // end if: bytes.length >= 2
+
+ return bytes;
+ } // end decode
+
+
+
+
+ /**
+ * Attempts to decode Base64 data and deserialize a Java
+ * Object within. Returns <tt>null</tt> if there was an error.
+ *
+ * @param encodedObject The Base64 data to decode
+ * @return The decoded and deserialized object
+ * @since 1.5
+ */
+ public static Object decodeToObject( String encodedObject )
+ {
+ // Decode and gunzip if necessary
+ byte[] objBytes = decode( encodedObject );
+
+ java.io.ByteArrayInputStream bais = null;
+ java.io.ObjectInputStream ois = null;
+ Object obj = null;
+
+ try
+ {
+ bais = new java.io.ByteArrayInputStream( objBytes );
+ ois = new java.io.ObjectInputStream( bais );
+
+ obj = ois.readObject();
+ } // end try
+ catch( java.io.IOException e )
+ {
+ e.printStackTrace();
+ obj = null;
+ } // end catch
+ catch( java.lang.ClassNotFoundException e )
+ {
+ e.printStackTrace();
+ obj = null;
+ } // end catch
+ finally
+ {
+ try{ bais.close(); } catch( Exception e ){}
+ try{ ois.close(); } catch( Exception e ){}
+ } // end finally
+
+ return obj;
+ } // end decodeObject
+
+
+
+ /**
+ * Convenience method for encoding data to a file.
+ *
+ * @param dataToEncode byte array of data to encode in base64 form
+ * @param filename Filename for saving encoded data
+ * @return <tt>true</tt> if successful, <tt>false</tt> otherwise
+ *
+ * @since 2.1
+ */
+ public static boolean encodeToFile( byte[] dataToEncode, String filename )
+ {
+ boolean success = false;
+ Base64.OutputStream bos = null;
+ try
+ {
+ bos = new Base64.OutputStream(
+ new java.io.FileOutputStream( filename ), Base64.ENCODE );
+ bos.write( dataToEncode );
+ success = true;
+ } // end try
+ catch( java.io.IOException e )
+ {
+
+ success = false;
+ } // end catch: IOException
+ finally
+ {
+ try{ bos.close(); } catch( Exception e ){}
+ } // end finally
+
+ return success;
+ } // end encodeToFile
+
+
+ /**
+ * Convenience method for decoding data to a file.
+ *
+ * @param dataToDecode Base64-encoded data as a string
+ * @param filename Filename for saving decoded data
+ * @return <tt>true</tt> if successful, <tt>false</tt> otherwise
+ *
+ * @since 2.1
+ */
+ public static boolean decodeToFile( String dataToDecode, String filename )
+ {
+ boolean success = false;
+ Base64.OutputStream bos = null;
+ try
+ {
+ bos = new Base64.OutputStream(
+ new java.io.FileOutputStream( filename ), Base64.DECODE );
+ bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) );
+ success = true;
+ } // end try
+ catch( java.io.IOException e )
+ {
+ success = false;
+ } // end catch: IOException
+ finally
+ {
+ try{ bos.close(); } catch( Exception e ){}
+ } // end finally
+
+ return success;
+ } // end decodeToFile
+
+
+
+
+ /**
+ * Convenience method for reading a base64-encoded
+ * file and decoding it.
+ *
+ * @param filename Filename for reading encoded data
+ * @return decoded byte array or null if unsuccessful
+ *
+ * @since 2.1
+ */
+ public static byte[] decodeFromFile( String filename )
+ {
+ byte[] decodedData = null;
+ Base64.InputStream bis = null;
+ try
+ {
+ // Set up some useful variables
+ java.io.File file = new java.io.File( filename );
+ byte[] buffer = null;
+ int length = 0;
+ int numBytes = 0;
+
+ // Check for size of file
+ if( file.length() > Integer.MAX_VALUE )
+ {
+ System.err.println( "File is too big for this convenience method (" + file.length() + " bytes)." );
+ return null;
+ } // end if: file too big for int index
+ buffer = new byte[ (int)file.length() ];
+
+ // Open a stream
+ bis = new Base64.InputStream(
+ new java.io.BufferedInputStream(
+ new java.io.FileInputStream( file ) ), Base64.DECODE );
+
+ // Read until done
+ while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 )
+ length += numBytes;
+
+ // Save in a variable to return
+ decodedData = new byte[ length ];
+ System.arraycopy( buffer, 0, decodedData, 0, length );
+
+ } // end try
+ catch( java.io.IOException e )
+ {
+ System.err.println( "Error decoding from file " + filename );
+ } // end catch: IOException
+ finally
+ {
+ try{ bis.close(); } catch( Exception e) {}
+ } // end finally
+
+ return decodedData;
+ } // end decodeFromFile
+
+
+
+ /**
+ * Convenience method for reading a binary file
+ * and base64-encoding it.
+ *
+ * @param filename Filename for reading binary data
+ * @return base64-encoded string or null if unsuccessful
+ *
+ * @since 2.1
+ */
+ public static String encodeFromFile( String filename )
+ {
+ String encodedData = null;
+ Base64.InputStream bis = null;
+ try
+ {
+ // Set up some useful variables
+ java.io.File file = new java.io.File( filename );
+ byte[] buffer = new byte[ Math.max((int)(file.length() * 1.4),40) ]; // Need max() for math on small files (v2.2.1)
+ int length = 0;
+ int numBytes = 0;
+
+ // Open a stream
+ bis = new Base64.InputStream(
+ new java.io.BufferedInputStream(
+ new java.io.FileInputStream( file ) ), Base64.ENCODE );
+
+ // Read until done
+ while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 )
+ length += numBytes;
+
+ // Save in a variable to return
+ encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING );
+
+ } // end try
+ catch( java.io.IOException e )
+ {
+ System.err.println( "Error encoding from file " + filename );
+ } // end catch: IOException
+ finally
+ {
+ try{ bis.close(); } catch( Exception e) {}
+ } // end finally
+
+ return encodedData;
+ } // end encodeFromFile
+
+ /**
+ * Reads <tt>infile</tt> and encodes it to <tt>outfile</tt>.
+ *
+ * @param infile Input file
+ * @param outfile Output file
+ * @since 2.2
+ */
+ public static void encodeFileToFile( String infile, String outfile )
+ {
+ String encoded = Base64.encodeFromFile( infile );
+ java.io.OutputStream out = null;
+ try{
+ out = new java.io.BufferedOutputStream(
+ new java.io.FileOutputStream( outfile ) );
+ out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output.
+ } // end try
+ catch( java.io.IOException ex ) {
+ ex.printStackTrace();
+ } // end catch
+ finally {
+ try { out.close(); }
+ catch( Exception ex ){}
+ } // end finally
+ } // end encodeFileToFile
+
+
+ /**
+ * Reads <tt>infile</tt> and decodes it to <tt>outfile</tt>.
+ *
+ * @param infile Input file
+ * @param outfile Output file
+ * @since 2.2
+ */
+ public static void decodeFileToFile( String infile, String outfile )
+ {
+ byte[] decoded = Base64.decodeFromFile( infile );
+ java.io.OutputStream out = null;
+ try{
+ out = new java.io.BufferedOutputStream(
+ new java.io.FileOutputStream( outfile ) );
+ out.write( decoded );
+ } // end try
+ catch( java.io.IOException ex ) {
+ ex.printStackTrace();
+ } // end catch
+ finally {
+ try { out.close(); }
+ catch( Exception ex ){}
+ } // end finally
+ } // end decodeFileToFile
+
+
+ /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */
+
+
+
+ /**
+ * A {@link Base64.InputStream} will read data from another
+ * <tt>java.io.InputStream</tt>, given in the constructor,
+ * and encode/decode to/from Base64 notation on the fly.
+ *
+ * @see Base64
+ * @since 1.3
+ */
+ public static class InputStream extends java.io.FilterInputStream
+ {
+ private boolean encode; // Encoding or decoding
+ private int position; // Current position in the buffer
+ private byte[] buffer; // Small buffer holding converted data
+ private int bufferLength; // Length of buffer (3 or 4)
+ private int numSigBytes; // Number of meaningful bytes in the buffer
+ private int lineLength;
+ private boolean breakLines; // Break lines at less than 80 characters
+ private int options; // Record options used to create the stream.
+ private byte[] alphabet; // Local copies to avoid extra method calls
+ private byte[] decodabet; // Local copies to avoid extra method calls
+
+
+ /**
+ * Constructs a {@link Base64.InputStream} in DECODE mode.
+ *
+ * @param in the <tt>java.io.InputStream</tt> from which to read data.
+ * @since 1.3
+ */
+ public InputStream( java.io.InputStream in )
+ {
+ this( in, DECODE );
+ } // end constructor
+
+
+ /**
+ * Constructs a {@link Base64.InputStream} in
+ * either ENCODE or DECODE mode.
+ * <p>
+ * Valid options:<pre>
+ * ENCODE or DECODE: Encode or Decode as data is read.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * (only meaningful when encoding)
+ * <i>Note: Technically, this makes your encoding non-compliant.</i>
+ * </pre>
+ * <p>
+ * Example: <code>new Base64.InputStream( in, Base64.DECODE )</code>
+ *
+ *
+ * @param in the <tt>java.io.InputStream</tt> from which to read data.
+ * @param options Specified options
+ * @see Base64#ENCODE
+ * @see Base64#DECODE
+ * @see Base64#DONT_BREAK_LINES
+ * @since 2.0
+ */
+ public InputStream( java.io.InputStream in, int options )
+ {
+ super( in );
+ this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES;
+ this.encode = (options & ENCODE) == ENCODE;
+ this.bufferLength = encode ? 4 : 3;
+ this.buffer = new byte[ bufferLength ];
+ this.position = -1;
+ this.lineLength = 0;
+ this.options = options; // Record for later, mostly to determine which alphabet to use
+ this.alphabet = getAlphabet(options);
+ this.decodabet = getDecodabet(options);
+ } // end constructor
+
+ /**
+ * Reads enough of the input stream to convert
+ * to/from Base64 and returns the next byte.
+ *
+ * @return next byte
+ * @since 1.3
+ */
+ public int read() throws java.io.IOException
+ {
+ // Do we need to get data?
+ if( position < 0 )
+ {
+ if( encode )
+ {
+ byte[] b3 = new byte[3];
+ int numBinaryBytes = 0;
+ for( int i = 0; i < 3; i++ )
+ {
+ try
+ {
+ int b = in.read();
+
+ // If end of stream, b is -1.
+ if( b >= 0 )
+ {
+ b3[i] = (byte)b;
+ numBinaryBytes++;
+ } // end if: not end of stream
+
+ } // end try: read
+ catch( java.io.IOException e )
+ {
+ // Only a problem if we got no data at all.
+ if( i == 0 )
+ throw e;
+
+ } // end catch
+ } // end for: each needed input byte
+
+ if( numBinaryBytes > 0 )
+ {
+ encode3to4( b3, 0, numBinaryBytes, buffer, 0, options );
+ position = 0;
+ numSigBytes = 4;
+ } // end if: got data
+ else
+ {
+ return -1;
+ } // end else
+ } // end if: encoding
+
+ // Else decoding
+ else
+ {
+ byte[] b4 = new byte[4];
+ int i = 0;
+ for( i = 0; i < 4; i++ )
+ {
+ // Read four "meaningful" bytes:
+ int b = 0;
+ do{ b = in.read(); }
+ while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC );
+
+ if( b < 0 )
+ break; // Reads a -1 if end of stream
+
+ b4[i] = (byte)b;
+ } // end for: each needed input byte
+
+ if( i == 4 )
+ {
+ numSigBytes = decode4to3( b4, 0, buffer, 0, options );
+ position = 0;
+ } // end if: got four characters
+ else if( i == 0 ){
+ return -1;
+ } // end else if: also padded correctly
+ else
+ {
+ // Must have broken out from above.
+ throw new java.io.IOException( "Improperly padded Base64 input." );
+ } // end
+
+ } // end else: decode
+ } // end else: get data
+
+ // Got data?
+ if( position >= 0 )
+ {
+ // End of relevant data?
+ if( /*!encode &&*/ position >= numSigBytes )
+ return -1;
+
+ if( encode && breakLines && lineLength >= MAX_LINE_LENGTH )
+ {
+ lineLength = 0;
+ return '\n';
+ } // end if
+ else
+ {
+ lineLength++; // This isn't important when decoding
+ // but throwing an extra "if" seems
+ // just as wasteful.
+
+ int b = buffer[ position++ ];
+
+ if( position >= bufferLength )
+ position = -1;
+
+ return b & 0xFF; // This is how you "cast" a byte that's
+ // intended to be unsigned.
+ } // end else
+ } // end if: position >= 0
+
+ // Else error
+ else
+ {
+ // When JDK1.4 is more accepted, use an assertion here.
+ throw new java.io.IOException( "Error in Base64 code reading stream." );
+ } // end else
+ } // end read
+
+
+ /**
+ * Calls {@link #read()} repeatedly until the end of stream
+ * is reached or <var>len</var> bytes are read.
+ * Returns number of bytes read into array or -1 if
+ * end of stream is encountered.
+ *
+ * @param dest array to hold values
+ * @param off offset for array
+ * @param len max number of bytes to read into array
+ * @return bytes read into array or -1 if end of stream is encountered.
+ * @since 1.3
+ */
+ public int read( byte[] dest, int off, int len ) throws java.io.IOException
+ {
+ int i;
+ int b;
+ for( i = 0; i < len; i++ )
+ {
+ b = read();
+
+ //if( b < 0 && i == 0 )
+ // return -1;
+
+ if( b >= 0 )
+ dest[off + i] = (byte)b;
+ else if( i == 0 )
+ return -1;
+ else
+ break; // Out of 'for' loop
+ } // end for: each byte read
+ return i;
+ } // end read
+
+ } // end inner class InputStream
+
+
+
+
+
+
+ /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */
+
+
+
+ /**
+ * A {@link Base64.OutputStream} will write data to another
+ * <tt>java.io.OutputStream</tt>, given in the constructor,
+ * and encode/decode to/from Base64 notation on the fly.
+ *
+ * @see Base64
+ * @since 1.3
+ */
+ public static class OutputStream extends java.io.FilterOutputStream
+ {
+ private boolean encode;
+ private int position;
+ private byte[] buffer;
+ private int bufferLength;
+ private int lineLength;
+ private boolean breakLines;
+ private byte[] b4; // Scratch used in a few places
+ private boolean suspendEncoding;
+ private int options; // Record for later
+ private byte[] alphabet; // Local copies to avoid extra method calls
+ private byte[] decodabet; // Local copies to avoid extra method calls
+
+ /**
+ * Constructs a {@link Base64.OutputStream} in ENCODE mode.
+ *
+ * @param out the <tt>java.io.OutputStream</tt> to which data will be written.
+ * @since 1.3
+ */
+ public OutputStream( java.io.OutputStream out )
+ {
+ this( out, ENCODE );
+ } // end constructor
+
+
+ /**
+ * Constructs a {@link Base64.OutputStream} in
+ * either ENCODE or DECODE mode.
+ * <p>
+ * Valid options:<pre>
+ * ENCODE or DECODE: Encode or Decode as data is read.
+ * DONT_BREAK_LINES: don't break lines at 76 characters
+ * (only meaningful when encoding)
+ * <i>Note: Technically, this makes your encoding non-compliant.</i>
+ * </pre>
+ * <p>
+ * Example: <code>new Base64.OutputStream( out, Base64.ENCODE )</code>
+ *
+ * @param out the <tt>java.io.OutputStream</tt> to which data will be written.
+ * @param options Specified options.
+ * @see Base64#ENCODE
+ * @see Base64#DECODE
+ * @see Base64#DONT_BREAK_LINES
+ * @since 1.3
+ */
+ public OutputStream( java.io.OutputStream out, int options )
+ {
+ super( out );
+ this.breakLines = (options & DONT_BREAK_LINES) != DONT_BREAK_LINES;
+ this.encode = (options & ENCODE) == ENCODE;
+ this.bufferLength = encode ? 3 : 4;
+ this.buffer = new byte[ bufferLength ];
+ this.position = 0;
+ this.lineLength = 0;
+ this.suspendEncoding = false;
+ this.b4 = new byte[4];
+ this.options = options;
+ this.alphabet = getAlphabet(options);
+ this.decodabet = getDecodabet(options);
+ } // end constructor
+
+
+ /**
+ * Writes the byte to the output stream after
+ * converting to/from Base64 notation.
+ * When encoding, bytes are buffered three
+ * at a time before the output stream actually
+ * gets a write() call.
+ * When decoding, bytes are buffered four
+ * at a time.
+ *
+ * @param theByte the byte to write
+ * @since 1.3
+ */
+ public void write(int theByte) throws java.io.IOException
+ {
+ // Encoding suspended?
+ if( suspendEncoding )
+ {
+ super.out.write( theByte );
+ return;
+ } // end if: supsended
+
+ // Encode?
+ if( encode )
+ {
+ buffer[ position++ ] = (byte)theByte;
+ if( position >= bufferLength ) // Enough to encode.
+ {
+ out.write( encode3to4( b4, buffer, bufferLength, options ) );
+
+ lineLength += 4;
+ if( breakLines && lineLength >= MAX_LINE_LENGTH )
+ {
+ out.write( NEW_LINE );
+ lineLength = 0;
+ } // end if: end of line
+
+ position = 0;
+ } // end if: enough to output
+ } // end if: encoding
+
+ // Else, Decoding
+ else
+ {
+ // Meaningful Base64 character?
+ if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC )
+ {
+ buffer[ position++ ] = (byte)theByte;
+ if( position >= bufferLength ) // Enough to output.
+ {
+ int len = Base64.decode4to3( buffer, 0, b4, 0, options );
+ out.write( b4, 0, len );
+ //out.write( Base64.decode4to3( buffer ) );
+ position = 0;
+ } // end if: enough to output
+ } // end if: meaningful base64 character
+ else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC )
+ {
+ throw new java.io.IOException( "Invalid character in Base64 data." );
+ } // end else: not white space either
+ } // end else: decoding
+ } // end write
+
+
+
+ /**
+ * Calls {@link #write(int)} repeatedly until <var>len</var>
+ * bytes are written.
+ *
+ * @param theBytes array from which to read bytes
+ * @param off offset for array
+ * @param len max number of bytes to read into array
+ * @since 1.3
+ */
+ public void write( byte[] theBytes, int off, int len ) throws java.io.IOException
+ {
+ // Encoding suspended?
+ if( suspendEncoding )
+ {
+ super.out.write( theBytes, off, len );
+ return;
+ } // end if: supsended
+
+ for( int i = 0; i < len; i++ )
+ {
+ write( theBytes[ off + i ] );
+ } // end for: each byte written
+
+ } // end write
+
+
+
+ /**
+ * Method added by PHIL. [Thanks, PHIL. -Rob]
+ * This pads the buffer without closing the stream.
+ */
+ public void flushBase64() throws java.io.IOException
+ {
+ if( position > 0 )
+ {
+ if( encode )
+ {
+ out.write( encode3to4( b4, buffer, position, options ) );
+ position = 0;
+ } // end if: encoding
+ else
+ {
+ throw new java.io.IOException( "Base64 input not properly padded." );
+ } // end else: decoding
+ } // end if: buffer partially full
+
+ } // end flush
+
+
+ /**
+ * Flushes and closes (I think, in the superclass) the stream.
+ *
+ * @since 1.3
+ */
+ public void close() throws java.io.IOException
+ {
+ // 1. Ensure that pending characters are written
+ flushBase64();
+
+ // 2. Actually close the stream
+ // Base class both flushes and closes.
+ super.close();
+
+ buffer = null;
+ out = null;
+ } // end close
+
+
+
+ /**
+ * Suspends encoding of the stream.
+ * May be helpful if you need to embed a piece of
+ * base640-encoded data in a stream.
+ *
+ * @since 1.5.1
+ */
+ public void suspendEncoding() throws java.io.IOException
+ {
+ flushBase64();
+ this.suspendEncoding = true;
+ } // end suspendEncoding
+
+
+ /**
+ * Resumes encoding of the stream.
+ * May be helpful if you need to embed a piece of
+ * base640-encoded data in a stream.
+ *
+ * @since 1.5.1
+ */
+ public void resumeEncoding()
+ {
+ this.suspendEncoding = false;
+ } // end resumeEncoding
+
+
+
+ } // end inner class OutputStream
+
+
+} // end class Base64
+
diff --git a/src/org/jivesoftware/smack/util/Base64Encoder.java b/src/org/jivesoftware/smack/util/Base64Encoder.java new file mode 100644 index 0000000..d53c0ed --- /dev/null +++ b/src/org/jivesoftware/smack/util/Base64Encoder.java @@ -0,0 +1,42 @@ +/** + * All rights reserved. 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.jivesoftware.smack.util; + + +/** + * A Base 64 encoding implementation. + * @author Florian Schmaus + */ +public class Base64Encoder implements StringEncoder { + + private static Base64Encoder instance = new Base64Encoder(); + + private Base64Encoder() { + // Use getInstance() + } + + public static Base64Encoder getInstance() { + return instance; + } + + public String encode(String s) { + return Base64.encodeBytes(s.getBytes()); + } + + public String decode(String s) { + return new String(Base64.decode(s)); + } + +} diff --git a/src/org/jivesoftware/smack/util/Base64FileUrlEncoder.java b/src/org/jivesoftware/smack/util/Base64FileUrlEncoder.java new file mode 100644 index 0000000..190b374 --- /dev/null +++ b/src/org/jivesoftware/smack/util/Base64FileUrlEncoder.java @@ -0,0 +1,48 @@ +/** + * All rights reserved. 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.jivesoftware.smack.util; + + +/** + * A Base 64 encoding implementation that generates filename and Url safe encodings. + * + * <p> + * Note: This does NOT produce standard Base 64 encodings, but a variant as defined in + * Section 4 of RFC3548: + * <a href="http://www.faqs.org/rfcs/rfc3548.html">http://www.faqs.org/rfcs/rfc3548.html</a>. + * + * @author Robin Collier + */ +public class Base64FileUrlEncoder implements StringEncoder { + + private static Base64FileUrlEncoder instance = new Base64FileUrlEncoder(); + + private Base64FileUrlEncoder() { + // Use getInstance() + } + + public static Base64FileUrlEncoder getInstance() { + return instance; + } + + public String encode(String s) { + return Base64.encodeBytes(s.getBytes(), Base64.URL_SAFE); + } + + public String decode(String s) { + return new String(Base64.decode(s, Base64.URL_SAFE)); + } + +} diff --git a/src/org/jivesoftware/smack/util/Cache.java b/src/org/jivesoftware/smack/util/Cache.java new file mode 100644 index 0000000..964ac23 --- /dev/null +++ b/src/org/jivesoftware/smack/util/Cache.java @@ -0,0 +1,678 @@ +/** + * $Revision: 1456 $ + * $Date: 2005-06-01 22:04:54 -0700 (Wed, 01 Jun 2005) $ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. 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.jivesoftware.smack.util; + +import org.jivesoftware.smack.util.collections.AbstractMapEntry; + +import java.util.*; + +/** + * A specialized Map that is size-limited (using an LRU algorithm) and + * has an optional expiration time for cache items. The Map is thread-safe.<p> + * + * The algorithm for cache is as follows: a HashMap is maintained for fast + * object lookup. Two linked lists are maintained: one keeps objects in the + * order they are accessed from cache, the other keeps objects in the order + * they were originally added to cache. When objects are added to cache, they + * are first wrapped by a CacheObject which maintains the following pieces + * of information:<ul> + * <li> A pointer to the node in the linked list that maintains accessed + * order for the object. Keeping a reference to the node lets us avoid + * linear scans of the linked list. + * <li> A pointer to the node in the linked list that maintains the age + * of the object in cache. Keeping a reference to the node lets us avoid + * linear scans of the linked list.</ul> + * <p/> + * To get an object from cache, a hash lookup is performed to get a reference + * to the CacheObject that wraps the real object we are looking for. + * The object is subsequently moved to the front of the accessed linked list + * and any necessary cache cleanups are performed. Cache deletion and expiration + * is performed as needed. + * + * @author Matt Tucker + */ +public class Cache<K, V> implements Map<K, V> { + + /** + * The map the keys and values are stored in. + */ + protected Map<K, CacheObject<V>> map; + + /** + * Linked list to maintain order that cache objects are accessed + * in, most used to least used. + */ + protected LinkedList lastAccessedList; + + /** + * Linked list to maintain time that cache objects were initially added + * to the cache, most recently added to oldest added. + */ + protected LinkedList ageList; + + /** + * Maximum number of items the cache will hold. + */ + protected int maxCacheSize; + + /** + * Maximum length of time objects can exist in cache before expiring. + */ + protected long maxLifetime; + + /** + * Maintain the number of cache hits and misses. A cache hit occurs every + * time the get method is called and the cache contains the requested + * object. A cache miss represents the opposite occurence.<p> + * + * Keeping track of cache hits and misses lets one measure how efficient + * the cache is; the higher the percentage of hits, the more efficient. + */ + protected long cacheHits, cacheMisses = 0L; + + /** + * Create a new cache and specify the maximum size of for the cache in + * bytes, and the maximum lifetime of objects. + * + * @param maxSize the maximum number of objects the cache will hold. -1 + * means the cache has no max size. + * @param maxLifetime the maximum amount of time (in ms) objects can exist in + * cache before being deleted. -1 means objects never expire. + */ + public Cache(int maxSize, long maxLifetime) { + if (maxSize == 0) { + throw new IllegalArgumentException("Max cache size cannot be 0."); + } + this.maxCacheSize = maxSize; + this.maxLifetime = maxLifetime; + + // Our primary data structure is a hash map. The default capacity of 11 + // is too small in almost all cases, so we set it bigger. + map = new HashMap<K, CacheObject<V>>(103); + + lastAccessedList = new LinkedList(); + ageList = new LinkedList(); + } + + public synchronized V put(K key, V value) { + V oldValue = null; + // Delete an old entry if it exists. + if (map.containsKey(key)) { + oldValue = remove(key, true); + } + + CacheObject<V> cacheObject = new CacheObject<V>(value); + map.put(key, cacheObject); + // Make an entry into the cache order list. + // Store the cache order list entry so that we can get back to it + // during later lookups. + cacheObject.lastAccessedListNode = lastAccessedList.addFirst(key); + // Add the object to the age list + LinkedListNode ageNode = ageList.addFirst(key); + ageNode.timestamp = System.currentTimeMillis(); + cacheObject.ageListNode = ageNode; + + // If cache is too full, remove least used cache entries until it is not too full. + cullCache(); + + return oldValue; + } + + public synchronized V get(Object key) { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + CacheObject<V> cacheObject = map.get(key); + if (cacheObject == null) { + // The object didn't exist in cache, so increment cache misses. + cacheMisses++; + return null; + } + // Remove the object from it's current place in the cache order list, + // and re-insert it at the front of the list. + cacheObject.lastAccessedListNode.remove(); + lastAccessedList.addFirst(cacheObject.lastAccessedListNode); + + // The object exists in cache, so increment cache hits. Also, increment + // the object's read count. + cacheHits++; + cacheObject.readCount++; + + return cacheObject.object; + } + + public synchronized V remove(Object key) { + return remove(key, false); + } + + /* + * Remove operation with a flag so we can tell coherence if the remove was + * caused by cache internal processing such as eviction or loading + */ + public synchronized V remove(Object key, boolean internal) { + //noinspection SuspiciousMethodCalls + CacheObject<V> cacheObject = map.remove(key); + // If the object is not in cache, stop trying to remove it. + if (cacheObject == null) { + return null; + } + // Remove from the cache order list + cacheObject.lastAccessedListNode.remove(); + cacheObject.ageListNode.remove(); + // Remove references to linked list nodes + cacheObject.ageListNode = null; + cacheObject.lastAccessedListNode = null; + + return cacheObject.object; + } + + public synchronized void clear() { + Object[] keys = map.keySet().toArray(); + for (Object key : keys) { + remove(key); + } + + // Now, reset all containers. + map.clear(); + lastAccessedList.clear(); + ageList.clear(); + + cacheHits = 0; + cacheMisses = 0; + } + + public synchronized int size() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return map.size(); + } + + public synchronized boolean isEmpty() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return map.isEmpty(); + } + + public synchronized Collection<V> values() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return Collections.unmodifiableCollection(new AbstractCollection<V>() { + Collection<CacheObject<V>> values = map.values(); + public Iterator<V> iterator() { + return new Iterator<V>() { + Iterator<CacheObject<V>> it = values.iterator(); + + public boolean hasNext() { + return it.hasNext(); + } + + public V next() { + return it.next().object; + } + + public void remove() { + it.remove(); + } + }; + } + + public int size() { + return values.size(); + } + }); + } + + public synchronized boolean containsKey(Object key) { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return map.containsKey(key); + } + + public void putAll(Map<? extends K, ? extends V> map) { + for (Entry<? extends K, ? extends V> entry : map.entrySet()) { + V value = entry.getValue(); + // If the map is another DefaultCache instance than the + // entry values will be CacheObject instances that need + // to be converted to the normal object form. + if (value instanceof CacheObject) { + //noinspection unchecked + value = ((CacheObject<V>) value).object; + } + put(entry.getKey(), value); + } + } + + public synchronized boolean containsValue(Object value) { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + //noinspection unchecked + CacheObject<V> cacheObject = new CacheObject<V>((V) value); + + return map.containsValue(cacheObject); + } + + public synchronized Set<Map.Entry<K, V>> entrySet() { + // Warning -- this method returns CacheObject instances and not Objects + // in the same form they were put into cache. + + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return new AbstractSet<Map.Entry<K, V>>() { + private final Set<Map.Entry<K, CacheObject<V>>> set = map.entrySet(); + + public Iterator<Entry<K, V>> iterator() { + return new Iterator<Entry<K, V>>() { + private final Iterator<Entry<K, CacheObject<V>>> it = set.iterator(); + public boolean hasNext() { + return it.hasNext(); + } + + public Entry<K, V> next() { + Map.Entry<K, CacheObject<V>> entry = it.next(); + return new AbstractMapEntry<K, V>(entry.getKey(), entry.getValue().object) { + @Override + public V setValue(V value) { + throw new UnsupportedOperationException("Cannot set"); + } + }; + } + + public void remove() { + it.remove(); + } + }; + + } + + public int size() { + return set.size(); + } + }; + } + + public synchronized Set<K> keySet() { + // First, clear all entries that have been in cache longer than the + // maximum defined age. + deleteExpiredEntries(); + + return Collections.unmodifiableSet(map.keySet()); + } + + public long getCacheHits() { + return cacheHits; + } + + public long getCacheMisses() { + return cacheMisses; + } + + public int getMaxCacheSize() { + return maxCacheSize; + } + + public synchronized void setMaxCacheSize(int maxCacheSize) { + this.maxCacheSize = maxCacheSize; + // It's possible that the new max size is smaller than our current cache + // size. If so, we need to delete infrequently used items. + cullCache(); + } + + public long getMaxLifetime() { + return maxLifetime; + } + + public void setMaxLifetime(long maxLifetime) { + this.maxLifetime = maxLifetime; + } + + /** + * Clears all entries out of cache where the entries are older than the + * maximum defined age. + */ + protected synchronized void deleteExpiredEntries() { + // Check if expiration is turned on. + if (maxLifetime <= 0) { + return; + } + + // Remove all old entries. To do this, we remove objects from the end + // of the linked list until they are no longer too old. We get to avoid + // any hash lookups or looking at any more objects than is strictly + // neccessary. + LinkedListNode node = ageList.getLast(); + // If there are no entries in the age list, return. + if (node == null) { + return; + } + + // Determine the expireTime, which is the moment in time that elements + // should expire from cache. Then, we can do an easy check to see + // if the expire time is greater than the expire time. + long expireTime = System.currentTimeMillis() - maxLifetime; + + while (expireTime > node.timestamp) { + if (remove(node.object, true) == null) { + System.err.println("Error attempting to remove(" + node.object.toString() + + ") - cacheObject not found in cache!"); + // remove from the ageList + node.remove(); + } + + // Get the next node. + node = ageList.getLast(); + // If there are no more entries in the age list, return. + if (node == null) { + return; + } + } + } + + /** + * Removes the least recently used elements if the cache size is greater than + * or equal to the maximum allowed size until the cache is at least 10% empty. + */ + protected synchronized void cullCache() { + // Check if a max cache size is defined. + if (maxCacheSize < 0) { + return; + } + + // See if the cache is too big. If so, clean out cache until it's 10% free. + if (map.size() > maxCacheSize) { + // First, delete any old entries to see how much memory that frees. + deleteExpiredEntries(); + // Next, delete the least recently used elements until 10% of the cache + // has been freed. + int desiredSize = (int) (maxCacheSize * .90); + for (int i=map.size(); i>desiredSize; i--) { + // Get the key and invoke the remove method on it. + if (remove(lastAccessedList.getLast().object, true) == null) { + System.err.println("Error attempting to cullCache with remove(" + + lastAccessedList.getLast().object.toString() + ") - " + + "cacheObject not found in cache!"); + lastAccessedList.getLast().remove(); + } + } + } + } + + /** + * Wrapper for all objects put into cache. It's primary purpose is to maintain + * references to the linked lists that maintain the creation time of the object + * and the ordering of the most used objects. + * + * This class is optimized for speed rather than strictly correct encapsulation. + */ + private static class CacheObject<V> { + + /** + * Underlying object wrapped by the CacheObject. + */ + public V object; + + /** + * A reference to the node in the cache order list. We keep the reference + * here to avoid linear scans of the list. Every time the object is + * accessed, the node is removed from its current spot in the list and + * moved to the front. + */ + public LinkedListNode lastAccessedListNode; + + /** + * A reference to the node in the age order list. We keep the reference + * here to avoid linear scans of the list. The reference is used if the + * object has to be deleted from the list. + */ + public LinkedListNode ageListNode; + + /** + * A count of the number of times the object has been read from cache. + */ + public int readCount = 0; + + /** + * Creates a new cache object wrapper. + * + * @param object the underlying Object to wrap. + */ + public CacheObject(V object) { + this.object = object; + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof CacheObject)) { + return false; + } + + final CacheObject<?> cacheObject = (CacheObject<?>) o; + + return object.equals(cacheObject.object); + + } + + public int hashCode() { + return object.hashCode(); + } + } + + /** + * Simple LinkedList implementation. The main feature is that list nodes + * are public, which allows very fast delete operations when one has a + * reference to the node that is to be deleted.<p> + */ + private static class LinkedList { + + /** + * The root of the list keeps a reference to both the first and last + * elements of the list. + */ + private LinkedListNode head = new LinkedListNode("head", null, null); + + /** + * Creates a new linked list. + */ + public LinkedList() { + head.next = head.previous = head; + } + + /** + * Returns the first linked list node in the list. + * + * @return the first element of the list. + */ + public LinkedListNode getFirst() { + LinkedListNode node = head.next; + if (node == head) { + return null; + } + return node; + } + + /** + * Returns the last linked list node in the list. + * + * @return the last element of the list. + */ + public LinkedListNode getLast() { + LinkedListNode node = head.previous; + if (node == head) { + return null; + } + return node; + } + + /** + * Adds a node to the beginning of the list. + * + * @param node the node to add to the beginning of the list. + * @return the node + */ + public LinkedListNode addFirst(LinkedListNode node) { + node.next = head.next; + node.previous = head; + node.previous.next = node; + node.next.previous = node; + return node; + } + + /** + * Adds an object to the beginning of the list by automatically creating a + * a new node and adding it to the beginning of the list. + * + * @param object the object to add to the beginning of the list. + * @return the node created to wrap the object. + */ + public LinkedListNode addFirst(Object object) { + LinkedListNode node = new LinkedListNode(object, head.next, head); + node.previous.next = node; + node.next.previous = node; + return node; + } + + /** + * Adds an object to the end of the list by automatically creating a + * a new node and adding it to the end of the list. + * + * @param object the object to add to the end of the list. + * @return the node created to wrap the object. + */ + public LinkedListNode addLast(Object object) { + LinkedListNode node = new LinkedListNode(object, head, head.previous); + node.previous.next = node; + node.next.previous = node; + return node; + } + + /** + * Erases all elements in the list and re-initializes it. + */ + public void clear() { + //Remove all references in the list. + LinkedListNode node = getLast(); + while (node != null) { + node.remove(); + node = getLast(); + } + + //Re-initialize. + head.next = head.previous = head; + } + + /** + * Returns a String representation of the linked list with a comma + * delimited list of all the elements in the list. + * + * @return a String representation of the LinkedList. + */ + public String toString() { + LinkedListNode node = head.next; + StringBuilder buf = new StringBuilder(); + while (node != head) { + buf.append(node.toString()).append(", "); + node = node.next; + } + return buf.toString(); + } + } + + /** + * Doubly linked node in a LinkedList. Most LinkedList implementations keep the + * equivalent of this class private. We make it public so that references + * to each node in the list can be maintained externally. + * + * Exposing this class lets us make remove operations very fast. Remove is + * built into this class and only requires two reference reassignments. If + * remove existed in the main LinkedList class, a linear scan would have to + * be performed to find the correct node to delete. + * + * The linked list implementation was specifically written for the Jive + * cache system. While it can be used as a general purpose linked list, for + * most applications, it is more suitable to use the linked list that is part + * of the Java Collections package. + */ + private static class LinkedListNode { + + public LinkedListNode previous; + public LinkedListNode next; + public Object object; + + /** + * This class is further customized for the Jive cache system. It + * maintains a timestamp of when a Cacheable object was first added to + * cache. Timestamps are stored as long values and represent the number + * of milliseconds passed since January 1, 1970 00:00:00.000 GMT.<p> + * + * The creation timestamp is used in the case that the cache has a + * maximum lifetime set. In that case, when + * [current time] - [creation time] > [max lifetime], the object will be + * deleted from cache. + */ + public long timestamp; + + /** + * Constructs a new linked list node. + * + * @param object the Object that the node represents. + * @param next a reference to the next LinkedListNode in the list. + * @param previous a reference to the previous LinkedListNode in the list. + */ + public LinkedListNode(Object object, LinkedListNode next, + LinkedListNode previous) + { + this.object = object; + this.next = next; + this.previous = previous; + } + + /** + * Removes this node from the linked list that it is a part of. + */ + public void remove() { + previous.next = next; + next.previous = previous; + } + + /** + * Returns a String representation of the linked list node by calling the + * toString method of the node's object. + * + * @return a String representation of the LinkedListNode. + */ + public String toString() { + return object.toString(); + } + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smack/util/DNSUtil.java b/src/org/jivesoftware/smack/util/DNSUtil.java new file mode 100644 index 0000000..628d8e8 --- /dev/null +++ b/src/org/jivesoftware/smack/util/DNSUtil.java @@ -0,0 +1,229 @@ +/** + * $Revision: 1456 $ + * $Date: 2005-06-01 22:04:54 -0700 (Wed, 01 Jun 2005) $ + * + * Copyright 2003-2005 Jive Software. + * + * All rights reserved. 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.jivesoftware.smack.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; +import org.jivesoftware.smack.util.dns.SRVRecord; + +/** + * Utility class to perform DNS lookups for XMPP services. + * + * @author Matt Tucker + */ +public class DNSUtil { + + /** + * Create a cache to hold the 100 most recently accessed DNS lookups for a period of + * 10 minutes. + */ + private static Map<String, List<HostAddress>> cache = new Cache<String, List<HostAddress>>(100, 1000*60*10); + + private static DNSResolver dnsResolver = null; + + /** + * Set the DNS resolver that should be used to perform DNS lookups. + * + * @param resolver + */ + public static void setDNSResolver(DNSResolver resolver) { + dnsResolver = resolver; + } + + /** + * Returns the current DNS resolved used to perform DNS lookups. + * + * @return + */ + public static DNSResolver getDNSResolver() { + return dnsResolver; + } + + /** + * Returns a list of HostAddresses under which the specified XMPP server can be + * reached at for client-to-server communication. A DNS lookup for a SRV + * record in the form "_xmpp-client._tcp.example.com" is attempted, according + * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form + * of "_jabber._tcp.example.com" is attempted since servers that implement an + * older version of the protocol may be listed using that notation. If that + * lookup fails as well, it's assumed that the XMPP server lives at the + * host resolved by a DNS lookup at the specified domain on the default port + * of 5222.<p> + * + * As an example, a lookup for "example.com" may return "im.example.com:5269". + * + * @param domain the domain. + * @return List of HostAddress, which encompasses the hostname and port that the + * XMPP server can be reached at for the specified domain. + */ + public static List<HostAddress> resolveXMPPDomain(String domain) { + return resolveDomain(domain, 'c'); + } + + /** + * Returns a list of HostAddresses under which the specified XMPP server can be + * reached at for server-to-server communication. A DNS lookup for a SRV + * record in the form "_xmpp-server._tcp.example.com" is attempted, according + * to section 14.4 of RFC 3920. If that lookup fails, a lookup in the older form + * of "_jabber._tcp.example.com" is attempted since servers that implement an + * older version of the protocol may be listed using that notation. If that + * lookup fails as well, it's assumed that the XMPP server lives at the + * host resolved by a DNS lookup at the specified domain on the default port + * of 5269.<p> + * + * As an example, a lookup for "example.com" may return "im.example.com:5269". + * + * @param domain the domain. + * @return List of HostAddress, which encompasses the hostname and port that the + * XMPP server can be reached at for the specified domain. + */ + public static List<HostAddress> resolveXMPPServerDomain(String domain) { + return resolveDomain(domain, 's'); + } + + private static List<HostAddress> resolveDomain(String domain, char keyPrefix) { + // Prefix the key with 's' to distinguish him from the client domain lookups + String key = keyPrefix + domain; + // Return item from cache if it exists. + if (cache.containsKey(key)) { + List<HostAddress> addresses = cache.get(key); + if (addresses != null) { + return addresses; + } + } + + if (dnsResolver == null) + throw new IllegalStateException("No DNS resolver active."); + + List<HostAddress> addresses = new ArrayList<HostAddress>(); + + // Step one: Do SRV lookups + String srvDomain; + if (keyPrefix == 's') { + srvDomain = "_xmpp-server._tcp." + domain; + } else if (keyPrefix == 'c') { + srvDomain = "_xmpp-client._tcp." + domain; + } else { + srvDomain = domain; + } + List<SRVRecord> srvRecords = dnsResolver.lookupSRVRecords(srvDomain); + List<HostAddress> sortedRecords = sortSRVRecords(srvRecords); + if (sortedRecords != null) + addresses.addAll(sortedRecords); + + // Step two: Add the hostname to the end of the list + addresses.add(new HostAddress(domain)); + + // Add item to cache. + cache.put(key, addresses); + + return addresses; + } + + /** + * Sort a given list of SRVRecords as described in RFC 2782 + * Note that we follow the RFC with one exception. In a group of the same priority, only the first entry + * is calculated by random. The others are ore simply ordered by their priority. + * + * @param records + * @return + */ + protected static List<HostAddress> sortSRVRecords(List<SRVRecord> records) { + // RFC 2782, Usage rules: "If there is precisely one SRV RR, and its Target is "." + // (the root domain), abort." + if (records.size() == 1 && records.get(0).getFQDN().equals(".")) + return null; + + // sorting the records improves the performance of the bisection later + Collections.sort(records); + + // create the priority buckets + SortedMap<Integer, List<SRVRecord>> buckets = new TreeMap<Integer, List<SRVRecord>>(); + for (SRVRecord r : records) { + Integer priority = r.getPriority(); + List<SRVRecord> bucket = buckets.get(priority); + // create the list of SRVRecords if it doesn't exist + if (bucket == null) { + bucket = new LinkedList<SRVRecord>(); + buckets.put(priority, bucket); + } + bucket.add(r); + } + + List<HostAddress> res = new ArrayList<HostAddress>(records.size()); + + for (Integer priority : buckets.keySet()) { + List<SRVRecord> bucket = buckets.get(priority); + int bucketSize; + while ((bucketSize = bucket.size()) > 0) { + int[] totals = new int[bucket.size()]; + int running_total = 0; + int count = 0; + int zeroWeight = 1; + + for (SRVRecord r : bucket) { + if (r.getWeight() > 0) + zeroWeight = 0; + } + + for (SRVRecord r : bucket) { + running_total += (r.getWeight() + zeroWeight); + totals[count] = running_total; + count++; + } + int selectedPos; + if (running_total == 0) { + // If running total is 0, then all weights in this priority + // group are 0. So we simply select one of the weights randomly + // as the other 'normal' algorithm is unable to handle this case + selectedPos = (int) (Math.random() * bucketSize); + } else { + double rnd = Math.random() * running_total; + selectedPos = bisect(totals, rnd); + } + // add the SRVRecord that was randomly chosen on it's weight + // to the start of the result list + SRVRecord chosenSRVRecord = bucket.remove(selectedPos); + res.add(chosenSRVRecord); + } + } + + return res; + } + + // TODO this is not yet really bisection just a stupid linear search + private static int bisect(int[] array, double value) { + int pos = 0; + for (int element : array) { + if (value < element) + break; + pos++; + } + return pos; + } +}
\ No newline at end of file diff --git a/src/org/jivesoftware/smack/util/DateFormatType.java b/src/org/jivesoftware/smack/util/DateFormatType.java new file mode 100644 index 0000000..9253038 --- /dev/null +++ b/src/org/jivesoftware/smack/util/DateFormatType.java @@ -0,0 +1,65 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2013 Robin Collier. + * + * All rights reserved. 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.jivesoftware.smack.util; + +import java.text.SimpleDateFormat; + +/** + * Defines the various date and time profiles used in XMPP along with their associated formats. + * + * @author Robin Collier + * + */ +public enum DateFormatType { + // @formatter:off + XEP_0082_DATE_PROFILE("yyyy-MM-dd"), + XEP_0082_DATETIME_PROFILE("yyyy-MM-dd'T'HH:mm:ssZ"), + XEP_0082_DATETIME_MILLIS_PROFILE("yyyy-MM-dd'T'HH:mm:ss.SSSZ"), + XEP_0082_TIME_PROFILE("hh:mm:ss"), + XEP_0082_TIME_ZONE_PROFILE("hh:mm:ssZ"), + XEP_0082_TIME_MILLIS_PROFILE("hh:mm:ss.SSS"), + XEP_0082_TIME_MILLIS_ZONE_PROFILE("hh:mm:ss.SSSZ"), + XEP_0091_DATETIME("yyyyMMdd'T'HH:mm:ss"); + // @formatter:on + + private String formatString; + + private DateFormatType(String dateFormat) { + formatString = dateFormat; + } + + /** + * Get the format string as defined in either XEP-0082 or XEP-0091. + * + * @return The defined string format for the date. + */ + public String getFormatString() { + return formatString; + } + + /** + * Create a {@link SimpleDateFormat} object with the format defined by {@link #getFormatString()}. + * + * @return A new date formatter. + */ + public SimpleDateFormat createFormatter() { + return new SimpleDateFormat(getFormatString()); + } +} diff --git a/src/org/jivesoftware/smack/util/ObservableReader.java b/src/org/jivesoftware/smack/util/ObservableReader.java new file mode 100644 index 0000000..8c64508 --- /dev/null +++ b/src/org/jivesoftware/smack/util/ObservableReader.java @@ -0,0 +1,118 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2007 Jive Software. + * + * All rights reserved. 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.jivesoftware.smack.util; + +import java.io.*; +import java.util.*; + +/** + * An ObservableReader is a wrapper on a Reader that notifies to its listeners when + * reading character streams. + * + * @author Gaston Dombiak + */ +public class ObservableReader extends Reader { + + Reader wrappedReader = null; + List<ReaderListener> listeners = new ArrayList<ReaderListener>(); + + public ObservableReader(Reader wrappedReader) { + this.wrappedReader = wrappedReader; + } + + public int read(char[] cbuf, int off, int len) throws IOException { + int count = wrappedReader.read(cbuf, off, len); + if (count > 0) { + String str = new String(cbuf, off, count); + // Notify that a new string has been read + ReaderListener[] readerListeners = null; + synchronized (listeners) { + readerListeners = new ReaderListener[listeners.size()]; + listeners.toArray(readerListeners); + } + for (int i = 0; i < readerListeners.length; i++) { + readerListeners[i].read(str); + } + } + return count; + } + + public void close() throws IOException { + wrappedReader.close(); + } + + public int read() throws IOException { + return wrappedReader.read(); + } + + public int read(char cbuf[]) throws IOException { + return wrappedReader.read(cbuf); + } + + public long skip(long n) throws IOException { + return wrappedReader.skip(n); + } + + public boolean ready() throws IOException { + return wrappedReader.ready(); + } + + public boolean markSupported() { + return wrappedReader.markSupported(); + } + + public void mark(int readAheadLimit) throws IOException { + wrappedReader.mark(readAheadLimit); + } + + public void reset() throws IOException { + wrappedReader.reset(); + } + + /** + * Adds a reader listener to this reader that will be notified when + * new strings are read. + * + * @param readerListener a reader listener. + */ + public void addReaderListener(ReaderListener readerListener) { + if (readerListener == null) { + return; + } + synchronized (listeners) { + if (!listeners.contains(readerListener)) { + listeners.add(readerListener); + } + } + } + + /** + * Removes a reader listener from this reader. + * + * @param readerListener a reader listener. + */ + public void removeReaderListener(ReaderListener readerListener) { + synchronized (listeners) { + listeners.remove(readerListener); + } + } + +} diff --git a/src/org/jivesoftware/smack/util/ObservableWriter.java b/src/org/jivesoftware/smack/util/ObservableWriter.java new file mode 100644 index 0000000..90cabb6 --- /dev/null +++ b/src/org/jivesoftware/smack/util/ObservableWriter.java @@ -0,0 +1,120 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2007 Jive Software. + * + * All rights reserved. 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.jivesoftware.smack.util; + +import java.io.*; +import java.util.*; + +/** + * An ObservableWriter is a wrapper on a Writer that notifies to its listeners when + * writing to character streams. + * + * @author Gaston Dombiak + */ +public class ObservableWriter extends Writer { + + Writer wrappedWriter = null; + List<WriterListener> listeners = new ArrayList<WriterListener>(); + + public ObservableWriter(Writer wrappedWriter) { + this.wrappedWriter = wrappedWriter; + } + + public void write(char cbuf[], int off, int len) throws IOException { + wrappedWriter.write(cbuf, off, len); + String str = new String(cbuf, off, len); + notifyListeners(str); + } + + public void flush() throws IOException { + wrappedWriter.flush(); + } + + public void close() throws IOException { + wrappedWriter.close(); + } + + public void write(int c) throws IOException { + wrappedWriter.write(c); + } + + public void write(char cbuf[]) throws IOException { + wrappedWriter.write(cbuf); + String str = new String(cbuf); + notifyListeners(str); + } + + public void write(String str) throws IOException { + wrappedWriter.write(str); + notifyListeners(str); + } + + public void write(String str, int off, int len) throws IOException { + wrappedWriter.write(str, off, len); + str = str.substring(off, off + len); + notifyListeners(str); + } + + /** + * Notify that a new string has been written. + * + * @param str the written String to notify + */ + private void notifyListeners(String str) { + WriterListener[] writerListeners = null; + synchronized (listeners) { + writerListeners = new WriterListener[listeners.size()]; + listeners.toArray(writerListeners); + } + for (int i = 0; i < writerListeners.length; i++) { + writerListeners[i].write(str); + } + } + + /** + * Adds a writer listener to this writer that will be notified when + * new strings are sent. + * + * @param writerListener a writer listener. + */ + public void addWriterListener(WriterListener writerListener) { + if (writerListener == null) { + return; + } + synchronized (listeners) { + if (!listeners.contains(writerListener)) { + listeners.add(writerListener); + } + } + } + + /** + * Removes a writer listener from this writer. + * + * @param writerListener a writer listener. + */ + public void removeWriterListener(WriterListener writerListener) { + synchronized (listeners) { + listeners.remove(writerListener); + } + } + +} diff --git a/src/org/jivesoftware/smack/util/PacketParserUtils.java b/src/org/jivesoftware/smack/util/PacketParserUtils.java new file mode 100644 index 0000000..aacbad5 --- /dev/null +++ b/src/org/jivesoftware/smack/util/PacketParserUtils.java @@ -0,0 +1,925 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2007 Jive Software. + * + * All rights reserved. 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.jivesoftware.smack.util; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.packet.Authentication; +import org.jivesoftware.smack.packet.Bind; +import org.jivesoftware.smack.packet.DefaultPacketExtension; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.Registration; +import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.packet.StreamError; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smack.sasl.SASLMechanism.Failure; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Utility class that helps to parse packets. Any parsing packets method that must be shared + * between many clients must be placed in this utility class. + * + * @author Gaston Dombiak + */ +public class PacketParserUtils { + + /** + * Namespace used to store packet properties. + */ + private static final String PROPERTIES_NAMESPACE = + "http://www.jivesoftware.com/xmlns/xmpp/properties"; + + /** + * Parses a message packet. + * + * @param parser the XML parser, positioned at the start of a message packet. + * @return a Message packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static Packet parseMessage(XmlPullParser parser) throws Exception { + Message message = new Message(); + String id = parser.getAttributeValue("", "id"); + message.setPacketID(id == null ? Packet.ID_NOT_AVAILABLE : id); + message.setTo(parser.getAttributeValue("", "to")); + message.setFrom(parser.getAttributeValue("", "from")); + message.setType(Message.Type.fromString(parser.getAttributeValue("", "type"))); + String language = getLanguageAttribute(parser); + + // determine message's default language + String defaultLanguage = null; + if (language != null && !"".equals(language.trim())) { + message.setLanguage(language); + defaultLanguage = language; + } + else { + defaultLanguage = Packet.getDefaultLanguage(); + } + + // Parse sub-elements. We include extra logic to make sure the values + // are only read once. This is because it's possible for the names to appear + // in arbitrary sub-elements. + boolean done = false; + String thread = null; + Map<String, Object> properties = null; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (elementName.equals("subject")) { + String xmlLang = getLanguageAttribute(parser); + if (xmlLang == null) { + xmlLang = defaultLanguage; + } + + String subject = parseContent(parser); + + if (message.getSubject(xmlLang) == null) { + message.addSubject(xmlLang, subject); + } + } + else if (elementName.equals("body")) { + String xmlLang = getLanguageAttribute(parser); + if (xmlLang == null) { + xmlLang = defaultLanguage; + } + + String body = parseContent(parser); + + if (message.getBody(xmlLang) == null) { + message.addBody(xmlLang, body); + } + } + else if (elementName.equals("thread")) { + if (thread == null) { + thread = parser.nextText(); + } + } + else if (elementName.equals("error")) { + message.setError(parseError(parser)); + } + else if (elementName.equals("properties") && + namespace.equals(PROPERTIES_NAMESPACE)) + { + properties = parseProperties(parser); + } + // Otherwise, it must be a packet extension. + else { + message.addExtension( + PacketParserUtils.parsePacketExtension(elementName, namespace, parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("message")) { + done = true; + } + } + } + + message.setThread(thread); + // Set packet properties. + if (properties != null) { + for (String name : properties.keySet()) { + message.setProperty(name, properties.get(name)); + } + } + return message; + } + + /** + * Returns the content of a tag as string regardless of any tags included. + * + * @param parser the XML pull parser + * @return the content of a tag as string + * @throws XmlPullParserException if parser encounters invalid XML + * @throws IOException if an IO error occurs + */ + private static String parseContent(XmlPullParser parser) + throws XmlPullParserException, IOException { + StringBuffer content = new StringBuffer(); + int parserDepth = parser.getDepth(); + while (!(parser.next() == XmlPullParser.END_TAG && parser + .getDepth() == parserDepth)) { + content.append(parser.getText()); + } + return content.toString(); + } + + /** + * Parses a presence packet. + * + * @param parser the XML parser, positioned at the start of a presence packet. + * @return a Presence packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static Presence parsePresence(XmlPullParser parser) throws Exception { + Presence.Type type = Presence.Type.available; + String typeString = parser.getAttributeValue("", "type"); + if (typeString != null && !typeString.equals("")) { + try { + type = Presence.Type.valueOf(typeString); + } + catch (IllegalArgumentException iae) { + System.err.println("Found invalid presence type " + typeString); + } + } + Presence presence = new Presence(type); + presence.setTo(parser.getAttributeValue("", "to")); + presence.setFrom(parser.getAttributeValue("", "from")); + String id = parser.getAttributeValue("", "id"); + presence.setPacketID(id == null ? Packet.ID_NOT_AVAILABLE : id); + + String language = getLanguageAttribute(parser); + if (language != null && !"".equals(language.trim())) { + presence.setLanguage(language); + } + presence.setPacketID(id == null ? Packet.ID_NOT_AVAILABLE : id); + + // Parse sub-elements + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (elementName.equals("status")) { + presence.setStatus(parser.nextText()); + } + else if (elementName.equals("priority")) { + try { + int priority = Integer.parseInt(parser.nextText()); + presence.setPriority(priority); + } + catch (NumberFormatException nfe) { + // Ignore. + } + catch (IllegalArgumentException iae) { + // Presence priority is out of range so assume priority to be zero + presence.setPriority(0); + } + } + else if (elementName.equals("show")) { + String modeText = parser.nextText(); + try { + presence.setMode(Presence.Mode.valueOf(modeText)); + } + catch (IllegalArgumentException iae) { + System.err.println("Found invalid presence mode " + modeText); + } + } + else if (elementName.equals("error")) { + presence.setError(parseError(parser)); + } + else if (elementName.equals("properties") && + namespace.equals(PROPERTIES_NAMESPACE)) + { + Map<String,Object> properties = parseProperties(parser); + // Set packet properties. + for (String name : properties.keySet()) { + presence.setProperty(name, properties.get(name)); + } + } + // Otherwise, it must be a packet extension. + else { + try { + presence.addExtension(PacketParserUtils.parsePacketExtension(elementName, namespace, parser)); + } + catch (Exception e) { + System.err.println("Failed to parse extension packet in Presence packet."); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("presence")) { + done = true; + } + } + } + return presence; + } + + /** + * Parses an IQ packet. + * + * @param parser the XML parser, positioned at the start of an IQ packet. + * @return an IQ object. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static IQ parseIQ(XmlPullParser parser, Connection connection) throws Exception { + IQ iqPacket = null; + + String id = parser.getAttributeValue("", "id"); + String to = parser.getAttributeValue("", "to"); + String from = parser.getAttributeValue("", "from"); + IQ.Type type = IQ.Type.fromString(parser.getAttributeValue("", "type")); + XMPPError error = null; + + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (elementName.equals("error")) { + error = PacketParserUtils.parseError(parser); + } + else if (elementName.equals("query") && namespace.equals("jabber:iq:auth")) { + iqPacket = parseAuthentication(parser); + } + else if (elementName.equals("query") && namespace.equals("jabber:iq:roster")) { + iqPacket = parseRoster(parser); + } + else if (elementName.equals("query") && namespace.equals("jabber:iq:register")) { + iqPacket = parseRegistration(parser); + } + else if (elementName.equals("bind") && + namespace.equals("urn:ietf:params:xml:ns:xmpp-bind")) { + iqPacket = parseResourceBinding(parser); + } + // Otherwise, see if there is a registered provider for + // this element name and namespace. + else { + Object provider = ProviderManager.getInstance().getIQProvider(elementName, namespace); + if (provider != null) { + if (provider instanceof IQProvider) { + iqPacket = ((IQProvider)provider).parseIQ(parser); + } + else if (provider instanceof Class) { + iqPacket = (IQ)PacketParserUtils.parseWithIntrospection(elementName, + (Class<?>)provider, parser); + } + } + // Only handle unknown IQs of type result. Types of 'get' and 'set' which are not understood + // have to be answered with an IQ error response. See the code a few lines below + else if (IQ.Type.RESULT == type){ + // No Provider found for the IQ stanza, parse it to an UnparsedIQ instance + // so that the content of the IQ can be examined later on + iqPacket = new UnparsedResultIQ(parseContent(parser)); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("iq")) { + done = true; + } + } + } + // Decide what to do when an IQ packet was not understood + if (iqPacket == null) { + if (IQ.Type.GET == type || IQ.Type.SET == type ) { + // If the IQ stanza is of type "get" or "set" containing a child element + // qualified by a namespace it does not understand, then answer an IQ of + // type "error" with code 501 ("feature-not-implemented") + iqPacket = new IQ() { + @Override + public String getChildElementXML() { + return null; + } + }; + iqPacket.setPacketID(id); + iqPacket.setTo(from); + iqPacket.setFrom(to); + iqPacket.setType(IQ.Type.ERROR); + iqPacket.setError(new XMPPError(XMPPError.Condition.feature_not_implemented)); + connection.sendPacket(iqPacket); + return null; + } + else { + // If an IQ packet wasn't created above, create an empty IQ packet. + iqPacket = new IQ() { + @Override + public String getChildElementXML() { + return null; + } + }; + } + } + + // Set basic values on the iq packet. + iqPacket.setPacketID(id); + iqPacket.setTo(to); + iqPacket.setFrom(from); + iqPacket.setType(type); + iqPacket.setError(error); + + return iqPacket; + } + + private static Authentication parseAuthentication(XmlPullParser parser) throws Exception { + Authentication authentication = new Authentication(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("username")) { + authentication.setUsername(parser.nextText()); + } + else if (parser.getName().equals("password")) { + authentication.setPassword(parser.nextText()); + } + else if (parser.getName().equals("digest")) { + authentication.setDigest(parser.nextText()); + } + else if (parser.getName().equals("resource")) { + authentication.setResource(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + return authentication; + } + + private static RosterPacket parseRoster(XmlPullParser parser) throws Exception { + RosterPacket roster = new RosterPacket(); + boolean done = false; + RosterPacket.Item item = null; + while (!done) { + if(parser.getEventType()==XmlPullParser.START_TAG && + parser.getName().equals("query")){ + String version = parser.getAttributeValue(null, "ver"); + roster.setVersion(version); + } + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + String jid = parser.getAttributeValue("", "jid"); + String name = parser.getAttributeValue("", "name"); + // Create packet. + item = new RosterPacket.Item(jid, name); + // Set status. + String ask = parser.getAttributeValue("", "ask"); + RosterPacket.ItemStatus status = RosterPacket.ItemStatus.fromString(ask); + item.setItemStatus(status); + // Set type. + String subscription = parser.getAttributeValue("", "subscription"); + RosterPacket.ItemType type = RosterPacket.ItemType.valueOf(subscription != null ? subscription : "none"); + item.setItemType(type); + } + if (parser.getName().equals("group") && item!= null) { + final String groupName = parser.nextText(); + if (groupName != null && groupName.trim().length() > 0) { + item.addGroupName(groupName); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + roster.addRosterItem(item); + } + if (parser.getName().equals("query")) { + done = true; + } + } + } + return roster; + } + + private static Registration parseRegistration(XmlPullParser parser) throws Exception { + Registration registration = new Registration(); + Map<String, String> fields = null; + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + // Any element that's in the jabber:iq:register namespace, + // attempt to parse it if it's in the form <name>value</name>. + if (parser.getNamespace().equals("jabber:iq:register")) { + String name = parser.getName(); + String value = ""; + if (fields == null) { + fields = new HashMap<String, String>(); + } + + if (parser.next() == XmlPullParser.TEXT) { + value = parser.getText(); + } + // Ignore instructions, but anything else should be added to the map. + if (!name.equals("instructions")) { + fields.put(name, value); + } + else { + registration.setInstructions(value); + } + } + // Otherwise, it must be a packet extension. + else { + registration.addExtension( + PacketParserUtils.parsePacketExtension( + parser.getName(), + parser.getNamespace(), + parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + registration.setAttributes(fields); + return registration; + } + + private static Bind parseResourceBinding(XmlPullParser parser) throws IOException, + XmlPullParserException { + Bind bind = new Bind(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("resource")) { + bind.setResource(parser.nextText()); + } + else if (parser.getName().equals("jid")) { + bind.setJid(parser.nextText()); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("bind")) { + done = true; + } + } + } + + return bind; + } + + /** + * Parse the available SASL mechanisms reported from the server. + * + * @param parser the XML parser, positioned at the start of the mechanisms stanza. + * @return a collection of Stings with the mechanisms included in the mechanisms stanza. + * @throws Exception if an exception occurs while parsing the stanza. + */ + public static Collection<String> parseMechanisms(XmlPullParser parser) throws Exception { + List<String> mechanisms = new ArrayList<String>(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + if (elementName.equals("mechanism")) { + mechanisms.add(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("mechanisms")) { + done = true; + } + } + } + return mechanisms; + } + + /** + * Parse the available compression methods reported from the server. + * + * @param parser the XML parser, positioned at the start of the compression stanza. + * @return a collection of Stings with the methods included in the compression stanza. + * @throws Exception if an exception occurs while parsing the stanza. + */ + public static Collection<String> parseCompressionMethods(XmlPullParser parser) + throws IOException, XmlPullParserException { + List<String> methods = new ArrayList<String>(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + if (elementName.equals("method")) { + methods.add(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("compression")) { + done = true; + } + } + } + return methods; + } + + /** + * Parse a properties sub-packet. If any errors occur while de-serializing Java object + * properties, an exception will be printed and not thrown since a thrown + * exception will shut down the entire connection. ClassCastExceptions will occur + * when both the sender and receiver of the packet don't have identical versions + * of the same class. + * + * @param parser the XML parser, positioned at the start of a properties sub-packet. + * @return a map of the properties. + * @throws Exception if an error occurs while parsing the properties. + */ + public static Map<String, Object> parseProperties(XmlPullParser parser) throws Exception { + Map<String, Object> properties = new HashMap<String, Object>(); + while (true) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && parser.getName().equals("property")) { + // Parse a property + boolean done = false; + String name = null; + String type = null; + String valueText = null; + Object value = null; + while (!done) { + eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + if (elementName.equals("name")) { + name = parser.nextText(); + } + else if (elementName.equals("value")) { + type = parser.getAttributeValue("", "type"); + valueText = parser.nextText(); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("property")) { + if ("integer".equals(type)) { + value = Integer.valueOf(valueText); + } + else if ("long".equals(type)) { + value = Long.valueOf(valueText); + } + else if ("float".equals(type)) { + value = Float.valueOf(valueText); + } + else if ("double".equals(type)) { + value = Double.valueOf(valueText); + } + else if ("boolean".equals(type)) { + value = Boolean.valueOf(valueText); + } + else if ("string".equals(type)) { + value = valueText; + } + else if ("java-object".equals(type)) { + try { + byte [] bytes = StringUtils.decodeBase64(valueText); + ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes)); + value = in.readObject(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + if (name != null && value != null) { + properties.put(name, value); + } + done = true; + } + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("properties")) { + break; + } + } + } + return properties; + } + + /** + * Parses SASL authentication error packets. + * + * @param parser the XML parser. + * @return a SASL Failure packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static Failure parseSASLFailure(XmlPullParser parser) throws Exception { + String condition = null; + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + if (!parser.getName().equals("failure")) { + condition = parser.getName(); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("failure")) { + done = true; + } + } + } + return new Failure(condition); + } + + /** + * Parses stream error packets. + * + * @param parser the XML parser. + * @return an stream error packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static StreamError parseStreamError(XmlPullParser parser) throws IOException, + XmlPullParserException { + StreamError streamError = null; + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + streamError = new StreamError(parser.getName()); + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("error")) { + done = true; + } + } + } + return streamError; +} + + /** + * Parses error sub-packets. + * + * @param parser the XML parser. + * @return an error sub-packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static XMPPError parseError(XmlPullParser parser) throws Exception { + final String errorNamespace = "urn:ietf:params:xml:ns:xmpp-stanzas"; + String errorCode = "-1"; + String type = null; + String message = null; + String condition = null; + List<PacketExtension> extensions = new ArrayList<PacketExtension>(); + + // Parse the error header + for (int i=0; i<parser.getAttributeCount(); i++) { + if (parser.getAttributeName(i).equals("code")) { + errorCode = parser.getAttributeValue("", "code"); + } + if (parser.getAttributeName(i).equals("type")) { + type = parser.getAttributeValue("", "type"); + } + } + boolean done = false; + // Parse the text and condition tags + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("text")) { + message = parser.nextText(); + } + else { + // Condition tag, it can be xmpp error or an application defined error. + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (errorNamespace.equals(namespace)) { + condition = elementName; + } + else { + extensions.add(parsePacketExtension(elementName, namespace, parser)); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("error")) { + done = true; + } + } + } + // Parse the error type. + XMPPError.Type errorType = XMPPError.Type.CANCEL; + try { + if (type != null) { + errorType = XMPPError.Type.valueOf(type.toUpperCase()); + } + } + catch (IllegalArgumentException iae) { + // Print stack trace. We shouldn't be getting an illegal error type. + iae.printStackTrace(); + } + return new XMPPError(Integer.parseInt(errorCode), errorType, condition, message, extensions); + } + + /** + * Parses a packet extension sub-packet. + * + * @param elementName the XML element name of the packet extension. + * @param namespace the XML namespace of the packet extension. + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public static PacketExtension parsePacketExtension(String elementName, String namespace, XmlPullParser parser) + throws Exception + { + // See if a provider is registered to handle the extension. + Object provider = ProviderManager.getInstance().getExtensionProvider(elementName, namespace); + if (provider != null) { + if (provider instanceof PacketExtensionProvider) { + return ((PacketExtensionProvider)provider).parseExtension(parser); + } + else if (provider instanceof Class) { + return (PacketExtension)parseWithIntrospection( + elementName, (Class<?>)provider, parser); + } + } + // No providers registered, so use a default extension. + DefaultPacketExtension extension = new DefaultPacketExtension(elementName, namespace); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String name = parser.getName(); + // If an empty element, set the value with the empty string. + if (parser.isEmptyElementTag()) { + extension.setValue(name,""); + } + // Otherwise, get the the element text. + else { + eventType = parser.next(); + if (eventType == XmlPullParser.TEXT) { + String value = parser.getText(); + extension.setValue(name, value); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(elementName)) { + done = true; + } + } + } + return extension; + } + + private static String getLanguageAttribute(XmlPullParser parser) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + String attributeName = parser.getAttributeName(i); + if ( "xml:lang".equals(attributeName) || + ("lang".equals(attributeName) && + "xml".equals(parser.getAttributePrefix(i)))) { + return parser.getAttributeValue(i); + } + } + return null; + } + + public static Object parseWithIntrospection(String elementName, + Class<?> objectClass, XmlPullParser parser) throws Exception + { + boolean done = false; + Object object = objectClass.newInstance(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String name = parser.getName(); + String stringValue = parser.nextText(); + Class propertyType = object.getClass().getMethod( + "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1)).getReturnType(); + // Get the value of the property by converting it from a + // String to the correct object type. + Object value = decode(propertyType, stringValue); + // Set the value of the bean. + object.getClass().getMethod("set" + Character.toUpperCase(name.charAt(0)) + name.substring(1), propertyType) + .invoke(object, value); + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(elementName)) { + done = true; + } + } + } + return object; + } + + /** + * Decodes a String into an object of the specified type. If the object + * type is not supported, null will be returned. + * + * @param type the type of the property. + * @param value the encode String value to decode. + * @return the String value decoded into the specified type. + * @throws Exception If decoding failed due to an error. + */ + private static Object decode(Class<?> type, String value) throws Exception { + if (type.getName().equals("java.lang.String")) { + return value; + } + if (type.getName().equals("boolean")) { + return Boolean.valueOf(value); + } + if (type.getName().equals("int")) { + return Integer.valueOf(value); + } + if (type.getName().equals("long")) { + return Long.valueOf(value); + } + if (type.getName().equals("float")) { + return Float.valueOf(value); + } + if (type.getName().equals("double")) { + return Double.valueOf(value); + } + if (type.getName().equals("java.lang.Class")) { + return Class.forName(value); + } + return null; + } + + /** + * This class represents and unparsed IQ of the type 'result'. Usually it's created when no IQProvider + * was found for the IQ element. + * + * The child elements can be examined with the getChildElementXML() method. + * + */ + public static class UnparsedResultIQ extends IQ { + public UnparsedResultIQ(String content) { + this.str = content; + } + + private final String str; + + @Override + public String getChildElementXML() { + return this.str; + } + } +} diff --git a/src/org/jivesoftware/smack/util/PacketParserUtils.java.orig b/src/org/jivesoftware/smack/util/PacketParserUtils.java.orig new file mode 100644 index 0000000..1c518f6 --- /dev/null +++ b/src/org/jivesoftware/smack/util/PacketParserUtils.java.orig @@ -0,0 +1,926 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2007 Jive Software. + * + * All rights reserved. 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.jivesoftware.smack.util; + +import java.beans.PropertyDescriptor; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jivesoftware.smack.Connection; +import org.jivesoftware.smack.packet.Authentication; +import org.jivesoftware.smack.packet.Bind; +import org.jivesoftware.smack.packet.DefaultPacketExtension; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.packet.Packet; +import org.jivesoftware.smack.packet.PacketExtension; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.Registration; +import org.jivesoftware.smack.packet.RosterPacket; +import org.jivesoftware.smack.packet.StreamError; +import org.jivesoftware.smack.packet.XMPPError; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.provider.PacketExtensionProvider; +import org.jivesoftware.smack.provider.ProviderManager; +import org.jivesoftware.smack.sasl.SASLMechanism.Failure; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Utility class that helps to parse packets. Any parsing packets method that must be shared + * between many clients must be placed in this utility class. + * + * @author Gaston Dombiak + */ +public class PacketParserUtils { + + /** + * Namespace used to store packet properties. + */ + private static final String PROPERTIES_NAMESPACE = + "http://www.jivesoftware.com/xmlns/xmpp/properties"; + + /** + * Parses a message packet. + * + * @param parser the XML parser, positioned at the start of a message packet. + * @return a Message packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static Packet parseMessage(XmlPullParser parser) throws Exception { + Message message = new Message(); + String id = parser.getAttributeValue("", "id"); + message.setPacketID(id == null ? Packet.ID_NOT_AVAILABLE : id); + message.setTo(parser.getAttributeValue("", "to")); + message.setFrom(parser.getAttributeValue("", "from")); + message.setType(Message.Type.fromString(parser.getAttributeValue("", "type"))); + String language = getLanguageAttribute(parser); + + // determine message's default language + String defaultLanguage = null; + if (language != null && !"".equals(language.trim())) { + message.setLanguage(language); + defaultLanguage = language; + } + else { + defaultLanguage = Packet.getDefaultLanguage(); + } + + // Parse sub-elements. We include extra logic to make sure the values + // are only read once. This is because it's possible for the names to appear + // in arbitrary sub-elements. + boolean done = false; + String thread = null; + Map<String, Object> properties = null; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (elementName.equals("subject")) { + String xmlLang = getLanguageAttribute(parser); + if (xmlLang == null) { + xmlLang = defaultLanguage; + } + + String subject = parseContent(parser); + + if (message.getSubject(xmlLang) == null) { + message.addSubject(xmlLang, subject); + } + } + else if (elementName.equals("body")) { + String xmlLang = getLanguageAttribute(parser); + if (xmlLang == null) { + xmlLang = defaultLanguage; + } + + String body = parseContent(parser); + + if (message.getBody(xmlLang) == null) { + message.addBody(xmlLang, body); + } + } + else if (elementName.equals("thread")) { + if (thread == null) { + thread = parser.nextText(); + } + } + else if (elementName.equals("error")) { + message.setError(parseError(parser)); + } + else if (elementName.equals("properties") && + namespace.equals(PROPERTIES_NAMESPACE)) + { + properties = parseProperties(parser); + } + // Otherwise, it must be a packet extension. + else { + message.addExtension( + PacketParserUtils.parsePacketExtension(elementName, namespace, parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("message")) { + done = true; + } + } + } + + message.setThread(thread); + // Set packet properties. + if (properties != null) { + for (String name : properties.keySet()) { + message.setProperty(name, properties.get(name)); + } + } + return message; + } + + /** + * Returns the content of a tag as string regardless of any tags included. + * + * @param parser the XML pull parser + * @return the content of a tag as string + * @throws XmlPullParserException if parser encounters invalid XML + * @throws IOException if an IO error occurs + */ + private static String parseContent(XmlPullParser parser) + throws XmlPullParserException, IOException { + StringBuffer content = new StringBuffer(); + int parserDepth = parser.getDepth(); + while (!(parser.next() == XmlPullParser.END_TAG && parser + .getDepth() == parserDepth)) { + content.append(parser.getText()); + } + return content.toString(); + } + + /** + * Parses a presence packet. + * + * @param parser the XML parser, positioned at the start of a presence packet. + * @return a Presence packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static Presence parsePresence(XmlPullParser parser) throws Exception { + Presence.Type type = Presence.Type.available; + String typeString = parser.getAttributeValue("", "type"); + if (typeString != null && !typeString.equals("")) { + try { + type = Presence.Type.valueOf(typeString); + } + catch (IllegalArgumentException iae) { + System.err.println("Found invalid presence type " + typeString); + } + } + Presence presence = new Presence(type); + presence.setTo(parser.getAttributeValue("", "to")); + presence.setFrom(parser.getAttributeValue("", "from")); + String id = parser.getAttributeValue("", "id"); + presence.setPacketID(id == null ? Packet.ID_NOT_AVAILABLE : id); + + String language = getLanguageAttribute(parser); + if (language != null && !"".equals(language.trim())) { + presence.setLanguage(language); + } + presence.setPacketID(id == null ? Packet.ID_NOT_AVAILABLE : id); + + // Parse sub-elements + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (elementName.equals("status")) { + presence.setStatus(parser.nextText()); + } + else if (elementName.equals("priority")) { + try { + int priority = Integer.parseInt(parser.nextText()); + presence.setPriority(priority); + } + catch (NumberFormatException nfe) { + // Ignore. + } + catch (IllegalArgumentException iae) { + // Presence priority is out of range so assume priority to be zero + presence.setPriority(0); + } + } + else if (elementName.equals("show")) { + String modeText = parser.nextText(); + try { + presence.setMode(Presence.Mode.valueOf(modeText)); + } + catch (IllegalArgumentException iae) { + System.err.println("Found invalid presence mode " + modeText); + } + } + else if (elementName.equals("error")) { + presence.setError(parseError(parser)); + } + else if (elementName.equals("properties") && + namespace.equals(PROPERTIES_NAMESPACE)) + { + Map<String,Object> properties = parseProperties(parser); + // Set packet properties. + for (String name : properties.keySet()) { + presence.setProperty(name, properties.get(name)); + } + } + // Otherwise, it must be a packet extension. + else { + try { + presence.addExtension(PacketParserUtils.parsePacketExtension(elementName, namespace, parser)); + } + catch (Exception e) { + System.err.println("Failed to parse extension packet in Presence packet."); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("presence")) { + done = true; + } + } + } + return presence; + } + + /** + * Parses an IQ packet. + * + * @param parser the XML parser, positioned at the start of an IQ packet. + * @return an IQ object. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static IQ parseIQ(XmlPullParser parser, Connection connection) throws Exception { + IQ iqPacket = null; + + String id = parser.getAttributeValue("", "id"); + String to = parser.getAttributeValue("", "to"); + String from = parser.getAttributeValue("", "from"); + IQ.Type type = IQ.Type.fromString(parser.getAttributeValue("", "type")); + XMPPError error = null; + + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (elementName.equals("error")) { + error = PacketParserUtils.parseError(parser); + } + else if (elementName.equals("query") && namespace.equals("jabber:iq:auth")) { + iqPacket = parseAuthentication(parser); + } + else if (elementName.equals("query") && namespace.equals("jabber:iq:roster")) { + iqPacket = parseRoster(parser); + } + else if (elementName.equals("query") && namespace.equals("jabber:iq:register")) { + iqPacket = parseRegistration(parser); + } + else if (elementName.equals("bind") && + namespace.equals("urn:ietf:params:xml:ns:xmpp-bind")) { + iqPacket = parseResourceBinding(parser); + } + // Otherwise, see if there is a registered provider for + // this element name and namespace. + else { + Object provider = ProviderManager.getInstance().getIQProvider(elementName, namespace); + if (provider != null) { + if (provider instanceof IQProvider) { + iqPacket = ((IQProvider)provider).parseIQ(parser); + } + else if (provider instanceof Class) { + iqPacket = (IQ)PacketParserUtils.parseWithIntrospection(elementName, + (Class<?>)provider, parser); + } + } + // Only handle unknown IQs of type result. Types of 'get' and 'set' which are not understood + // have to be answered with an IQ error response. See the code a few lines below + else if (IQ.Type.RESULT == type){ + // No Provider found for the IQ stanza, parse it to an UnparsedIQ instance + // so that the content of the IQ can be examined later on + iqPacket = new UnparsedResultIQ(parseContent(parser)); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("iq")) { + done = true; + } + } + } + // Decide what to do when an IQ packet was not understood + if (iqPacket == null) { + if (IQ.Type.GET == type || IQ.Type.SET == type ) { + // If the IQ stanza is of type "get" or "set" containing a child element + // qualified by a namespace it does not understand, then answer an IQ of + // type "error" with code 501 ("feature-not-implemented") + iqPacket = new IQ() { + @Override + public String getChildElementXML() { + return null; + } + }; + iqPacket.setPacketID(id); + iqPacket.setTo(from); + iqPacket.setFrom(to); + iqPacket.setType(IQ.Type.ERROR); + iqPacket.setError(new XMPPError(XMPPError.Condition.feature_not_implemented)); + connection.sendPacket(iqPacket); + return null; + } + else { + // If an IQ packet wasn't created above, create an empty IQ packet. + iqPacket = new IQ() { + @Override + public String getChildElementXML() { + return null; + } + }; + } + } + + // Set basic values on the iq packet. + iqPacket.setPacketID(id); + iqPacket.setTo(to); + iqPacket.setFrom(from); + iqPacket.setType(type); + iqPacket.setError(error); + + return iqPacket; + } + + private static Authentication parseAuthentication(XmlPullParser parser) throws Exception { + Authentication authentication = new Authentication(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("username")) { + authentication.setUsername(parser.nextText()); + } + else if (parser.getName().equals("password")) { + authentication.setPassword(parser.nextText()); + } + else if (parser.getName().equals("digest")) { + authentication.setDigest(parser.nextText()); + } + else if (parser.getName().equals("resource")) { + authentication.setResource(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + return authentication; + } + + private static RosterPacket parseRoster(XmlPullParser parser) throws Exception { + RosterPacket roster = new RosterPacket(); + boolean done = false; + RosterPacket.Item item = null; + while (!done) { + if(parser.getEventType()==XmlPullParser.START_TAG && + parser.getName().equals("query")){ + String version = parser.getAttributeValue(null, "ver"); + roster.setVersion(version); + } + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + String jid = parser.getAttributeValue("", "jid"); + String name = parser.getAttributeValue("", "name"); + // Create packet. + item = new RosterPacket.Item(jid, name); + // Set status. + String ask = parser.getAttributeValue("", "ask"); + RosterPacket.ItemStatus status = RosterPacket.ItemStatus.fromString(ask); + item.setItemStatus(status); + // Set type. + String subscription = parser.getAttributeValue("", "subscription"); + RosterPacket.ItemType type = RosterPacket.ItemType.valueOf(subscription != null ? subscription : "none"); + item.setItemType(type); + } + if (parser.getName().equals("group") && item!= null) { + final String groupName = parser.nextText(); + if (groupName != null && groupName.trim().length() > 0) { + item.addGroupName(groupName); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("item")) { + roster.addRosterItem(item); + } + if (parser.getName().equals("query")) { + done = true; + } + } + } + return roster; + } + + private static Registration parseRegistration(XmlPullParser parser) throws Exception { + Registration registration = new Registration(); + Map<String, String> fields = null; + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + // Any element that's in the jabber:iq:register namespace, + // attempt to parse it if it's in the form <name>value</name>. + if (parser.getNamespace().equals("jabber:iq:register")) { + String name = parser.getName(); + String value = ""; + if (fields == null) { + fields = new HashMap<String, String>(); + } + + if (parser.next() == XmlPullParser.TEXT) { + value = parser.getText(); + } + // Ignore instructions, but anything else should be added to the map. + if (!name.equals("instructions")) { + fields.put(name, value); + } + else { + registration.setInstructions(value); + } + } + // Otherwise, it must be a packet extension. + else { + registration.addExtension( + PacketParserUtils.parsePacketExtension( + parser.getName(), + parser.getNamespace(), + parser)); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("query")) { + done = true; + } + } + } + registration.setAttributes(fields); + return registration; + } + + private static Bind parseResourceBinding(XmlPullParser parser) throws IOException, + XmlPullParserException { + Bind bind = new Bind(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("resource")) { + bind.setResource(parser.nextText()); + } + else if (parser.getName().equals("jid")) { + bind.setJid(parser.nextText()); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("bind")) { + done = true; + } + } + } + + return bind; + } + + /** + * Parse the available SASL mechanisms reported from the server. + * + * @param parser the XML parser, positioned at the start of the mechanisms stanza. + * @return a collection of Stings with the mechanisms included in the mechanisms stanza. + * @throws Exception if an exception occurs while parsing the stanza. + */ + public static Collection<String> parseMechanisms(XmlPullParser parser) throws Exception { + List<String> mechanisms = new ArrayList<String>(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + if (elementName.equals("mechanism")) { + mechanisms.add(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("mechanisms")) { + done = true; + } + } + } + return mechanisms; + } + + /** + * Parse the available compression methods reported from the server. + * + * @param parser the XML parser, positioned at the start of the compression stanza. + * @return a collection of Stings with the methods included in the compression stanza. + * @throws Exception if an exception occurs while parsing the stanza. + */ + public static Collection<String> parseCompressionMethods(XmlPullParser parser) + throws IOException, XmlPullParserException { + List<String> methods = new ArrayList<String>(); + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + if (elementName.equals("method")) { + methods.add(parser.nextText()); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("compression")) { + done = true; + } + } + } + return methods; + } + + /** + * Parse a properties sub-packet. If any errors occur while de-serializing Java object + * properties, an exception will be printed and not thrown since a thrown + * exception will shut down the entire connection. ClassCastExceptions will occur + * when both the sender and receiver of the packet don't have identical versions + * of the same class. + * + * @param parser the XML parser, positioned at the start of a properties sub-packet. + * @return a map of the properties. + * @throws Exception if an error occurs while parsing the properties. + */ + public static Map<String, Object> parseProperties(XmlPullParser parser) throws Exception { + Map<String, Object> properties = new HashMap<String, Object>(); + while (true) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG && parser.getName().equals("property")) { + // Parse a property + boolean done = false; + String name = null; + String type = null; + String valueText = null; + Object value = null; + while (!done) { + eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String elementName = parser.getName(); + if (elementName.equals("name")) { + name = parser.nextText(); + } + else if (elementName.equals("value")) { + type = parser.getAttributeValue("", "type"); + valueText = parser.nextText(); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("property")) { + if ("integer".equals(type)) { + value = Integer.valueOf(valueText); + } + else if ("long".equals(type)) { + value = Long.valueOf(valueText); + } + else if ("float".equals(type)) { + value = Float.valueOf(valueText); + } + else if ("double".equals(type)) { + value = Double.valueOf(valueText); + } + else if ("boolean".equals(type)) { + value = Boolean.valueOf(valueText); + } + else if ("string".equals(type)) { + value = valueText; + } + else if ("java-object".equals(type)) { + try { + byte [] bytes = StringUtils.decodeBase64(valueText); + ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes)); + value = in.readObject(); + } + catch (Exception e) { + e.printStackTrace(); + } + } + if (name != null && value != null) { + properties.put(name, value); + } + done = true; + } + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("properties")) { + break; + } + } + } + return properties; + } + + /** + * Parses SASL authentication error packets. + * + * @param parser the XML parser. + * @return a SASL Failure packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static Failure parseSASLFailure(XmlPullParser parser) throws Exception { + String condition = null; + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + if (!parser.getName().equals("failure")) { + condition = parser.getName(); + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("failure")) { + done = true; + } + } + } + return new Failure(condition); + } + + /** + * Parses stream error packets. + * + * @param parser the XML parser. + * @return an stream error packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static StreamError parseStreamError(XmlPullParser parser) throws IOException, + XmlPullParserException { + StreamError streamError = null; + boolean done = false; + while (!done) { + int eventType = parser.next(); + + if (eventType == XmlPullParser.START_TAG) { + streamError = new StreamError(parser.getName()); + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("error")) { + done = true; + } + } + } + return streamError; +} + + /** + * Parses error sub-packets. + * + * @param parser the XML parser. + * @return an error sub-packet. + * @throws Exception if an exception occurs while parsing the packet. + */ + public static XMPPError parseError(XmlPullParser parser) throws Exception { + final String errorNamespace = "urn:ietf:params:xml:ns:xmpp-stanzas"; + String errorCode = "-1"; + String type = null; + String message = null; + String condition = null; + List<PacketExtension> extensions = new ArrayList<PacketExtension>(); + + // Parse the error header + for (int i=0; i<parser.getAttributeCount(); i++) { + if (parser.getAttributeName(i).equals("code")) { + errorCode = parser.getAttributeValue("", "code"); + } + if (parser.getAttributeName(i).equals("type")) { + type = parser.getAttributeValue("", "type"); + } + } + boolean done = false; + // Parse the text and condition tags + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("text")) { + message = parser.nextText(); + } + else { + // Condition tag, it can be xmpp error or an application defined error. + String elementName = parser.getName(); + String namespace = parser.getNamespace(); + if (errorNamespace.equals(namespace)) { + condition = elementName; + } + else { + extensions.add(parsePacketExtension(elementName, namespace, parser)); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("error")) { + done = true; + } + } + } + // Parse the error type. + XMPPError.Type errorType = XMPPError.Type.CANCEL; + try { + if (type != null) { + errorType = XMPPError.Type.valueOf(type.toUpperCase()); + } + } + catch (IllegalArgumentException iae) { + // Print stack trace. We shouldn't be getting an illegal error type. + iae.printStackTrace(); + } + return new XMPPError(Integer.parseInt(errorCode), errorType, condition, message, extensions); + } + + /** + * Parses a packet extension sub-packet. + * + * @param elementName the XML element name of the packet extension. + * @param namespace the XML namespace of the packet extension. + * @param parser the XML parser, positioned at the starting element of the extension. + * @return a PacketExtension. + * @throws Exception if a parsing error occurs. + */ + public static PacketExtension parsePacketExtension(String elementName, String namespace, XmlPullParser parser) + throws Exception + { + // See if a provider is registered to handle the extension. + Object provider = ProviderManager.getInstance().getExtensionProvider(elementName, namespace); + if (provider != null) { + if (provider instanceof PacketExtensionProvider) { + return ((PacketExtensionProvider)provider).parseExtension(parser); + } + else if (provider instanceof Class) { + return (PacketExtension)parseWithIntrospection( + elementName, (Class<?>)provider, parser); + } + } + // No providers registered, so use a default extension. + DefaultPacketExtension extension = new DefaultPacketExtension(elementName, namespace); + boolean done = false; + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String name = parser.getName(); + // If an empty element, set the value with the empty string. + if (parser.isEmptyElementTag()) { + extension.setValue(name,""); + } + // Otherwise, get the the element text. + else { + eventType = parser.next(); + if (eventType == XmlPullParser.TEXT) { + String value = parser.getText(); + extension.setValue(name, value); + } + } + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(elementName)) { + done = true; + } + } + } + return extension; + } + + private static String getLanguageAttribute(XmlPullParser parser) { + for (int i = 0; i < parser.getAttributeCount(); i++) { + String attributeName = parser.getAttributeName(i); + if ( "xml:lang".equals(attributeName) || + ("lang".equals(attributeName) && + "xml".equals(parser.getAttributePrefix(i)))) { + return parser.getAttributeValue(i); + } + } + return null; + } + + public static Object parseWithIntrospection(String elementName, + Class<?> objectClass, XmlPullParser parser) throws Exception + { + boolean done = false; + Object object = objectClass.newInstance(); + while (!done) { + int eventType = parser.next(); + if (eventType == XmlPullParser.START_TAG) { + String name = parser.getName(); + String stringValue = parser.nextText(); + PropertyDescriptor descriptor = new PropertyDescriptor(name, objectClass); + // Load the class type of the property. + Class<?> propertyType = descriptor.getPropertyType(); + // Get the value of the property by converting it from a + // String to the correct object type. + Object value = decode(propertyType, stringValue); + // Set the value of the bean. + descriptor.getWriteMethod().invoke(object, value); + } + else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(elementName)) { + done = true; + } + } + } + return object; + } + + /** + * Decodes a String into an object of the specified type. If the object + * type is not supported, null will be returned. + * + * @param type the type of the property. + * @param value the encode String value to decode. + * @return the String value decoded into the specified type. + * @throws Exception If decoding failed due to an error. + */ + private static Object decode(Class<?> type, String value) throws Exception { + if (type.getName().equals("java.lang.String")) { + return value; + } + if (type.getName().equals("boolean")) { + return Boolean.valueOf(value); + } + if (type.getName().equals("int")) { + return Integer.valueOf(value); + } + if (type.getName().equals("long")) { + return Long.valueOf(value); + } + if (type.getName().equals("float")) { + return Float.valueOf(value); + } + if (type.getName().equals("double")) { + return Double.valueOf(value); + } + if (type.getName().equals("java.lang.Class")) { + return Class.forName(value); + } + return null; + } + + /** + * This class represents and unparsed IQ of the type 'result'. Usually it's created when no IQProvider + * was found for the IQ element. + * + * The child elements can be examined with the getChildElementXML() method. + * + */ + public static class UnparsedResultIQ extends IQ { + public UnparsedResultIQ(String content) { + this.str = content; + } + + private final String str; + + @Override + public String getChildElementXML() { + return this.str; + } + } +} diff --git a/src/org/jivesoftware/smack/util/ReaderListener.java b/src/org/jivesoftware/smack/util/ReaderListener.java new file mode 100644 index 0000000..9f1f5bb --- /dev/null +++ b/src/org/jivesoftware/smack/util/ReaderListener.java @@ -0,0 +1,41 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2007 Jive Software. + * + * All rights reserved. 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.jivesoftware.smack.util; + +/** + * Interface that allows for implementing classes to listen for string reading + * events. Listeners are registered with ObservableReader objects. + * + * @see ObservableReader#addReaderListener + * @see ObservableReader#removeReaderListener + * + * @author Gaston Dombiak + */ +public interface ReaderListener { + + /** + * Notification that the Reader has read a new string. + * + * @param str the read String + */ + public abstract void read(String str); + +} diff --git a/src/org/jivesoftware/smack/util/StringEncoder.java b/src/org/jivesoftware/smack/util/StringEncoder.java new file mode 100644 index 0000000..4c3d373 --- /dev/null +++ b/src/org/jivesoftware/smack/util/StringEncoder.java @@ -0,0 +1,36 @@ +/** + * All rights reserved. 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. + */ + +/** + * @author Florian Schmaus + */ +package org.jivesoftware.smack.util; + +public interface StringEncoder { + /** + * Encodes an string to another representation + * + * @param string + * @return + */ + String encode(String string); + + /** + * Decodes an string back to it's initial representation + * + * @param string + * @return + */ + String decode(String string); +} diff --git a/src/org/jivesoftware/smack/util/StringUtils.java b/src/org/jivesoftware/smack/util/StringUtils.java new file mode 100644 index 0000000..7e3cfdc --- /dev/null +++ b/src/org/jivesoftware/smack/util/StringUtils.java @@ -0,0 +1,800 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2007 Jive Software. + * + * All rights reserved. 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.jivesoftware.smack.util; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Random; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A collection of utility methods for String objects. + */ +public class StringUtils { + + /** + * Date format as defined in XEP-0082 - XMPP Date and Time Profiles. The time zone is set to + * UTC. + * <p> + * Date formats are not synchronized. Since multiple threads access the format concurrently, it + * must be synchronized externally or you can use the convenience methods + * {@link #parseXEP0082Date(String)} and {@link #formatXEP0082Date(Date)}. + * @deprecated This public version will be removed in favor of using the methods defined within this class. + */ + public static final DateFormat XEP_0082_UTC_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); + + /* + * private version to use internally so we don't have to be concerned with thread safety. + */ + private static final DateFormat dateFormatter = DateFormatType.XEP_0082_DATE_PROFILE.createFormatter(); + private static final Pattern datePattern = Pattern.compile("^\\d+-\\d+-\\d+$"); + + private static final DateFormat timeFormatter = DateFormatType.XEP_0082_TIME_MILLIS_ZONE_PROFILE.createFormatter(); + private static final Pattern timePattern = Pattern.compile("^(\\d+:){2}\\d+.\\d+(Z|([+-](\\d+:\\d+)))$"); + private static final DateFormat timeNoZoneFormatter = DateFormatType.XEP_0082_TIME_MILLIS_PROFILE.createFormatter(); + private static final Pattern timeNoZonePattern = Pattern.compile("^(\\d+:){2}\\d+.\\d+$"); + + private static final DateFormat timeNoMillisFormatter = DateFormatType.XEP_0082_TIME_ZONE_PROFILE.createFormatter(); + private static final Pattern timeNoMillisPattern = Pattern.compile("^(\\d+:){2}\\d+(Z|([+-](\\d+:\\d+)))$"); + private static final DateFormat timeNoMillisNoZoneFormatter = DateFormatType.XEP_0082_TIME_PROFILE.createFormatter(); + private static final Pattern timeNoMillisNoZonePattern = Pattern.compile("^(\\d+:){2}\\d+$"); + + private static final DateFormat dateTimeFormatter = DateFormatType.XEP_0082_DATETIME_MILLIS_PROFILE.createFormatter(); + private static final Pattern dateTimePattern = Pattern.compile("^\\d+(-\\d+){2}+T(\\d+:){2}\\d+.\\d+(Z|([+-](\\d+:\\d+)))?$"); + private static final DateFormat dateTimeNoMillisFormatter = DateFormatType.XEP_0082_DATETIME_PROFILE.createFormatter(); + private static final Pattern dateTimeNoMillisPattern = Pattern.compile("^\\d+(-\\d+){2}+T(\\d+:){2}\\d+(Z|([+-](\\d+:\\d+)))?$"); + + private static final DateFormat xep0091Formatter = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss"); + private static final DateFormat xep0091Date6DigitFormatter = new SimpleDateFormat("yyyyMd'T'HH:mm:ss"); + private static final DateFormat xep0091Date7Digit1MonthFormatter = new SimpleDateFormat("yyyyMdd'T'HH:mm:ss"); + private static final DateFormat xep0091Date7Digit2MonthFormatter = new SimpleDateFormat("yyyyMMd'T'HH:mm:ss"); + private static final Pattern xep0091Pattern = Pattern.compile("^\\d+T\\d+:\\d+:\\d+$"); + + private static final List<PatternCouplings> couplings = new ArrayList<PatternCouplings>(); + + static { + TimeZone utc = TimeZone.getTimeZone("UTC"); + XEP_0082_UTC_FORMAT.setTimeZone(utc); + dateFormatter.setTimeZone(utc); + timeFormatter.setTimeZone(utc); + timeNoZoneFormatter.setTimeZone(utc); + timeNoMillisFormatter.setTimeZone(utc); + timeNoMillisNoZoneFormatter.setTimeZone(utc); + dateTimeFormatter.setTimeZone(utc); + dateTimeNoMillisFormatter.setTimeZone(utc); + + xep0091Formatter.setTimeZone(utc); + xep0091Date6DigitFormatter.setTimeZone(utc); + xep0091Date7Digit1MonthFormatter.setTimeZone(utc); + xep0091Date7Digit1MonthFormatter.setLenient(false); + xep0091Date7Digit2MonthFormatter.setTimeZone(utc); + xep0091Date7Digit2MonthFormatter.setLenient(false); + + couplings.add(new PatternCouplings(datePattern, dateFormatter)); + couplings.add(new PatternCouplings(dateTimePattern, dateTimeFormatter, true)); + couplings.add(new PatternCouplings(dateTimeNoMillisPattern, dateTimeNoMillisFormatter, true)); + couplings.add(new PatternCouplings(timePattern, timeFormatter, true)); + couplings.add(new PatternCouplings(timeNoZonePattern, timeNoZoneFormatter)); + couplings.add(new PatternCouplings(timeNoMillisPattern, timeNoMillisFormatter, true)); + couplings.add(new PatternCouplings(timeNoMillisNoZonePattern, timeNoMillisNoZoneFormatter)); + } + + private static final char[] QUOTE_ENCODE = """.toCharArray(); + private static final char[] APOS_ENCODE = "'".toCharArray(); + private static final char[] AMP_ENCODE = "&".toCharArray(); + private static final char[] LT_ENCODE = "<".toCharArray(); + private static final char[] GT_ENCODE = ">".toCharArray(); + + /** + * Parses the given date string in the <a href="http://xmpp.org/extensions/xep-0082.html">XEP-0082 - XMPP Date and Time Profiles</a>. + * + * @param dateString the date string to parse + * @return the parsed Date + * @throws ParseException if the specified string cannot be parsed + * @deprecated Use {@link #parseDate(String)} instead. + * + */ + public static Date parseXEP0082Date(String dateString) throws ParseException { + return parseDate(dateString); + } + + /** + * Parses the given date string in either of the three profiles of <a href="http://xmpp.org/extensions/xep-0082.html">XEP-0082 - XMPP Date and Time Profiles</a> + * or <a href="http://xmpp.org/extensions/xep-0091.html">XEP-0091 - Legacy Delayed Delivery</a> format. + * <p> + * This method uses internal date formatters and is thus threadsafe. + * @param dateString the date string to parse + * @return the parsed Date + * @throws ParseException if the specified string cannot be parsed + */ + public static Date parseDate(String dateString) throws ParseException { + Matcher matcher = xep0091Pattern.matcher(dateString); + + /* + * if date is in XEP-0091 format handle ambiguous dates missing the + * leading zero in month and day + */ + if (matcher.matches()) { + int length = dateString.split("T")[0].length(); + + if (length < 8) { + Date date = handleDateWithMissingLeadingZeros(dateString, length); + + if (date != null) + return date; + } + else { + synchronized (xep0091Formatter) { + return xep0091Formatter.parse(dateString); + } + } + } + else { + for (PatternCouplings coupling : couplings) { + matcher = coupling.pattern.matcher(dateString); + + if (matcher.matches()) + { + if (coupling.needToConvertTimeZone) { + dateString = coupling.convertTime(dateString); + } + + synchronized (coupling.formatter) { + return coupling.formatter.parse(dateString); + } + } + } + } + + /* + * We assume it is the XEP-0082 DateTime profile with no milliseconds at this point. If it isn't, is is just not parseable, then we attempt + * to parse it regardless and let it throw the ParseException. + */ + synchronized (dateTimeNoMillisFormatter) { + return dateTimeNoMillisFormatter.parse(dateString); + } + } + + /** + * Parses the given date string in different ways and returns the date that + * lies in the past and/or is nearest to the current date-time. + * + * @param stampString date in string representation + * @param dateLength + * @param noFuture + * @return the parsed date + * @throws ParseException The date string was of an unknown format + */ + private static Date handleDateWithMissingLeadingZeros(String stampString, int dateLength) throws ParseException { + if (dateLength == 6) { + synchronized (xep0091Date6DigitFormatter) { + return xep0091Date6DigitFormatter.parse(stampString); + } + } + Calendar now = Calendar.getInstance(); + + Calendar oneDigitMonth = parseXEP91Date(stampString, xep0091Date7Digit1MonthFormatter); + Calendar twoDigitMonth = parseXEP91Date(stampString, xep0091Date7Digit2MonthFormatter); + + List<Calendar> dates = filterDatesBefore(now, oneDigitMonth, twoDigitMonth); + + if (!dates.isEmpty()) { + return determineNearestDate(now, dates).getTime(); + } + return null; + } + + private static Calendar parseXEP91Date(String stampString, DateFormat dateFormat) { + try { + synchronized (dateFormat) { + dateFormat.parse(stampString); + return dateFormat.getCalendar(); + } + } + catch (ParseException e) { + return null; + } + } + + private static List<Calendar> filterDatesBefore(Calendar now, Calendar... dates) { + List<Calendar> result = new ArrayList<Calendar>(); + + for (Calendar calendar : dates) { + if (calendar != null && calendar.before(now)) { + result.add(calendar); + } + } + + return result; + } + + private static Calendar determineNearestDate(final Calendar now, List<Calendar> dates) { + + Collections.sort(dates, new Comparator<Calendar>() { + + public int compare(Calendar o1, Calendar o2) { + Long diff1 = new Long(now.getTimeInMillis() - o1.getTimeInMillis()); + Long diff2 = new Long(now.getTimeInMillis() - o2.getTimeInMillis()); + return diff1.compareTo(diff2); + } + + }); + + return dates.get(0); + } + + /** + * Formats a Date into a XEP-0082 - XMPP Date and Time Profiles string. + * + * @param date the time value to be formatted into a time string + * @return the formatted time string in XEP-0082 format + */ + public static String formatXEP0082Date(Date date) { + synchronized (dateTimeFormatter) { + return dateTimeFormatter.format(date); + } + } + + public static String formatDate(Date toFormat, DateFormatType type) + { + return null; + } + + /** + * Returns the name portion of a XMPP address. For example, for the + * address "matt@jivesoftware.com/Smack", "matt" would be returned. If no + * username is present in the address, the empty string will be returned. + * + * @param XMPPAddress the XMPP address. + * @return the name portion of the XMPP address. + */ + public static String parseName(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int atIndex = XMPPAddress.lastIndexOf("@"); + if (atIndex <= 0) { + return ""; + } + else { + return XMPPAddress.substring(0, atIndex); + } + } + + /** + * Returns the server portion of a XMPP address. For example, for the + * address "matt@jivesoftware.com/Smack", "jivesoftware.com" would be returned. + * If no server is present in the address, the empty string will be returned. + * + * @param XMPPAddress the XMPP address. + * @return the server portion of the XMPP address. + */ + public static String parseServer(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int atIndex = XMPPAddress.lastIndexOf("@"); + // If the String ends with '@', return the empty string. + if (atIndex + 1 > XMPPAddress.length()) { + return ""; + } + int slashIndex = XMPPAddress.indexOf("/"); + if (slashIndex > 0 && slashIndex > atIndex) { + return XMPPAddress.substring(atIndex + 1, slashIndex); + } + else { + return XMPPAddress.substring(atIndex + 1); + } + } + + /** + * Returns the resource portion of a XMPP address. For example, for the + * address "matt@jivesoftware.com/Smack", "Smack" would be returned. If no + * resource is present in the address, the empty string will be returned. + * + * @param XMPPAddress the XMPP address. + * @return the resource portion of the XMPP address. + */ + public static String parseResource(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int slashIndex = XMPPAddress.indexOf("/"); + if (slashIndex + 1 > XMPPAddress.length() || slashIndex < 0) { + return ""; + } + else { + return XMPPAddress.substring(slashIndex + 1); + } + } + + /** + * Returns the XMPP address with any resource information removed. For example, + * for the address "matt@jivesoftware.com/Smack", "matt@jivesoftware.com" would + * be returned. + * + * @param XMPPAddress the XMPP address. + * @return the bare XMPP address without resource information. + */ + public static String parseBareAddress(String XMPPAddress) { + if (XMPPAddress == null) { + return null; + } + int slashIndex = XMPPAddress.indexOf("/"); + if (slashIndex < 0) { + return XMPPAddress; + } + else if (slashIndex == 0) { + return ""; + } + else { + return XMPPAddress.substring(0, slashIndex); + } + } + + /** + * Returns true if jid is a full JID (i.e. a JID with resource part). + * + * @param jid + * @return true if full JID, false otherwise + */ + public static boolean isFullJID(String jid) { + if (parseName(jid).length() <= 0 || parseServer(jid).length() <= 0 + || parseResource(jid).length() <= 0) { + return false; + } + return true; + } + + /** + * Escapes the node portion of a JID according to "JID Escaping" (JEP-0106). + * Escaping replaces characters prohibited by node-prep with escape sequences, + * as follows:<p> + * + * <table border="1"> + * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr> + * <tr><td><space></td><td>\20</td></tr> + * <tr><td>"</td><td>\22</td></tr> + * <tr><td>&</td><td>\26</td></tr> + * <tr><td>'</td><td>\27</td></tr> + * <tr><td>/</td><td>\2f</td></tr> + * <tr><td>:</td><td>\3a</td></tr> + * <tr><td><</td><td>\3c</td></tr> + * <tr><td>></td><td>\3e</td></tr> + * <tr><td>@</td><td>\40</td></tr> + * <tr><td>\</td><td>\5c</td></tr> + * </table><p> + * + * This process is useful when the node comes from an external source that doesn't + * conform to nodeprep. For example, a username in LDAP may be "Joe Smith". Because + * the <space> character isn't a valid part of a node, the username should + * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com" + * after case-folding, etc. has been applied).<p> + * + * All node escaping and un-escaping must be performed manually at the appropriate + * time; the JID class will not escape or un-escape automatically. + * + * @param node the node. + * @return the escaped version of the node. + */ + public static String escapeNode(String node) { + if (node == null) { + return null; + } + StringBuilder buf = new StringBuilder(node.length() + 8); + for (int i=0, n=node.length(); i<n; i++) { + char c = node.charAt(i); + switch (c) { + case '"': buf.append("\\22"); break; + case '&': buf.append("\\26"); break; + case '\'': buf.append("\\27"); break; + case '/': buf.append("\\2f"); break; + case ':': buf.append("\\3a"); break; + case '<': buf.append("\\3c"); break; + case '>': buf.append("\\3e"); break; + case '@': buf.append("\\40"); break; + case '\\': buf.append("\\5c"); break; + default: { + if (Character.isWhitespace(c)) { + buf.append("\\20"); + } + else { + buf.append(c); + } + } + } + } + return buf.toString(); + } + + /** + * Un-escapes the node portion of a JID according to "JID Escaping" (JEP-0106).<p> + * Escaping replaces characters prohibited by node-prep with escape sequences, + * as follows:<p> + * + * <table border="1"> + * <tr><td><b>Unescaped Character</b></td><td><b>Encoded Sequence</b></td></tr> + * <tr><td><space></td><td>\20</td></tr> + * <tr><td>"</td><td>\22</td></tr> + * <tr><td>&</td><td>\26</td></tr> + * <tr><td>'</td><td>\27</td></tr> + * <tr><td>/</td><td>\2f</td></tr> + * <tr><td>:</td><td>\3a</td></tr> + * <tr><td><</td><td>\3c</td></tr> + * <tr><td>></td><td>\3e</td></tr> + * <tr><td>@</td><td>\40</td></tr> + * <tr><td>\</td><td>\5c</td></tr> + * </table><p> + * + * This process is useful when the node comes from an external source that doesn't + * conform to nodeprep. For example, a username in LDAP may be "Joe Smith". Because + * the <space> character isn't a valid part of a node, the username should + * be escaped to "Joe\20Smith" before being made into a JID (e.g. "joe\20smith@example.com" + * after case-folding, etc. has been applied).<p> + * + * All node escaping and un-escaping must be performed manually at the appropriate + * time; the JID class will not escape or un-escape automatically. + * + * @param node the escaped version of the node. + * @return the un-escaped version of the node. + */ + public static String unescapeNode(String node) { + if (node == null) { + return null; + } + char [] nodeChars = node.toCharArray(); + StringBuilder buf = new StringBuilder(nodeChars.length); + for (int i=0, n=nodeChars.length; i<n; i++) { + compare: { + char c = node.charAt(i); + if (c == '\\' && i+2<n) { + char c2 = nodeChars[i+1]; + char c3 = nodeChars[i+2]; + if (c2 == '2') { + switch (c3) { + case '0': buf.append(' '); i+=2; break compare; + case '2': buf.append('"'); i+=2; break compare; + case '6': buf.append('&'); i+=2; break compare; + case '7': buf.append('\''); i+=2; break compare; + case 'f': buf.append('/'); i+=2; break compare; + } + } + else if (c2 == '3') { + switch (c3) { + case 'a': buf.append(':'); i+=2; break compare; + case 'c': buf.append('<'); i+=2; break compare; + case 'e': buf.append('>'); i+=2; break compare; + } + } + else if (c2 == '4') { + if (c3 == '0') { + buf.append("@"); + i+=2; + break compare; + } + } + else if (c2 == '5') { + if (c3 == 'c') { + buf.append("\\"); + i+=2; + break compare; + } + } + } + buf.append(c); + } + } + return buf.toString(); + } + + /** + * Escapes all necessary characters in the String so that it can be used + * in an XML doc. + * + * @param string the string to escape. + * @return the string with appropriate characters escaped. + */ + public static String escapeForXML(String string) { + if (string == null) { + return null; + } + char ch; + int i=0; + int last=0; + char[] input = string.toCharArray(); + int len = input.length; + StringBuilder out = new StringBuilder((int)(len*1.3)); + for (; i < len; i++) { + ch = input[i]; + if (ch > '>') { + } + else if (ch == '<') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(LT_ENCODE); + } + else if (ch == '>') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(GT_ENCODE); + } + + else if (ch == '&') { + if (i > last) { + out.append(input, last, i - last); + } + // Do nothing if the string is of the form ë (unicode value) + if (!(len > i + 5 + && input[i + 1] == '#' + && Character.isDigit(input[i + 2]) + && Character.isDigit(input[i + 3]) + && Character.isDigit(input[i + 4]) + && input[i + 5] == ';')) { + last = i + 1; + out.append(AMP_ENCODE); + } + } + else if (ch == '"') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(QUOTE_ENCODE); + } + else if (ch == '\'') { + if (i > last) { + out.append(input, last, i - last); + } + last = i + 1; + out.append(APOS_ENCODE); + } + } + if (last == 0) { + return string; + } + if (i > last) { + out.append(input, last, i - last); + } + return out.toString(); + } + + /** + * Used by the hash method. + */ + private static MessageDigest digest = null; + + /** + * Hashes a String using the SHA-1 algorithm and returns the result as a + * String of hexadecimal numbers. This method is synchronized to avoid + * excessive MessageDigest object creation. If calling this method becomes + * a bottleneck in your code, you may wish to maintain a pool of + * MessageDigest objects instead of using this method. + * <p> + * A hash is a one-way function -- that is, given an + * input, an output is easily computed. However, given the output, the + * input is almost impossible to compute. This is useful for passwords + * since we can store the hash and a hacker will then have a very hard time + * determining the original password. + * + * @param data the String to compute the hash of. + * @return a hashed version of the passed-in String + */ + public synchronized static String hash(String data) { + if (digest == null) { + try { + digest = MessageDigest.getInstance("SHA-1"); + } + catch (NoSuchAlgorithmException nsae) { + System.err.println("Failed to load the SHA-1 MessageDigest. " + + "Jive will be unable to function normally."); + } + } + // Now, compute hash. + try { + digest.update(data.getBytes("UTF-8")); + } + catch (UnsupportedEncodingException e) { + System.err.println(e); + } + return encodeHex(digest.digest()); + } + + /** + * Encodes an array of bytes as String representation of hexadecimal. + * + * @param bytes an array of bytes to convert to a hex string. + * @return generated hex string. + */ + public static String encodeHex(byte[] bytes) { + StringBuilder hex = new StringBuilder(bytes.length * 2); + + for (byte aByte : bytes) { + if (((int) aByte & 0xff) < 0x10) { + hex.append("0"); + } + hex.append(Integer.toString((int) aByte & 0xff, 16)); + } + + return hex.toString(); + } + + /** + * Encodes a String as a base64 String. + * + * @param data a String to encode. + * @return a base64 encoded String. + */ + public static String encodeBase64(String data) { + byte [] bytes = null; + try { + bytes = data.getBytes("ISO-8859-1"); + } + catch (UnsupportedEncodingException uee) { + uee.printStackTrace(); + } + return encodeBase64(bytes); + } + + /** + * Encodes a byte array into a base64 String. + * + * @param data a byte array to encode. + * @return a base64 encode String. + */ + public static String encodeBase64(byte[] data) { + return encodeBase64(data, false); + } + + /** + * Encodes a byte array into a bse64 String. + * + * @param data The byte arry to encode. + * @param lineBreaks True if the encoding should contain line breaks and false if it should not. + * @return A base64 encoded String. + */ + public static String encodeBase64(byte[] data, boolean lineBreaks) { + return encodeBase64(data, 0, data.length, lineBreaks); + } + + /** + * Encodes a byte array into a bse64 String. + * + * @param data The byte arry to encode. + * @param offset the offset of the bytearray to begin encoding at. + * @param len the length of bytes to encode. + * @param lineBreaks True if the encoding should contain line breaks and false if it should not. + * @return A base64 encoded String. + */ + public static String encodeBase64(byte[] data, int offset, int len, boolean lineBreaks) { + return Base64.encodeBytes(data, offset, len, (lineBreaks ? Base64.NO_OPTIONS : Base64.DONT_BREAK_LINES)); + } + + /** + * Decodes a base64 String. + * Unlike Base64.decode() this method does not try to detect and decompress a gzip-compressed input. + * + * @param data a base64 encoded String to decode. + * @return the decoded String. + */ + public static byte[] decodeBase64(String data) { + byte[] bytes; + try { + bytes = data.getBytes("UTF-8"); + } catch (java.io.UnsupportedEncodingException uee) { + bytes = data.getBytes(); + } + + bytes = Base64.decode(bytes, 0, bytes.length, Base64.NO_OPTIONS); + return bytes; + } + + /** + * Pseudo-random number generator object for use with randomString(). + * The Random class is not considered to be cryptographically secure, so + * only use these random Strings for low to medium security applications. + */ + private static Random randGen = new Random(); + + /** + * Array of numbers and letters of mixed case. Numbers appear in the list + * twice so that there is a more equal chance that a number will be picked. + * We can use the array to get a random number or letter by picking a random + * array index. + */ + private static char[] numbersAndLetters = ("0123456789abcdefghijklmnopqrstuvwxyz" + + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ").toCharArray(); + + /** + * Returns a random String of numbers and letters (lower and upper case) + * of the specified length. The method uses the Random class that is + * built-in to Java which is suitable for low to medium grade security uses. + * This means that the output is only pseudo random, i.e., each number is + * mathematically generated so is not truly random.<p> + * + * The specified length must be at least one. If not, the method will return + * null. + * + * @param length the desired length of the random String to return. + * @return a random String of numbers and letters of the specified length. + */ + public static String randomString(int length) { + if (length < 1) { + return null; + } + // Create a char buffer to put random letters and numbers in. + char [] randBuffer = new char[length]; + for (int i=0; i<randBuffer.length; i++) { + randBuffer[i] = numbersAndLetters[randGen.nextInt(71)]; + } + return new String(randBuffer); + } + + private StringUtils() { + // Not instantiable. + } + + private static class PatternCouplings { + Pattern pattern; + DateFormat formatter; + boolean needToConvertTimeZone = false; + + public PatternCouplings(Pattern datePattern, DateFormat dateFormat) { + pattern = datePattern; + formatter = dateFormat; + } + + public PatternCouplings(Pattern datePattern, DateFormat dateFormat, boolean shouldConvertToRFC822) { + pattern = datePattern; + formatter = dateFormat; + needToConvertTimeZone = shouldConvertToRFC822; + } + + public String convertTime(String dateString) { + if (dateString.charAt(dateString.length() - 1) == 'Z') { + return dateString.replace("Z", "+0000"); + } + else { + // If the time zone wasn't specified with 'Z', then it's in + // ISO8601 format (i.e. '(+|-)HH:mm') + // RFC822 needs a similar format just without the colon (i.e. + // '(+|-)HHmm)'), so remove it + return dateString.replaceAll("([\\+\\-]\\d\\d):(\\d\\d)","$1$2"); + } + } + } + +} diff --git a/src/org/jivesoftware/smack/util/SyncPacketSend.java b/src/org/jivesoftware/smack/util/SyncPacketSend.java new file mode 100644 index 0000000..a1c238a --- /dev/null +++ b/src/org/jivesoftware/smack/util/SyncPacketSend.java @@ -0,0 +1,63 @@ +/**
+ * All rights reserved. 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.jivesoftware.smack.util;
+
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Utility class for doing synchronous calls to the server. Provides several
+ * methods for sending a packet to the server and waiting for the reply.
+ *
+ * @author Robin Collier
+ */
+final public class SyncPacketSend
+{
+ private SyncPacketSend()
+ { }
+
+ static public Packet getReply(Connection connection, Packet packet, long timeout)
+ throws XMPPException
+ {
+ PacketFilter responseFilter = new PacketIDFilter(packet.getPacketID());
+ PacketCollector response = connection.createPacketCollector(responseFilter);
+
+ connection.sendPacket(packet);
+
+ // Wait up to a certain number of seconds for a reply.
+ Packet result = response.nextResult(timeout);
+
+ // Stop queuing results
+ response.cancel();
+
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getError() != null) {
+ throw new XMPPException(result.getError());
+ }
+ return result;
+ }
+
+ static public Packet getReply(Connection connection, Packet packet)
+ throws XMPPException
+ {
+ return getReply(connection, packet, SmackConfiguration.getPacketReplyTimeout());
+ }
+}
diff --git a/src/org/jivesoftware/smack/util/WriterListener.java b/src/org/jivesoftware/smack/util/WriterListener.java new file mode 100644 index 0000000..dcf56d9 --- /dev/null +++ b/src/org/jivesoftware/smack/util/WriterListener.java @@ -0,0 +1,41 @@ +/** + * $RCSfile$ + * $Revision$ + * $Date$ + * + * Copyright 2003-2007 Jive Software. + * + * All rights reserved. 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.jivesoftware.smack.util; + +/** + * Interface that allows for implementing classes to listen for string writing + * events. Listeners are registered with ObservableWriter objects. + * + * @see ObservableWriter#addWriterListener + * @see ObservableWriter#removeWriterListener + * + * @author Gaston Dombiak + */ +public interface WriterListener { + + /** + * Notification that the Writer has written a new string. + * + * @param str the written string + */ + public abstract void write(String str); + +} diff --git a/src/org/jivesoftware/smack/util/collections/AbstractEmptyIterator.java b/src/org/jivesoftware/smack/util/collections/AbstractEmptyIterator.java new file mode 100644 index 0000000..c2ec156 --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/AbstractEmptyIterator.java @@ -0,0 +1,89 @@ +// GenericsNote: Converted. +/* + * Copyright 2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +import java.util.NoSuchElementException; + +/** + * Provides an implementation of an empty iterator. + * + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:24 $ + * @since Commons Collections 3.1 + */ +abstract class AbstractEmptyIterator <E> { + + /** + * Constructor. + */ + protected AbstractEmptyIterator() { + super(); + } + + public boolean hasNext() { + return false; + } + + public E next() { + throw new NoSuchElementException("Iterator contains no elements"); + } + + public boolean hasPrevious() { + return false; + } + + public E previous() { + throw new NoSuchElementException("Iterator contains no elements"); + } + + public int nextIndex() { + return 0; + } + + public int previousIndex() { + return -1; + } + + public void add(E obj) { + throw new UnsupportedOperationException("add() not supported for empty Iterator"); + } + + public void set(E obj) { + throw new IllegalStateException("Iterator contains no elements"); + } + + public void remove() { + throw new IllegalStateException("Iterator contains no elements"); + } + + public E getKey() { + throw new IllegalStateException("Iterator contains no elements"); + } + + public E getValue() { + throw new IllegalStateException("Iterator contains no elements"); + } + + public E setValue(E value) { + throw new IllegalStateException("Iterator contains no elements"); + } + + public void reset() { + // do nothing + } + +} diff --git a/src/org/jivesoftware/smack/util/collections/AbstractHashedMap.java b/src/org/jivesoftware/smack/util/collections/AbstractHashedMap.java new file mode 100644 index 0000000..f6fb34a --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/AbstractHashedMap.java @@ -0,0 +1,1338 @@ +// GenericsNote: Converted -- However, null keys will now be represented in the internal structures, a big change. +/* + * Copyright 2003-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.*; + +/** + * An abstract implementation of a hash-based map which provides numerous points for + * subclasses to override. + * <p/> + * This class implements all the features necessary for a subclass hash-based map. + * Key-value entries are stored in instances of the <code>HashEntry</code> class, + * which can be overridden and replaced. The iterators can similarly be replaced, + * without the need to replace the KeySet, EntrySet and Values view classes. + * <p/> + * Overridable methods are provided to change the default hashing behaviour, and + * to change how entries are added to and removed from the map. Hopefully, all you + * need for unusual subclasses is here. + * <p/> + * NOTE: From Commons Collections 3.1 this class extends AbstractMap. + * This is to provide backwards compatibility for ReferenceMap between v3.0 and v3.1. + * This extends clause will be removed in v4.0. + * + * @author java util HashMap + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $ + * @since Commons Collections 3.0 + */ +public class AbstractHashedMap <K,V> extends AbstractMap<K, V> implements IterableMap<K, V> { + + protected static final String NO_NEXT_ENTRY = "No next() entry in the iteration"; + protected static final String NO_PREVIOUS_ENTRY = "No previous() entry in the iteration"; + protected static final String REMOVE_INVALID = "remove() can only be called once after next()"; + protected static final String GETKEY_INVALID = "getKey() can only be called after next() and before remove()"; + protected static final String GETVALUE_INVALID = "getValue() can only be called after next() and before remove()"; + protected static final String SETVALUE_INVALID = "setValue() can only be called after next() and before remove()"; + + /** + * The default capacity to use + */ + protected static final int DEFAULT_CAPACITY = 16; + /** + * The default threshold to use + */ + protected static final int DEFAULT_THRESHOLD = 12; + /** + * The default load factor to use + */ + protected static final float DEFAULT_LOAD_FACTOR = 0.75f; + /** + * The maximum capacity allowed + */ + protected static final int MAXIMUM_CAPACITY = 1 << 30; + /** + * An object for masking null + */ + protected static final Object NULL = new Object(); + + /** + * Load factor, normally 0.75 + */ + protected transient float loadFactor; + /** + * The size of the map + */ + protected transient int size; + /** + * Map entries + */ + protected transient HashEntry<K, V>[] data; + /** + * Size at which to rehash + */ + protected transient int threshold; + /** + * Modification count for iterators + */ + protected transient int modCount; + /** + * Entry set + */ + protected transient EntrySet<K, V> entrySet; + /** + * Key set + */ + protected transient KeySet<K, V> keySet; + /** + * Values + */ + protected transient Values<K, V> values; + + /** + * Constructor only used in deserialization, do not use otherwise. + */ + protected AbstractHashedMap() { + super(); + } + + /** + * Constructor which performs no validation on the passed in parameters. + * + * @param initialCapacity the initial capacity, must be a power of two + * @param loadFactor the load factor, must be > 0.0f and generally < 1.0f + * @param threshold the threshold, must be sensible + */ + protected AbstractHashedMap(int initialCapacity, float loadFactor, int threshold) { + super(); + this.loadFactor = loadFactor; + this.data = new HashEntry[initialCapacity]; + this.threshold = threshold; + init(); + } + + /** + * Constructs a new, empty map with the specified initial capacity and + * default load factor. + * + * @param initialCapacity the initial capacity + * @throws IllegalArgumentException if the initial capacity is less than one + */ + protected AbstractHashedMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * Constructs a new, empty map with the specified initial capacity and + * load factor. + * + * @param initialCapacity the initial capacity + * @param loadFactor the load factor + * @throws IllegalArgumentException if the initial capacity is less than one + * @throws IllegalArgumentException if the load factor is less than or equal to zero + */ + protected AbstractHashedMap(int initialCapacity, float loadFactor) { + super(); + if (initialCapacity < 1) { + throw new IllegalArgumentException("Initial capacity must be greater than 0"); + } + if (loadFactor <= 0.0f || Float.isNaN(loadFactor)) { + throw new IllegalArgumentException("Load factor must be greater than 0"); + } + this.loadFactor = loadFactor; + this.threshold = calculateThreshold(initialCapacity, loadFactor); + initialCapacity = calculateNewCapacity(initialCapacity); + this.data = new HashEntry[initialCapacity]; + init(); + } + + /** + * Constructor copying elements from another map. + * + * @param map the map to copy + * @throws NullPointerException if the map is null + */ + protected AbstractHashedMap(Map<? extends K, ? extends V> map) { + this(Math.max(2 * map.size(), DEFAULT_CAPACITY), DEFAULT_LOAD_FACTOR); + putAll(map); + } + + /** + * Initialise subclasses during construction, cloning or deserialization. + */ + protected void init() { + } + + //----------------------------------------------------------------------- + /** + * Gets the value mapped to the key specified. + * + * @param key the key + * @return the mapped value, null if no match + */ + public V get(Object key) { + int hashCode = hash((key == null) ? NULL : key); + HashEntry<K, V> entry = data[hashIndex(hashCode, data.length)]; // no local for hash index + while (entry != null) { + if (entry.hashCode == hashCode && isEqualKey(key, entry.key)) { + return entry.getValue(); + } + entry = entry.next; + } + return null; + } + + /** + * Gets the size of the map. + * + * @return the size + */ + public int size() { + return size; + } + + /** + * Checks whether the map is currently empty. + * + * @return true if the map is currently size zero + */ + public boolean isEmpty() { + return (size == 0); + } + + //----------------------------------------------------------------------- + /** + * Checks whether the map contains the specified key. + * + * @param key the key to search for + * @return true if the map contains the key + */ + public boolean containsKey(Object key) { + int hashCode = hash((key == null) ? NULL : key); + HashEntry entry = data[hashIndex(hashCode, data.length)]; // no local for hash index + while (entry != null) { + if (entry.hashCode == hashCode && isEqualKey(key, entry.getKey())) { + return true; + } + entry = entry.next; + } + return false; + } + + /** + * Checks whether the map contains the specified value. + * + * @param value the value to search for + * @return true if the map contains the value + */ + public boolean containsValue(Object value) { + if (value == null) { + for (int i = 0, isize = data.length; i < isize; i++) { + HashEntry entry = data[i]; + while (entry != null) { + if (entry.getValue() == null) { + return true; + } + entry = entry.next; + } + } + } else { + for (int i = 0, isize = data.length; i < isize; i++) { + HashEntry entry = data[i]; + while (entry != null) { + if (isEqualValue(value, entry.getValue())) { + return true; + } + entry = entry.next; + } + } + } + return false; + } + + //----------------------------------------------------------------------- + /** + * Puts a key-value mapping into this map. + * + * @param key the key to add + * @param value the value to add + * @return the value previously mapped to this key, null if none + */ + public V put(K key, V value) { + int hashCode = hash((key == null) ? NULL : key); + int index = hashIndex(hashCode, data.length); + HashEntry<K, V> entry = data[index]; + while (entry != null) { + if (entry.hashCode == hashCode && isEqualKey(key, entry.getKey())) { + V oldValue = entry.getValue(); + updateEntry(entry, value); + return oldValue; + } + entry = entry.next; + } + addMapping(index, hashCode, key, value); + return null; + } + + /** + * Puts all the values from the specified map into this map. + * <p/> + * This implementation iterates around the specified map and + * uses {@link #put(Object, Object)}. + * + * @param map the map to add + * @throws NullPointerException if the map is null + */ + public void putAll(Map<? extends K, ? extends V> map) { + int mapSize = map.size(); + if (mapSize == 0) { + return; + } + int newSize = (int) ((size + mapSize) / loadFactor + 1); + ensureCapacity(calculateNewCapacity(newSize)); + // Have to cast here because of compiler inference problems. + for (Iterator it = map.entrySet().iterator(); it.hasNext();) { + Map.Entry<? extends K, ? extends V> entry = (Map.Entry<? extends K, ? extends V>) it.next(); + put(entry.getKey(), entry.getValue()); + } + } + + /** + * Removes the specified mapping from this map. + * + * @param key the mapping to remove + * @return the value mapped to the removed key, null if key not in map + */ + public V remove(Object key) { + int hashCode = hash((key == null) ? NULL : key); + int index = hashIndex(hashCode, data.length); + HashEntry<K, V> entry = data[index]; + HashEntry<K, V> previous = null; + while (entry != null) { + if (entry.hashCode == hashCode && isEqualKey(key, entry.getKey())) { + V oldValue = entry.getValue(); + removeMapping(entry, index, previous); + return oldValue; + } + previous = entry; + entry = entry.next; + } + return null; + } + + /** + * Clears the map, resetting the size to zero and nullifying references + * to avoid garbage collection issues. + */ + public void clear() { + modCount++; + HashEntry[] data = this.data; + for (int i = data.length - 1; i >= 0; i--) { + data[i] = null; + } + size = 0; + } + + /** + * Gets the hash code for the key specified. + * This implementation uses the additional hashing routine from JDK1.4. + * Subclasses can override this to return alternate hash codes. + * + * @param key the key to get a hash code for + * @return the hash code + */ + protected int hash(Object key) { + // same as JDK 1.4 + int h = key.hashCode(); + h += ~(h << 9); + h ^= (h >>> 14); + h += (h << 4); + h ^= (h >>> 10); + return h; + } + + /** + * Compares two keys, in internal converted form, to see if they are equal. + * This implementation uses the equals method. + * Subclasses can override this to match differently. + * + * @param key1 the first key to compare passed in from outside + * @param key2 the second key extracted from the entry via <code>entry.key</code> + * @return true if equal + */ + protected boolean isEqualKey(Object key1, Object key2) { + return (key1 == key2 || ((key1 != null) && key1.equals(key2))); + } + + /** + * Compares two values, in external form, to see if they are equal. + * This implementation uses the equals method and assumes neither value is null. + * Subclasses can override this to match differently. + * + * @param value1 the first value to compare passed in from outside + * @param value2 the second value extracted from the entry via <code>getValue()</code> + * @return true if equal + */ + protected boolean isEqualValue(Object value1, Object value2) { + return (value1 == value2 || value1.equals(value2)); + } + + /** + * Gets the index into the data storage for the hashCode specified. + * This implementation uses the least significant bits of the hashCode. + * Subclasses can override this to return alternate bucketing. + * + * @param hashCode the hash code to use + * @param dataSize the size of the data to pick a bucket from + * @return the bucket index + */ + protected int hashIndex(int hashCode, int dataSize) { + return hashCode & (dataSize - 1); + } + + //----------------------------------------------------------------------- + /** + * Gets the entry mapped to the key specified. + * <p/> + * This method exists for subclasses that may need to perform a multi-step + * process accessing the entry. The public methods in this class don't use this + * method to gain a small performance boost. + * + * @param key the key + * @return the entry, null if no match + */ + protected HashEntry<K, V> getEntry(Object key) { + int hashCode = hash((key == null) ? NULL : key); + HashEntry<K, V> entry = data[hashIndex(hashCode, data.length)]; // no local for hash index + while (entry != null) { + if (entry.hashCode == hashCode && isEqualKey(key, entry.getKey())) { + return entry; + } + entry = entry.next; + } + return null; + } + + //----------------------------------------------------------------------- + /** + * Updates an existing key-value mapping to change the value. + * <p/> + * This implementation calls <code>setValue()</code> on the entry. + * Subclasses could override to handle changes to the map. + * + * @param entry the entry to update + * @param newValue the new value to store + */ + protected void updateEntry(HashEntry<K, V> entry, V newValue) { + entry.setValue(newValue); + } + + /** + * Reuses an existing key-value mapping, storing completely new data. + * <p/> + * This implementation sets all the data fields on the entry. + * Subclasses could populate additional entry fields. + * + * @param entry the entry to update, not null + * @param hashIndex the index in the data array + * @param hashCode the hash code of the key to add + * @param key the key to add + * @param value the value to add + */ + protected void reuseEntry(HashEntry<K, V> entry, int hashIndex, int hashCode, K key, V value) { + entry.next = data[hashIndex]; + entry.hashCode = hashCode; + entry.key = key; + entry.value = value; + } + + //----------------------------------------------------------------------- + /** + * Adds a new key-value mapping into this map. + * <p/> + * This implementation calls <code>createEntry()</code>, <code>addEntry()</code> + * and <code>checkCapacity()</code>. + * It also handles changes to <code>modCount</code> and <code>size</code>. + * Subclasses could override to fully control adds to the map. + * + * @param hashIndex the index into the data array to store at + * @param hashCode the hash code of the key to add + * @param key the key to add + * @param value the value to add + */ + protected void addMapping(int hashIndex, int hashCode, K key, V value) { + modCount++; + HashEntry<K, V> entry = createEntry(data[hashIndex], hashCode, key, value); + addEntry(entry, hashIndex); + size++; + checkCapacity(); + } + + /** + * Creates an entry to store the key-value data. + * <p/> + * This implementation creates a new HashEntry instance. + * Subclasses can override this to return a different storage class, + * or implement caching. + * + * @param next the next entry in sequence + * @param hashCode the hash code to use + * @param key the key to store + * @param value the value to store + * @return the newly created entry + */ + protected HashEntry<K, V> createEntry(HashEntry<K, V> next, int hashCode, K key, V value) { + return new HashEntry<K, V>(next, hashCode, key, value); + } + + /** + * Adds an entry into this map. + * <p/> + * This implementation adds the entry to the data storage table. + * Subclasses could override to handle changes to the map. + * + * @param entry the entry to add + * @param hashIndex the index into the data array to store at + */ + protected void addEntry(HashEntry<K, V> entry, int hashIndex) { + data[hashIndex] = entry; + } + + //----------------------------------------------------------------------- + /** + * Removes a mapping from the map. + * <p/> + * This implementation calls <code>removeEntry()</code> and <code>destroyEntry()</code>. + * It also handles changes to <code>modCount</code> and <code>size</code>. + * Subclasses could override to fully control removals from the map. + * + * @param entry the entry to remove + * @param hashIndex the index into the data structure + * @param previous the previous entry in the chain + */ + protected void removeMapping(HashEntry<K, V> entry, int hashIndex, HashEntry<K, V> previous) { + modCount++; + removeEntry(entry, hashIndex, previous); + size--; + destroyEntry(entry); + } + + /** + * Removes an entry from the chain stored in a particular index. + * <p/> + * This implementation removes the entry from the data storage table. + * The size is not updated. + * Subclasses could override to handle changes to the map. + * + * @param entry the entry to remove + * @param hashIndex the index into the data structure + * @param previous the previous entry in the chain + */ + protected void removeEntry(HashEntry<K, V> entry, int hashIndex, HashEntry<K, V> previous) { + if (previous == null) { + data[hashIndex] = entry.next; + } else { + previous.next = entry.next; + } + } + + /** + * Kills an entry ready for the garbage collector. + * <p/> + * This implementation prepares the HashEntry for garbage collection. + * Subclasses can override this to implement caching (override clear as well). + * + * @param entry the entry to destroy + */ + protected void destroyEntry(HashEntry<K, V> entry) { + entry.next = null; + entry.key = null; + entry.value = null; + } + + //----------------------------------------------------------------------- + /** + * Checks the capacity of the map and enlarges it if necessary. + * <p/> + * This implementation uses the threshold to check if the map needs enlarging + */ + protected void checkCapacity() { + if (size >= threshold) { + int newCapacity = data.length * 2; + if (newCapacity <= MAXIMUM_CAPACITY) { + ensureCapacity(newCapacity); + } + } + } + + /** + * Changes the size of the data structure to the capacity proposed. + * + * @param newCapacity the new capacity of the array (a power of two, less or equal to max) + */ + protected void ensureCapacity(int newCapacity) { + int oldCapacity = data.length; + if (newCapacity <= oldCapacity) { + return; + } + if (size == 0) { + threshold = calculateThreshold(newCapacity, loadFactor); + data = new HashEntry[newCapacity]; + } else { + HashEntry<K, V> oldEntries[] = data; + HashEntry<K, V> newEntries[] = new HashEntry[newCapacity]; + + modCount++; + for (int i = oldCapacity - 1; i >= 0; i--) { + HashEntry<K, V> entry = oldEntries[i]; + if (entry != null) { + oldEntries[i] = null; // gc + do { + HashEntry<K, V> next = entry.next; + int index = hashIndex(entry.hashCode, newCapacity); + entry.next = newEntries[index]; + newEntries[index] = entry; + entry = next; + } while (entry != null); + } + } + threshold = calculateThreshold(newCapacity, loadFactor); + data = newEntries; + } + } + + /** + * Calculates the new capacity of the map. + * This implementation normalizes the capacity to a power of two. + * + * @param proposedCapacity the proposed capacity + * @return the normalized new capacity + */ + protected int calculateNewCapacity(int proposedCapacity) { + int newCapacity = 1; + if (proposedCapacity > MAXIMUM_CAPACITY) { + newCapacity = MAXIMUM_CAPACITY; + } else { + while (newCapacity < proposedCapacity) { + newCapacity <<= 1; // multiply by two + } + if (newCapacity > MAXIMUM_CAPACITY) { + newCapacity = MAXIMUM_CAPACITY; + } + } + return newCapacity; + } + + /** + * Calculates the new threshold of the map, where it will be resized. + * This implementation uses the load factor. + * + * @param newCapacity the new capacity + * @param factor the load factor + * @return the new resize threshold + */ + protected int calculateThreshold(int newCapacity, float factor) { + return (int) (newCapacity * factor); + } + + //----------------------------------------------------------------------- + /** + * Gets the <code>next</code> field from a <code>HashEntry</code>. + * Used in subclasses that have no visibility of the field. + * + * @param entry the entry to query, must not be null + * @return the <code>next</code> field of the entry + * @throws NullPointerException if the entry is null + * @since Commons Collections 3.1 + */ + protected HashEntry<K, V> entryNext(HashEntry<K, V> entry) { + return entry.next; + } + + /** + * Gets the <code>hashCode</code> field from a <code>HashEntry</code>. + * Used in subclasses that have no visibility of the field. + * + * @param entry the entry to query, must not be null + * @return the <code>hashCode</code> field of the entry + * @throws NullPointerException if the entry is null + * @since Commons Collections 3.1 + */ + protected int entryHashCode(HashEntry<K, V> entry) { + return entry.hashCode; + } + + /** + * Gets the <code>key</code> field from a <code>HashEntry</code>. + * Used in subclasses that have no visibility of the field. + * + * @param entry the entry to query, must not be null + * @return the <code>key</code> field of the entry + * @throws NullPointerException if the entry is null + * @since Commons Collections 3.1 + */ + protected K entryKey(HashEntry<K, V> entry) { + return entry.key; + } + + /** + * Gets the <code>value</code> field from a <code>HashEntry</code>. + * Used in subclasses that have no visibility of the field. + * + * @param entry the entry to query, must not be null + * @return the <code>value</code> field of the entry + * @throws NullPointerException if the entry is null + * @since Commons Collections 3.1 + */ + protected V entryValue(HashEntry<K, V> entry) { + return entry.value; + } + + //----------------------------------------------------------------------- + /** + * Gets an iterator over the map. + * Changes made to the iterator affect this map. + * <p/> + * A MapIterator returns the keys in the map. It also provides convenient + * methods to get the key and value, and set the value. + * It avoids the need to create an entrySet/keySet/values object. + * It also avoids creating the Map.Entry object. + * + * @return the map iterator + */ + public MapIterator<K, V> mapIterator() { + if (size == 0) { + return EmptyMapIterator.INSTANCE; + } + return new HashMapIterator<K, V>(this); + } + + /** + * MapIterator implementation. + */ + protected static class HashMapIterator <K,V> extends HashIterator<K, V> implements MapIterator<K, V> { + + protected HashMapIterator(AbstractHashedMap<K, V> parent) { + super(parent); + } + + public K next() { + return super.nextEntry().getKey(); + } + + public K getKey() { + HashEntry<K, V> current = currentEntry(); + if (current == null) { + throw new IllegalStateException(AbstractHashedMap.GETKEY_INVALID); + } + return current.getKey(); + } + + public V getValue() { + HashEntry<K, V> current = currentEntry(); + if (current == null) { + throw new IllegalStateException(AbstractHashedMap.GETVALUE_INVALID); + } + return current.getValue(); + } + + public V setValue(V value) { + HashEntry<K, V> current = currentEntry(); + if (current == null) { + throw new IllegalStateException(AbstractHashedMap.SETVALUE_INVALID); + } + return current.setValue(value); + } + } + + //----------------------------------------------------------------------- + /** + * Gets the entrySet view of the map. + * Changes made to the view affect this map. + * To simply iterate through the entries, use {@link #mapIterator()}. + * + * @return the entrySet view + */ + public Set<Map.Entry<K, V>> entrySet() { + if (entrySet == null) { + entrySet = new EntrySet<K, V>(this); + } + return entrySet; + } + + /** + * Creates an entry set iterator. + * Subclasses can override this to return iterators with different properties. + * + * @return the entrySet iterator + */ + protected Iterator<Map.Entry<K, V>> createEntrySetIterator() { + if (size() == 0) { + return EmptyIterator.INSTANCE; + } + return new EntrySetIterator<K, V>(this); + } + + /** + * EntrySet implementation. + */ + protected static class EntrySet <K,V> extends AbstractSet<Map.Entry<K, V>> { + /** + * The parent map + */ + protected final AbstractHashedMap<K, V> parent; + + protected EntrySet(AbstractHashedMap<K, V> parent) { + super(); + this.parent = parent; + } + + public int size() { + return parent.size(); + } + + public void clear() { + parent.clear(); + } + + public boolean contains(Map.Entry<K, V> entry) { + Map.Entry<K, V> e = entry; + Entry<K, V> match = parent.getEntry(e.getKey()); + return (match != null && match.equals(e)); + } + + public boolean remove(Object obj) { + if (obj instanceof Map.Entry == false) { + return false; + } + if (contains(obj) == false) { + return false; + } + Map.Entry<K, V> entry = (Map.Entry<K, V>) obj; + K key = entry.getKey(); + parent.remove(key); + return true; + } + + public Iterator<Map.Entry<K, V>> iterator() { + return parent.createEntrySetIterator(); + } + } + + /** + * EntrySet iterator. + */ + protected static class EntrySetIterator <K,V> extends HashIterator<K, V> implements Iterator<Map.Entry<K, V>> { + + protected EntrySetIterator(AbstractHashedMap<K, V> parent) { + super(parent); + } + + public HashEntry<K, V> next() { + return super.nextEntry(); + } + } + + //----------------------------------------------------------------------- + /** + * Gets the keySet view of the map. + * Changes made to the view affect this map. + * To simply iterate through the keys, use {@link #mapIterator()}. + * + * @return the keySet view + */ + public Set<K> keySet() { + if (keySet == null) { + keySet = new KeySet<K, V>(this); + } + return keySet; + } + + /** + * Creates a key set iterator. + * Subclasses can override this to return iterators with different properties. + * + * @return the keySet iterator + */ + protected Iterator<K> createKeySetIterator() { + if (size() == 0) { + return EmptyIterator.INSTANCE; + } + return new KeySetIterator<K, V>(this); + } + + /** + * KeySet implementation. + */ + protected static class KeySet <K,V> extends AbstractSet<K> { + /** + * The parent map + */ + protected final AbstractHashedMap<K, V> parent; + + protected KeySet(AbstractHashedMap<K, V> parent) { + super(); + this.parent = parent; + } + + public int size() { + return parent.size(); + } + + public void clear() { + parent.clear(); + } + + public boolean contains(Object key) { + return parent.containsKey(key); + } + + public boolean remove(Object key) { + boolean result = parent.containsKey(key); + parent.remove(key); + return result; + } + + public Iterator<K> iterator() { + return parent.createKeySetIterator(); + } + } + + /** + * KeySet iterator. + */ + protected static class KeySetIterator <K,V> extends HashIterator<K, V> implements Iterator<K> { + + protected KeySetIterator(AbstractHashedMap<K, V> parent) { + super(parent); + } + + public K next() { + return super.nextEntry().getKey(); + } + } + + //----------------------------------------------------------------------- + /** + * Gets the values view of the map. + * Changes made to the view affect this map. + * To simply iterate through the values, use {@link #mapIterator()}. + * + * @return the values view + */ + public Collection<V> values() { + if (values == null) { + values = new Values(this); + } + return values; + } + + /** + * Creates a values iterator. + * Subclasses can override this to return iterators with different properties. + * + * @return the values iterator + */ + protected Iterator<V> createValuesIterator() { + if (size() == 0) { + return EmptyIterator.INSTANCE; + } + return new ValuesIterator<K, V>(this); + } + + /** + * Values implementation. + */ + protected static class Values <K,V> extends AbstractCollection<V> { + /** + * The parent map + */ + protected final AbstractHashedMap<K, V> parent; + + protected Values(AbstractHashedMap<K, V> parent) { + super(); + this.parent = parent; + } + + public int size() { + return parent.size(); + } + + public void clear() { + parent.clear(); + } + + public boolean contains(Object value) { + return parent.containsValue(value); + } + + public Iterator<V> iterator() { + return parent.createValuesIterator(); + } + } + + /** + * Values iterator. + */ + protected static class ValuesIterator <K,V> extends HashIterator<K, V> implements Iterator<V> { + + protected ValuesIterator(AbstractHashedMap<K, V> parent) { + super(parent); + } + + public V next() { + return super.nextEntry().getValue(); + } + } + + //----------------------------------------------------------------------- + /** + * HashEntry used to store the data. + * <p/> + * If you subclass <code>AbstractHashedMap</code> but not <code>HashEntry</code> + * then you will not be able to access the protected fields. + * The <code>entryXxx()</code> methods on <code>AbstractHashedMap</code> exist + * to provide the necessary access. + */ + protected static class HashEntry <K,V> implements Map.Entry<K, V>, KeyValue<K, V> { + /** + * The next entry in the hash chain + */ + protected HashEntry<K, V> next; + /** + * The hash code of the key + */ + protected int hashCode; + /** + * The key + */ + private K key; + /** + * The value + */ + private V value; + + protected HashEntry(HashEntry<K, V> next, int hashCode, K key, V value) { + super(); + this.next = next; + this.hashCode = hashCode; + this.key = key; + this.value = value; + } + + public K getKey() { + return key; + } + + public void setKey(K key) { + this.key = key; + } + + public V getValue() { + return value; + } + + public V setValue(V value) { + V old = this.value; + this.value = value; + return old; + } + + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Map.Entry == false) { + return false; + } + Map.Entry other = (Map.Entry) obj; + return (getKey() == null ? other.getKey() == null : getKey().equals(other.getKey())) && (getValue() == null ? other.getValue() == null : getValue().equals(other.getValue())); + } + + public int hashCode() { + return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode()); + } + + public String toString() { + return new StringBuilder().append(getKey()).append('=').append(getValue()).toString(); + } + } + + /** + * Base Iterator + */ + protected static abstract class HashIterator <K,V> { + + /** + * The parent map + */ + protected final AbstractHashedMap parent; + /** + * The current index into the array of buckets + */ + protected int hashIndex; + /** + * The last returned entry + */ + protected HashEntry<K, V> last; + /** + * The next entry + */ + protected HashEntry<K, V> next; + /** + * The modification count expected + */ + protected int expectedModCount; + + protected HashIterator(AbstractHashedMap<K, V> parent) { + super(); + this.parent = parent; + HashEntry<K, V>[] data = parent.data; + int i = data.length; + HashEntry<K, V> next = null; + while (i > 0 && next == null) { + next = data[--i]; + } + this.next = next; + this.hashIndex = i; + this.expectedModCount = parent.modCount; + } + + public boolean hasNext() { + return (next != null); + } + + protected HashEntry<K, V> nextEntry() { + if (parent.modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + HashEntry<K, V> newCurrent = next; + if (newCurrent == null) { + throw new NoSuchElementException(AbstractHashedMap.NO_NEXT_ENTRY); + } + HashEntry<K, V>[] data = parent.data; + int i = hashIndex; + HashEntry<K, V> n = newCurrent.next; + while (n == null && i > 0) { + n = data[--i]; + } + next = n; + hashIndex = i; + last = newCurrent; + return newCurrent; + } + + protected HashEntry<K, V> currentEntry() { + return last; + } + + public void remove() { + if (last == null) { + throw new IllegalStateException(AbstractHashedMap.REMOVE_INVALID); + } + if (parent.modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + parent.remove(last.getKey()); + last = null; + expectedModCount = parent.modCount; + } + + public String toString() { + if (last != null) { + return "Iterator[" + last.getKey() + "=" + last.getValue() + "]"; + } else { + return "Iterator[]"; + } + } + } + + //----------------------------------------------------------------------- + /** + * Writes the map data to the stream. This method must be overridden if a + * subclass must be setup before <code>put()</code> is used. + * <p/> + * Serialization is not one of the JDK's nicest topics. Normal serialization will + * initialise the superclass before the subclass. Sometimes however, this isn't + * what you want, as in this case the <code>put()</code> method on read can be + * affected by subclass state. + * <p/> + * The solution adopted here is to serialize the state data of this class in + * this protected method. This method must be called by the + * <code>writeObject()</code> of the first serializable subclass. + * <p/> + * Subclasses may override if they have a specific field that must be present + * on read before this implementation will work. Generally, the read determines + * what must be serialized here, if anything. + * + * @param out the output stream + */ + protected void doWriteObject(ObjectOutputStream out) throws IOException { + out.writeFloat(loadFactor); + out.writeInt(data.length); + out.writeInt(size); + for (MapIterator it = mapIterator(); it.hasNext();) { + out.writeObject(it.next()); + out.writeObject(it.getValue()); + } + } + + /** + * Reads the map data from the stream. This method must be overridden if a + * subclass must be setup before <code>put()</code> is used. + * <p/> + * Serialization is not one of the JDK's nicest topics. Normal serialization will + * initialise the superclass before the subclass. Sometimes however, this isn't + * what you want, as in this case the <code>put()</code> method on read can be + * affected by subclass state. + * <p/> + * The solution adopted here is to deserialize the state data of this class in + * this protected method. This method must be called by the + * <code>readObject()</code> of the first serializable subclass. + * <p/> + * Subclasses may override if the subclass has a specific field that must be present + * before <code>put()</code> or <code>calculateThreshold()</code> will work correctly. + * + * @param in the input stream + */ + protected void doReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + loadFactor = in.readFloat(); + int capacity = in.readInt(); + int size = in.readInt(); + init(); + data = new HashEntry[capacity]; + for (int i = 0; i < size; i++) { + K key = (K) in.readObject(); + V value = (V) in.readObject(); + put(key, value); + } + threshold = calculateThreshold(data.length, loadFactor); + } + + //----------------------------------------------------------------------- + /** + * Clones the map without cloning the keys or values. + * <p/> + * To implement <code>clone()</code>, a subclass must implement the + * <code>Cloneable</code> interface and make this method public. + * + * @return a shallow clone + */ + protected Object clone() { + try { + AbstractHashedMap cloned = (AbstractHashedMap) super.clone(); + cloned.data = new HashEntry[data.length]; + cloned.entrySet = null; + cloned.keySet = null; + cloned.values = null; + cloned.modCount = 0; + cloned.size = 0; + cloned.init(); + cloned.putAll(this); + return cloned; + + } catch (CloneNotSupportedException ex) { + return null; // should never happen + } + } + + /** + * Compares this map with another. + * + * @param obj the object to compare to + * @return true if equal + */ + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Map == false) { + return false; + } + Map map = (Map) obj; + if (map.size() != size()) { + return false; + } + MapIterator it = mapIterator(); + try { + while (it.hasNext()) { + Object key = it.next(); + Object value = it.getValue(); + if (value == null) { + if (map.get(key) != null || map.containsKey(key) == false) { + return false; + } + } else { + if (value.equals(map.get(key)) == false) { + return false; + } + } + } + } catch (ClassCastException ignored) { + return false; + } catch (NullPointerException ignored) { + return false; + } + return true; + } + + /** + * Gets the standard Map hashCode. + * + * @return the hash code defined in the Map interface + */ + public int hashCode() { + int total = 0; + Iterator it = createEntrySetIterator(); + while (it.hasNext()) { + total += it.next().hashCode(); + } + return total; + } + + /** + * Gets the map as a String. + * + * @return a string version of the map + */ + public String toString() { + if (size() == 0) { + return "{}"; + } + StringBuilder buf = new StringBuilder(32 * size()); + buf.append('{'); + + MapIterator it = mapIterator(); + boolean hasNext = it.hasNext(); + while (hasNext) { + Object key = it.next(); + Object value = it.getValue(); + buf.append(key == this ? "(this Map)" : key).append('=').append(value == this ? "(this Map)" : value); + + hasNext = it.hasNext(); + if (hasNext) { + buf.append(',').append(' '); + } + } + + buf.append('}'); + return buf.toString(); + } +} diff --git a/src/org/jivesoftware/smack/util/collections/AbstractKeyValue.java b/src/org/jivesoftware/smack/util/collections/AbstractKeyValue.java new file mode 100644 index 0000000..decc342 --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/AbstractKeyValue.java @@ -0,0 +1,80 @@ +// GenericsNote: Converted. +/* + * Copyright 2003-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + + +/** + * Abstract pair class to assist with creating KeyValue and MapEntry implementations. + * + * @author James Strachan + * @author Michael A. Smith + * @author Neil O'Toole + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $ + * @since Commons Collections 3.0 + */ +public abstract class AbstractKeyValue <K,V> implements KeyValue<K, V> { + + /** + * The key + */ + protected K key; + /** + * The value + */ + protected V value; + + /** + * Constructs a new pair with the specified key and given value. + * + * @param key the key for the entry, may be null + * @param value the value for the entry, may be null + */ + protected AbstractKeyValue(K key, V value) { + super(); + this.key = key; + this.value = value; + } + + /** + * Gets the key from the pair. + * + * @return the key + */ + public K getKey() { + return key; + } + + /** + * Gets the value from the pair. + * + * @return the value + */ + public V getValue() { + return value; + } + + /** + * Gets a debugging String view of the pair. + * + * @return a String view of the entry + */ + public String toString() { + return new StringBuilder().append(getKey()).append('=').append(getValue()).toString(); + } + +} diff --git a/src/org/jivesoftware/smack/util/collections/AbstractMapEntry.java b/src/org/jivesoftware/smack/util/collections/AbstractMapEntry.java new file mode 100644 index 0000000..2feb308 --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/AbstractMapEntry.java @@ -0,0 +1,89 @@ +// GenericsNote: Converted. +/* + * Copyright 2003-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +import java.util.Map; + +/** + * Abstract Pair class to assist with creating correct Map Entry implementations. + * + * @author James Strachan + * @author Michael A. Smith + * @author Neil O'Toole + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $ + * @since Commons Collections 3.0 + */ +public abstract class AbstractMapEntry <K,V> extends AbstractKeyValue<K, V> implements Map.Entry<K, V> { + + /** + * Constructs a new entry with the given key and given value. + * + * @param key the key for the entry, may be null + * @param value the value for the entry, may be null + */ + protected AbstractMapEntry(K key, V value) { + super(key, value); + } + + // Map.Entry interface + //------------------------------------------------------------------------- + /** + * Sets the value stored in this Map Entry. + * <p/> + * This Map Entry is not connected to a Map, so only the local data is changed. + * + * @param value the new value + * @return the previous value + */ + public V setValue(V value) { + V answer = this.value; + this.value = value; + return answer; + } + + /** + * Compares this Map Entry with another Map Entry. + * <p/> + * Implemented per API documentation of {@link java.util.Map.Entry#equals(Object)} + * + * @param obj the object to compare to + * @return true if equal key and value + */ + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Map.Entry == false) { + return false; + } + Map.Entry other = (Map.Entry) obj; + return (getKey() == null ? other.getKey() == null : getKey().equals(other.getKey())) && (getValue() == null ? other.getValue() == null : getValue().equals(other.getValue())); + } + + /** + * Gets a hashCode compatible with the equals method. + * <p/> + * Implemented per API documentation of {@link java.util.Map.Entry#hashCode()} + * + * @return a suitable hash code + */ + public int hashCode() { + return (getKey() == null ? 0 : getKey().hashCode()) ^ (getValue() == null ? 0 : getValue().hashCode()); + } + +} diff --git a/src/org/jivesoftware/smack/util/collections/AbstractReferenceMap.java b/src/org/jivesoftware/smack/util/collections/AbstractReferenceMap.java new file mode 100644 index 0000000..b57f17d --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/AbstractReferenceMap.java @@ -0,0 +1,1025 @@ +// Converted, with some major refactors required. Not as memory-efficient as before, could use additional refactoring. +// Perhaps use four different types of HashEntry classes for max efficiency: +// normal HashEntry for HARD,HARD +// HardRefEntry for HARD,(SOFT|WEAK) +// RefHardEntry for (SOFT|WEAK),HARD +// RefRefEntry for (SOFT|WEAK),(SOFT|WEAK) +/* + * Copyright 2002-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.*; + +/** + * An abstract implementation of a hash-based map that allows the entries to + * be removed by the garbage collector. + * <p/> + * This class implements all the features necessary for a subclass reference + * hash-based map. Key-value entries are stored in instances of the + * <code>ReferenceEntry</code> class which can be overridden and replaced. + * The iterators can similarly be replaced, without the need to replace the KeySet, + * EntrySet and Values view classes. + * <p/> + * Overridable methods are provided to change the default hashing behaviour, and + * to change how entries are added to and removed from the map. Hopefully, all you + * need for unusual subclasses is here. + * <p/> + * When you construct an <code>AbstractReferenceMap</code>, you can specify what + * kind of references are used to store the map's keys and values. + * If non-hard references are used, then the garbage collector can remove + * mappings if a key or value becomes unreachable, or if the JVM's memory is + * running low. For information on how the different reference types behave, + * see {@link Reference}. + * <p/> + * Different types of references can be specified for keys and values. + * The keys can be configured to be weak but the values hard, + * in which case this class will behave like a + * <a href="http://java.sun.com/j2se/1.4/docs/api/java/util/WeakHashMap.html"> + * <code>WeakHashMap</code></a>. However, you can also specify hard keys and + * weak values, or any other combination. The default constructor uses + * hard keys and soft values, providing a memory-sensitive cache. + * <p/> + * This {@link Map} implementation does <i>not</i> allow null elements. + * Attempting to add a null key or value to the map will raise a + * <code>NullPointerException</code>. + * <p/> + * All the available iterators can be reset back to the start by casting to + * <code>ResettableIterator</code> and calling <code>reset()</code>. + * <p/> + * This implementation is not synchronized. + * You can use {@link java.util.Collections#synchronizedMap} to + * provide synchronized access to a <code>ReferenceMap</code>. + * + * @author Paul Jack + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $ + * @see java.lang.ref.Reference + * @since Commons Collections 3.1 (extracted from ReferenceMap in 3.0) + */ +public abstract class AbstractReferenceMap <K,V> extends AbstractHashedMap<K, V> { + + /** + * Constant indicating that hard references should be used + */ + public static final int HARD = 0; + + /** + * Constant indicating that soft references should be used + */ + public static final int SOFT = 1; + + /** + * Constant indicating that weak references should be used + */ + public static final int WEAK = 2; + + /** + * The reference type for keys. Must be HARD, SOFT, WEAK. + * + * @serial + */ + protected int keyType; + + /** + * The reference type for values. Must be HARD, SOFT, WEAK. + * + * @serial + */ + protected int valueType; + + /** + * Should the value be automatically purged when the associated key has been collected? + */ + protected boolean purgeValues; + + /** + * ReferenceQueue used to eliminate stale mappings. + * See purge. + */ + private transient ReferenceQueue queue; + + //----------------------------------------------------------------------- + /** + * Constructor used during deserialization. + */ + protected AbstractReferenceMap() { + super(); + } + + /** + * Constructs a new empty map with the specified reference types, + * load factor and initial capacity. + * + * @param keyType the type of reference to use for keys; + * must be {@link #SOFT} or {@link #WEAK} + * @param valueType the type of reference to use for values; + * must be {@link #SOFT} or {@link #WEAK} + * @param capacity the initial capacity for the map + * @param loadFactor the load factor for the map + * @param purgeValues should the value be automatically purged when the + * key is garbage collected + */ + protected AbstractReferenceMap(int keyType, int valueType, int capacity, float loadFactor, boolean purgeValues) { + super(capacity, loadFactor); + verify("keyType", keyType); + verify("valueType", valueType); + this.keyType = keyType; + this.valueType = valueType; + this.purgeValues = purgeValues; + } + + /** + * Initialise this subclass during construction, cloning or deserialization. + */ + protected void init() { + queue = new ReferenceQueue(); + } + + //----------------------------------------------------------------------- + /** + * Checks the type int is a valid value. + * + * @param name the name for error messages + * @param type the type value to check + * @throws IllegalArgumentException if the value if invalid + */ + private static void verify(String name, int type) { + if ((type < HARD) || (type > WEAK)) { + throw new IllegalArgumentException(name + " must be HARD, SOFT, WEAK."); + } + } + + //----------------------------------------------------------------------- + /** + * Gets the size of the map. + * + * @return the size + */ + public int size() { + purgeBeforeRead(); + return super.size(); + } + + /** + * Checks whether the map is currently empty. + * + * @return true if the map is currently size zero + */ + public boolean isEmpty() { + purgeBeforeRead(); + return super.isEmpty(); + } + + /** + * Checks whether the map contains the specified key. + * + * @param key the key to search for + * @return true if the map contains the key + */ + public boolean containsKey(Object key) { + purgeBeforeRead(); + Entry entry = getEntry(key); + if (entry == null) { + return false; + } + return (entry.getValue() != null); + } + + /** + * Checks whether the map contains the specified value. + * + * @param value the value to search for + * @return true if the map contains the value + */ + public boolean containsValue(Object value) { + purgeBeforeRead(); + if (value == null) { + return false; + } + return super.containsValue(value); + } + + /** + * Gets the value mapped to the key specified. + * + * @param key the key + * @return the mapped value, null if no match + */ + public V get(Object key) { + purgeBeforeRead(); + Entry<K, V> entry = getEntry(key); + if (entry == null) { + return null; + } + return entry.getValue(); + } + + + /** + * Puts a key-value mapping into this map. + * Neither the key nor the value may be null. + * + * @param key the key to add, must not be null + * @param value the value to add, must not be null + * @return the value previously mapped to this key, null if none + * @throws NullPointerException if either the key or value is null + */ + public V put(K key, V value) { + if (key == null) { + throw new NullPointerException("null keys not allowed"); + } + if (value == null) { + throw new NullPointerException("null values not allowed"); + } + + purgeBeforeWrite(); + return super.put(key, value); + } + + /** + * Removes the specified mapping from this map. + * + * @param key the mapping to remove + * @return the value mapped to the removed key, null if key not in map + */ + public V remove(Object key) { + if (key == null) { + return null; + } + purgeBeforeWrite(); + return super.remove(key); + } + + /** + * Clears this map. + */ + public void clear() { + super.clear(); + while (queue.poll() != null) { + } // drain the queue + } + + //----------------------------------------------------------------------- + /** + * Gets a MapIterator over the reference map. + * The iterator only returns valid key/value pairs. + * + * @return a map iterator + */ + public MapIterator<K, V> mapIterator() { + return new ReferenceMapIterator<K, V>(this); + } + + /** + * Returns a set view of this map's entries. + * An iterator returned entry is valid until <code>next()</code> is called again. + * The <code>setValue()</code> method on the <code>toArray</code> entries has no effect. + * + * @return a set view of this map's entries + */ + public Set<Map.Entry<K, V>> entrySet() { + if (entrySet == null) { + entrySet = new ReferenceEntrySet<K, V>(this); + } + return entrySet; + } + + /** + * Returns a set view of this map's keys. + * + * @return a set view of this map's keys + */ + public Set<K> keySet() { + if (keySet == null) { + keySet = new ReferenceKeySet<K, V>(this); + } + return keySet; + } + + /** + * Returns a collection view of this map's values. + * + * @return a set view of this map's values + */ + public Collection<V> values() { + if (values == null) { + values = new ReferenceValues<K, V>(this); + } + return values; + } + + //----------------------------------------------------------------------- + /** + * Purges stale mappings from this map before read operations. + * <p/> + * This implementation calls {@link #purge()} to maintain a consistent state. + */ + protected void purgeBeforeRead() { + purge(); + } + + /** + * Purges stale mappings from this map before write operations. + * <p/> + * This implementation calls {@link #purge()} to maintain a consistent state. + */ + protected void purgeBeforeWrite() { + purge(); + } + + /** + * Purges stale mappings from this map. + * <p/> + * Note that this method is not synchronized! Special + * care must be taken if, for instance, you want stale + * mappings to be removed on a periodic basis by some + * background thread. + */ + protected void purge() { + Reference ref = queue.poll(); + while (ref != null) { + purge(ref); + ref = queue.poll(); + } + } + + /** + * Purges the specified reference. + * + * @param ref the reference to purge + */ + protected void purge(Reference ref) { + // The hashCode of the reference is the hashCode of the + // mapping key, even if the reference refers to the + // mapping value... + int hash = ref.hashCode(); + int index = hashIndex(hash, data.length); + HashEntry<K, V> previous = null; + HashEntry<K, V> entry = data[index]; + while (entry != null) { + if (((ReferenceEntry<K, V>) entry).purge(ref)) { + if (previous == null) { + data[index] = entry.next; + } else { + previous.next = entry.next; + } + this.size--; + return; + } + previous = entry; + entry = entry.next; + } + + } + + //----------------------------------------------------------------------- + /** + * Gets the entry mapped to the key specified. + * + * @param key the key + * @return the entry, null if no match + */ + protected HashEntry<K, V> getEntry(Object key) { + if (key == null) { + return null; + } else { + return super.getEntry(key); + } + } + + /** + * Gets the hash code for a MapEntry. + * Subclasses can override this, for example to use the identityHashCode. + * + * @param key the key to get a hash code for, may be null + * @param value the value to get a hash code for, may be null + * @return the hash code, as per the MapEntry specification + */ + protected int hashEntry(Object key, Object value) { + return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); + } + + /** + * Compares two keys, in internal converted form, to see if they are equal. + * <p/> + * This implementation converts the key from the entry to a real reference + * before comparison. + * + * @param key1 the first key to compare passed in from outside + * @param key2 the second key extracted from the entry via <code>entry.key</code> + * @return true if equal + */ + protected boolean isEqualKey(Object key1, Object key2) { + //if ((key1 == null) && (key2 != null) || (key1 != null) || (key2 == null)) { + // return false; + //} + // GenericsNote: Conversion from reference handled by getKey() which replaced all .key references + //key2 = (keyType > HARD ? ((Reference) key2).get() : key2); + return (key1 == key2 || key1.equals(key2)); + } + + /** + * Creates a ReferenceEntry instead of a HashEntry. + * + * @param next the next entry in sequence + * @param hashCode the hash code to use + * @param key the key to store + * @param value the value to store + * @return the newly created entry + */ + public HashEntry<K, V> createEntry(HashEntry<K, V> next, int hashCode, K key, V value) { + return new ReferenceEntry<K, V>(this, (ReferenceEntry<K, V>) next, hashCode, key, value); + } + + /** + * Creates an entry set iterator. + * + * @return the entrySet iterator + */ + protected Iterator<Map.Entry<K, V>> createEntrySetIterator() { + return new ReferenceEntrySetIterator<K, V>(this); + } + + /** + * Creates an key set iterator. + * + * @return the keySet iterator + */ + protected Iterator<K> createKeySetIterator() { + return new ReferenceKeySetIterator<K, V>(this); + } + + /** + * Creates an values iterator. + * + * @return the values iterator + */ + protected Iterator<V> createValuesIterator() { + return new ReferenceValuesIterator<K, V>(this); + } + + //----------------------------------------------------------------------- + /** + * EntrySet implementation. + */ + static class ReferenceEntrySet <K,V> extends EntrySet<K, V> { + + protected ReferenceEntrySet(AbstractHashedMap<K, V> parent) { + super(parent); + } + + public Object[] toArray() { + return toArray(new Object[0]); + } + + public <T> T[] toArray(T[] arr) { + // special implementation to handle disappearing entries + ArrayList<Map.Entry<K, V>> list = new ArrayList<Map.Entry<K, V>>(); + Iterator<Map.Entry<K, V>> iterator = iterator(); + while (iterator.hasNext()) { + Map.Entry<K, V> e = iterator.next(); + list.add(new DefaultMapEntry<K, V>(e.getKey(), e.getValue())); + } + return list.toArray(arr); + } + } + + //----------------------------------------------------------------------- + /** + * KeySet implementation. + */ + static class ReferenceKeySet <K,V> extends KeySet<K, V> { + + protected ReferenceKeySet(AbstractHashedMap<K, V> parent) { + super(parent); + } + + public Object[] toArray() { + return toArray(new Object[0]); + } + + public <T> T[] toArray(T[] arr) { + // special implementation to handle disappearing keys + List<K> list = new ArrayList<K>(parent.size()); + for (Iterator<K> it = iterator(); it.hasNext();) { + list.add(it.next()); + } + return list.toArray(arr); + } + } + + //----------------------------------------------------------------------- + /** + * Values implementation. + */ + static class ReferenceValues <K,V> extends Values<K, V> { + + protected ReferenceValues(AbstractHashedMap<K, V> parent) { + super(parent); + } + + public Object[] toArray() { + return toArray(new Object[0]); + } + + public <T> T[] toArray(T[] arr) { + // special implementation to handle disappearing values + List<V> list = new ArrayList<V>(parent.size()); + for (Iterator<V> it = iterator(); it.hasNext();) { + list.add(it.next()); + } + return list.toArray(arr); + } + } + + //----------------------------------------------------------------------- + /** + * A MapEntry implementation for the map. + * <p/> + * If getKey() or getValue() returns null, it means + * the mapping is stale and should be removed. + * + * @since Commons Collections 3.1 + */ + protected static class ReferenceEntry <K,V> extends HashEntry<K, V> { + /** + * The parent map + */ + protected final AbstractReferenceMap<K, V> parent; + + protected Reference<K> refKey; + protected Reference<V> refValue; + + /** + * Creates a new entry object for the ReferenceMap. + * + * @param parent the parent map + * @param next the next entry in the hash bucket + * @param hashCode the hash code of the key + * @param key the key + * @param value the value + */ + public ReferenceEntry(AbstractReferenceMap<K, V> parent, ReferenceEntry<K, V> next, int hashCode, K key, V value) { + super(next, hashCode, null, null); + this.parent = parent; + if (parent.keyType != HARD) { + refKey = toReference(parent.keyType, key, hashCode); + } else { + this.setKey(key); + } + if (parent.valueType != HARD) { + refValue = toReference(parent.valueType, value, hashCode); // the key hashCode is passed in deliberately + } else { + this.setValue(value); + } + } + + /** + * Gets the key from the entry. + * This method dereferences weak and soft keys and thus may return null. + * + * @return the key, which may be null if it was garbage collected + */ + public K getKey() { + return (parent.keyType > HARD) ? refKey.get() : super.getKey(); + } + + /** + * Gets the value from the entry. + * This method dereferences weak and soft value and thus may return null. + * + * @return the value, which may be null if it was garbage collected + */ + public V getValue() { + return (parent.valueType > HARD) ? refValue.get() : super.getValue(); + } + + /** + * Sets the value of the entry. + * + * @param obj the object to store + * @return the previous value + */ + public V setValue(V obj) { + V old = getValue(); + if (parent.valueType > HARD) { + refValue.clear(); + refValue = toReference(parent.valueType, obj, hashCode); + } else { + super.setValue(obj); + } + return old; + } + + /** + * Compares this map entry to another. + * <p/> + * This implementation uses <code>isEqualKey</code> and + * <code>isEqualValue</code> on the main map for comparison. + * + * @param obj the other map entry to compare to + * @return true if equal, false if not + */ + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof Map.Entry == false) { + return false; + } + + Map.Entry entry = (Map.Entry) obj; + Object entryKey = entry.getKey(); // convert to hard reference + Object entryValue = entry.getValue(); // convert to hard reference + if ((entryKey == null) || (entryValue == null)) { + return false; + } + // compare using map methods, aiding identity subclass + // note that key is direct access and value is via method + return parent.isEqualKey(entryKey, getKey()) && parent.isEqualValue(entryValue, getValue()); + } + + /** + * Gets the hashcode of the entry using temporary hard references. + * <p/> + * This implementation uses <code>hashEntry</code> on the main map. + * + * @return the hashcode of the entry + */ + public int hashCode() { + return parent.hashEntry(getKey(), getValue()); + } + + /** + * Constructs a reference of the given type to the given referent. + * The reference is registered with the queue for later purging. + * + * @param type HARD, SOFT or WEAK + * @param referent the object to refer to + * @param hash the hash code of the <i>key</i> of the mapping; + * this number might be different from referent.hashCode() if + * the referent represents a value and not a key + */ + protected <T> Reference<T> toReference(int type, T referent, int hash) { + switch (type) { + case SOFT: + return new SoftRef<T>(hash, referent, parent.queue); + case WEAK: + return new WeakRef<T>(hash, referent, parent.queue); + default: + throw new Error("Attempt to create hard reference in ReferenceMap!"); + } + } + + /** + * Purges the specified reference + * + * @param ref the reference to purge + * @return true or false + */ + boolean purge(Reference ref) { + boolean r = (parent.keyType > HARD) && (refKey == ref); + r = r || ((parent.valueType > HARD) && (refValue == ref)); + if (r) { + if (parent.keyType > HARD) { + refKey.clear(); + } + if (parent.valueType > HARD) { + refValue.clear(); + } else if (parent.purgeValues) { + setValue(null); + } + } + return r; + } + + /** + * Gets the next entry in the bucket. + * + * @return the next entry in the bucket + */ + protected ReferenceEntry<K, V> next() { + return (ReferenceEntry<K, V>) next; + } + } + + //----------------------------------------------------------------------- + /** + * The EntrySet iterator. + */ + static class ReferenceIteratorBase <K,V> { + /** + * The parent map + */ + final AbstractReferenceMap<K, V> parent; + + // These fields keep track of where we are in the table. + int index; + ReferenceEntry<K, V> entry; + ReferenceEntry<K, V> previous; + + // These Object fields provide hard references to the + // current and next entry; this assures that if hasNext() + // returns true, next() will actually return a valid element. + K nextKey; + V nextValue; + K currentKey; + V currentValue; + + int expectedModCount; + + public ReferenceIteratorBase(AbstractReferenceMap<K, V> parent) { + super(); + this.parent = parent; + index = (parent.size() != 0 ? parent.data.length : 0); + // have to do this here! size() invocation above + // may have altered the modCount. + expectedModCount = parent.modCount; + } + + public boolean hasNext() { + checkMod(); + while (nextNull()) { + ReferenceEntry<K, V> e = entry; + int i = index; + while ((e == null) && (i > 0)) { + i--; + e = (ReferenceEntry<K, V>) parent.data[i]; + } + entry = e; + index = i; + if (e == null) { + currentKey = null; + currentValue = null; + return false; + } + nextKey = e.getKey(); + nextValue = e.getValue(); + if (nextNull()) { + entry = entry.next(); + } + } + return true; + } + + private void checkMod() { + if (parent.modCount != expectedModCount) { + throw new ConcurrentModificationException(); + } + } + + private boolean nextNull() { + return (nextKey == null) || (nextValue == null); + } + + protected ReferenceEntry<K, V> nextEntry() { + checkMod(); + if (nextNull() && !hasNext()) { + throw new NoSuchElementException(); + } + previous = entry; + entry = entry.next(); + currentKey = nextKey; + currentValue = nextValue; + nextKey = null; + nextValue = null; + return previous; + } + + protected ReferenceEntry<K, V> currentEntry() { + checkMod(); + return previous; + } + + public ReferenceEntry<K, V> superNext() { + return nextEntry(); + } + + public void remove() { + checkMod(); + if (previous == null) { + throw new IllegalStateException(); + } + parent.remove(currentKey); + previous = null; + currentKey = null; + currentValue = null; + expectedModCount = parent.modCount; + } + } + + /** + * The EntrySet iterator. + */ + static class ReferenceEntrySetIterator <K,V> extends ReferenceIteratorBase<K, V> implements Iterator<Map.Entry<K, V>> { + + public ReferenceEntrySetIterator(AbstractReferenceMap<K, V> abstractReferenceMap) { + super(abstractReferenceMap); + } + + public ReferenceEntry<K, V> next() { + return superNext(); + } + + } + + /** + * The keySet iterator. + */ + static class ReferenceKeySetIterator <K,V> extends ReferenceIteratorBase<K, V> implements Iterator<K> { + + ReferenceKeySetIterator(AbstractReferenceMap<K, V> parent) { + super(parent); + } + + public K next() { + return nextEntry().getKey(); + } + } + + /** + * The values iterator. + */ + static class ReferenceValuesIterator <K,V> extends ReferenceIteratorBase<K, V> implements Iterator<V> { + + ReferenceValuesIterator(AbstractReferenceMap<K, V> parent) { + super(parent); + } + + public V next() { + return nextEntry().getValue(); + } + } + + /** + * The MapIterator implementation. + */ + static class ReferenceMapIterator <K,V> extends ReferenceIteratorBase<K, V> implements MapIterator<K, V> { + + protected ReferenceMapIterator(AbstractReferenceMap<K, V> parent) { + super(parent); + } + + public K next() { + return nextEntry().getKey(); + } + + public K getKey() { + HashEntry<K, V> current = currentEntry(); + if (current == null) { + throw new IllegalStateException(AbstractHashedMap.GETKEY_INVALID); + } + return current.getKey(); + } + + public V getValue() { + HashEntry<K, V> current = currentEntry(); + if (current == null) { + throw new IllegalStateException(AbstractHashedMap.GETVALUE_INVALID); + } + return current.getValue(); + } + + public V setValue(V value) { + HashEntry<K, V> current = currentEntry(); + if (current == null) { + throw new IllegalStateException(AbstractHashedMap.SETVALUE_INVALID); + } + return current.setValue(value); + } + } + + //----------------------------------------------------------------------- + // These two classes store the hashCode of the key of + // of the mapping, so that after they're dequeued a quick + // lookup of the bucket in the table can occur. + + /** + * A soft reference holder. + */ + static class SoftRef <T> extends SoftReference<T> { + /** + * the hashCode of the key (even if the reference points to a value) + */ + private int hash; + + public SoftRef(int hash, T r, ReferenceQueue q) { + super(r, q); + this.hash = hash; + } + + public int hashCode() { + return hash; + } + } + + /** + * A weak reference holder. + */ + static class WeakRef <T> extends WeakReference<T> { + /** + * the hashCode of the key (even if the reference points to a value) + */ + private int hash; + + public WeakRef(int hash, T r, ReferenceQueue q) { + super(r, q); + this.hash = hash; + } + + public int hashCode() { + return hash; + } + } + + //----------------------------------------------------------------------- + /** + * Replaces the superclass method to store the state of this class. + * <p/> + * Serialization is not one of the JDK's nicest topics. Normal serialization will + * initialise the superclass before the subclass. Sometimes however, this isn't + * what you want, as in this case the <code>put()</code> method on read can be + * affected by subclass state. + * <p/> + * The solution adopted here is to serialize the state data of this class in + * this protected method. This method must be called by the + * <code>writeObject()</code> of the first serializable subclass. + * <p/> + * Subclasses may override if they have a specific field that must be present + * on read before this implementation will work. Generally, the read determines + * what must be serialized here, if anything. + * + * @param out the output stream + */ + protected void doWriteObject(ObjectOutputStream out) throws IOException { + out.writeInt(keyType); + out.writeInt(valueType); + out.writeBoolean(purgeValues); + out.writeFloat(loadFactor); + out.writeInt(data.length); + for (MapIterator it = mapIterator(); it.hasNext();) { + out.writeObject(it.next()); + out.writeObject(it.getValue()); + } + out.writeObject(null); // null terminate map + // do not call super.doWriteObject() as code there doesn't work for reference map + } + + /** + * Replaces the superclassm method to read the state of this class. + * <p/> + * Serialization is not one of the JDK's nicest topics. Normal serialization will + * initialise the superclass before the subclass. Sometimes however, this isn't + * what you want, as in this case the <code>put()</code> method on read can be + * affected by subclass state. + * <p/> + * The solution adopted here is to deserialize the state data of this class in + * this protected method. This method must be called by the + * <code>readObject()</code> of the first serializable subclass. + * <p/> + * Subclasses may override if the subclass has a specific field that must be present + * before <code>put()</code> or <code>calculateThreshold()</code> will work correctly. + * + * @param in the input stream + */ + protected void doReadObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + this.keyType = in.readInt(); + this.valueType = in.readInt(); + this.purgeValues = in.readBoolean(); + this.loadFactor = in.readFloat(); + int capacity = in.readInt(); + init(); + data = new HashEntry[capacity]; + while (true) { + K key = (K) in.readObject(); + if (key == null) { + break; + } + V value = (V) in.readObject(); + put(key, value); + } + threshold = calculateThreshold(data.length, loadFactor); + // do not call super.doReadObject() as code there doesn't work for reference map + } + +} diff --git a/src/org/jivesoftware/smack/util/collections/DefaultMapEntry.java b/src/org/jivesoftware/smack/util/collections/DefaultMapEntry.java new file mode 100644 index 0000000..ef752d0 --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/DefaultMapEntry.java @@ -0,0 +1,65 @@ +// GenericsNote: Converted. +/* + * Copyright 2001-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + + +import java.util.Map; + +/** + * A restricted implementation of {@link java.util.Map.Entry} that prevents + * the MapEntry contract from being broken. + * + * @author James Strachan + * @author Michael A. Smith + * @author Neil O'Toole + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $ + * @since Commons Collections 3.0 + */ +public final class DefaultMapEntry <K,V> extends AbstractMapEntry<K, V> { + + /** + * Constructs a new entry with the specified key and given value. + * + * @param key the key for the entry, may be null + * @param value the value for the entry, may be null + */ + public DefaultMapEntry(final K key, final V value) { + super(key, value); + } + + /** + * Constructs a new entry from the specified KeyValue. + * + * @param pair the pair to copy, must not be null + * @throws NullPointerException if the entry is null + */ + public DefaultMapEntry(final KeyValue<K, V> pair) { + super(pair.getKey(), pair.getValue()); + } + + /** + * Constructs a new entry from the specified MapEntry. + * + * @param entry the entry to copy, must not be null + * @throws NullPointerException if the entry is null + */ + public DefaultMapEntry(final Map.Entry<K, V> entry) { + super(entry.getKey(), entry.getValue()); + } + +} diff --git a/src/org/jivesoftware/smack/util/collections/EmptyIterator.java b/src/org/jivesoftware/smack/util/collections/EmptyIterator.java new file mode 100644 index 0000000..6a8707f --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/EmptyIterator.java @@ -0,0 +1,58 @@ +// GenericsNote: Converted. +/* + * Copyright 2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +import java.util.Iterator; + +/** + * Provides an implementation of an empty iterator. + * <p/> + * This class provides an implementation of an empty iterator. + * This class provides for binary compatability between Commons Collections + * 2.1.1 and 3.1 due to issues with <code>IteratorUtils</code>. + * + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:24 $ + * @since Commons Collections 2.1.1 and 3.1 + */ +public class EmptyIterator <E> extends AbstractEmptyIterator<E> implements ResettableIterator<E> { + + /** + * Singleton instance of the iterator. + * + * @since Commons Collections 3.1 + */ + public static final ResettableIterator RESETTABLE_INSTANCE = new EmptyIterator(); + /** + * Singleton instance of the iterator. + * + * @since Commons Collections 2.1.1 and 3.1 + */ + public static final Iterator INSTANCE = RESETTABLE_INSTANCE; + + public static <T> Iterator<T> getInstance() { + return INSTANCE; + } + + /** + * Constructor. + */ + protected EmptyIterator() { + super(); + } + +} diff --git a/src/org/jivesoftware/smack/util/collections/EmptyMapIterator.java b/src/org/jivesoftware/smack/util/collections/EmptyMapIterator.java new file mode 100644 index 0000000..013f5ed --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/EmptyMapIterator.java @@ -0,0 +1,42 @@ +// GenericsNote: Converted. +/* + * Copyright 2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +/** + * Provides an implementation of an empty map iterator. + * + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:24 $ + * @since Commons Collections 3.1 + */ +public class EmptyMapIterator extends AbstractEmptyIterator implements MapIterator, ResettableIterator { + + /** + * Singleton instance of the iterator. + * + * @since Commons Collections 3.1 + */ + public static final MapIterator INSTANCE = new EmptyMapIterator(); + + /** + * Constructor. + */ + protected EmptyMapIterator() { + super(); + } + +} diff --git a/src/org/jivesoftware/smack/util/collections/IterableMap.java b/src/org/jivesoftware/smack/util/collections/IterableMap.java new file mode 100644 index 0000000..251b587 --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/IterableMap.java @@ -0,0 +1,61 @@ +// GenericsNote: Converted. +/* + * Copyright 2003-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +import java.util.Map; + +/** + * Defines a map that can be iterated directly without needing to create an entry set. + * <p/> + * A map iterator is an efficient way of iterating over maps. + * There is no need to access the entry set or cast to Map Entry objects. + * <pre> + * IterableMap map = new HashedMap(); + * MapIterator it = map.mapIterator(); + * while (it.hasNext()) { + * Object key = it.next(); + * Object value = it.getValue(); + * it.setValue("newValue"); + * } + * </pre> + * + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:19 $ + * @since Commons Collections 3.0 + */ +public interface IterableMap <K,V> extends Map<K, V> { + + /** + * Obtains a <code>MapIterator</code> over the map. + * <p/> + * A map iterator is an efficient way of iterating over maps. + * There is no need to access the entry set or cast to Map Entry objects. + * <pre> + * IterableMap map = new HashedMap(); + * MapIterator it = map.mapIterator(); + * while (it.hasNext()) { + * Object key = it.next(); + * Object value = it.getValue(); + * it.setValue("newValue"); + * } + * </pre> + * + * @return a map iterator + */ + MapIterator<K, V> mapIterator(); + +} diff --git a/src/org/jivesoftware/smack/util/collections/KeyValue.java b/src/org/jivesoftware/smack/util/collections/KeyValue.java new file mode 100644 index 0000000..c73621d --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/KeyValue.java @@ -0,0 +1,46 @@ +// GenericsNote: Converted. +/* + * Copyright 2003-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +/** + * Defines a simple key value pair. + * <p/> + * A Map Entry has considerable additional semantics over and above a simple + * key-value pair. This interface defines the minimum key value, with just the + * two get methods. + * + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:19 $ + * @since Commons Collections 3.0 + */ +public interface KeyValue <K,V> { + + /** + * Gets the key from the pair. + * + * @return the key + */ + K getKey(); + + /** + * Gets the value from the pair. + * + * @return the value + */ + V getValue(); + +} diff --git a/src/org/jivesoftware/smack/util/collections/MapIterator.java b/src/org/jivesoftware/smack/util/collections/MapIterator.java new file mode 100644 index 0000000..fe2398c --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/MapIterator.java @@ -0,0 +1,109 @@ +// GenericsNote: Converted. +/* + * Copyright 2003-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +import java.util.Iterator; + +/** + * Defines an iterator that operates over a <code>Map</code>. + * <p/> + * This iterator is a special version designed for maps. It can be more + * efficient to use this rather than an entry set iterator where the option + * is available, and it is certainly more convenient. + * <p/> + * A map that provides this interface may not hold the data internally using + * Map Entry objects, thus this interface can avoid lots of object creation. + * <p/> + * In use, this iterator iterates through the keys in the map. After each call + * to <code>next()</code>, the <code>getValue()</code> method provides direct + * access to the value. The value can also be set using <code>setValue()</code>. + * <pre> + * MapIterator it = map.mapIterator(); + * while (it.hasNext()) { + * Object key = it.next(); + * Object value = it.getValue(); + * it.setValue(newValue); + * } + * </pre> + * + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:19 $ + * @since Commons Collections 3.0 + */ +public interface MapIterator <K,V> extends Iterator<K> { + + /** + * Checks to see if there are more entries still to be iterated. + * + * @return <code>true</code> if the iterator has more elements + */ + boolean hasNext(); + + /** + * Gets the next <em>key</em> from the <code>Map</code>. + * + * @return the next key in the iteration + * @throws java.util.NoSuchElementException + * if the iteration is finished + */ + K next(); + + //----------------------------------------------------------------------- + /** + * Gets the current key, which is the key returned by the last call + * to <code>next()</code>. + * + * @return the current key + * @throws IllegalStateException if <code>next()</code> has not yet been called + */ + K getKey(); + + /** + * Gets the current value, which is the value associated with the last key + * returned by <code>next()</code>. + * + * @return the current value + * @throws IllegalStateException if <code>next()</code> has not yet been called + */ + V getValue(); + + //----------------------------------------------------------------------- + /** + * Removes the last returned key from the underlying <code>Map</code> (optional operation). + * <p/> + * This method can be called once per call to <code>next()</code>. + * + * @throws UnsupportedOperationException if remove is not supported by the map + * @throws IllegalStateException if <code>next()</code> has not yet been called + * @throws IllegalStateException if <code>remove()</code> has already been called + * since the last call to <code>next()</code> + */ + void remove(); + + /** + * Sets the value associated with the current key (optional operation). + * + * @param value the new value + * @return the previous value + * @throws UnsupportedOperationException if setValue is not supported by the map + * @throws IllegalStateException if <code>next()</code> has not yet been called + * @throws IllegalStateException if <code>remove()</code> has been called since the + * last call to <code>next()</code> + */ + V setValue(V value); + +} diff --git a/src/org/jivesoftware/smack/util/collections/ReferenceMap.java b/src/org/jivesoftware/smack/util/collections/ReferenceMap.java new file mode 100644 index 0000000..f30954d --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/ReferenceMap.java @@ -0,0 +1,161 @@ +// GenericsNote: Converted. +/* + * Copyright 2002-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; + +/** + * A <code>Map</code> implementation that allows mappings to be + * removed by the garbage collector. + * <p/> + * When you construct a <code>ReferenceMap</code>, you can specify what kind + * of references are used to store the map's keys and values. + * If non-hard references are used, then the garbage collector can remove + * mappings if a key or value becomes unreachable, or if the JVM's memory is + * running low. For information on how the different reference types behave, + * see {@link java.lang.ref.Reference}. + * <p/> + * Different types of references can be specified for keys and values. + * The keys can be configured to be weak but the values hard, + * in which case this class will behave like a + * <a href="http://java.sun.com/j2se/1.4/docs/api/java/util/WeakHashMap.html"> + * <code>WeakHashMap</code></a>. However, you can also specify hard keys and + * weak values, or any other combination. The default constructor uses + * hard keys and soft values, providing a memory-sensitive cache. + * <p/> + * This map is similar to ReferenceIdentityMap. + * It differs in that keys and values in this class are compared using <code>equals()</code>. + * <p/> + * This {@link java.util.Map} implementation does <i>not</i> allow null elements. + * Attempting to add a null key or value to the map will raise a <code>NullPointerException</code>. + * <p/> + * This implementation is not synchronized. + * You can use {@link java.util.Collections#synchronizedMap} to + * provide synchronized access to a <code>ReferenceMap</code>. + * Remember that synchronization will not stop the garbage collecter removing entries. + * <p/> + * All the available iterators can be reset back to the start by casting to + * <code>ResettableIterator</code> and calling <code>reset()</code>. + * <p/> + * NOTE: As from Commons Collections 3.1 this map extends <code>AbstractReferenceMap</code> + * (previously it extended AbstractMap). As a result, the implementation is now + * extensible and provides a <code>MapIterator</code>. + * + * @author Paul Jack + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:32 $ + * @see java.lang.ref.Reference + * @since Commons Collections 3.0 (previously in main package v2.1) + */ +public class ReferenceMap <K,V> extends AbstractReferenceMap<K, V> implements Serializable { + + /** + * Serialization version + */ + private static final long serialVersionUID = 1555089888138299607L; + + /** + * Constructs a new <code>ReferenceMap</code> that will + * use hard references to keys and soft references to values. + */ + public ReferenceMap() { + super(HARD, SOFT, DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, false); + } + + /** + * Constructs a new <code>ReferenceMap</code> that will + * use the specified types of references. + * + * @param keyType the type of reference to use for keys; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param valueType the type of reference to use for values; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + */ + public ReferenceMap(int keyType, int valueType) { + super(keyType, valueType, DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, false); + } + + /** + * Constructs a new <code>ReferenceMap</code> that will + * use the specified types of references. + * + * @param keyType the type of reference to use for keys; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param valueType the type of reference to use for values; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param purgeValues should the value be automatically purged when the + * key is garbage collected + */ + public ReferenceMap(int keyType, int valueType, boolean purgeValues) { + super(keyType, valueType, DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR, purgeValues); + } + + /** + * Constructs a new <code>ReferenceMap</code> with the + * specified reference types, load factor and initial + * capacity. + * + * @param keyType the type of reference to use for keys; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param valueType the type of reference to use for values; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param capacity the initial capacity for the map + * @param loadFactor the load factor for the map + */ + public ReferenceMap(int keyType, int valueType, int capacity, float loadFactor) { + super(keyType, valueType, capacity, loadFactor, false); + } + + /** + * Constructs a new <code>ReferenceMap</code> with the + * specified reference types, load factor and initial + * capacity. + * + * @param keyType the type of reference to use for keys; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param valueType the type of reference to use for values; + * must be {@link #HARD}, {@link #SOFT}, {@link #WEAK} + * @param capacity the initial capacity for the map + * @param loadFactor the load factor for the map + * @param purgeValues should the value be automatically purged when the + * key is garbage collected + */ + public ReferenceMap(int keyType, int valueType, int capacity, float loadFactor, boolean purgeValues) { + super(keyType, valueType, capacity, loadFactor, purgeValues); + } + + //----------------------------------------------------------------------- + /** + * Write the map out using a custom routine. + */ + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + doWriteObject(out); + } + + /** + * Read the map in using a custom routine. + */ + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + doReadObject(in); + } + +} diff --git a/src/org/jivesoftware/smack/util/collections/ResettableIterator.java b/src/org/jivesoftware/smack/util/collections/ResettableIterator.java new file mode 100644 index 0000000..cf814f7 --- /dev/null +++ b/src/org/jivesoftware/smack/util/collections/ResettableIterator.java @@ -0,0 +1,38 @@ +// GenericsNote: Converted. +/* + * Copyright 2003-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.collections; + +import java.util.Iterator; + +/** + * Defines an iterator that can be reset back to an initial state. + * <p/> + * This interface allows an iterator to be repeatedly reused. + * + * @author Matt Hall, John Watkinson, Stephen Colebourne + * @version $Revision: 1.1 $ $Date: 2005/10/11 17:05:19 $ + * @since Commons Collections 3.0 + */ +public interface ResettableIterator <E> extends Iterator<E> { + + /** + * Resets the iterator back to the position at which the iterator + * was created. + */ + public void reset(); + +} diff --git a/src/org/jivesoftware/smack/util/dns/DNSJavaResolver.java b/src/org/jivesoftware/smack/util/dns/DNSJavaResolver.java new file mode 100644 index 0000000..dd93fd3 --- /dev/null +++ b/src/org/jivesoftware/smack/util/dns/DNSJavaResolver.java @@ -0,0 +1,73 @@ +/** + * Copyright 2013 Florian Schmaus + * + * All rights reserved. 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.jivesoftware.smack.util.dns; + +import java.util.ArrayList; +import java.util.List; + +import org.xbill.DNS.Lookup; +import org.xbill.DNS.Record; +import org.xbill.DNS.Type; + +/** + * This implementation uses the <a href="http://www.dnsjava.org/">dnsjava</a> implementation for resolving DNS addresses. + * + */ +public class DNSJavaResolver implements DNSResolver { + + private static DNSJavaResolver instance = new DNSJavaResolver(); + + private DNSJavaResolver() { + + } + + public static DNSResolver getInstance() { + return instance; + } + + @Override + public List<SRVRecord> lookupSRVRecords(String name) { + List<SRVRecord> res = new ArrayList<SRVRecord>(); + + try { + Lookup lookup = new Lookup(name, Type.SRV); + Record recs[] = lookup.run(); + if (recs == null) + return res; + + for (Record record : recs) { + org.xbill.DNS.SRVRecord srvRecord = (org.xbill.DNS.SRVRecord) record; + if (srvRecord != null && srvRecord.getTarget() != null) { + String host = srvRecord.getTarget().toString(); + int port = srvRecord.getPort(); + int priority = srvRecord.getPriority(); + int weight = srvRecord.getWeight(); + + SRVRecord r; + try { + r = new SRVRecord(host, port, priority, weight); + } catch (Exception e) { + continue; + } + res.add(r); + } + } + + } catch (Exception e) { + } + return res; + } +} diff --git a/src/org/jivesoftware/smack/util/dns/DNSResolver.java b/src/org/jivesoftware/smack/util/dns/DNSResolver.java new file mode 100644 index 0000000..86f037b --- /dev/null +++ b/src/org/jivesoftware/smack/util/dns/DNSResolver.java @@ -0,0 +1,33 @@ +/** + * Copyright 2013 Florian Schmaus + * + * All rights reserved. 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.jivesoftware.smack.util.dns; + +import java.util.List; + +/** + * Implementations of this interface define a class that is capable of resolving DNS addresses. + * + */ +public interface DNSResolver { + + /** + * Gets a list of service records for the specified service. + * @param name The symbolic name of the service. + * @return The list of SRV records mapped to the service name. + */ + List<SRVRecord> lookupSRVRecords(String name); + +} diff --git a/src/org/jivesoftware/smack/util/dns/HostAddress.java b/src/org/jivesoftware/smack/util/dns/HostAddress.java new file mode 100644 index 0000000..eb8b07a --- /dev/null +++ b/src/org/jivesoftware/smack/util/dns/HostAddress.java @@ -0,0 +1,109 @@ +/** + * Copyright 2013 Florian Schmaus + * + * All rights reserved. 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.jivesoftware.smack.util.dns; + +public class HostAddress { + private String fqdn; + private int port; + private Exception exception; + + /** + * Creates a new HostAddress with the given FQDN. The port will be set to the default XMPP client port: 5222 + * + * @param fqdn Fully qualified domain name. + * @throws IllegalArgumentException If the fqdn is null. + */ + public HostAddress(String fqdn) { + if (fqdn == null) + throw new IllegalArgumentException("FQDN is null"); + if (fqdn.charAt(fqdn.length() - 1) == '.') { + this.fqdn = fqdn.substring(0, fqdn.length() - 1); + } + else { + this.fqdn = fqdn; + } + // Set port to the default port for XMPP client communication + this.port = 5222; + } + + /** + * Creates a new HostAddress with the given FQDN. The port will be set to the default XMPP client port: 5222 + * + * @param fqdn Fully qualified domain name. + * @param port The port to connect on. + * @throws IllegalArgumentException If the fqdn is null or port is out of valid range (0 - 65535). + */ + public HostAddress(String fqdn, int port) { + this(fqdn); + if (port < 0 || port > 65535) + throw new IllegalArgumentException( + "DNS SRV records weight must be a 16-bit unsiged integer (i.e. between 0-65535. Port was: " + port); + + this.port = port; + } + + public String getFQDN() { + return fqdn; + } + + public int getPort() { + return port; + } + + public void setException(Exception e) { + this.exception = e; + } + + @Override + public String toString() { + return fqdn + ":" + port; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof HostAddress)) { + return false; + } + + final HostAddress address = (HostAddress) o; + + if (!fqdn.equals(address.fqdn)) { + return false; + } + return port == address.port; + } + + @Override + public int hashCode() { + int result = 1; + result = 37 * result + fqdn.hashCode(); + return result * 37 + port; + } + + public String getErrorMessage() { + String error; + if (exception == null) { + error = "No error logged"; + } + else { + error = exception.getMessage(); + } + return toString() + " Exception: " + error; + } +} diff --git a/src/org/jivesoftware/smack/util/dns/SRVRecord.java b/src/org/jivesoftware/smack/util/dns/SRVRecord.java new file mode 100644 index 0000000..457e40e --- /dev/null +++ b/src/org/jivesoftware/smack/util/dns/SRVRecord.java @@ -0,0 +1,79 @@ +/** + * Copyright 2013 Florian Schmaus + * + * All rights reserved. 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.jivesoftware.smack.util.dns; + +/** + * @see <a href="http://tools.ietf.org/html/rfc2782>RFC 2782: A DNS RR for specifying the location of services (DNS + * SRV)<a> + * @author Florian Schmaus + * + */ +public class SRVRecord extends HostAddress implements Comparable<SRVRecord> { + + private int weight; + private int priority; + + /** + * Create a new SRVRecord + * + * @param fqdn Fully qualified domain name + * @param port The connection port + * @param priority Priority of the target host + * @param weight Relative weight for records with same priority + * @throws IllegalArgumentException fqdn is null or any other field is not in valid range (0-65535). + */ + public SRVRecord(String fqdn, int port, int priority, int weight) { + super(fqdn, port); + if (weight < 0 || weight > 65535) + throw new IllegalArgumentException( + "DNS SRV records weight must be a 16-bit unsiged integer (i.e. between 0-65535. Weight was: " + + weight); + + if (priority < 0 || priority > 65535) + throw new IllegalArgumentException( + "DNS SRV records priority must be a 16-bit unsiged integer (i.e. between 0-65535. Priority was: " + + priority); + + this.priority = priority; + this.weight = weight; + + } + + public int getPriority() { + return priority; + } + + public int getWeight() { + return weight; + } + + @Override + public int compareTo(SRVRecord other) { + // According to RFC2782, + // "[a] client MUST attempt to contact the target host with the lowest-numbered priority it can reach". + // This means that a SRV record with a higher priority is 'less' then one with a lower. + int res = other.priority - this.priority; + if (res == 0) { + res = this.weight - other.weight; + } + return res; + } + + @Override + public String toString() { + return super.toString() + " prio:" + priority + ":w:" + weight; + } +} diff --git a/src/org/jivesoftware/smack/util/package.html b/src/org/jivesoftware/smack/util/package.html new file mode 100644 index 0000000..e34bfe3 --- /dev/null +++ b/src/org/jivesoftware/smack/util/package.html @@ -0,0 +1 @@ +<body>Utility classes.</body>
\ No newline at end of file |