aboutsummaryrefslogtreecommitdiff
path: root/src/org/jivesoftware/smack/util
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/jivesoftware/smack/util')
-rw-r--r--src/org/jivesoftware/smack/util/Base32Encoder.java184
-rw-r--r--src/org/jivesoftware/smack/util/Base64.java1689
-rw-r--r--src/org/jivesoftware/smack/util/Base64Encoder.java42
-rw-r--r--src/org/jivesoftware/smack/util/Base64FileUrlEncoder.java48
-rw-r--r--src/org/jivesoftware/smack/util/Cache.java678
-rw-r--r--src/org/jivesoftware/smack/util/DNSUtil.java229
-rw-r--r--src/org/jivesoftware/smack/util/DateFormatType.java65
-rw-r--r--src/org/jivesoftware/smack/util/ObservableReader.java118
-rw-r--r--src/org/jivesoftware/smack/util/ObservableWriter.java120
-rw-r--r--src/org/jivesoftware/smack/util/PacketParserUtils.java925
-rw-r--r--src/org/jivesoftware/smack/util/PacketParserUtils.java.orig926
-rw-r--r--src/org/jivesoftware/smack/util/ReaderListener.java41
-rw-r--r--src/org/jivesoftware/smack/util/StringEncoder.java36
-rw-r--r--src/org/jivesoftware/smack/util/StringUtils.java800
-rw-r--r--src/org/jivesoftware/smack/util/SyncPacketSend.java63
-rw-r--r--src/org/jivesoftware/smack/util/WriterListener.java41
-rw-r--r--src/org/jivesoftware/smack/util/collections/AbstractEmptyIterator.java89
-rw-r--r--src/org/jivesoftware/smack/util/collections/AbstractHashedMap.java1338
-rw-r--r--src/org/jivesoftware/smack/util/collections/AbstractKeyValue.java80
-rw-r--r--src/org/jivesoftware/smack/util/collections/AbstractMapEntry.java89
-rw-r--r--src/org/jivesoftware/smack/util/collections/AbstractReferenceMap.java1025
-rw-r--r--src/org/jivesoftware/smack/util/collections/DefaultMapEntry.java65
-rw-r--r--src/org/jivesoftware/smack/util/collections/EmptyIterator.java58
-rw-r--r--src/org/jivesoftware/smack/util/collections/EmptyMapIterator.java42
-rw-r--r--src/org/jivesoftware/smack/util/collections/IterableMap.java61
-rw-r--r--src/org/jivesoftware/smack/util/collections/KeyValue.java46
-rw-r--r--src/org/jivesoftware/smack/util/collections/MapIterator.java109
-rw-r--r--src/org/jivesoftware/smack/util/collections/ReferenceMap.java161
-rw-r--r--src/org/jivesoftware/smack/util/collections/ResettableIterator.java38
-rw-r--r--src/org/jivesoftware/smack/util/dns/DNSJavaResolver.java73
-rw-r--r--src/org/jivesoftware/smack/util/dns/DNSResolver.java33
-rw-r--r--src/org/jivesoftware/smack/util/dns/HostAddress.java109
-rw-r--r--src/org/jivesoftware/smack/util/dns/SRVRecord.java79
-rw-r--r--src/org/jivesoftware/smack/util/package.html1
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 = "&quot;".toCharArray();
+ private static final char[] APOS_ENCODE = "&apos;".toCharArray();
+ private static final char[] AMP_ENCODE = "&amp;".toCharArray();
+ private static final char[] LT_ENCODE = "&lt;".toCharArray();
+ private static final char[] GT_ENCODE = "&gt;".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>&lt;space&gt;</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>&lt;</td><td>\3c</td></tr>
+ * <tr><td>&gt;</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 &lt;space&gt; 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>&lt;space&gt;</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>&lt;</td><td>\3c</td></tr>
+ * <tr><td>&gt;</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 &lt;space&gt; 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 &#235; (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 &gt; 0.0f and generally &lt; 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