Add android smack source.

Change-Id: I49ce97136c17173c4ae3965c694af6e7bc49897d
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..b503e0e
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,28 @@
+# Copyright 2011, The Android Open Source Project
+#
+# 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.
+
+LOCAL_PATH := $(call my-dir)
+
+##################################################
+# Static library
+##################################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := smackxmpp
+LOCAL_MODULE_TAGS := optional
+LOCAL_SDK_VERSION := 9
+
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/README b/README
new file mode 100644
index 0000000..c986de5
--- /dev/null
+++ b/README
@@ -0,0 +1,7 @@
+The directory contains the build environment of smack for Android (located in asmack-master/)
+and the generated source files (located in src/).
+
+To update to the latest smack source. Please run the following:
+
+./update.sh
+m -j24 smackxmpp
diff --git a/asmack-master/build.bash b/asmack-master/build.bash
index 874f238..d291220 100755
--- a/asmack-master/build.bash
+++ b/asmack-master/build.bash
@@ -542,6 +542,16 @@
 createVersionTag
 createbuildsrc
 patchsrc "patch"
+
+##
+## BEGIN Modification for android platform build system
+##
+echo done with android modifications
+exit
+##
+## END Modification for android platform build system
+##
+
 if $BUILD_JINGLE ; then
   patchsrc "jingle"
   JINGLE_ARGS="-Djingle=lib/jstun.jar"
diff --git a/src/META-INF/services/com.kenai.jbosh.HTTPSender b/src/META-INF/services/com.kenai.jbosh.HTTPSender
new file mode 100644
index 0000000..3608d8e
--- /dev/null
+++ b/src/META-INF/services/com.kenai.jbosh.HTTPSender
@@ -0,0 +1 @@
+com.kenai.jbosh.ApacheHTTPSender
diff --git a/src/com/kenai/jbosh/AbstractAttr.java b/src/com/kenai/jbosh/AbstractAttr.java
new file mode 100644
index 0000000..0d6f84c
--- /dev/null
+++ b/src/com/kenai/jbosh/AbstractAttr.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ package com.kenai.jbosh;
+
+/**
+ * Abstract base class for creating BOSH attribute classes.  Concrete
+ * implementations of this class will naturally inherit the underlying
+ * type's behavior for {@code equals()}, {@code hashCode()},
+ * {@code toString()}, and {@code compareTo()}, allowing for the easy
+ * creation of objects which extend existing trivial types.  This was done
+ * to comply with the prefactoring rule declaring, "when you are being
+ * abstract, be abstract all the way".
+ *
+ * @param <T> type of the extension object
+ */
+abstract class AbstractAttr<T extends Comparable>
+    implements Comparable {
+
+    /**
+     * Captured value.
+     */
+    private final T value;
+
+    /**
+     * Creates a new encapsulated object instance.
+     *
+     * @param aValue encapsulated getValue
+     */
+    protected AbstractAttr(final T aValue) {
+        value = aValue;
+    }
+
+    /**
+     * Gets the encapsulated data value.
+     *
+     * @return data value
+     */
+    public final T getValue() {
+        return value;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Object method overrides:
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param otherObj object to compare to
+     * @return true if the objects are equal, false otherwise
+     */
+    @Override
+    public boolean equals(final Object otherObj) {
+        if (otherObj == null) {
+            return false;
+        } else if (otherObj instanceof AbstractAttr) {
+            AbstractAttr other =
+                    (AbstractAttr) otherObj;
+            return value.equals(other.value);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return hashCode of the encapsulated object
+     */
+    @Override
+    public int hashCode() {
+        return value.hashCode();
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return string representation of the encapsulated object
+     */
+    @Override
+    public String toString() {
+        return value.toString();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Comparable interface:
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param otherObj object to compare to
+     * @return -1, 0, or 1
+     */
+    @SuppressWarnings("unchecked")
+    public int compareTo(final Object otherObj) {
+        if (otherObj == null) {
+            return 1;
+        } else {
+            return value.compareTo(otherObj);
+        }
+    }
+
+}
diff --git a/src/com/kenai/jbosh/AbstractBody.java b/src/com/kenai/jbosh/AbstractBody.java
new file mode 100644
index 0000000..4d66c8c
--- /dev/null
+++ b/src/com/kenai/jbosh/AbstractBody.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Class representing a single message to or from the BOSH connection
+ * manager (CM).
+ * <p/>
+ * These messages consist of a single {@code body} element
+ * (qualified within the BOSH namespace:
+ * {@code http://jabber.org/protocol/httpbind}) and contain zero or more
+ * child elements (of any namespace).  These child elements constitute the
+ * message payload.
+ * <p/>
+ * In addition to the message payload, the attributes of the wrapper
+ * {@code body} element may also need to be used as part of the communication
+ * protocol being implemented on top of BOSH, or to define additional
+ * namespaces used by the child "payload" elements.  These attributes are
+ * exposed via accessors.
+ */
+public abstract class AbstractBody {
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructor:
+
+    /**
+     * Restrict subclasses to the local package.
+     */
+    AbstractBody() {
+        // Empty
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Public methods:
+
+    /**
+     * Get a set of all defined attribute names.
+     *
+     * @return set of qualified attribute names
+     */
+    public final Set<BodyQName> getAttributeNames() {
+        Map<BodyQName, String> attrs = getAttributes();
+        return Collections.unmodifiableSet(attrs.keySet());
+    }
+
+    /**
+     * Get the value of the specified attribute.
+     *
+     * @param attr name of the attribute to retriece
+     * @return attribute value, or {@code null} if not defined
+     */
+    public final String getAttribute(final BodyQName attr) {
+        Map<BodyQName, String> attrs = getAttributes();
+        return attrs.get(attr);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Abstract methods:
+
+    /**
+     * Get a map of all defined attribute names with their corresponding values.
+     *
+     * @return map of qualified attributes
+     */
+    public abstract Map<BodyQName, String> getAttributes();
+
+    /**
+     * Get an XML String representation of this message. 
+     *
+     * @return XML string representing the body message
+     */
+    public abstract String toXML();
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Package-private methods:
+
+    /**
+     * Returns the qualified name of the root/wrapper element.
+     *
+     * @return qualified name
+     */
+    static BodyQName getBodyQName() {
+        return BodyQName.createBOSH("body");
+    }
+
+}
diff --git a/src/com/kenai/jbosh/AbstractIntegerAttr.java b/src/com/kenai/jbosh/AbstractIntegerAttr.java
new file mode 100644
index 0000000..1b827f9
--- /dev/null
+++ b/src/com/kenai/jbosh/AbstractIntegerAttr.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Abstract base class for attribute implementations based on {@code Integer}
+ * types.  Additional support for parsing of integer values from their
+ * {@code String} representations as well as callback handling of value
+ * validity checks are also provided.
+ */
+abstract class AbstractIntegerAttr extends AbstractAttr<Integer> {
+    
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute value
+     * @throws BOSHException on parse or validation failure
+     */
+    protected AbstractIntegerAttr(final int val) throws BOSHException {
+        super(Integer.valueOf(val));
+    }
+    
+    /**
+     * Creates a new attribute object.
+     *
+     * @param val attribute value in string form
+     * @throws BOSHException on parse or validation failure
+     */
+    protected AbstractIntegerAttr(final String val) throws BOSHException {
+        super(parseInt(val));
+    }
+
+    /**
+     * Utility method intended to be called by concrete implementation
+     * classes from within the {@code check()} method when the concrete
+     * class needs to ensure that the integer value does not drop below
+     * the specified minimum value.
+     *
+     * @param minVal minimum value to allow
+     * @throws BOSHException if the integer value is below the specific
+     *  minimum
+     */
+    protected final void checkMinValue(int minVal) throws BOSHException {
+        int intVal = getValue();
+        if (intVal < minVal) {
+            throw(new BOSHException(
+                    "Illegal attribute value '" + intVal + "' provided.  "
+                    + "Must be >= " + minVal));
+        }
+    }
+
+    /**
+     * Utility method to parse a {@code String} into an {@code Integer},
+     * converting any possible {@code NumberFormatException} thrown into
+     * a {@code BOSHException}.
+     *
+     * @param str string to parse
+     * @return integer value
+     * @throws BOSHException on {@code NumberFormatException}
+     */
+    private static int parseInt(final String str) throws BOSHException {
+        try {
+            return Integer.parseInt(str);
+        } catch (NumberFormatException nfx) {
+            throw(new BOSHException(
+                    "Could not parse an integer from the value provided: "
+                    + str,
+                    nfx));
+        }
+    }
+
+    /**
+     * Returns the native {@code int} value of the underlying {@code Integer}.
+     * Will throw {@code NullPointerException} if the underlying
+     * integer was {@code null}.
+     *
+     * @return native {@code int} value
+     */
+    public int intValue() {
+        return getValue().intValue();
+    }
+
+}
diff --git a/src/com/kenai/jbosh/ApacheHTTPResponse.java b/src/com/kenai/jbosh/ApacheHTTPResponse.java
new file mode 100644
index 0000000..9f6731f
--- /dev/null
+++ b/src/com/kenai/jbosh/ApacheHTTPResponse.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2009 Guenther Niess
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.io.IOException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ByteArrayEntity;
+
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.EntityUtils;
+
+final class ApacheHTTPResponse implements HTTPResponse {
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constants:
+
+    /**
+     * Name of the accept encoding header.
+     */
+    private static final String ACCEPT_ENCODING = "Accept-Encoding";
+
+    /**
+     * Value to use for the ACCEPT_ENCODING header.
+     */
+    private static final String ACCEPT_ENCODING_VAL =
+            ZLIBCodec.getID() + ", " + GZIPCodec.getID();
+
+    /**
+     * Name of the character set to encode the body to/from.
+     */
+    private static final String CHARSET = "UTF-8";
+
+    /**
+     * Content type to use when transmitting the body data.
+     */
+    private static final String CONTENT_TYPE = "text/xml; charset=utf-8";
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Class variables:
+
+    /**
+     * Lock used for internal synchronization.
+     */
+    private final Lock lock = new ReentrantLock();
+
+    /**
+     * The execution state of an HTTP process.
+     */
+    private final HttpContext context;
+
+    /**
+     * HttpClient instance to use to communicate.
+     */
+    private final HttpClient client;
+
+    /**
+     * The HTTP POST request is sent to the server.
+     */
+    private final HttpPost post;
+
+    /**
+     * A flag which indicates if the transmission was already done.
+     */
+    private boolean sent;
+
+    /**
+     * Exception to throw when the response data is attempted to be accessed,
+     * or {@code null} if no exception should be thrown.
+     */
+    private BOSHException toThrow;
+
+    /**
+     * The response body which was received from the server or {@code null}
+     * if that has not yet happened.
+     */
+    private AbstractBody body;
+
+    /**
+     * The HTTP response status code.
+     */
+    private int statusCode;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructors:
+
+    /**
+     * Create and send a new request to the upstream connection manager,
+     * providing deferred access to the results to be returned.
+     *
+     * @param client client instance to use when sending the request
+     * @param cfg client configuration
+     * @param params connection manager parameters from the session creation
+     *  response, or {@code null} if the session has not yet been established
+     * @param request body of the client request
+     */
+    ApacheHTTPResponse(
+            final HttpClient client,
+            final BOSHClientConfig cfg,
+            final CMSessionParams params,
+            final AbstractBody request) {
+        super();
+        this.client = client;
+        this.context = new BasicHttpContext();
+        this.post = new HttpPost(cfg.getURI().toString());
+        this.sent = false;
+
+        try {
+            String xml = request.toXML();
+            byte[] data = xml.getBytes(CHARSET);
+
+            String encoding = null;
+            if (cfg.isCompressionEnabled() && params != null) {
+                AttrAccept accept = params.getAccept();
+                if (accept != null) {
+                    if (accept.isAccepted(ZLIBCodec.getID())) {
+                        encoding = ZLIBCodec.getID();
+                        data = ZLIBCodec.encode(data);
+                    } else if (accept.isAccepted(GZIPCodec.getID())) {
+                        encoding = GZIPCodec.getID();
+                        data = GZIPCodec.encode(data);
+                    }
+                }
+            }
+
+            ByteArrayEntity entity = new ByteArrayEntity(data);
+            entity.setContentType(CONTENT_TYPE);
+            if (encoding != null) {
+                entity.setContentEncoding(encoding);
+            }
+            post.setEntity(entity);
+            if (cfg.isCompressionEnabled()) {
+                post.setHeader(ACCEPT_ENCODING, ACCEPT_ENCODING_VAL);
+            }
+        } catch (Exception e) {
+            toThrow = new BOSHException("Could not generate request", e);
+        }
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // HTTPResponse interface methods:
+
+    /**
+     * Abort the client transmission and response processing.
+     */
+    public void abort() {
+        if (post != null) {
+            post.abort();
+            toThrow = new BOSHException("HTTP request aborted");
+        }
+    }
+
+    /**
+     * Wait for and then return the response body.
+     *
+     * @return body of the response
+     * @throws InterruptedException if interrupted while awaiting the response
+     * @throws BOSHException on communication failure
+     */
+    public AbstractBody getBody() throws InterruptedException, BOSHException {
+        if (toThrow != null) {
+            throw(toThrow);
+        }
+        lock.lock();
+        try {
+            if (!sent) {
+                awaitResponse();
+            }
+        } finally {
+            lock.unlock();
+        }
+        return body;
+    }
+
+    /**
+     * Wait for and then return the response HTTP status code.
+     *
+     * @return HTTP status code of the response
+     * @throws InterruptedException if interrupted while awaiting the response
+     * @throws BOSHException on communication failure
+     */
+    public int getHTTPStatus() throws InterruptedException, BOSHException {
+        if (toThrow != null) {
+            throw(toThrow);
+        }
+        lock.lock();
+        try {
+            if (!sent) {
+                awaitResponse();
+            }
+        } finally {
+            lock.unlock();
+        }
+        return statusCode;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Package-private methods:
+
+    /**
+     * Await the response, storing the result in the instance variables of
+     * this class when they arrive.
+     *
+     * @throws InterruptedException if interrupted while awaiting the response
+     * @throws BOSHException on communication failure
+     */
+    private synchronized void awaitResponse() throws BOSHException {
+        HttpEntity entity = null;
+        try {
+            HttpResponse httpResp = client.execute(post, context);
+            entity = httpResp.getEntity();
+            byte[] data = EntityUtils.toByteArray(entity);
+            String encoding = entity.getContentEncoding() != null ?
+                    entity.getContentEncoding().getValue() :
+                    null;
+            if (ZLIBCodec.getID().equalsIgnoreCase(encoding)) {
+                data = ZLIBCodec.decode(data);
+            } else if (GZIPCodec.getID().equalsIgnoreCase(encoding)) {
+                data = GZIPCodec.decode(data);
+            }
+            body = StaticBody.fromString(new String(data, CHARSET));
+            statusCode = httpResp.getStatusLine().getStatusCode();
+            sent = true;
+        } catch (IOException iox) {
+            abort();
+            toThrow = new BOSHException("Could not obtain response", iox);
+            throw(toThrow);
+        } catch (RuntimeException ex) {
+            abort();
+            throw(ex);
+        }
+    }
+}
diff --git a/src/com/kenai/jbosh/ApacheHTTPSender.java b/src/com/kenai/jbosh/ApacheHTTPSender.java
new file mode 100644
index 0000000..b3d3c93
--- /dev/null
+++ b/src/com/kenai/jbosh/ApacheHTTPSender.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2009 Guenther Niess
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.http.HttpHost;
+import org.apache.http.HttpVersion;
+import org.apache.http.client.HttpClient;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.params.ConnManagerParams;
+import org.apache.http.conn.params.ConnRoutePNames;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+
+/**
+ * Implementation of the {@code HTTPSender} interface which uses the
+ * Apache HttpClient API to send messages to the connection manager.
+ */
+final class ApacheHTTPSender implements HTTPSender {
+
+    /**
+     * Lock used for internal synchronization.
+     */
+    private final Lock lock = new ReentrantLock();
+
+    /**
+     * Session configuration.
+     */
+    private BOSHClientConfig cfg;
+
+    /**
+     * HttpClient instance to use to communicate.
+     */
+    private HttpClient httpClient;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructors:
+
+    /**
+     * Prevent construction apart from our package.
+     */
+    ApacheHTTPSender() {
+        // Load Apache HTTP client class
+        HttpClient.class.getName();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // HTTPSender interface methods:
+
+    /**
+     * {@inheritDoc}
+     */
+    public void init(final BOSHClientConfig session) {
+        lock.lock();
+        try {
+            cfg = session;
+            httpClient = initHttpClient(session);
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void destroy() {
+        lock.lock();
+        try {
+            if (httpClient != null) {
+                httpClient.getConnectionManager().shutdown();
+            }
+        } finally {
+            cfg = null;
+            httpClient = null;
+            lock.unlock();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public HTTPResponse send(
+            final CMSessionParams params,
+            final AbstractBody body) {
+        HttpClient mClient;
+        BOSHClientConfig mCfg;
+        lock.lock();
+        try {
+            if (httpClient == null) {
+                httpClient = initHttpClient(cfg);
+            }
+            mClient = httpClient;
+            mCfg = cfg;
+        } finally {
+            lock.unlock();
+        }
+        return new ApacheHTTPResponse(mClient, mCfg, params, body);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Package-private methods:
+
+    private synchronized HttpClient initHttpClient(final BOSHClientConfig config) {
+        // Create and initialize HTTP parameters
+        HttpParams params = new BasicHttpParams();
+        ConnManagerParams.setMaxTotalConnections(params, 100);
+        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
+        HttpProtocolParams.setUseExpectContinue(params, false);
+        if (config != null &&
+                config.getProxyHost() != null &&
+                config.getProxyPort() != 0) {
+            HttpHost proxy = new HttpHost(
+                    config.getProxyHost(),
+                    config.getProxyPort());
+            params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
+        }
+
+        // Create and initialize scheme registry 
+        SchemeRegistry schemeRegistry = new SchemeRegistry();
+        schemeRegistry.register(
+                new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
+            SSLSocketFactory sslFactory = SSLSocketFactory.getSocketFactory();
+            sslFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+            schemeRegistry.register(
+                    new Scheme("https", sslFactory, 443));
+
+        // Create an HttpClient with the ThreadSafeClientConnManager.
+        // This connection manager must be used if more than one thread will
+        // be using the HttpClient.
+        ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
+        return new DefaultHttpClient(cm, params);
+    }
+}
diff --git a/src/com/kenai/jbosh/AttrAccept.java b/src/com/kenai/jbosh/AttrAccept.java
new file mode 100644
index 0000000..4f767df
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrAccept.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code accept} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrAccept extends AbstractAttr<String> {
+
+    /**
+     * Array of the accepted encodings.
+     */
+    private final String[] encodings;
+
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute getValue
+     * @throws BOSHException on parse or validation failure
+     */
+    private AttrAccept(final String val) {
+        super(val);
+        encodings = val.split("[\\s,]+");
+    }
+    
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return attribute instance or {@code null} if provided string is
+     *  {@code null}
+     * @throws BOSHException on parse or validation failure
+     */
+    static AttrAccept createFromString(final String str)
+    throws BOSHException {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrAccept(str);
+        }
+    }
+
+    /**
+     * Determines whether or not the specified encoding is supported.
+     *
+     * @param name encoding name
+     * @result {@code true} if the encoding is accepted, {@code false}
+     *  otherwise
+     */
+    boolean isAccepted(final String name) {
+        for (String str : encodings) {
+            if (str.equalsIgnoreCase(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/src/com/kenai/jbosh/AttrAck.java b/src/com/kenai/jbosh/AttrAck.java
new file mode 100644
index 0000000..6cfe22b
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrAck.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code ack} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrAck extends AbstractAttr<String> {
+
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute getValue
+     * @throws BOSHException on parse or validation failure
+     */
+    private AttrAck(final String val) throws BOSHException {
+        super(val);
+    }
+    
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return attribute instance or {@code null} if provided string is
+     *  {@code null}
+     * @throws BOSHException on parse or validation failure
+     */
+    static AttrAck createFromString(final String str)
+    throws BOSHException {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrAck(str);
+        }
+    }
+
+}
diff --git a/src/com/kenai/jbosh/AttrCharsets.java b/src/com/kenai/jbosh/AttrCharsets.java
new file mode 100644
index 0000000..45ce78c
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrCharsets.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code charsets} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrCharsets extends AbstractAttr<String> {
+
+    /**
+     * Array of the accepted character sets.
+     */
+    private final String[] charsets;
+
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute getValue
+     */
+    private AttrCharsets(final String val) {
+        super(val);
+        charsets = val.split("\\ +");
+    }
+    
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return attribute instance or {@code null} if provided string is
+     *  {@code null}
+     */
+    static AttrCharsets createFromString(final String str) {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrCharsets(str);
+        }
+    }
+
+    /**
+     * Determines whether or not the specified charset is supported.
+     *
+     * @param name encoding name
+     * @result {@code true} if the encoding is accepted, {@code false}
+     *  otherwise
+     */
+    boolean isAccepted(final String name) {
+        for (String str : charsets) {
+            if (str.equalsIgnoreCase(name)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/src/com/kenai/jbosh/AttrHold.java b/src/com/kenai/jbosh/AttrHold.java
new file mode 100644
index 0000000..56f21dd
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrHold.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code hold} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrHold extends AbstractIntegerAttr {
+    
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute getValue
+     * @throws BOSHException on parse or validation failure
+     */
+    private AttrHold(final String val) throws BOSHException {
+        super(val);
+        checkMinValue(0);
+    }
+
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return attribute instance or {@code null} if provided string is
+     *  {@code null}
+     * @throws BOSHException on parse or validation failure
+     */
+    static AttrHold createFromString(final String str)
+    throws BOSHException {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrHold(str);
+        }
+    }
+    
+}
diff --git a/src/com/kenai/jbosh/AttrInactivity.java b/src/com/kenai/jbosh/AttrInactivity.java
new file mode 100644
index 0000000..14ab7d4
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrInactivity.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the value of the {@code inactivity} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrInactivity extends AbstractIntegerAttr {
+
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute value
+     * @throws BOSHException on parse or validation failure
+     */
+    private AttrInactivity(final String val) throws BOSHException {
+        super(val);
+        checkMinValue(0);
+    }
+
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return instance of the attribute for the specified string, or
+     *  {@code null} if input string is {@code null}
+     * @throws BOSHException on parse or validation failure
+     */
+    static AttrInactivity createFromString(final String str)
+    throws BOSHException {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrInactivity(str);
+        }
+    }
+    
+}
diff --git a/src/com/kenai/jbosh/AttrMaxPause.java b/src/com/kenai/jbosh/AttrMaxPause.java
new file mode 100644
index 0000000..8d1d98b
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrMaxPause.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Data type representing the getValue of the {@code maxpause} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrMaxPause extends AbstractIntegerAttr {
+    
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute getValue
+     * @throws BOSHException on parse or validation failure
+     */
+    private AttrMaxPause(final String val) throws BOSHException {
+        super(val);
+        checkMinValue(1);
+    }
+
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return attribute instance or {@code null} if provided string is
+     *  {@code null}
+     * @throws BOSHException on parse or validation failure
+     */
+    static AttrMaxPause createFromString(final String str)
+    throws BOSHException {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrMaxPause(str);
+        }
+    }
+    
+    /**
+     * Get the max pause time in milliseconds.
+     *
+     * @return pause tme in milliseconds
+     */
+    public int getInMilliseconds() {
+        return (int) TimeUnit.MILLISECONDS.convert(
+                intValue(), TimeUnit.SECONDS);
+    }
+
+}
diff --git a/src/com/kenai/jbosh/AttrPause.java b/src/com/kenai/jbosh/AttrPause.java
new file mode 100644
index 0000000..5fb3282
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrPause.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Data type representing the getValue of the {@code pause} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrPause extends AbstractIntegerAttr {
+    
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute getValue
+     * @throws BOSHException on parse or validation failure
+     */
+    private AttrPause(final String val) throws BOSHException {
+        super(val);
+        checkMinValue(1);
+    }
+
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return attribute instance or {@code null} if provided string is
+     *  {@code null}
+     * @throws BOSHException on parse or validation failure
+     */
+    static AttrPause createFromString(final String str)
+    throws BOSHException {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrPause(str);
+        }
+    }
+    
+    /**
+     * Get the pause time in milliseconds.
+     *
+     * @return pause tme in milliseconds
+     */
+    public int getInMilliseconds() {
+        return (int) TimeUnit.MILLISECONDS.convert(
+                intValue(), TimeUnit.SECONDS);
+    }
+
+}
diff --git a/src/com/kenai/jbosh/AttrPolling.java b/src/com/kenai/jbosh/AttrPolling.java
new file mode 100644
index 0000000..3f0b08d
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrPolling.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Data type representing the getValue of the {@code polling} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrPolling extends AbstractIntegerAttr {
+
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute getValue
+     * @throws BOSHException on parse or validation failure
+     */
+    private AttrPolling(final String str) throws BOSHException {
+        super(str);
+        checkMinValue(0);
+    }
+
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return instance of the attribute for the specified string, or
+     *  {@code null} if input string is {@code null}
+     * @throws BOSHException on parse or validation failure
+     */
+    static AttrPolling createFromString(final String str)
+    throws BOSHException {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrPolling(str);
+        }
+    }
+
+    /**
+     * Get the polling interval in milliseconds.
+     *
+     * @return polling interval in milliseconds
+     */
+    public int getInMilliseconds() {
+        return (int) TimeUnit.MILLISECONDS.convert(
+                intValue(), TimeUnit.SECONDS);
+    }
+    
+}
diff --git a/src/com/kenai/jbosh/AttrRequests.java b/src/com/kenai/jbosh/AttrRequests.java
new file mode 100644
index 0000000..bfdc529
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrRequests.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the value of the {@code requests} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrRequests extends AbstractIntegerAttr {
+    
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute value
+     * @throws BOSHException on parse or validation failure
+     */
+    private AttrRequests(final String val) throws BOSHException {
+        super(val);
+        checkMinValue(1);
+    }
+
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return instance of the attribute for the specified string, or
+     *  {@code null} if input string is {@code null}
+     * @throws BOSHException on parse or validation failure
+     */
+    static AttrRequests createFromString(final String str)
+    throws BOSHException {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrRequests(str);
+        }
+    }
+    
+}
diff --git a/src/com/kenai/jbosh/AttrSessionID.java b/src/com/kenai/jbosh/AttrSessionID.java
new file mode 100644
index 0000000..1998968
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrSessionID.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code sid} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrSessionID extends AbstractAttr<String> {
+    
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute getValue
+     */
+    private AttrSessionID(final String val) {
+        super(val);
+    }
+    
+    /**
+     * Creates a new attribute instance from the provided String.
+     *
+     * @param str string representation of the attribute
+     * @return attribute instance
+     */
+    static AttrSessionID createFromString(final String str) {
+        return new AttrSessionID(str);
+    }
+    
+}
diff --git a/src/com/kenai/jbosh/AttrVersion.java b/src/com/kenai/jbosh/AttrVersion.java
new file mode 100644
index 0000000..9396e3b
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrVersion.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code ver} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrVersion extends AbstractAttr<String> implements Comparable {
+
+    /**
+     * Default value if none is provided.
+     */
+    private static final AttrVersion DEFAULT;
+    static {
+        try {
+            DEFAULT = createFromString("1.8");
+        } catch (BOSHException boshx) {
+            throw(new IllegalStateException(boshx));
+        }
+    }
+
+    /**
+     * Major portion of the version.
+     */
+    private final int major;
+    
+    /**
+     * Minor portion of the version.
+     */
+    private final int minor;
+
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute getValue
+     * @throws BOSHException on parse or validation failure
+     */
+    private AttrVersion(final String val) throws BOSHException {
+        super(val);
+
+        int idx = val.indexOf('.');
+        if (idx <= 0) {
+            throw(new BOSHException(
+                    "Illegal ver attribute value (not in major.minor form): "
+                    + val));
+        }
+
+        String majorStr = val.substring(0, idx);
+        try {
+            major = Integer.parseInt(majorStr);
+        } catch (NumberFormatException nfx) {
+            throw(new BOSHException(
+                    "Could not parse ver attribute value (major ver): "
+                    + majorStr,
+                    nfx));
+        }
+        if (major < 0) {
+            throw(new BOSHException(
+                    "Major version may not be < 0"));
+        }
+
+        String minorStr = val.substring(idx + 1);
+        try {
+            minor = Integer.parseInt(minorStr);
+        } catch (NumberFormatException nfx) {
+            throw(new BOSHException(
+                    "Could not parse ver attribute value (minor ver): "
+                    + minorStr,
+                    nfx));
+        }
+        if (minor < 0) {
+            throw(new BOSHException(
+                    "Minor version may not be < 0"));
+        }
+    }
+    
+    /**
+     * Get the version of specifcation that we support.
+     *
+     * @return max spec version the code supports
+     */
+    static AttrVersion getSupportedVersion() {
+        return DEFAULT;
+    }
+
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return attribute instance or {@code null} if provided string is
+     *  {@code null}
+     * @throws BOSHException on parse or validation failure
+     */
+    static AttrVersion createFromString(final String str)
+    throws BOSHException {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrVersion(str);
+        }
+    }
+
+    /**
+     * Returns the 'major' portion of the version number.
+     *
+     * @return major digits only
+     */
+    int getMajor() {
+        return major;
+    }
+
+    /**
+     * Returns the 'minor' portion of the version number.
+     *
+     * @return minor digits only
+     */
+    int getMinor() {
+        return minor;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Comparable interface:
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param otherObj object to compare to
+     * @return -1, 0, or 1
+     */
+    @Override
+    public int compareTo(final Object otherObj) {
+        if (otherObj instanceof AttrVersion) {
+            AttrVersion other = (AttrVersion) otherObj;
+            if (major < other.major) {
+                return -1;
+            } else if (major > other.major) {
+                return 1;
+            } else if (minor < other.minor) {
+                return -1;
+            } else if (minor > other.minor) {
+                return 1;
+            } else {
+                return 0;
+            }
+        } else {
+            return 0;
+        }
+    }
+
+}
diff --git a/src/com/kenai/jbosh/AttrWait.java b/src/com/kenai/jbosh/AttrWait.java
new file mode 100644
index 0000000..d2c95f7
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrWait.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code wait} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrWait extends AbstractIntegerAttr {
+    
+    /**
+     * Creates a new attribute object.
+     * 
+     * @param val attribute getValue
+     * @throws BOSHException on parse or validation failure
+     */
+    private AttrWait(final String val) throws BOSHException {
+        super(val);
+        checkMinValue(1);
+    }
+
+    /**
+     * Creates a new attribute instance from the provided String.
+     * 
+     * @param str string representation of the attribute
+     * @return attribute instance or {@code null} if provided string is
+     *  {@code null}
+     * @throws BOSHException on parse or validation failure
+     */
+    static AttrWait createFromString(final String str)
+    throws BOSHException {
+        if (str == null) {
+            return null;
+        } else {
+            return new AttrWait(str);
+        }
+    }
+    
+}
diff --git a/src/com/kenai/jbosh/Attributes.java b/src/com/kenai/jbosh/Attributes.java
new file mode 100644
index 0000000..d01541e
--- /dev/null
+++ b/src/com/kenai/jbosh/Attributes.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import javax.xml.XMLConstants;
+
+/**
+ * Class containing constants for attribute definitions used by the
+ * XEP-0124 specification.  We shouldn't need to expose these outside
+ * our package, since nobody else should be needing to worry about
+ * them.
+ */
+final class Attributes {
+
+    /**
+     * Private constructor to prevent construction of library class.
+     */
+    private Attributes() {
+        super();
+    }
+
+    static final BodyQName ACCEPT = BodyQName.createBOSH("accept");
+    static final BodyQName AUTHID = BodyQName.createBOSH("authid");
+    static final BodyQName ACK = BodyQName.createBOSH("ack");
+    static final BodyQName CHARSETS = BodyQName.createBOSH("charsets");
+    static final BodyQName CONDITION = BodyQName.createBOSH("condition");
+    static final BodyQName CONTENT = BodyQName.createBOSH("content");
+    static final BodyQName FROM = BodyQName.createBOSH("from");
+    static final BodyQName HOLD = BodyQName.createBOSH("hold");
+    static final BodyQName INACTIVITY = BodyQName.createBOSH("inactivity");
+    static final BodyQName KEY = BodyQName.createBOSH("key");
+    static final BodyQName MAXPAUSE = BodyQName.createBOSH("maxpause");
+    static final BodyQName NEWKEY = BodyQName.createBOSH("newkey");
+    static final BodyQName PAUSE = BodyQName.createBOSH("pause");
+    static final BodyQName POLLING = BodyQName.createBOSH("polling");
+    static final BodyQName REPORT = BodyQName.createBOSH("report");
+    static final BodyQName REQUESTS = BodyQName.createBOSH("requests");
+    static final BodyQName RID = BodyQName.createBOSH("rid");
+    static final BodyQName ROUTE = BodyQName.createBOSH("route");
+    static final BodyQName SECURE = BodyQName.createBOSH("secure");
+    static final BodyQName SID = BodyQName.createBOSH("sid");
+    static final BodyQName STREAM = BodyQName.createBOSH("stream");
+    static final BodyQName TIME = BodyQName.createBOSH("time");
+    static final BodyQName TO = BodyQName.createBOSH("to");
+    static final BodyQName TYPE = BodyQName.createBOSH("type");
+    static final BodyQName VER = BodyQName.createBOSH("ver");
+    static final BodyQName WAIT = BodyQName.createBOSH("wait");
+    static final BodyQName XML_LANG =
+            BodyQName.createWithPrefix(XMLConstants.XML_NS_URI, "lang", "xml");
+}
diff --git a/src/com/kenai/jbosh/BOSHClient.java b/src/com/kenai/jbosh/BOSHClient.java
new file mode 100644
index 0000000..b96d188
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClient.java
@@ -0,0 +1,1536 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import com.kenai.jbosh.ComposableBody.Builder;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * BOSH Client session instance.  Each communication session with a remote
+ * connection manager is represented and handled by an instance of this
+ * class.  This is the main entry point for client-side communications.
+ * To create a new session, a client configuration must first be created
+ * and then used to create a client instance:
+ * <pre>
+ * BOSHClientConfig cfg = BOSHClientConfig.Builder.create(
+ *         "http://server:1234/httpbind", "jabber.org")
+ *     .setFrom("user@jabber.org")
+ *     .build();
+ * BOSHClient client = BOSHClient.create(cfg);
+ * </pre>
+ * Additional client configuration options are available.  See the
+ * {@code BOSHClientConfig.Builder} class for more information.
+ * <p/>
+ * Once a {@code BOSHClient} instance has been created, communication with
+ * the remote connection manager can begin.  No attempt will be made to
+ * establish a connection to the connection manager until the first call
+ * is made to the {@code send(ComposableBody)} method.  Note that it is
+ * possible to send an empty body to cause an immediate connection attempt
+ * to the connection manager.  Sending an empty message would look like
+ * the following:
+ * <pre>
+ * client.send(ComposableBody.builder().build());
+ * </pre>
+ * For more information on creating body messages with content, see the
+ * {@code ComposableBody.Builder} class documentation.
+ * <p/>
+ * Once a session has been successfully started, the client instance can be
+ * used to send arbitrary payload data.  All aspects of the BOSH
+ * protocol involving setting and processing attributes in the BOSH
+ * namespace will be handled by the client code transparently and behind the
+ * scenes.  The user of the client instance can therefore concentrate
+ * entirely on the content of the message payload, leaving the semantics of
+ * the BOSH protocol to the client implementation.
+ * <p/>
+ * To be notified of incoming messages from the remote connection manager,
+ * a {@code BOSHClientResponseListener} should be added to the client instance.
+ * All incoming messages will be published to all response listeners as they
+ * arrive and are processed.  As with the transmission of payload data via
+ * the {@code send(ComposableBody)} method, there is no need to worry about
+ * handling of the BOSH attributes, since this is handled behind the scenes.
+ * <p/>
+ * If the connection to the remote connection manager is terminated (either
+ * explicitly or due to a terminal condition of some sort), all connection
+ * listeners will be notified.  After the connection has been closed, the
+ * client instance is considered dead and a new one must be created in order
+ * to resume communications with the remote server.
+ * <p/>
+ * Instances of this class are thread-safe.
+ *
+ * @see BOSHClientConfig.Builder
+ * @see BOSHClientResponseListener
+ * @see BOSHClientConnListener
+ * @see ComposableBody.Builder
+ */
+public final class BOSHClient {
+
+    /**
+     * Logger.
+     */
+    private static final Logger LOG = Logger.getLogger(
+            BOSHClient.class.getName());
+
+    /**
+     * Value of the 'type' attribute used for session termination.
+     */
+    private static final String TERMINATE = "terminate";
+    
+    /**
+     * Value of the 'type' attribute used for recoverable errors.
+     */
+    private static final String ERROR = "error";
+
+    /**
+     * Message to use for interrupted exceptions.
+     */
+    private static final String INTERRUPTED = "Interrupted";
+
+    /**
+     * Message used for unhandled exceptions.
+     */
+    private static final String UNHANDLED = "Unhandled Exception";
+
+    /**
+     * Message used whena null listener is detected.
+     */
+    private static final String NULL_LISTENER = "Listener may not b enull";
+
+    /**
+     * Default empty request delay.
+     */
+    private static final int DEFAULT_EMPTY_REQUEST_DELAY = 100;
+
+    /**
+     * Amount of time to wait before sending an empty request, in
+     * milliseconds.
+     */
+    private static final int EMPTY_REQUEST_DELAY = Integer.getInteger(
+            BOSHClient.class.getName() + ".emptyRequestDelay",
+            DEFAULT_EMPTY_REQUEST_DELAY);
+
+    /**
+     * Default value for the pause margin.
+     */
+    private static final int DEFAULT_PAUSE_MARGIN = 500;
+
+    /**
+     * The amount of time in milliseconds which will be reserved as a
+     * safety margin when scheduling empty requests against a maxpause
+     * value.   This should give us enough time to build the message
+     * and transport it to the remote host.
+     */
+    private static final int PAUSE_MARGIN = Integer.getInteger(
+            BOSHClient.class.getName() + ".pauseMargin",
+            DEFAULT_PAUSE_MARGIN);
+    
+    /**
+     * Flag indicating whether or not we want to perform assertions.
+     */
+    private static final boolean ASSERTIONS;
+
+    /**
+     * Connection listeners.
+     */
+    private final Set<BOSHClientConnListener> connListeners =
+            new CopyOnWriteArraySet<BOSHClientConnListener>();
+
+    /**
+     * Request listeners.
+     */
+    private final Set<BOSHClientRequestListener> requestListeners =
+            new CopyOnWriteArraySet<BOSHClientRequestListener>();
+
+    /**
+     * Response listeners.
+     */
+    private final Set<BOSHClientResponseListener> responseListeners =
+            new CopyOnWriteArraySet<BOSHClientResponseListener>();
+
+    /**
+     * Lock instance.
+     */
+    private final ReentrantLock lock = new ReentrantLock();
+
+    /**
+     * Condition indicating that there are messages to be exchanged.
+     */
+    private final Condition notEmpty = lock.newCondition();
+
+    /**
+     * Condition indicating that there are available slots for sending
+     * messages.
+     */
+    private final Condition notFull = lock.newCondition();
+
+    /**
+     * Condition indicating that there are no outstanding connections.
+     */
+    private final Condition drained = lock.newCondition();
+
+    /**
+     * Session configuration.
+     */
+    private final BOSHClientConfig cfg;
+
+    /**
+     * Processor thread runnable instance.
+     */
+    private final Runnable procRunnable = new Runnable() {
+        /**
+         * Process incoming messages.
+         */
+        public void run() {
+            processMessages();
+        }
+    };
+
+    /**
+     * Processor thread runnable instance.
+     */
+    private final Runnable emptyRequestRunnable = new Runnable() {
+        /**
+         * Process incoming messages.
+         */
+        public void run() {
+            sendEmptyRequest();
+        }
+    };
+
+    /**
+     * HTTPSender instance.
+     */
+    private final HTTPSender httpSender =
+            new ApacheHTTPSender();
+
+    /**
+     * Storage for test hook implementation.
+     */
+    private final AtomicReference<ExchangeInterceptor> exchInterceptor =
+            new AtomicReference<ExchangeInterceptor>();
+
+    /**
+     * Request ID sequence to use for the session.
+     */
+    private final RequestIDSequence requestIDSeq = new RequestIDSequence();
+
+    /**
+     * ScheduledExcecutor to use for deferred tasks.
+     */
+    private final ScheduledExecutorService schedExec =
+            Executors.newSingleThreadScheduledExecutor();
+
+    /************************************************************
+     * The following vars must be accessed via the lock instance.
+     */
+
+    /**
+     * Thread which is used to process responses from the connection
+     * manager.  Becomes null when session is terminated.
+     */
+    private Thread procThread;
+
+    /**
+     * Future for sending a deferred empty request, if needed.
+     */
+    private ScheduledFuture emptyRequestFuture;
+
+    /**
+     * Connection Manager session parameters.  Only available when in a
+     * connected state.
+     */
+    private CMSessionParams cmParams;
+
+    /**
+     * List of active/outstanding requests.
+     */
+    private Queue<HTTPExchange> exchanges = new LinkedList<HTTPExchange>();
+
+    /**
+     * Set of RIDs which have been received, for the purpose of sending
+     * response acknowledgements.
+     */
+    private SortedSet<Long> pendingResponseAcks = new TreeSet<Long>();
+    
+    /**
+     * The highest RID that we've already received a response for.  This value
+     * is used to implement response acks.
+     */
+    private Long responseAck = Long.valueOf(-1L);
+
+    /**
+     * List of requests which have been made but not yet acknowledged.  This
+     * list remains unpopulated if the CM is not acking requests.
+     */
+    private List<ComposableBody> pendingRequestAcks =
+            new ArrayList<ComposableBody>();
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Classes:
+
+    /**
+     * Class used in testing to dynamically manipulate received exchanges
+     * at test runtime.
+     */
+    abstract static class ExchangeInterceptor {
+        /**
+         * Limit construction.
+         */
+        ExchangeInterceptor() {
+            // Empty;
+        }
+
+        /**
+         * Hook to manipulate an HTTPExchange as is is about to be processed.
+         *
+         * @param exch original exchange that would be processed
+         * @return replacement exchange instance, or {@code null} to skip
+         *  processing of this exchange
+         */
+        abstract HTTPExchange interceptExchange(final HTTPExchange exch);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructors:
+
+    /**
+     * Determine whether or not we should perform assertions.  Assertions
+     * can be specified via system property explicitly, or defaulted to
+     * the JVM assertions status.
+     */
+    static {
+        final String prop =
+                BOSHClient.class.getSimpleName() + ".assertionsEnabled";
+        boolean enabled = false;
+        if (System.getProperty(prop) == null) {
+            assert enabled = true;
+        } else {
+            enabled = Boolean.getBoolean(prop);
+        }
+        ASSERTIONS = enabled;
+    }
+
+    /**
+     * Prevent direct construction.
+     */
+    private BOSHClient(final BOSHClientConfig sessCfg) {
+        cfg = sessCfg;
+        init();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Public methods:
+
+    /**
+     * Create a new BOSH client session using the client configuration
+     * information provided.
+     *
+     * @param clientCfg session configuration
+     * @return BOSH session instance
+     */
+    public static BOSHClient create(final BOSHClientConfig clientCfg) {
+        if (clientCfg == null) {
+            throw(new IllegalArgumentException(
+                    "Client configuration may not be null"));
+        }
+        return new BOSHClient(clientCfg);
+    }
+
+    /**
+     * Get the client configuration that was used to create this client
+     * instance.
+     *
+     * @return client configuration
+     */
+    public BOSHClientConfig getBOSHClientConfig() {
+        return cfg;
+    }
+
+    /**
+     * Adds a connection listener to the session.
+     *
+     * @param listener connection listener to add, if not already added
+     */
+    public void addBOSHClientConnListener(
+            final BOSHClientConnListener listener) {
+        if (listener == null) {
+            throw(new IllegalArgumentException(NULL_LISTENER));
+        }
+        connListeners.add(listener);
+    }
+
+    /**
+     * Removes a connection listener from the session.
+     *
+     * @param listener connection listener to remove, if previously added
+     */
+    public void removeBOSHClientConnListener(
+            final BOSHClientConnListener listener) {
+        if (listener == null) {
+            throw(new IllegalArgumentException(NULL_LISTENER));
+        }
+        connListeners.remove(listener);
+    }
+
+    /**
+     * Adds a request message listener to the session.
+     *
+     * @param listener request listener to add, if not already added
+     */
+    public void addBOSHClientRequestListener(
+            final BOSHClientRequestListener listener) {
+        if (listener == null) {
+            throw(new IllegalArgumentException(NULL_LISTENER));
+        }
+        requestListeners.add(listener);
+    }
+
+    /**
+     * Removes a request message listener from the session, if previously
+     * added.
+     *
+     * @param listener instance to remove
+     */
+    public void removeBOSHClientRequestListener(
+            final BOSHClientRequestListener listener) {
+        if (listener == null) {
+            throw(new IllegalArgumentException(NULL_LISTENER));
+        }
+        requestListeners.remove(listener);
+    }
+
+    /**
+     * Adds a response message listener to the session.
+     *
+     * @param listener response listener to add, if not already added
+     */
+    public void addBOSHClientResponseListener(
+            final BOSHClientResponseListener listener) {
+        if (listener == null) {
+            throw(new IllegalArgumentException(NULL_LISTENER));
+        }
+        responseListeners.add(listener);
+    }
+
+    /**
+     * Removes a response message listener from the session, if previously
+     * added.
+     *
+     * @param listener instance to remove
+     */
+    public void removeBOSHClientResponseListener(
+            final BOSHClientResponseListener listener) {
+        if (listener == null) {
+            throw(new IllegalArgumentException(NULL_LISTENER));
+        }
+        responseListeners.remove(listener);
+    }
+
+    /**
+     * Send the provided message data to the remote connection manager.  The
+     * provided message body does not need to have any BOSH-specific attribute
+     * information set.  It only needs to contain the actual message payload
+     * that should be delivered to the remote server.
+     * <p/>
+     * The first call to this method will result in a connection attempt
+     * to the remote connection manager.  Subsequent calls to this method
+     * will block until the underlying session state allows for the message
+     * to be transmitted.  In certain scenarios - such as when the maximum
+     * number of outbound connections has been reached - calls to this method
+     * will block for short periods of time.
+     *
+     * @param body message data to send to remote server
+     * @throws BOSHException on message transmission failure
+     */
+    public void send(final ComposableBody body) throws BOSHException {
+        assertUnlocked();
+        if (body == null) {
+            throw(new IllegalArgumentException(
+                    "Message body may not be null"));
+        }
+
+        HTTPExchange exch;
+        CMSessionParams params;
+        lock.lock();
+        try {
+            blockUntilSendable(body);
+            if (!isWorking() && !isTermination(body)) {
+                throw(new BOSHException(
+                        "Cannot send message when session is closed"));
+            }
+            
+            long rid = requestIDSeq.getNextRID();
+            ComposableBody request = body;
+            params = cmParams;
+            if (params == null && exchanges.isEmpty()) {
+                // This is the first message being sent
+                request = applySessionCreationRequest(rid, body);
+            } else {
+                request = applySessionData(rid, body);
+                if (cmParams.isAckingRequests()) {
+                    pendingRequestAcks.add(request);
+                }
+            }
+            exch = new HTTPExchange(request);
+            exchanges.add(exch);
+            notEmpty.signalAll();
+            clearEmptyRequest();
+        } finally {
+            lock.unlock();
+        }
+        AbstractBody finalReq = exch.getRequest();
+        HTTPResponse resp = httpSender.send(params, finalReq);
+        exch.setHTTPResponse(resp);
+        fireRequestSent(finalReq);
+    }
+
+    /**
+     * Attempt to pause the current session.  When supported by the remote
+     * connection manager, pausing the session will result in the connection
+     * manager closing out all outstanding requests (including the pause
+     * request) and increases the inactivity timeout of the session.  The
+     * exact value of the temporary timeout is dependent upon the connection
+     * manager.  This method should be used if a client encounters an
+     * exceptional temporary situation during which it will be unable to send
+     * requests to the connection manager for a period of time greater than
+     * the maximum inactivity period.
+     *
+     * The session will revert back to it's normal, unpaused state when the
+     * client sends it's next message.
+     *
+     * @return {@code true} if the connection manager supports session pausing,
+     *  {@code false} if the connection manager does not support session
+     *  pausing or if the session has not yet been established
+     */
+    public boolean pause() {
+        assertUnlocked();
+        lock.lock();
+        AttrMaxPause maxPause = null;
+        try {
+            if (cmParams == null) {
+                return false;
+            }
+
+            maxPause = cmParams.getMaxPause();
+            if (maxPause == null) {
+                return false;
+            }
+        } finally {
+            lock.unlock();
+        }
+        try {
+            send(ComposableBody.builder()
+                    .setAttribute(Attributes.PAUSE, maxPause.toString())
+                    .build());
+        } catch (BOSHException boshx) {
+            LOG.log(Level.FINEST, "Could not send pause", boshx);
+        }
+        return true;
+    }
+
+    /**
+     * End the BOSH session by disconnecting from the remote BOSH connection
+     * manager.
+     *
+     * @throws BOSHException when termination message cannot be sent
+     */
+    public void disconnect() throws BOSHException {
+        disconnect(ComposableBody.builder().build());
+    }
+
+    /**
+     * End the BOSH session by disconnecting from the remote BOSH connection
+     * manager, sending the provided content in the final connection
+     * termination message.
+     *
+     * @param msg final message to send
+     * @throws BOSHException when termination message cannot be sent
+     */
+    public void disconnect(final ComposableBody msg) throws BOSHException {
+        if (msg == null) {
+            throw(new IllegalArgumentException(
+                    "Message body may not be null"));
+        }
+
+        Builder builder = msg.rebuild();
+        builder.setAttribute(Attributes.TYPE, TERMINATE);
+        send(builder.build());
+    }
+
+    /**
+     * Forcibly close this client session instance.  The preferred mechanism
+     * to close the connection is to send a disconnect message and wait for
+     * organic termination.  Calling this method simply shuts down the local
+     * session without sending a termination message, releasing all resources
+     * associated with the session.
+     */
+    public void close() {
+        dispose(new BOSHException("Session explicitly closed by caller"));
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Package-private methods:
+
+    /**
+     * Get the current CM session params.
+     *
+     * @return current session params, or {@code null}
+     */
+    CMSessionParams getCMSessionParams() {
+        lock.lock();
+        try {
+            return cmParams;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * Wait until no more messages are waiting to be processed.
+     */
+    void drain() {
+        lock.lock();
+        try {
+            LOG.finest("Waiting while draining...");
+            while (isWorking()
+                    && (emptyRequestFuture == null
+                    || emptyRequestFuture.isDone())) {
+                try {
+                    drained.await();
+                } catch (InterruptedException intx) {
+                    LOG.log(Level.FINEST, INTERRUPTED, intx);
+                }
+            }
+            LOG.finest("Drained");
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * Test method used to forcibly discard next exchange.
+     *
+     * @param interceptor exchange interceptor
+     */
+    void setExchangeInterceptor(final ExchangeInterceptor interceptor) {
+        exchInterceptor.set(interceptor);
+    }
+
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Private methods:
+
+    /**
+     * Initialize the session.  This initializes the underlying HTTP
+     * transport implementation and starts the receive thread.
+     */
+    private void init() {
+        assertUnlocked();
+        
+        lock.lock();
+        try {
+            httpSender.init(cfg);
+            procThread = new Thread(procRunnable);
+            procThread.setDaemon(true);
+            procThread.setName(BOSHClient.class.getSimpleName()
+                    + "[" + System.identityHashCode(this)
+                    + "]: Receive thread");
+            procThread.start();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * Destroy this session.
+     *
+     * @param cause the reason for the session termination, or {@code null}
+     *  for normal termination
+     */
+    private void dispose(final Throwable cause) {
+        assertUnlocked();
+        
+        lock.lock();
+        try {
+            if (procThread == null) {
+                // Already disposed
+                return;
+            }
+            procThread = null;
+        } finally {
+            lock.unlock();
+        }
+
+        if (cause == null) {
+            fireConnectionClosed();
+        } else {
+            fireConnectionClosedOnError(cause);
+        }
+
+        lock.lock();
+        try {
+            clearEmptyRequest();
+            exchanges = null;
+            cmParams = null;
+            pendingResponseAcks = null;
+            pendingRequestAcks = null;
+            notEmpty.signalAll();
+            notFull.signalAll();
+            drained.signalAll();
+        } finally {
+            lock.unlock();
+        }
+        
+        httpSender.destroy();
+        schedExec.shutdownNow();
+    }
+
+    /**
+     * Determines if the message body specified indicates a request to
+     * pause the session.
+     *
+     * @param msg message to evaluate
+     * @return {@code true} if the message is a pause request, {@code false}
+     *  otherwise
+     */
+    private static boolean isPause(final AbstractBody msg) {
+        return msg.getAttribute(Attributes.PAUSE) != null;
+    }
+    
+    /**
+     * Determines if the message body specified indicates a termination of
+     * the session.
+     *
+     * @param msg message to evaluate
+     * @return {@code true} if the message is a session termination,
+     *  {@code false} otherwise
+     */
+    private static boolean isTermination(final AbstractBody msg) {
+        return TERMINATE.equals(msg.getAttribute(Attributes.TYPE));
+    }
+
+    /**
+     * Evaluates the HTTP response code and response message and returns the
+     * terminal binding condition that it describes, if any.
+     *
+     * @param respCode HTTP response code
+     * @param respBody response body
+     * @return terminal binding condition, or {@code null} if not a terminal
+     *  binding condition message
+     */
+    private TerminalBindingCondition getTerminalBindingCondition(
+            final int respCode,
+            final AbstractBody respBody) {
+        assertLocked();
+
+        if (isTermination(respBody)) {
+            String str = respBody.getAttribute(Attributes.CONDITION);
+            return TerminalBindingCondition.forString(str);
+        }
+        // Check for deprecated HTTP Error Conditions
+        if (cmParams != null && cmParams.getVersion() == null) {
+            return TerminalBindingCondition.forHTTPResponseCode(respCode);
+        }
+        return null;
+    }
+
+    /**
+     * Determines if the message specified is immediately sendable or if it
+     * needs to block until the session state changes.
+     *
+     * @param msg message to evaluate
+     * @return {@code true} if the message can be immediately sent,
+     *  {@code false} otherwise
+     */
+    private boolean isImmediatelySendable(final AbstractBody msg) {
+        assertLocked();
+
+        if (cmParams == null) {
+            // block if we're waiting for a response to our first request
+            return exchanges.isEmpty();
+        }
+
+        AttrRequests requests = cmParams.getRequests();
+        if (requests == null) {
+            return true;
+        }
+        int maxRequests = requests.intValue();
+        if (exchanges.size() < maxRequests) {
+            return true;
+        }
+        if (exchanges.size() == maxRequests
+                && (isTermination(msg) || isPause(msg))) {
+            // One additional terminate or pause message is allowed
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Determines whether or not the session is still active.
+     *
+     * @return {@code true} if it is, {@code false} otherwise
+     */
+    private boolean isWorking() {
+        assertLocked();
+
+        return procThread != null;
+    }
+
+    /**
+     * Blocks until either the message provided becomes immediately
+     * sendable or until the session is terminated.
+     *
+     * @param msg message to evaluate
+     */
+    private void blockUntilSendable(final AbstractBody msg) {
+        assertLocked();
+
+        while (isWorking() && !isImmediatelySendable(msg)) {
+            try {
+                notFull.await();
+            } catch (InterruptedException intx) {
+                LOG.log(Level.FINEST, INTERRUPTED, intx);
+            }
+        }
+    }
+
+    /**
+     * Modifies the specified body message such that it becomes a new
+     * BOSH session creation request.
+     *
+     * @param rid request ID to use
+     * @param orig original body to modify
+     * @return modified message which acts as a session creation request
+     */
+    private ComposableBody applySessionCreationRequest(
+            final long rid, final ComposableBody orig) throws BOSHException {
+        assertLocked();
+        
+        Builder builder = orig.rebuild();
+        builder.setAttribute(Attributes.TO, cfg.getTo());
+        builder.setAttribute(Attributes.XML_LANG, cfg.getLang());
+        builder.setAttribute(Attributes.VER,
+                AttrVersion.getSupportedVersion().toString());
+        builder.setAttribute(Attributes.WAIT, "60");
+        builder.setAttribute(Attributes.HOLD, "1");
+        builder.setAttribute(Attributes.RID, Long.toString(rid));
+        applyRoute(builder);
+        applyFrom(builder);
+        builder.setAttribute(Attributes.ACK, "1");
+
+        // Make sure the following are NOT present (i.e., during retries)
+        builder.setAttribute(Attributes.SID, null);
+        return builder.build();
+    }
+
+    /**
+     * Applies routing information to the request message who's builder has
+     * been provided.
+     *
+     * @param builder builder instance to add routing information to
+     */
+    private void applyRoute(final Builder builder) {
+        assertLocked();
+        
+        String route = cfg.getRoute();
+        if (route != null) {
+            builder.setAttribute(Attributes.ROUTE, route);
+        }
+    }
+
+    /**
+     * Applies the local station ID information to the request message who's
+     * builder has been provided.
+     *
+     * @param builder builder instance to add station ID information to
+     */
+    private void applyFrom(final Builder builder) {
+        assertLocked();
+
+        String from = cfg.getFrom();
+        if (from != null) {
+            builder.setAttribute(Attributes.FROM, from);
+        }
+    }
+
+    /**
+     * Applies existing session data to the outbound request, returning the
+     * modified request.
+     *
+     * This method assumes the lock is currently held.
+     *
+     * @param rid request ID to use
+     * @param orig original/raw request
+     * @return modified request with session information applied
+     */
+    private ComposableBody applySessionData(
+            final long rid,
+            final ComposableBody orig) throws BOSHException {
+        assertLocked();
+
+        Builder builder = orig.rebuild();
+        builder.setAttribute(Attributes.SID,
+                cmParams.getSessionID().toString());
+        builder.setAttribute(Attributes.RID, Long.toString(rid));
+        applyResponseAcknowledgement(builder, rid);
+        return builder.build();
+    }
+
+    /**
+     * Sets the 'ack' attribute of the request to the value of the highest
+     * 'rid' of a request for which it has already received a response in the
+     * case where it has also received all responses associated with lower
+     * 'rid' values.  The only exception is that, after its session creation
+     * request, the client SHOULD NOT include an 'ack' attribute in any request
+     * if it has received responses to all its previous requests.
+     *
+     * @param builder message builder
+     * @param rid current request RID
+     */
+    private void applyResponseAcknowledgement(
+            final Builder builder,
+            final long rid) {
+        assertLocked();
+
+        if (responseAck.equals(Long.valueOf(-1L))) {
+            // We have not received any responses yet
+            return;
+        }
+
+        Long prevRID = Long.valueOf(rid - 1L);
+        if (responseAck.equals(prevRID)) {
+            // Implicit ack
+            return;
+        }
+        
+        builder.setAttribute(Attributes.ACK, responseAck.toString());
+    }
+
+    /**
+     * While we are "connected", process received responses.
+     *
+     * This method is run in the processing thread.
+     */
+    private void processMessages() {
+        LOG.log(Level.FINEST, "Processing thread starting");
+        try {
+            HTTPExchange exch;
+            do {
+                exch = nextExchange();
+                if (exch == null) {
+                    break;
+                }
+
+                // Test hook to manipulate what the client sees:
+                ExchangeInterceptor interceptor = exchInterceptor.get();
+                if (interceptor != null) {
+                    HTTPExchange newExch = interceptor.interceptExchange(exch);
+                    if (newExch == null) {
+                        LOG.log(Level.FINE, "Discarding exchange on request "
+                                + "of test hook: RID="
+                                + exch.getRequest().getAttribute(
+                                    Attributes.RID));
+                        lock.lock();
+                        try {
+                            exchanges.remove(exch);
+                        } finally {
+                            lock.unlock();
+                        }
+                        continue;
+                    }
+                    exch = newExch;
+                }
+
+                processExchange(exch);
+            } while (true);
+        } finally {
+            LOG.log(Level.FINEST, "Processing thread exiting");
+        }
+
+    }
+
+    /**
+     * Get the next message exchange to process, blocking until one becomes
+     * available if nothing is already waiting for processing.
+     *
+     * @return next available exchange to process, or {@code null} if no
+     *  exchanges are immediately available
+     */
+    private HTTPExchange nextExchange() {
+        assertUnlocked();
+
+        final Thread thread = Thread.currentThread();
+        HTTPExchange exch = null;
+        lock.lock();
+        try {
+            do {
+                if (!thread.equals(procThread)) {
+                    break;
+                }
+                exch = exchanges.peek();
+                if (exch == null) {
+                    try {
+                        notEmpty.await();
+                    } catch (InterruptedException intx) {
+                        LOG.log(Level.FINEST, INTERRUPTED, intx);
+                    }
+                }
+            } while (exch == null);
+        } finally {
+            lock.unlock();
+        }
+        return exch;
+    }
+
+    /**
+     * Process the next, provided exchange.  This is the main processing
+     * method of the receive thread.
+     *
+     * @param exch message exchange to process
+     */
+    private void processExchange(final HTTPExchange exch) {
+        assertUnlocked();
+
+        HTTPResponse resp;
+        AbstractBody body;
+        int respCode;
+        try {
+            resp = exch.getHTTPResponse();
+            body = resp.getBody();
+            respCode = resp.getHTTPStatus();
+        } catch (BOSHException boshx) {
+            LOG.log(Level.FINEST, "Could not obtain response", boshx);
+            dispose(boshx);
+            return;
+        } catch (InterruptedException intx) {
+            LOG.log(Level.FINEST, INTERRUPTED, intx);
+            dispose(intx);
+            return;
+        }
+        fireResponseReceived(body);
+
+        // Process the message with the current session state
+        AbstractBody req = exch.getRequest();
+        CMSessionParams params;
+        List<HTTPExchange> toResend = null;
+        lock.lock();
+        try {
+            // Check for session creation response info, if needed
+            if (cmParams == null) {
+                cmParams = CMSessionParams.fromSessionInit(req, body);
+
+                // The following call handles the lock. It's not an escape.
+                fireConnectionEstablished();
+            }
+            params = cmParams;
+
+            checkForTerminalBindingConditions(body, respCode);
+            if (isTermination(body)) {
+                // Explicit termination
+                lock.unlock();
+                dispose(null);
+                return;
+            }
+            
+            if (isRecoverableBindingCondition(body)) {
+                // Retransmit outstanding requests
+                if (toResend == null) {
+                    toResend = new ArrayList<HTTPExchange>(exchanges.size());
+                }
+                for (HTTPExchange exchange : exchanges) {
+                    HTTPExchange resendExch =
+                            new HTTPExchange(exchange.getRequest());
+                    toResend.add(resendExch);
+                }
+                for (HTTPExchange exchange : toResend) {
+                    exchanges.add(exchange);
+                }
+            } else {
+                // Process message as normal
+                processRequestAcknowledgements(req, body);
+                processResponseAcknowledgementData(req);
+                HTTPExchange resendExch =
+                        processResponseAcknowledgementReport(body);
+                if (resendExch != null && toResend == null) {
+                    toResend = new ArrayList<HTTPExchange>(1);
+                    toResend.add(resendExch);
+                    exchanges.add(resendExch);
+                }
+            }
+        } catch (BOSHException boshx) {
+            LOG.log(Level.FINEST, "Could not process response", boshx);
+            lock.unlock();
+            dispose(boshx);
+            return;
+        } finally {
+            if (lock.isHeldByCurrentThread()) {
+                try {
+                    exchanges.remove(exch);
+                    if (exchanges.isEmpty()) {
+                        scheduleEmptyRequest(processPauseRequest(req));
+                    }
+                    notFull.signalAll();
+                } finally {
+                    lock.unlock();
+                }
+            }
+        }
+
+        if (toResend != null) {
+            for (HTTPExchange resend : toResend) {
+                HTTPResponse response =
+                        httpSender.send(params, resend.getRequest());
+                resend.setHTTPResponse(response);
+                fireRequestSent(resend.getRequest());
+            }
+        }
+    }
+    
+    /**
+     * Clears any scheduled empty requests.
+     */
+    private void clearEmptyRequest() {
+        assertLocked();
+
+        if (emptyRequestFuture != null) {
+            emptyRequestFuture.cancel(false);
+            emptyRequestFuture = null;
+        }
+    }
+
+    /**
+     * Calculates the default empty request delay/interval to use for the
+     * active session.
+     *
+     * @return delay in milliseconds
+     */
+    private long getDefaultEmptyRequestDelay() {
+        assertLocked();
+        
+        // Figure out how long we should wait before sending an empty request
+        AttrPolling polling = cmParams.getPollingInterval();
+        long delay;
+        if (polling == null) {
+            delay = EMPTY_REQUEST_DELAY;
+        } else {
+            delay = polling.getInMilliseconds();
+        }
+        return delay;
+    }
+
+    /**
+     * Schedule an empty request to be sent if no other requests are
+     * sent in a reasonable amount of time.
+     */
+    private void scheduleEmptyRequest(long delay) {
+        assertLocked();
+        if (delay < 0L) {
+            throw(new IllegalArgumentException(
+                    "Empty request delay must be >= 0 (was: " + delay + ")"));
+        }
+
+        clearEmptyRequest();
+        if (!isWorking()) {
+            return;
+        }
+        
+        // Schedule the transmission
+        if (LOG.isLoggable(Level.FINER)) {
+            LOG.finer("Scheduling empty request in " + delay + "ms");
+        }
+        try {
+            emptyRequestFuture = schedExec.schedule(emptyRequestRunnable,
+                    delay, TimeUnit.MILLISECONDS);
+        } catch (RejectedExecutionException rex) {
+            LOG.log(Level.FINEST, "Could not schedule empty request", rex);
+        }
+        drained.signalAll();
+    }
+
+    /**
+     * Sends an empty request to maintain session requirements.  If a request
+     * is sent within a reasonable time window, the empty request transmission
+     * will be cancelled.
+     */
+    private void sendEmptyRequest() {
+        assertUnlocked();
+        // Send an empty request
+        LOG.finest("Sending empty request");
+        try {
+            send(ComposableBody.builder().build());
+        } catch (BOSHException boshx) {
+            dispose(boshx);
+        }
+    }
+
+    /**
+     * Assert that the internal lock is held.
+     */
+    private void assertLocked() {
+        if (ASSERTIONS) {
+            if (!lock.isHeldByCurrentThread()) {
+                throw(new AssertionError("Lock is not held by current thread"));
+            }
+            return;
+        }
+    }
+
+    /**
+     * Assert that the internal lock is *not* held.
+     */
+    private void assertUnlocked() {
+        if (ASSERTIONS) {
+            if (lock.isHeldByCurrentThread()) {
+                throw(new AssertionError("Lock is held by current thread"));
+            }
+            return;
+        }
+    }
+
+    /**
+     * Checks to see if the response indicates a terminal binding condition
+     * (as per XEP-0124 section 17).  If it does, an exception is thrown.
+     *
+     * @param body response body to evaluate
+     * @param code HTTP response code
+     * @throws BOSHException if a terminal binding condition is detected
+     */
+    private void checkForTerminalBindingConditions(
+            final AbstractBody body,
+            final int code)
+            throws BOSHException {
+        TerminalBindingCondition cond =
+                getTerminalBindingCondition(code, body);
+        if (cond != null) {
+            throw(new BOSHException(
+                    "Terminal binding condition encountered: "
+                    + cond.getCondition() + "  ("
+                    + cond.getMessage() + ")"));
+        }
+    }
+
+    /**
+     * Determines whether or not the response indicates a recoverable
+     * binding condition (as per XEP-0124 section 17).
+     *
+     * @param resp response body
+     * @return {@code true} if it does, {@code false} otherwise
+     */
+    private static boolean isRecoverableBindingCondition(
+            final AbstractBody resp) {
+        return ERROR.equals(resp.getAttribute(Attributes.TYPE));
+    }
+
+    /**
+     * Process the request to determine if the empty request delay
+     * can be determined by looking to see if the request is a pause
+     * request.  If it can, the request's delay is returned, otherwise
+     * the default delay is returned.
+     * 
+     * @return delay in milliseconds that should elapse prior to an
+     *  empty message being sent
+     */
+    private long processPauseRequest(
+            final AbstractBody req) {
+        assertLocked();
+
+        if (cmParams != null && cmParams.getMaxPause() != null) {
+            try {
+                AttrPause pause = AttrPause.createFromString(
+                        req.getAttribute(Attributes.PAUSE));
+                if (pause != null) {
+                    long delay = pause.getInMilliseconds() - PAUSE_MARGIN;
+                    if (delay < 0) {
+                        delay = EMPTY_REQUEST_DELAY;
+                    }
+                    return delay;
+                }
+            } catch (BOSHException boshx) {
+                LOG.log(Level.FINEST, "Could not extract", boshx);
+            }
+        }
+
+        return getDefaultEmptyRequestDelay();
+    }
+
+    /**
+     * Check the response for request acknowledgements and take appropriate
+     * action.
+     *
+     * This method assumes the lock is currently held.
+     *
+     * @param req request
+     * @param resp response
+     */
+    private void processRequestAcknowledgements(
+            final AbstractBody req, final AbstractBody resp) {
+        assertLocked();
+        
+        if (!cmParams.isAckingRequests()) {
+            return;
+        }
+
+        // If a report or time attribute is set, we aren't acking anything
+        if (resp.getAttribute(Attributes.REPORT) != null) {
+            return;
+        }
+
+        // Figure out what the highest acked RID is
+        String acked = resp.getAttribute(Attributes.ACK);
+        Long ackUpTo;
+        if (acked == null) {
+            // Implicit ack of all prior requests up until RID
+            ackUpTo = Long.parseLong(req.getAttribute(Attributes.RID));
+        } else {
+            ackUpTo = Long.parseLong(acked);
+        }
+
+        // Remove the acked requests from the list
+        if (LOG.isLoggable(Level.FINEST)) {
+            LOG.finest("Removing pending acks up to: " + ackUpTo);
+        }
+        Iterator<ComposableBody> iter = pendingRequestAcks.iterator();
+        while (iter.hasNext()) {
+            AbstractBody pending = iter.next();
+            Long pendingRID = Long.parseLong(
+                    pending.getAttribute(Attributes.RID));
+            if (pendingRID.compareTo(ackUpTo) <= 0) {
+                iter.remove();
+            }
+        }
+    }
+
+    /**
+     * Process the response in order to update the response acknowlegement
+     * data.
+     *
+     * This method assumes the lock is currently held.
+     *
+     * @param req request
+     */
+    private void processResponseAcknowledgementData(
+            final AbstractBody req) {
+        assertLocked();
+        
+        Long rid = Long.parseLong(req.getAttribute(Attributes.RID));
+        if (responseAck.equals(Long.valueOf(-1L))) {
+            // This is the first request
+            responseAck = rid;
+        } else {
+            pendingResponseAcks.add(rid);
+            // Remove up until the first missing response (or end of queue)
+            Long whileVal = responseAck;
+            while (whileVal.equals(pendingResponseAcks.first())) {
+                responseAck = whileVal;
+                pendingResponseAcks.remove(whileVal);
+                whileVal = Long.valueOf(whileVal.longValue() + 1);
+            }
+        }
+    }
+
+    /**
+     * Process the response in order to check for and respond to any potential
+     * ack reports.
+     *
+     * This method assumes the lock is currently held.
+     *
+     * @param resp response
+     * @return exchange to transmit if a resend is to be performed, or
+     *  {@code null} if no resend is necessary
+     * @throws BOSHException when a a retry is needed but cannot be performed
+     */
+    private HTTPExchange processResponseAcknowledgementReport(
+            final AbstractBody resp)
+            throws BOSHException {
+        assertLocked();
+        
+        String reportStr = resp.getAttribute(Attributes.REPORT);
+        if (reportStr == null) {
+            // No report on this message
+            return null;
+        }
+        
+        Long report = Long.parseLong(reportStr);
+        Long time = Long.parseLong(resp.getAttribute(Attributes.TIME));
+        if (LOG.isLoggable(Level.FINE)) {
+            LOG.fine("Received report of missing request (RID="
+                    + report + ", time=" + time + "ms)");
+        }
+
+        // Find the missing request
+        Iterator<ComposableBody> iter = pendingRequestAcks.iterator();
+        AbstractBody req = null;
+        while (iter.hasNext() && req == null) {
+            AbstractBody pending = iter.next();
+            Long pendingRID = Long.parseLong(
+                    pending.getAttribute(Attributes.RID));
+            if (report.equals(pendingRID)) {
+                req = pending;
+            }
+        }
+
+        if (req == null) {
+            throw(new BOSHException("Report of missing message with RID '"
+                    + reportStr
+                    + "' but local copy of that request was not found"));
+        }
+
+        // Resend the missing request
+        HTTPExchange exch = new HTTPExchange(req);
+        exchanges.add(exch);
+        notEmpty.signalAll();
+        return exch;
+    }
+
+    /**
+     * Notifies all request listeners that the specified request is being
+     * sent.
+     *
+     * @param request request being sent
+     */
+    private void fireRequestSent(final AbstractBody request) {
+        assertUnlocked();
+
+        BOSHMessageEvent event = null;
+        for (BOSHClientRequestListener listener : requestListeners) {
+            if (event == null) {
+                event = BOSHMessageEvent.createRequestSentEvent(this, request);
+            }
+            try {
+                listener.requestSent(event);
+            } catch (Exception ex) {
+                LOG.log(Level.WARNING, UNHANDLED, ex);
+            }
+        }
+    }
+
+    /**
+     * Notifies all response listeners that the specified response has been
+     * received.
+     *
+     * @param response response received
+     */
+    private void fireResponseReceived(final AbstractBody response) {
+        assertUnlocked();
+
+        BOSHMessageEvent event = null;
+        for (BOSHClientResponseListener listener : responseListeners) {
+            if (event == null) {
+                event = BOSHMessageEvent.createResponseReceivedEvent(
+                        this, response);
+            }
+            try {
+                listener.responseReceived(event);
+            } catch (Exception ex) {
+                LOG.log(Level.WARNING, UNHANDLED, ex);
+            }
+        }
+    }
+
+    /**
+     * Notifies all connection listeners that the session has been successfully
+     * established.
+     */
+    private void fireConnectionEstablished() {
+        final boolean hadLock = lock.isHeldByCurrentThread();
+        if (hadLock) {
+            lock.unlock();
+        }
+        try {
+            BOSHClientConnEvent event = null;
+            for (BOSHClientConnListener listener : connListeners) {
+                if (event == null) {
+                    event = BOSHClientConnEvent
+                            .createConnectionEstablishedEvent(this);
+                }
+                try {
+                    listener.connectionEvent(event);
+                } catch (Exception ex) {
+                    LOG.log(Level.WARNING, UNHANDLED, ex);
+                }
+            }
+        } finally {
+            if (hadLock) {
+                lock.lock();
+            }
+        }
+    }
+
+    /**
+     * Notifies all connection listeners that the session has been
+     * terminated normally.
+     */
+    private void fireConnectionClosed() {
+        assertUnlocked();
+
+        BOSHClientConnEvent event = null;
+        for (BOSHClientConnListener listener : connListeners) {
+            if (event == null) {
+                event = BOSHClientConnEvent.createConnectionClosedEvent(this);
+            }
+            try {
+                listener.connectionEvent(event);
+            } catch (Exception ex) {
+                LOG.log(Level.WARNING, UNHANDLED, ex);
+            }
+        }
+    }
+
+    /**
+     * Notifies all connection listeners that the session has been
+     * terminated due to the exceptional condition provided.
+     *
+     * @param cause cause of the termination
+     */
+    private void fireConnectionClosedOnError(
+            final Throwable cause) {
+        assertUnlocked();
+
+        BOSHClientConnEvent event = null;
+        for (BOSHClientConnListener listener : connListeners) {
+            if (event == null) {
+                event = BOSHClientConnEvent
+                        .createConnectionClosedOnErrorEvent(
+                        this, pendingRequestAcks, cause);
+            }
+            try {
+                listener.connectionEvent(event);
+            } catch (Exception ex) {
+                LOG.log(Level.WARNING, UNHANDLED, ex);
+            }
+        }
+    }
+
+}
diff --git a/src/com/kenai/jbosh/BOSHClientConfig.java b/src/com/kenai/jbosh/BOSHClientConfig.java
new file mode 100644
index 0000000..23915b6
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClientConfig.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.net.URI;
+import javax.net.ssl.SSLContext;
+
+/**
+ * BOSH client configuration information.  Instances of this class contain
+ * all information necessary to establish connectivity with a remote
+ * connection manager.
+ * <p/>
+ * Instances of this class are immutable, thread-safe,
+ * and can be re-used to configure multiple client session instances.
+ */
+public final class BOSHClientConfig {
+
+    /**
+     * Connection manager URI.
+     */
+    private final URI uri;
+
+    /**
+     * Target domain.
+     */
+    private final String to;
+
+    /**
+     * Client ID of this station.
+     */
+    private final String from;
+
+    /**
+     * Default XML language.
+     */
+    private final String lang;
+
+    /**
+     * Routing information for messages sent to CM.
+     */
+    private final String route;
+
+    /**
+     * Proxy host.
+     */
+    private final String proxyHost;
+
+    /**
+     * Proxy port.
+     */
+    private final int proxyPort;
+
+    /**
+     * SSL context.
+     */
+    private final SSLContext sslContext;
+
+    /**
+     * Flag indicating that compression should be attempted, if possible.
+     */
+    private final boolean compressionEnabled;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Classes:
+
+    /**
+     * Class instance builder, after the builder pattern.  This allows each
+     * {@code BOSHClientConfig} instance to be immutable while providing
+     * flexibility when building new {@code BOSHClientConfig} instances.
+     * <p/>
+     * Instances of this class are <b>not</b> thread-safe.  If template-style
+     * use is desired, see the {@code create(BOSHClientConfig)} method.
+     */
+    public static final class Builder {
+        // Required args
+        private final URI bURI;
+        private final String bDomain;
+
+        // Optional args
+        private String bFrom;
+        private String bLang;
+        private String bRoute;
+        private String bProxyHost;
+        private int bProxyPort;
+        private SSLContext bSSLContext;
+        private Boolean bCompression;
+
+        /**
+         * Creates a new builder instance, used to create instances of the
+         * {@code BOSHClientConfig} class.
+         *
+         * @param cmURI URI to use to contact the connection manager
+         * @param domain target domain to communicate with
+         */
+        private Builder(final URI cmURI, final String domain) {
+            bURI = cmURI;
+            bDomain = domain;
+        }
+
+        /**
+         * Creates a new builder instance, used to create instances of the
+         * {@code BOSHClientConfig} class.
+         *
+         * @param cmURI URI to use to contact the connection manager
+         * @param domain target domain to communicate with
+         * @return builder instance
+         */
+        public static Builder create(final URI cmURI, final String domain) {
+            if (cmURI == null) {
+                throw(new IllegalArgumentException(
+                        "Connection manager URI must not be null"));
+            }
+            if (domain == null) {
+                throw(new IllegalArgumentException(
+                        "Target domain must not be null"));
+            }
+            String scheme = cmURI.getScheme();
+            if (!("http".equals(scheme) || "https".equals(scheme))) {
+                throw(new IllegalArgumentException(
+                        "Only 'http' and 'https' URI are allowed"));
+            }
+            return new Builder(cmURI, domain);
+        }
+
+        /**
+         * Creates a new builder instance using the existing configuration
+         * provided as a starting point.
+         *
+         * @param cfg configuration to copy
+         * @return builder instance
+         */
+        public static Builder create(final BOSHClientConfig cfg) {
+            Builder result = new Builder(cfg.getURI(), cfg.getTo());
+            result.bFrom = cfg.getFrom();
+            result.bLang = cfg.getLang();
+            result.bRoute = cfg.getRoute();
+            result.bProxyHost = cfg.getProxyHost();
+            result.bProxyPort = cfg.getProxyPort();
+            result.bSSLContext = cfg.getSSLContext();
+            result.bCompression = cfg.isCompressionEnabled();
+            return result;
+        }
+
+        /**
+         * Set the ID of the client station, to be forwarded to the connection
+         * manager when new sessions are created.
+         *
+         * @param id client ID
+         * @return builder instance
+         */
+        public Builder setFrom(final String id) {
+            if (id == null) {
+                throw(new IllegalArgumentException(
+                        "Client ID must not be null"));
+            }
+            bFrom = id;
+            return this;
+        }
+        
+        /**
+         * Set the default language of any human-readable content within the
+         * XML.
+         *
+         * @param lang XML language ID
+         * @return builder instance
+         */
+        public Builder setXMLLang(final String lang) {
+            if (lang == null) {
+                throw(new IllegalArgumentException(
+                        "Default language ID must not be null"));
+            }
+            bLang = lang;
+            return this;
+        }
+
+        /**
+         * Sets the destination server/domain that the client should connect to.
+         * Connection managers may be configured to enable sessions with more
+         * that one server in different domains.  When requesting a session with
+         * such a "proxy" connection manager, a client should use this method to
+         * specify the server with which it wants to communicate.
+         *
+         * @param protocol connection protocol (e.g, "xmpp")
+         * @param host host or domain to be served by the remote server.  Note
+         *  that this is not necessarily the host name or domain name of the
+         *  remote server.
+         * @param port port number of the remote server
+         * @return builder instance
+         */
+        public Builder setRoute(
+                final String protocol,
+                final String host,
+                final int port) {
+            if (protocol == null) {
+                throw(new IllegalArgumentException("Protocol cannot be null"));
+            }
+            if (protocol.contains(":")) {
+                throw(new IllegalArgumentException(
+                        "Protocol cannot contain the ':' character"));
+            }
+            if (host == null) {
+                throw(new IllegalArgumentException("Host cannot be null"));
+            }
+            if (host.contains(":")) {
+                throw(new IllegalArgumentException(
+                        "Host cannot contain the ':' character"));
+            }
+            if (port <= 0) {
+                throw(new IllegalArgumentException("Port number must be > 0"));
+            }
+            bRoute = protocol + ":" + host + ":" + port;
+            return this;
+        }
+
+        /**
+         * Specify the hostname and port of an HTTP proxy to connect through.
+         *
+         * @param hostName proxy hostname
+         * @param port proxy port number
+         * @return builder instance
+         */
+        public Builder setProxy(final String hostName, final int port) {
+            if (hostName == null || hostName.length() == 0) {
+                throw(new IllegalArgumentException(
+                        "Proxy host name cannot be null or empty"));
+            }
+            if (port <= 0) {
+                throw(new IllegalArgumentException(
+                        "Proxy port must be > 0"));
+            }
+            bProxyHost = hostName;
+            bProxyPort = port;
+            return this;
+        }
+
+        /**
+         * Set the SSL context to use for this session.  This can be used
+         * to configure certificate-based authentication, etc..
+         *
+         * @param ctx SSL context
+         * @return builder instance
+         */
+        public Builder setSSLContext(final SSLContext ctx) {
+            if (ctx == null) {
+                throw(new IllegalArgumentException(
+                        "SSL context cannot be null"));
+            }
+            bSSLContext = ctx;
+            return this;
+        }
+
+        /**
+         * Set whether or not compression of the underlying data stream
+         * should be attempted.  By default, compression is disabled.
+         *
+         * @param enabled set to {@code true} if compression should be
+         *  attempted when possible, {@code false} to disable compression
+         * @return builder instance
+         */
+        public Builder setCompressionEnabled(final boolean enabled) {
+            bCompression = Boolean.valueOf(enabled);
+            return this;
+        }
+
+        /**
+         * Build the immutable object instance with the current configuration.
+         *
+         * @return BOSHClientConfig instance
+         */
+        public BOSHClientConfig build() {
+            // Default XML language
+            String lang;
+            if (bLang == null) {
+                lang = "en";
+            } else {
+                lang = bLang;
+            }
+
+            // Default proxy port
+            int port;
+            if (bProxyHost == null) {
+                port = 0;
+            } else {
+                port = bProxyPort;
+            }
+
+            // Default compression
+            boolean compression;
+            if (bCompression == null) {
+                compression = false;
+            } else {
+                compression = bCompression.booleanValue();
+            }
+
+            return new BOSHClientConfig(
+                    bURI,
+                    bDomain,
+                    bFrom,
+                    lang,
+                    bRoute,
+                    bProxyHost,
+                    port,
+                    bSSLContext,
+                    compression);
+        }
+
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructor:
+
+    /**
+     * Prevent direct construction.
+     *
+     * @param cURI URI of the connection manager to connect to
+     * @param cDomain the target domain of the first stream
+     * @param cFrom client ID
+     * @param cLang default XML language
+     * @param cRoute target route
+     * @param cProxyHost proxy host
+     * @param cProxyPort proxy port
+     * @param cSSLContext SSL context
+     * @param cCompression compression enabled flag
+     */
+    private BOSHClientConfig(
+            final URI cURI,
+            final String cDomain,
+            final String cFrom,
+            final String cLang,
+            final String cRoute,
+            final String cProxyHost,
+            final int cProxyPort,
+            final SSLContext cSSLContext,
+            final boolean cCompression) {
+        uri = cURI;
+        to = cDomain;
+        from = cFrom;
+        lang = cLang;
+        route = cRoute;
+        proxyHost = cProxyHost;
+        proxyPort = cProxyPort;
+        sslContext = cSSLContext;
+        compressionEnabled = cCompression;
+    }
+
+    /**
+     * Get the URI to use to contact the connection manager.
+     *
+     * @return connection manager URI.
+     */
+    public URI getURI() {
+        return uri;
+    }
+
+    /**
+     * Get the ID of the target domain.
+     *
+     * @return domain id
+     */
+    public String getTo() {
+        return to;
+    }
+
+    /**
+     * Get the ID of the local client.
+     *
+     * @return client id, or {@code null}
+     */
+    public String getFrom() {
+        return from;
+    }
+
+    /**
+     * Get the default language of any human-readable content within the
+     * XML.  Defaults to "en".
+     *
+     * @return XML language ID
+     */
+    public String getLang() {
+        return lang;
+    }
+
+    /**
+     * Get the routing information for messages sent to the CM.
+     *
+     * @return route attribute string, or {@code null} if no routing
+     *  info was provided.
+     */
+    public String getRoute() {
+        return route;
+    }
+
+    /**
+     * Get the HTTP proxy host to use.
+     *
+     * @return proxy host, or {@code null} if no proxy information was specified
+     */
+    public String getProxyHost() {
+        return proxyHost;
+    }
+
+    /**
+     * Get the HTTP proxy port to use.
+     *
+     * @return proxy port, or 0 if no proxy information was specified
+     */
+    public int getProxyPort() {
+        return proxyPort;
+    }
+
+    /**
+     * Get the SSL context to use for this session.
+     *
+     * @return SSL context instance to use, or {@code null} if no
+     *  context instance was provided.
+     */
+    public SSLContext getSSLContext() {
+        return sslContext;
+    }
+
+    /**
+     * Determines whether or not compression of the underlying data stream
+     * should be attempted/allowed.  Defaults to {@code false}.
+     *
+     * @return {@code true} if compression should be attempted, {@code false}
+     *  if compression is disabled or was not specified
+     */
+    boolean isCompressionEnabled() {
+        return compressionEnabled;
+    }
+
+}
diff --git a/src/com/kenai/jbosh/BOSHClientConnEvent.java b/src/com/kenai/jbosh/BOSHClientConnEvent.java
new file mode 100644
index 0000000..0ac7943
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClientConnEvent.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.List;
+
+/**
+ * Client connection event, notifying of changes in connection state.
+ * <p/>
+ * This class is immutable and thread-safe.
+ */
+public final class BOSHClientConnEvent extends EventObject {
+
+    /**
+     * Serialized version.
+     */
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * Boolean flag indicating whether or not a session has been established
+     * and is currently active.
+     */
+    private final boolean connected;
+
+    /**
+     * List of outstanding requests which may not have been sent and/or
+     * acknowledged by the remote CM.
+     */
+    private final List<ComposableBody> requests;
+
+    /**
+     * Cause of the session termination, or {@code null}.
+     */
+    private final Throwable cause;
+
+    /**
+     * Creates a new connection event instance.
+     *
+     * @param source event source
+     * @param cConnected flag indicating whether or not the session is
+     *  currently active
+     * @param cRequests outstanding requests when an error condition is
+     *  detected, or {@code null} when not an error condition
+     * @param cCause cause of the error condition, or {@code null} when no
+     *  error condition is present
+     */
+    private BOSHClientConnEvent(
+            final BOSHClient source,
+            final boolean cConnected,
+            final List<ComposableBody> cRequests,
+            final Throwable cCause) {
+        super(source);
+        connected = cConnected;
+        cause = cCause;
+
+        if (connected) {
+            if (cCause != null) {
+                throw(new IllegalStateException(
+                        "Cannot be connected and have a cause"));
+            }
+            if (cRequests != null && cRequests.size() > 0) {
+                throw(new IllegalStateException(
+                        "Cannot be connected and have outstanding requests"));
+            }
+        }
+
+        if (cRequests == null) {
+            requests = Collections.emptyList();
+        } else {
+            // Defensive copy:
+            requests = Collections.unmodifiableList(
+                    new ArrayList<ComposableBody>(cRequests));
+        }
+    }
+
+    /**
+     * Creates a new connection establishment event.
+     *
+     * @param source client which has become connected
+     * @return event instance
+     */
+    static BOSHClientConnEvent createConnectionEstablishedEvent(
+            final BOSHClient source) {
+        return new BOSHClientConnEvent(source, true, null, null);
+    }
+
+    /**
+     * Creates a new successful connection closed event.  This represents
+     * a clean termination of the client session.
+     *
+     * @param source client which has been disconnected
+     * @return event instance
+     */
+    static BOSHClientConnEvent createConnectionClosedEvent(
+            final BOSHClient source) {
+        return new BOSHClientConnEvent(source, false, null, null);
+    }
+
+    /**
+     * Creates a connection closed on error event.  This represents
+     * an unexpected termination of the client session.
+     *
+     * @param source client which has been disconnected
+     * @param outstanding list of requests which may not have been received
+     *  by the remote connection manager
+     * @param cause cause of termination
+     * @return event instance
+     */
+    static BOSHClientConnEvent createConnectionClosedOnErrorEvent(
+            final BOSHClient source,
+            final List<ComposableBody> outstanding,
+            final Throwable cause) {
+        return new BOSHClientConnEvent(source, false, outstanding, cause);
+    }
+
+    /**
+     * Gets the client from which this event originated.
+     *
+     * @return client instance
+     */
+    public BOSHClient getBOSHClient() {
+        return (BOSHClient) getSource();
+    }
+
+    /**
+     * Returns whether or not the session has been successfully established
+     * and is currently active.
+     *
+     * @return {@code true} if a session is active, {@code false} otherwise
+     */
+    public boolean isConnected() {
+        return connected;
+    }
+
+    /**
+     * Returns whether or not this event indicates an error condition.  This
+     * will never return {@code true} when {@code isConnected()} returns
+     * {@code true}.
+     *
+     * @return {@code true} if the event indicates a terminal error has
+     *  occurred, {@code false} otherwise.
+     */
+    public boolean isError() {
+        return cause != null;
+    }
+
+    /**
+     * Returns the underlying cause of the error condition.  This method is
+     * guaranteed to return {@code null} when @{code isError()} returns
+     * {@code false}.  Similarly, this method is guaranteed to return
+     * non-@{code null} if {@code isError()} returns {@code true}.
+     *
+     * @return underlying cause of the error condition, or {@code null} if
+     *  this event does not represent an error condition
+     */
+    public Throwable getCause() {
+        return cause;
+    }
+
+    /**
+     * Get the list of requests which may not have been sent or were not
+     * acknowledged by the remote connection manager prior to session
+     * termination.
+     *
+     * @return list of messages which may not have been received by the remote
+     *  connection manager, or an empty list if the session is still connected
+     */
+    public List<ComposableBody> getOutstandingRequests() {
+        return requests;
+    }
+    
+}
diff --git a/src/com/kenai/jbosh/BOSHClientConnListener.java b/src/com/kenai/jbosh/BOSHClientConnListener.java
new file mode 100644
index 0000000..6d646cb
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClientConnListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Interface used by parties interested in monitoring the connection state
+ * of a client session.
+ */
+public interface BOSHClientConnListener {
+
+    /**
+     * Called when the connection state of the client which the listener
+     * is registered against has changed.  The event object supplied can
+     * be used to determine the current session state.
+     *
+     * @param connEvent connection event describing the state
+     */
+    void connectionEvent(BOSHClientConnEvent connEvent);
+    
+}
diff --git a/src/com/kenai/jbosh/BOSHClientRequestListener.java b/src/com/kenai/jbosh/BOSHClientRequestListener.java
new file mode 100644
index 0000000..2cc92f3
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClientRequestListener.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Interface used by parties interested in monitoring outbound requests made
+ * by the client to the connection manager (CM).  No opportunity is provided
+ * to manipulate the outbound request.
+ * <p/>
+ * The messages being sent are typically modified copies of the message
+ * body provided to the {@code BOSHClient} instance, built from the
+ * originally provided message body plus additional BOSH protocol
+ * state and information.  Messages may also be sent automatically when the
+ * protocol requires it, such as maintaining a minimum number of open
+ * connections to the connection manager.
+ * <p/>
+ * Listeners are executed by the sending thread immediately prior to
+ * message transmission and should not block for any significant amount
+ * of time.
+ */
+public interface BOSHClientRequestListener {
+
+    /**
+     * Called when the listener is being notified that a client request is
+     * about to be sent to the connection manager.
+     *
+     * @param event event instance containing the message being sent
+     */
+    void requestSent(BOSHMessageEvent event);
+
+}
diff --git a/src/com/kenai/jbosh/BOSHClientResponseListener.java b/src/com/kenai/jbosh/BOSHClientResponseListener.java
new file mode 100644
index 0000000..1d86e4f
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClientResponseListener.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Interface used by parties interested in monitoring inbound responses
+ * to the client from the connection manager (CM).  No opportunity is provided
+ * to manipulate the response.
+ * <p/>
+ * Listeners are executed by the message processing thread and should not
+ * block for any significant amount of time.
+ */
+public interface BOSHClientResponseListener {
+
+    /**
+     * Called when the listener is being notified that a response has been
+     * received from the connection manager.
+     *
+     * @param event event instance containing the message being sent
+     */
+    void responseReceived(BOSHMessageEvent event);
+
+}
diff --git a/src/com/kenai/jbosh/BOSHException.java b/src/com/kenai/jbosh/BOSHException.java
new file mode 100644
index 0000000..e0bc05b
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHException.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Exception class used by the BOSH API to minimize the number of checked
+ * exceptions which must be handled by the user of the API.
+ */
+public class BOSHException extends Exception {
+
+    /**
+     * Servial version UID.
+     */
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Creates a new exception isntance with the specified descriptive message.
+     *
+     * @param msg description of the exceptional condition
+     */
+    public BOSHException(final String msg) {
+        super(msg);
+    }
+
+    /**
+     * Creates a new exception isntance with the specified descriptive
+     * message and the underlying root cause of the exceptional condition.
+     *
+     * @param msg description of the exceptional condition
+     * @param cause root cause or instigator of the condition
+     */
+    public BOSHException(final String msg, final Throwable cause) {
+        super(msg, cause);
+    }
+
+}
diff --git a/src/com/kenai/jbosh/BOSHMessageEvent.java b/src/com/kenai/jbosh/BOSHMessageEvent.java
new file mode 100644
index 0000000..550903e
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHMessageEvent.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.EventObject;
+
+/**
+ * Event representing a message sent to or from a BOSH connection manager.
+ * <p/>
+ * This class is immutable and thread-safe.
+ */
+public final class BOSHMessageEvent extends EventObject {
+
+    /**
+     * Serialized version.
+     */
+    private static final long serialVersionUID = 1L;
+    
+    /**
+     * Message which was sent or received.
+     */
+    private final AbstractBody body;
+
+    /**
+     * Creates a new message event instance.
+     *
+     * @param source event source
+     * @param cBody message body
+     */
+    private BOSHMessageEvent(
+            final Object source,
+            final AbstractBody cBody) {
+        super(source);
+        if (cBody == null) {
+            throw(new IllegalArgumentException(
+                    "message body may not be null"));
+        }
+        body = cBody;
+    }
+
+    /**
+     * Creates a new message event for clients sending events to the
+     * connection manager.
+     *
+     * @param source sender of the message
+     * @param body message body
+     * @return event instance
+     */
+    static BOSHMessageEvent createRequestSentEvent(
+            final BOSHClient source,
+            final AbstractBody body) {
+        return new BOSHMessageEvent(source, body);
+    }
+
+    /**
+     * Creates a new message event for clients receiving new messages
+     * from the connection manager.
+     *
+     * @param source receiver of the message
+     * @param body message body
+     * @return event instance
+     */
+    static BOSHMessageEvent createResponseReceivedEvent(
+            final BOSHClient source,
+            final AbstractBody body) {
+        return new BOSHMessageEvent(source, body);
+    }
+
+    /**
+     * Gets the message body which was sent or received.
+     *
+     * @return message body
+     */
+    public AbstractBody getBody() {
+        return body;
+    }
+    
+}
diff --git a/src/com/kenai/jbosh/BodyParser.java b/src/com/kenai/jbosh/BodyParser.java
new file mode 100644
index 0000000..5ef5276
--- /dev/null
+++ b/src/com/kenai/jbosh/BodyParser.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Interface for parser implementations to implement in order to abstract the
+ * business of XML parsing out of the Body class.  This allows us to leverage
+ * a variety of parser implementations to gain performance advantages.
+ */
+interface BodyParser {
+
+    /**
+     * Parses the XML message, extracting the useful data from the initial
+     * body element and returning it in a results object.
+     *
+     * @param xml XML to parse
+     * @return useful data parsed out of the XML
+     * @throws BOSHException on parse error
+     */
+    BodyParserResults parse(String xml) throws BOSHException;
+
+}
diff --git a/src/com/kenai/jbosh/BodyParserResults.java b/src/com/kenai/jbosh/BodyParserResults.java
new file mode 100644
index 0000000..955e4bf
--- /dev/null
+++ b/src/com/kenai/jbosh/BodyParserResults.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Data extracted from a raw XML message by a BodyParser implementation.
+ * Currently, this is limited to the attributes of the wrapper element.
+ */
+final class BodyParserResults {
+
+    /**
+     * Map of qualified names to their values.  This map is defined to
+     * match the requirement of the {@code Body} class to prevent
+     * excessive copying.
+     */
+    private final Map<BodyQName, String> attrs =
+            new HashMap<BodyQName, String>();
+
+    /**
+     * Constructor.
+     */
+    BodyParserResults() {
+        // Empty
+    }
+
+    /**
+     * Add an attribute definition to the results.
+     *
+     * @param name attribute's qualified name
+     * @param value attribute value
+     */
+    void addBodyAttributeValue(
+            final BodyQName name,
+            final String value) {
+        attrs.put(name, value);
+    }
+
+    /**
+     * Returns the map of attributes added by the parser.
+     *
+     * @return map of atributes. Note: This is the live instance, not a copy.
+     */
+    Map<BodyQName, String> getAttributes() {
+        return attrs;
+    }
+
+}
diff --git a/src/com/kenai/jbosh/BodyParserSAX.java b/src/com/kenai/jbosh/BodyParserSAX.java
new file mode 100644
index 0000000..54c6c01
--- /dev/null
+++ b/src/com/kenai/jbosh/BodyParserSAX.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.SoftReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Implementation of the BodyParser interface which uses the SAX API
+ * that is part of the JDK.  Due to the fact that we can cache and reuse
+ * SAXPArser instances, this has proven to be significantly faster than the
+ * use of the javax.xml.stream API introduced in Java 6 while simultaneously
+ * providing an implementation accessible to Java 5 users.
+ */
+final class BodyParserSAX implements BodyParser {
+
+    /**
+     * Logger.
+     */
+    private static final Logger LOG =
+            Logger.getLogger(BodyParserSAX.class.getName());
+
+    /**
+     * SAX parser factory.
+     */
+    private static final SAXParserFactory SAX_FACTORY;
+    static {
+        SAX_FACTORY = SAXParserFactory.newInstance();
+        SAX_FACTORY.setNamespaceAware(true);
+        SAX_FACTORY.setValidating(false);
+    }
+
+    /**
+     * Thread local to contain a SAX parser instance for each thread that
+     * attempts to use one.  This allows us to gain an order of magnitude of
+     * performance as a result of not constructing parsers for each
+     * invocation while retaining thread safety.
+     */
+    private static final ThreadLocal<SoftReference<SAXParser>> PARSER =
+        new ThreadLocal<SoftReference<SAXParser>>() {
+            @Override protected SoftReference<SAXParser> initialValue() {
+                return new SoftReference<SAXParser>(null);
+            }
+        };
+
+    /**
+     * SAX event handler class.
+     */
+    private static final class Handler extends DefaultHandler {
+        private final BodyParserResults result;
+        private final SAXParser parser;
+        private String defaultNS = null;
+
+        private Handler(SAXParser theParser, BodyParserResults results) {
+            parser = theParser;
+            result = results;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void startElement(
+                final String uri,
+                final String localName,
+                final String qName,
+                final Attributes attributes) {
+            if (LOG.isLoggable(Level.FINEST)) {
+                LOG.finest("Start element: " + qName);
+                LOG.finest("    URI: " + uri);
+                LOG.finest("    local: " + localName);
+            }
+
+            BodyQName bodyName = AbstractBody.getBodyQName();
+            // Make sure the first element is correct
+            if (!(bodyName.getNamespaceURI().equals(uri)
+                    && bodyName.getLocalPart().equals(localName))) {
+                throw(new IllegalStateException(
+                        "Root element was not '" + bodyName.getLocalPart()
+                        + "' in the '" + bodyName.getNamespaceURI()
+                        + "' namespace.  (Was '" + localName + "' in '" + uri
+                        + "')"));
+            }
+
+            // Read in the attributes, making sure to expand the namespaces
+            // as needed.
+            for (int idx=0; idx < attributes.getLength(); idx++) {
+                String attrURI = attributes.getURI(idx);
+                if (attrURI.length() == 0) {
+                    attrURI = defaultNS;
+                }
+                String attrLN = attributes.getLocalName(idx);
+                String attrVal = attributes.getValue(idx);
+                if (LOG.isLoggable(Level.FINEST)) {
+                    LOG.finest("    Attribute: {" + attrURI + "}"
+                            + attrLN + " = '" + attrVal + "'");
+                }
+
+                BodyQName aqn = BodyQName.create(attrURI, attrLN);
+                result.addBodyAttributeValue(aqn, attrVal);
+            }
+            
+            parser.reset();
+        }
+
+        /**
+         * {@inheritDoc}
+         *
+         * This implementation uses this event hook to keep track of the
+         * default namespace on the body element.
+         */
+        @Override
+        public void startPrefixMapping(
+                final String prefix,
+                final String uri) {
+            if (prefix.length() == 0) {
+                if (LOG.isLoggable(Level.FINEST)) {
+                    LOG.finest("Prefix mapping: <DEFAULT> => " + uri);
+                }
+                defaultNS = uri;
+            } else {
+                if (LOG.isLoggable(Level.FINEST)) {
+                    LOG.info("Prefix mapping: " + prefix + " => " + uri);
+                }
+            }
+        }
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // BodyParser interface methods:
+
+    /**
+     * {@inheritDoc}
+     */
+    public BodyParserResults parse(String xml) throws BOSHException {
+        BodyParserResults result = new BodyParserResults();
+        Exception thrown;
+        try {
+            InputStream inStream = new ByteArrayInputStream(xml.getBytes());
+            SAXParser parser = getSAXParser();
+            parser.parse(inStream, new Handler(parser, result));
+            return result;
+        } catch (SAXException saxx) {
+            thrown = saxx;
+        } catch (IOException iox) {
+            thrown = iox;
+        }
+        throw(new BOSHException("Could not parse body:\n" + xml, thrown));
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Private methods:
+
+    /**
+     * Gets a SAXParser for use in parsing incoming messages.
+     *
+     * @return parser instance
+     */
+    private static SAXParser getSAXParser() {
+        SoftReference<SAXParser> ref = PARSER.get();
+        SAXParser result = ref.get();
+        if (result == null) {
+            Exception thrown;
+            try {
+                result = SAX_FACTORY.newSAXParser();
+                ref = new SoftReference<SAXParser>(result);
+                PARSER.set(ref);
+                return result;
+            } catch (ParserConfigurationException ex) {
+                thrown = ex;
+            } catch (SAXException ex) {
+                thrown = ex;
+            }
+            throw(new IllegalStateException(
+                    "Could not create SAX parser", thrown));
+        } else {
+            result.reset();
+            return result;
+        }
+    }
+    
+}
diff --git a/src/com/kenai/jbosh/BodyParserXmlPull.java b/src/com/kenai/jbosh/BodyParserXmlPull.java
new file mode 100644
index 0000000..5f23b06
--- /dev/null
+++ b/src/com/kenai/jbosh/BodyParserXmlPull.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.ref.SoftReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.xml.XMLConstants;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+/**
+ * Implementation of the BodyParser interface which uses the XmlPullParser
+ * API.  When available, this API provides an order of magnitude performance
+ * improvement over the default SAX parser implementation.
+ */
+final class BodyParserXmlPull implements BodyParser {
+
+    /**
+     * Logger.
+     */
+    private static final Logger LOG =
+            Logger.getLogger(BodyParserXmlPull.class.getName());
+
+    /**
+     * Thread local to contain a XmlPullParser instance for each thread that
+     * attempts to use one.  This allows us to gain an order of magnitude of
+     * performance as a result of not constructing parsers for each
+     * invocation while retaining thread safety.
+     */
+    private static final ThreadLocal<SoftReference<XmlPullParser>> XPP_PARSER =
+        new ThreadLocal<SoftReference<XmlPullParser>>() {
+            @Override protected SoftReference<XmlPullParser> initialValue() {
+                return new SoftReference<XmlPullParser>(null);
+            }
+        };
+
+    ///////////////////////////////////////////////////////////////////////////
+    // BodyParser interface methods:
+
+    /**
+     * {@inheritDoc}
+     */
+    public BodyParserResults parse(final String xml) throws BOSHException {
+        BodyParserResults result = new BodyParserResults();
+        Exception thrown;
+        try {
+            XmlPullParser xpp = getXmlPullParser();
+
+            xpp.setInput(new StringReader(xml));
+            int eventType = xpp.getEventType();
+            while (eventType != XmlPullParser.END_DOCUMENT) {
+                if (eventType == XmlPullParser.START_TAG) {
+                    if (LOG.isLoggable(Level.FINEST)) {
+                        LOG.finest("Start tag: " + xpp.getName());
+                    }
+                } else {
+                    eventType = xpp.next();
+                    continue;
+                }
+
+                String prefix = xpp.getPrefix();
+                if (prefix == null) {
+                    prefix = XMLConstants.DEFAULT_NS_PREFIX;
+                }
+                String uri = xpp.getNamespace();
+                String localName = xpp.getName();
+                QName name = new QName(uri, localName, prefix);
+                if (LOG.isLoggable(Level.FINEST)) {
+                    LOG.finest("Start element: ");
+                    LOG.finest("    prefix: " + prefix);
+                    LOG.finest("    URI: " + uri);
+                    LOG.finest("    local: " + localName);
+                }
+
+                BodyQName bodyName = AbstractBody.getBodyQName();
+                if (!bodyName.equalsQName(name)) {
+                    throw(new IllegalStateException(
+                            "Root element was not '" + bodyName.getLocalPart()
+                            + "' in the '" + bodyName.getNamespaceURI()
+                            + "' namespace.  (Was '" + localName
+                            + "' in '" + uri + "')"));
+                }
+
+                for (int idx=0; idx < xpp.getAttributeCount(); idx++) {
+                    String attrURI = xpp.getAttributeNamespace(idx);
+                    if (attrURI.length() == 0) {
+                        attrURI = xpp.getNamespace(null);
+                    }
+                    String attrPrefix = xpp.getAttributePrefix(idx);
+                    if (attrPrefix == null) {
+                        attrPrefix = XMLConstants.DEFAULT_NS_PREFIX;
+                    }
+                    String attrLN = xpp.getAttributeName(idx);
+                    String attrVal = xpp.getAttributeValue(idx);
+                    BodyQName aqn = BodyQName.createWithPrefix(
+                            attrURI, attrLN, attrPrefix);
+                    if (LOG.isLoggable(Level.FINEST)) {
+                        LOG.finest("        Attribute: {" + attrURI + "}"
+                                + attrLN + " = '" + attrVal + "'");
+                    }
+                    result.addBodyAttributeValue(aqn, attrVal);
+                }
+                break;
+            }
+            return result;
+        } catch (RuntimeException rtx) {
+            thrown = rtx;
+        } catch (XmlPullParserException xmlppx) {
+            thrown = xmlppx;
+        } catch (IOException iox) {
+            thrown = iox;
+        }
+        throw(new BOSHException("Could not parse body:\n" + xml, thrown));
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Private methods:
+
+    /**
+     * Gets a XmlPullParser for use in parsing incoming messages.
+     *
+     * @return parser instance
+     */
+    private static XmlPullParser getXmlPullParser() {
+        SoftReference<XmlPullParser> ref = XPP_PARSER.get();
+        XmlPullParser result = ref.get();
+        if (result == null) {
+            Exception thrown;
+            try {
+                XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+                factory.setNamespaceAware(true);
+                factory.setValidating(false);
+                result = factory.newPullParser();
+                ref = new SoftReference<XmlPullParser>(result);
+                XPP_PARSER.set(ref);
+                return result;
+            } catch (Exception ex) {
+                thrown = ex;
+            }
+            throw(new IllegalStateException(
+                    "Could not create XmlPull parser", thrown));
+        } else {
+            return result;
+        }
+    }
+
+}
diff --git a/src/com/kenai/jbosh/BodyQName.java b/src/com/kenai/jbosh/BodyQName.java
new file mode 100644
index 0000000..83acdf1
--- /dev/null
+++ b/src/com/kenai/jbosh/BodyQName.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Qualified name of an attribute of the wrapper element.  This class is
+ * analagous to the {@code javax.xml.namespace.QName} class.
+ * Each qualified name consists of a namespace URI and a local name.
+ * <p/>
+ * Instances of this class are immutable and thread-safe.
+ */
+public final class BodyQName {
+
+    /**
+     * BOSH namespace URI.
+     */
+    static final String BOSH_NS_URI =
+            "http://jabber.org/protocol/httpbind";
+
+    /**
+     * Namespace URI.
+     */
+    private final QName qname;
+
+    /**
+     * Private constructor to prevent direct construction.
+     *
+     * @param wrapped QName instance to wrap
+     */
+    private BodyQName(
+            final QName wrapped) {
+        qname = wrapped;
+    }
+
+    /**
+     * Creates a new qualified name using a namespace URI and local name.
+     *
+     * @param uri namespace URI
+     * @param local local name
+     * @return BodyQName instance
+     */
+    public static BodyQName create(
+            final String uri,
+            final String local) {
+        return createWithPrefix(uri, local, null);
+    }
+
+    /**
+     * Creates a new qualified name using a namespace URI and local name
+     * along with an optional prefix.
+     *
+     * @param uri namespace URI
+     * @param local local name
+     * @param prefix optional prefix or @{code null} for no prefix
+     * @return BodyQName instance
+     */
+    public static BodyQName createWithPrefix(
+            final String uri,
+            final String local,
+            final String prefix) {
+        if (uri == null || uri.length() == 0) {
+            throw(new IllegalArgumentException(
+                    "URI is required and may not be null/empty"));
+        }
+        if (local == null || local.length() == 0) {
+            throw(new IllegalArgumentException(
+                    "Local arg is required and may not be null/empty"));
+        }
+        if (prefix == null || prefix.length() == 0) {
+            return new BodyQName(new QName(uri, local));
+        } else {
+            return new BodyQName(new QName(uri, local, prefix));
+        }
+    }
+
+    /**
+     * Get the namespace URI of this qualified name.
+     *
+     * @return namespace uri
+     */
+    public String getNamespaceURI() {
+        return qname.getNamespaceURI();
+    }
+
+    /**
+     * Get the local part of this qualified name.
+     *
+     * @return local name
+     */
+    public String  getLocalPart() {
+        return qname.getLocalPart();
+    }
+
+    /**
+     * Get the optional prefix used with this qualified name, or {@code null}
+     * if no prefix has been assiciated.
+     *
+     * @return prefix, or {@code null} if no prefix was supplied
+     */
+    public String getPrefix() {
+        return qname.getPrefix();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj instanceof BodyQName) {
+            BodyQName other = (BodyQName) obj;
+            return qname.equals(other.qname);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return qname.hashCode();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Package-private methods:
+
+    /**
+     * Creates a new qualified name using the BOSH namespace URI and local name.
+     *
+     * @param local local name
+     * @return BodyQName instance
+     */
+    static BodyQName createBOSH(
+            final String local) {
+        return createWithPrefix(BOSH_NS_URI, local, null);
+    }
+
+    /**
+     * Convenience method to compare this qualified name with a
+     * {@code javax.xml.namespace.QName}.
+     *
+     * @param otherName QName to compare to
+     * @return @{code true} if the qualified name is the same, {@code false}
+     *  otherwise
+     */
+    boolean equalsQName(final QName otherName) {
+        return qname.equals(otherName);
+    }
+
+}
diff --git a/src/com/kenai/jbosh/CMSessionParams.java b/src/com/kenai/jbosh/CMSessionParams.java
new file mode 100644
index 0000000..bbed628
--- /dev/null
+++ b/src/com/kenai/jbosh/CMSessionParams.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * A BOSH connection manager session instance.  This consolidates the
+ * configuration knowledge related to the CM session and provides a
+ * mechanism by which
+ */
+final class CMSessionParams {
+
+    private final AttrSessionID sid;
+
+    private final AttrWait wait;
+
+    private final AttrVersion ver;
+
+    private final AttrPolling polling;
+
+    private final AttrInactivity inactivity;
+
+    private final AttrRequests requests;
+
+    private final AttrHold hold;
+
+    private final AttrAccept accept;
+
+    private final AttrMaxPause maxPause;
+
+    private final AttrAck ack;
+
+    private final AttrCharsets charsets;
+
+    private final boolean ackingRequests;
+
+    /**
+     * Prevent direct construction.
+     */
+    private CMSessionParams(
+            final AttrSessionID aSid,
+            final AttrWait aWait,
+            final AttrVersion aVer,
+            final AttrPolling aPolling,
+            final AttrInactivity aInactivity,
+            final AttrRequests aRequests,
+            final AttrHold aHold,
+            final AttrAccept aAccept,
+            final AttrMaxPause aMaxPause,
+            final AttrAck aAck,
+            final AttrCharsets aCharsets,
+            final boolean amAckingRequests) {
+        sid = aSid;
+        wait = aWait;
+        ver = aVer;
+        polling = aPolling;
+        inactivity = aInactivity;
+        requests = aRequests;
+        hold = aHold;
+        accept = aAccept;
+        maxPause = aMaxPause;
+        ack = aAck;
+        charsets = aCharsets;
+        ackingRequests = amAckingRequests;
+    }
+
+    static CMSessionParams fromSessionInit(
+            final AbstractBody req,
+            final AbstractBody resp)
+    throws BOSHException {
+        AttrAck aAck = AttrAck.createFromString(
+                resp.getAttribute(Attributes.ACK));
+        String rid = req.getAttribute(Attributes.RID);
+        boolean acking = (aAck != null && aAck.getValue().equals(rid));
+
+        return new CMSessionParams(
+            AttrSessionID.createFromString(
+                getRequiredAttribute(resp, Attributes.SID)),
+            AttrWait.createFromString(
+                getRequiredAttribute(resp, Attributes.WAIT)),
+            AttrVersion.createFromString(
+                resp.getAttribute(Attributes.VER)),
+            AttrPolling.createFromString(
+                resp.getAttribute(Attributes.POLLING)),
+            AttrInactivity.createFromString(
+                resp.getAttribute(Attributes.INACTIVITY)),
+            AttrRequests.createFromString(
+                resp.getAttribute(Attributes.REQUESTS)),
+            AttrHold.createFromString(
+                resp.getAttribute(Attributes.HOLD)),
+            AttrAccept.createFromString(
+                resp.getAttribute(Attributes.ACCEPT)),
+            AttrMaxPause.createFromString(
+                resp.getAttribute(Attributes.MAXPAUSE)),
+            aAck,
+            AttrCharsets.createFromString(
+                resp.getAttribute(Attributes.CHARSETS)),
+            acking
+            );
+    }
+
+    private static String getRequiredAttribute(
+            final AbstractBody body,
+            final BodyQName name)
+    throws BOSHException {
+        String attrStr = body.getAttribute(name);
+        if (attrStr == null) {
+            throw(new BOSHException(
+                    "Connection Manager session creation response did not "
+                    + "include required '" + name.getLocalPart()
+                    + "' attribute"));
+        }
+        return attrStr;
+    }
+
+    AttrSessionID getSessionID() {
+        return sid;
+    }
+
+    AttrWait getWait() {
+        return wait;
+    }
+
+    AttrVersion getVersion() {
+        return ver;
+    }
+
+    AttrPolling getPollingInterval() {
+        return polling;
+    }
+
+    AttrInactivity getInactivityPeriod() {
+        return inactivity;
+    }
+
+    AttrRequests getRequests() {
+        return requests;
+    }
+
+    AttrHold getHold() {
+        return hold;
+    }
+
+    AttrAccept getAccept() {
+        return accept;
+    }
+
+    AttrMaxPause getMaxPause() {
+        return maxPause;
+    }
+
+    AttrAck getAck() {
+        return ack;
+    }
+    
+    AttrCharsets getCharsets() {
+        return charsets;
+    }
+
+    boolean isAckingRequests() {
+        return ackingRequests;
+    }
+
+}
diff --git a/src/com/kenai/jbosh/ComposableBody.java b/src/com/kenai/jbosh/ComposableBody.java
new file mode 100644
index 0000000..d375478
--- /dev/null
+++ b/src/com/kenai/jbosh/ComposableBody.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.xml.XMLConstants;
+
+/**
+ * Implementation of the {@code AbstractBody} class which allows for the
+ * definition of  messages from individual elements of a body.
+ * <p/>
+ * A message is constructed by creating a builder, manipulating the
+ * configuration of the builder, and then building it into a class instance,
+ * as in the following example:
+ * <pre>
+ * ComposableBody body = ComposableBody.builder()
+ *     .setNamespaceDefinition("foo", "http://foo.com/bar")
+ *     .setPayloadXML("<foo:data>Data to send to remote server</foo:data>")
+ *     .build();
+ * </pre>
+ * Class instances can also be "rebuilt", allowing them to be used as templates
+ * when building many similar messages:
+ * <pre>
+ * ComposableBody body2 = body.rebuild()
+ *     .setPayloadXML("<foo:data>More data to send</foo:data>")
+ *     .build();
+ * </pre>
+ * This class does only minimal syntactic and semantic checking with respect
+ * to what the generated XML will look like.  It is up to the developer to
+ * protect against the definition of malformed XML messages when building
+ * instances of this class.
+ * <p/>
+ * Instances of this class are immutable and thread-safe.
+ */
+public final class ComposableBody extends AbstractBody {
+
+    /**
+     * Pattern used to identify the beginning {@code body} element of a
+     * BOSH message.
+     */
+    private static final Pattern BOSH_START =
+            Pattern.compile("<" + "(?:(?:[^:\t\n\r >]+:)|(?:\\{[^\\}>]*?}))?"
+            + "body" + "(?:[\t\n\r ][^>]*?)?" + "(/>|>)");
+
+    /**
+     * Map of all attributes to their values.
+     */
+    private final Map<BodyQName, String> attrs;
+
+    /**
+     * Payload XML.
+     */
+    private final String payload;
+
+    /**
+     * Computed raw XML.
+     */
+    private final AtomicReference<String> computed =
+            new AtomicReference<String>();
+
+    /**
+     * Class instance builder, after the builder pattern.  This allows each
+     * message instance to be immutable while providing flexibility when
+     * building new messages.
+     * <p/>
+     * Instances of this class are <b>not</b> thread-safe.
+     */
+    public static final class Builder {
+        private Map<BodyQName, String> map;
+        private boolean doMapCopy;
+        private String payloadXML;
+
+        /**
+         * Prevent direct construction.
+         */
+        private Builder() {
+            // Empty
+        }
+
+        /**
+         * Creates a builder which is initialized to the values of the
+         * provided {@code ComposableBody} instance.  This allows an
+         * existing {@code ComposableBody} to be used as a
+         * template/starting point.
+         *
+         * @param source body template
+         * @return builder instance
+         */
+        private static Builder fromBody(final ComposableBody source) {
+            Builder result = new Builder();
+            result.map = source.getAttributes();
+            result.doMapCopy = true;
+            result.payloadXML = source.payload;
+            return result;
+        }
+
+        /**
+         * Set the body message's wrapped payload content.  Any previous
+         * content will be replaced.
+         *
+         * @param xml payload XML content
+         * @return builder instance
+         */
+        public Builder setPayloadXML(final String xml) {
+            if (xml == null) {
+                throw(new IllegalArgumentException(
+                        "payload XML argument cannot be null"));
+            }
+            payloadXML = xml;
+            return this;
+        }
+
+        /**
+         * Set an attribute on the message body / wrapper element.
+         *
+         * @param name qualified name of the attribute
+         * @param value value of the attribute
+         * @return builder instance
+         */
+        public Builder setAttribute(
+                final BodyQName name, final String value) {
+            if (map == null) {
+                map = new HashMap<BodyQName, String>();
+            } else if (doMapCopy) {
+                map = new HashMap<BodyQName, String>(map);
+                doMapCopy = false;
+            }
+            if (value == null) {
+                map.remove(name);
+            } else {
+                map.put(name, value);
+            }
+            return this;
+        }
+
+        /**
+         * Convenience method to set a namespace definition. This would result
+         * in a namespace prefix definition similar to:
+         * {@code <body xmlns:prefix="uri"/>}
+         *
+         * @param prefix prefix to define
+         * @param uri namespace URI to associate with the prefix
+         * @return builder instance
+         */
+        public Builder setNamespaceDefinition(
+                final String prefix, final String uri) {
+            BodyQName qname = BodyQName.createWithPrefix(
+                    XMLConstants.XML_NS_URI, prefix,
+                    XMLConstants.XMLNS_ATTRIBUTE);
+            return setAttribute(qname, uri);
+        }
+
+        /**
+         * Build the immutable object instance with the current configuration.
+         *
+         * @return composable body instance
+         */
+        public ComposableBody build() {
+            if (map == null) {
+                map = new HashMap<BodyQName, String>();
+            }
+            if (payloadXML == null) {
+                payloadXML = "";
+            }
+            return new ComposableBody(map, payloadXML);
+        }
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructors:
+
+    /**
+     * Prevent direct construction.  This constructor is for body messages
+     * which are dynamically assembled.
+     */
+    private ComposableBody(
+            final Map<BodyQName, String> attrMap,
+            final String payloadXML) {
+        super();
+        attrs = attrMap;
+        payload = payloadXML;
+    }
+
+    /**
+     * Parse a static body instance into a composable instance.  This is an
+     * expensive operation and should not be used lightly.
+     * <p/>
+     * The current implementation does not obtain the payload XML by means of
+     * a proper XML parser.  It uses some string pattern searching to find the
+     * first @{code body} element and the last element's closing tag.  It is
+     * assumed that the static body's XML is well formed, etc..  This
+     * implementation may change in the future.
+     *
+     * @param body static body instance to convert
+     * @return composable bosy instance
+     * @throws BOSHException
+     */
+    static ComposableBody fromStaticBody(final StaticBody body)
+    throws BOSHException {
+        String raw = body.toXML();
+        Matcher matcher = BOSH_START.matcher(raw);
+        if (!matcher.find()) {
+            throw(new BOSHException(
+                    "Could not locate 'body' element in XML.  The raw XML did"
+                    + " not match the pattern: " + BOSH_START));
+        }
+        String payload;
+        if (">".equals(matcher.group(1))) {
+            int first = matcher.end();
+            int last = raw.lastIndexOf("</");
+            if (last < first) {
+                last = first;
+            }
+            payload = raw.substring(first, last);
+        } else {
+            payload = "";
+        }
+
+        return new ComposableBody(body.getAttributes(), payload);
+    }
+
+    /**
+     * Create a builder instance to build new instances of this class.
+     *
+     * @return AbstractBody instance
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * If this {@code ComposableBody} instance is a dynamic instance, uses this
+     * {@code ComposableBody} instance as a starting point, create a builder
+     * which can be used to create another {@code ComposableBody} instance
+     * based on this one. This allows a {@code ComposableBody} instance to be
+     * used as a template.  Note that the use of the returned builder in no
+     * way modifies or manipulates the current {@code ComposableBody} instance.
+     *
+     * @return builder instance which can be used to build similar
+     *  {@code ComposableBody} instances
+     */
+    public Builder rebuild() {
+        return Builder.fromBody(this);
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Accessors:
+    
+    /**
+     * {@inheritDoc}
+     */
+    public Map<BodyQName, String> getAttributes() {
+        return Collections.unmodifiableMap(attrs);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String toXML() {
+        String comp = computed.get();
+        if (comp == null) {
+            comp = computeXML();
+            computed.set(comp);
+        }
+        return comp;
+    }
+
+    /**
+     * Get the paylaod XML in String form.
+     *
+     * @return payload XML
+     */
+    public String getPayloadXML() {
+        return payload;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Private methods:
+
+    /**
+     * Escape the value of an attribute to ensure we maintain valid
+     * XML syntax.
+     *
+     * @param value value to escape
+     * @return escaped value
+     */
+    private String escape(final String value) {
+        return value.replace("'", "&apos;");
+    }
+
+    /**
+     * Generate a String representation of the message body.
+     *
+     * @return XML string representation of the body
+     */
+    private String computeXML() {
+        BodyQName bodyName = getBodyQName();
+        StringBuilder builder = new StringBuilder();
+        builder.append("<");
+        builder.append(bodyName.getLocalPart());
+        for (Map.Entry<BodyQName, String> entry : attrs.entrySet()) {
+            builder.append(" ");
+            BodyQName name = entry.getKey();
+            String prefix = name.getPrefix();
+            if (prefix != null && prefix.length() > 0) {
+                builder.append(prefix);
+                builder.append(":");
+            }
+            builder.append(name.getLocalPart());
+            builder.append("='");
+            builder.append(escape(entry.getValue()));
+            builder.append("'");
+        }
+        builder.append(" ");
+        builder.append(XMLConstants.XMLNS_ATTRIBUTE);
+        builder.append("='");
+        builder.append(bodyName.getNamespaceURI());
+        builder.append("'>");
+        if (payload != null) {
+            builder.append(payload);
+        }
+        builder.append("</body>");
+        return builder.toString();
+    }
+
+}
diff --git a/src/com/kenai/jbosh/GZIPCodec.java b/src/com/kenai/jbosh/GZIPCodec.java
new file mode 100644
index 0000000..988f27f
--- /dev/null
+++ b/src/com/kenai/jbosh/GZIPCodec.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * Codec methods for compressing and uncompressing using GZIP.
+ */
+final class GZIPCodec {
+
+    /**
+     * Size of the internal buffer when decoding.
+     */
+    private static final int BUFFER_SIZE = 512;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructors:
+
+    /**
+     * Prevent construction.
+     */
+    private GZIPCodec() {
+        // Empty
+    }
+
+    /**
+     * Returns the name of the codec.
+     *
+     * @return string name of the codec (i.e., "gzip")
+     */
+    public static String getID() {
+        return "gzip";
+    }
+
+    /**
+     * Compress/encode the data provided using the GZIP format.
+     *
+     * @param data data to compress
+     * @return compressed data
+     * @throws IOException on compression failure
+     */
+    public static byte[] encode(final byte[] data) throws IOException {
+        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+        GZIPOutputStream gzOut = null;
+        try {
+            gzOut = new GZIPOutputStream(byteOut);
+            gzOut.write(data);
+            gzOut.close();
+            byteOut.close();
+            return byteOut.toByteArray();
+        } finally {
+            gzOut.close();
+            byteOut.close();
+        }
+    }
+
+    /**
+     * Uncompress/decode the data provided using the GZIP format.
+     *
+     * @param data data to uncompress
+     * @return uncompressed data
+     * @throws IOException on decompression failure
+     */
+    public static byte[] decode(final byte[] compressed) throws IOException {
+        ByteArrayInputStream byteIn = new ByteArrayInputStream(compressed);
+        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+        GZIPInputStream gzIn = null;
+        try {
+            gzIn = new GZIPInputStream(byteIn);
+            int read;
+            byte[] buffer = new byte[BUFFER_SIZE];
+            do {
+                read = gzIn.read(buffer);
+                if (read > 0) {
+                    byteOut.write(buffer, 0, read);
+                }
+            } while (read >= 0);
+            return byteOut.toByteArray();
+        } finally {
+            gzIn.close();
+            byteOut.close();
+        }
+    }
+
+}
diff --git a/src/com/kenai/jbosh/HTTPExchange.java b/src/com/kenai/jbosh/HTTPExchange.java
new file mode 100644
index 0000000..c77caf0
--- /dev/null
+++ b/src/com/kenai/jbosh/HTTPExchange.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * A request and response pair representing a single exchange with a remote
+ * content manager.  This is primarily a container class intended to maintain
+ * the relationship between the request and response but allows the response
+ * to be added after the fact.
+ */
+final class HTTPExchange {
+
+    /**
+     * Logger.
+     */
+    private static final Logger LOG =
+            Logger.getLogger(HTTPExchange.class.getName());
+    
+    /**
+     * Request body.
+     */
+    private final AbstractBody request;
+
+    /**
+     * Lock instance used to protect and provide conditions.
+     */
+    private final Lock lock = new ReentrantLock();
+
+    /**
+     * Condition used to signal when the response has been set.
+     */
+    private final Condition ready = lock.newCondition();
+
+    /**
+     * HTTPResponse instance.
+     */
+    private HTTPResponse response;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructor:
+
+    /**
+     * Create a new request/response pair object.
+     *
+     * @param req request message body
+     */
+    HTTPExchange(final AbstractBody req) {
+        if (req == null) {
+            throw(new IllegalArgumentException("Request body cannot be null"));
+        }
+        request = req;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Package-private methods:
+
+    /**
+     * Get the original request message.
+     *
+     * @return request message body.
+     */
+    AbstractBody getRequest() {
+        return request;
+    }
+
+    /**
+     * Set the HTTPResponse instance.
+     *
+     * @return HTTPResponse instance associated with the request.
+     */
+    void setHTTPResponse(HTTPResponse resp) {
+        lock.lock();
+        try {
+            if (response != null) {
+                throw(new IllegalStateException(
+                        "HTTPResponse was already set"));
+            }
+            response = resp;
+            ready.signalAll();
+        } finally {
+            lock.unlock();
+        }
+    }
+
+    /**
+     * Get the HTTPResponse instance.
+     *
+     * @return HTTPResponse instance associated with the request.
+     */
+    HTTPResponse getHTTPResponse() {
+        lock.lock();
+        try {
+            while (response == null) {
+                try {
+                    ready.await();
+                } catch (InterruptedException intx) {
+                    LOG.log(Level.FINEST, "Interrupted", intx);
+                }
+            }
+            return response;
+        } finally {
+            lock.unlock();
+        }
+    }
+
+}
diff --git a/src/com/kenai/jbosh/HTTPResponse.java b/src/com/kenai/jbosh/HTTPResponse.java
new file mode 100644
index 0000000..f1f301c
--- /dev/null
+++ b/src/com/kenai/jbosh/HTTPResponse.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * This class represents a complete HTTP response to a request made via
+ * a {@code HTTPSender} send request.  Instances of this interface are
+ * intended to represent a deferred, future response, not necessarily a
+ * response which is immediately available.
+ */
+interface HTTPResponse {
+
+    /**
+     * Close out any resources still held by the original request.  The
+     * conversation may need to be aborted if the session it was a part of
+     * gets abruptly terminated.
+     */
+    void abort();
+
+    /**
+     * Get the HTTP status code of the response (e.g., 200, 404, etc.).  If
+     * the response has not yet been received from the remote server, this
+     * method should block until the response has arrived.
+     *
+     * @return HTTP status code
+     * @throws InterruptedException if interrupted while awaiting response
+     */
+    int getHTTPStatus() throws InterruptedException, BOSHException;
+
+    /**
+     * Get the HTTP response message body.  If the response has not yet been
+     * received from the remote server, this method should block until the
+     * response has arrived.
+     *
+     * @return response message body
+     * @throws InterruptedException if interrupted while awaiting response
+     */
+    AbstractBody getBody() throws InterruptedException, BOSHException;
+    
+}
diff --git a/src/com/kenai/jbosh/HTTPSender.java b/src/com/kenai/jbosh/HTTPSender.java
new file mode 100644
index 0000000..486d274
--- /dev/null
+++ b/src/com/kenai/jbosh/HTTPSender.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Interface used to represent code which can send a BOSH XML body over
+ * HTTP to a connection manager.
+ */
+interface HTTPSender {
+
+    /**
+     * Initialize the HTTP sender instance for use with the session provided.
+     * This method will be called once before use of the service instance.
+     *
+     * @param sessionCfg session configuration
+     */
+    void init(BOSHClientConfig sessionCfg);
+
+    /**
+     * Dispose of all resources used to provide the required services.  This
+     * method will be called once when the service instance is no longer
+     * required.
+     */
+    void destroy();
+
+    /**
+     * Create a {@code Callable} instance which can be used to send the
+     * request specified to the connection manager.  This method should
+     * return immediately, prior to doing any real work.  The invocation
+     * of the returned {@code Callable} should send the request (if it has
+     * not already been sent by the time of the call), block while waiting
+     * for the response, and then return the response body.
+     *
+     * @param params CM session creation resopnse params
+     * @param body request body to send
+     * @return callable used to access the response
+     */
+    HTTPResponse send(CMSessionParams params, AbstractBody body);
+    
+}
diff --git a/src/com/kenai/jbosh/QName.java b/src/com/kenai/jbosh/QName.java
new file mode 100644
index 0000000..d395a06
--- /dev/null
+++ b/src/com/kenai/jbosh/QName.java
@@ -0,0 +1,269 @@
+/*
+ * The Apache Software License, Version 1.1
+ *
+ *
+ * Copyright (c) 2001-2003 The Apache Software Foundation.  All rights
+ * reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in
+ *    the documentation and/or other materials provided with the
+ *    distribution.
+ *
+ * 3. The end-user documentation included with the redistribution,
+ *    if any, must include the following acknowledgment:
+ *       "This product includes software developed by the
+ *        Apache Software Foundation (http://www.apache.org/)."
+ *    Alternately, this acknowledgment may appear in the software itself,
+ *    if and wherever such third-party acknowledgments normally appear.
+ *
+ * 4. The names "Axis" and "Apache Software Foundation" must
+ *    not be used to endorse or promote products derived from this
+ *    software without prior written permission. For written
+ *    permission, please contact apache@apache.org.
+ *
+ * 5. Products derived from this software may not be called "Apache",
+ *    nor may "Apache" appear in their name, without prior written
+ *    permission of the Apache Software Foundation.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
+ * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+ * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+ * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ */
+package com.kenai.jbosh;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+
+/**
+ * <code>QName</code> class represents the value of a qualified name
+ * as specified in <a href="http://www.w3.org/TR/xmlschema-2/#QName">XML
+ * Schema Part2: Datatypes specification</a>.
+ * <p>
+ * The value of a QName contains a <b>namespaceURI</b>, a <b>localPart</b> and a <b>prefix</b>.
+ * The localPart provides the local part of the qualified name. The
+ * namespaceURI is a URI reference identifying the namespace.
+ *
+ * @version 1.1
+ */
+public class QName implements Serializable {
+
+    /** comment/shared empty string */
+    private static final String emptyString = "".intern();
+
+    /** Field namespaceURI */
+    private String namespaceURI;
+
+    /** Field localPart */
+    private String localPart;
+
+    /** Field prefix */
+    private String prefix;
+
+    /**
+     * Constructor for the QName.
+     *
+     * @param localPart Local part of the QName
+     */
+    public QName(String localPart) {
+        this(emptyString, localPart, emptyString);
+    }
+
+    /**
+     * Constructor for the QName.
+     *
+     * @param namespaceURI Namespace URI for the QName
+     * @param localPart Local part of the QName.
+     */
+    public QName(String namespaceURI, String localPart) {
+        this(namespaceURI, localPart, emptyString);
+    }
+
+    /**
+     * Constructor for the QName.
+     *
+     * @param namespaceURI Namespace URI for the QName
+     * @param localPart Local part of the QName.
+     * @param prefix Prefix of the QName.
+     */
+    public QName(String namespaceURI, String localPart, String prefix) {
+        this.namespaceURI = (namespaceURI == null)
+                ? emptyString
+                : namespaceURI.intern();
+        if (localPart == null) {
+            throw new IllegalArgumentException("invalid QName local part");
+        } else {
+            this.localPart = localPart.intern();
+        }
+
+        if (prefix == null) {
+            throw new IllegalArgumentException("invalid QName prefix");
+        } else {
+            this.prefix = prefix.intern();
+        }
+    }
+
+    /**
+     * Gets the Namespace URI for this QName
+     *
+     * @return Namespace URI
+     */
+    public String getNamespaceURI() {
+        return namespaceURI;
+    }
+
+    /**
+     * Gets the Local part for this QName
+     *
+     * @return Local part
+     */
+    public String getLocalPart() {
+        return localPart;
+    }
+
+    /**
+     * Gets the Prefix for this QName
+     *
+     * @return Prefix
+     */
+    public String getPrefix() {
+        return prefix;
+    }
+
+    /**
+     * Returns a string representation of this QName
+     *
+     * @return  a string representation of the QName
+     */
+    public String toString() {
+
+        return ((namespaceURI == emptyString)
+                ? localPart
+                : '{' + namespaceURI + '}' + localPart);
+    }
+
+    /**
+     * Tests this QName for equality with another object.
+     * <p>
+     * If the given object is not a QName or is null then this method
+     * returns <tt>false</tt>.
+     * <p>
+     * For two QNames to be considered equal requires that both
+     * localPart and namespaceURI must be equal. This method uses
+     * <code>String.equals</code> to check equality of localPart
+     * and namespaceURI. Any class that extends QName is required
+     * to satisfy this equality contract.
+     * <p>
+     * This method satisfies the general contract of the <code>Object.equals</code> method.
+     *
+     * @param obj the reference object with which to compare
+     *
+     * @return <code>true</code> if the given object is identical to this
+     *      QName: <code>false</code> otherwise.
+     */
+    public final boolean equals(Object obj) {
+
+        if (obj == this) {
+            return true;
+        }
+
+        if (!(obj instanceof QName)) {
+            return false;
+        }
+
+        if ((namespaceURI == ((QName) obj).namespaceURI)
+                && (localPart == ((QName) obj).localPart)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns a QName holding the value of the specified String.
+     * <p>
+     * The string must be in the form returned by the QName.toString()
+     * method, i.e. "{namespaceURI}localPart", with the "{namespaceURI}"
+     * part being optional.
+     * <p>
+     * This method doesn't do a full validation of the resulting QName.
+     * In particular, it doesn't check that the resulting namespace URI
+     * is a legal URI (per RFC 2396 and RFC 2732), nor that the resulting
+     * local part is a legal NCName per the XML Namespaces specification.
+     *
+     * @param s the string to be parsed
+     * @throws java.lang.IllegalArgumentException If the specified String cannot be parsed as a QName
+     * @return QName corresponding to the given String
+     */
+    public static QName valueOf(String s) {
+
+        if ((s == null) || s.equals("")) {
+            throw new IllegalArgumentException("invalid QName literal");
+        }
+
+        if (s.charAt(0) == '{') {
+            int i = s.indexOf('}');
+
+            if (i == -1) {
+                throw new IllegalArgumentException("invalid QName literal");
+            }
+
+            if (i == s.length() - 1) {
+                throw new IllegalArgumentException("invalid QName literal");
+            } else {
+                return new QName(s.substring(1, i), s.substring(i + 1));
+            }
+        } else {
+            return new QName(s);
+        }
+    }
+
+    /**
+     * Returns a hash code value for this QName object. The hash code
+     * is based on both the localPart and namespaceURI parts of the
+     * QName. This method satisfies the  general contract of the
+     * <code>Object.hashCode</code> method.
+     *
+     * @return a hash code value for this Qname object
+     */
+    public final int hashCode() {
+        return namespaceURI.hashCode() ^ localPart.hashCode();
+    }
+
+    /**
+     * Ensure that deserialization properly interns the results.
+     * @param in the ObjectInputStream to be read
+     */
+    private void readObject(ObjectInputStream in) throws
+            IOException, ClassNotFoundException {
+        in.defaultReadObject();
+
+        namespaceURI = namespaceURI.intern();
+        localPart = localPart.intern();
+        prefix = prefix.intern();
+    }
+}
+
diff --git a/src/com/kenai/jbosh/RequestIDSequence.java b/src/com/kenai/jbosh/RequestIDSequence.java
new file mode 100644
index 0000000..14b1475
--- /dev/null
+++ b/src/com/kenai/jbosh/RequestIDSequence.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.security.SecureRandom;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Request ID sequence generator.  This generator generates a random first
+ * RID and then manages the sequence from there on out.
+ */
+final class RequestIDSequence {
+
+    /**
+     * Maximum number of bits available for representing request IDs, according
+     * to the XEP-0124 spec.s
+     */
+    private static final int MAX_BITS = 53;
+
+    /**
+     * Bits devoted to incremented values.
+     */
+    private static final int INCREMENT_BITS = 32;
+
+    /**
+     * Minimum number of times the initial RID can be incremented before
+     * exceeding the maximum.
+     */
+    private static final long MIN_INCREMENTS = 1L << INCREMENT_BITS;
+
+    /**
+     * Max initial value. 
+     */
+    private static final long MAX_INITIAL = (1L << MAX_BITS) - MIN_INCREMENTS;
+
+    /**
+     * Max bits mask.
+     */
+    private static final long MASK = ~(Long.MAX_VALUE << MAX_BITS);
+
+    /**
+     * Random number generator.
+     */
+    private static final SecureRandom RAND = new SecureRandom();
+
+    /**
+     * Internal lock.
+     */
+    private static final Lock LOCK = new ReentrantLock();
+
+    /**
+     * The last reqest ID used, or &lt;= 0 if a new request ID needs to be
+     * generated.
+     */
+    private AtomicLong nextRequestID = new AtomicLong();
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructors:
+
+    /**
+     * Prevent direct construction.
+     */
+    RequestIDSequence() {
+        nextRequestID = new AtomicLong(generateInitialValue());
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Public methods:
+
+    /**
+     * Calculates the next request ID value to use.  This number must be
+     * initialized such that it is unlikely to ever exceed 2 ^ 53, according
+     * to XEP-0124.
+     *
+     * @return next request ID value
+     */
+    public long getNextRID() {
+        return nextRequestID.getAndIncrement();
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Private methods:
+
+    /**
+     * Generates an initial RID value by generating numbers until a number is
+     * found which is smaller than the maximum allowed value and greater
+     * than zero.
+     *
+     * @return random initial value
+     */
+    private long generateInitialValue() {
+        long result;
+        LOCK.lock();
+        try {
+            do {
+                result = RAND.nextLong() & MASK;
+            } while (result > MAX_INITIAL);
+        } finally {
+            LOCK.unlock();
+        }
+        return result;
+    }
+
+}
diff --git a/src/com/kenai/jbosh/ServiceLib.java b/src/com/kenai/jbosh/ServiceLib.java
new file mode 100644
index 0000000..07d0556
--- /dev/null
+++ b/src/com/kenai/jbosh/ServiceLib.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.io.BufferedReader;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Utility library for use in loading services using the Jar Service
+ * Provider Interface (Jar SPI).  This can be replaced once the minimum
+ * java rev moves beyond Java 5.
+ */
+final class ServiceLib {
+
+    /**
+     * Logger.
+     */
+    private static final Logger LOG =
+            Logger.getLogger(ServiceLib.class.getName());
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Package-private methods:
+
+    /**
+     * Prevent construction.
+     */
+    private ServiceLib() {
+        // Empty
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Package-private methods:
+
+    /**
+     * Probe for and select an implementation of the specified service
+     * type by using the a modified Jar SPI mechanism.  Modified in that
+     * the system properties will be checked to see if there is a value
+     * set for the naem of the class to be loaded.  If so, that value is
+     * treated as the class name of the first implementation class to be
+     * attempted to be loaded.  This provides a (unsupported) mechanism
+     * to insert other implementations.  Note that the supported mechanism
+     * is by properly ordering the classpath.
+     *
+     * @return service instance
+     * @throws IllegalStateException is no service implementations could be
+     *  instantiated
+     */
+    static <T> T loadService(Class<T> ofType) {
+        List<String> implClasses = loadServicesImplementations(ofType);
+        for (String implClass : implClasses) {
+            T result = attemptLoad(ofType, implClass);
+            if (result != null) {
+                if (LOG.isLoggable(Level.FINEST)) {
+                    LOG.finest("Selected " + ofType.getSimpleName()
+                            + " implementation: "
+                            + result.getClass().getName());
+                }
+                return result;
+            }
+        }
+        throw(new IllegalStateException(
+                "Could not load " + ofType.getName() + " implementation"));
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Private methods:
+
+    /**
+     * Generates a list of implementation class names by using
+     * the Jar SPI technique.  The order in which the class names occur
+     * in the service manifest is significant.
+     *
+     * @return list of all declared implementation class names
+     */
+    private static List<String> loadServicesImplementations(
+            final Class ofClass) {
+        List<String> result = new ArrayList<String>();
+
+        // Allow a sysprop to specify the first candidate
+        String override = System.getProperty(ofClass.getName());
+        if (override != null) {
+            result.add(override);
+        }
+
+        ClassLoader loader = ServiceLib.class.getClassLoader();
+        URL url = loader.getResource("META-INF/services/" + ofClass.getName());
+        InputStream inStream = null;
+        InputStreamReader reader = null;
+        BufferedReader bReader = null;
+        try {
+            inStream = url.openStream();
+            reader = new InputStreamReader(inStream);
+            bReader = new BufferedReader(reader);
+            String line;
+            while ((line = bReader.readLine()) != null) {
+                if (!line.matches("\\s*(#.*)?")) {
+                    // not a comment or blank line
+                    result.add(line.trim());
+                }
+            }
+        } catch (IOException iox) {
+            LOG.log(Level.WARNING,
+                    "Could not load services descriptor: " + url.toString(),
+                    iox);
+        } finally {
+            finalClose(bReader);
+            finalClose(reader);
+            finalClose(inStream);
+        }
+        return result;
+    }
+
+    /**
+     * Attempts to load the specified implementation class.
+     * Attempts will fail if - for example - the implementation depends
+     * on a class not found on the classpath.
+     *
+     * @param className implementation class to attempt to load
+     * @return service instance, or {@code null} if the instance could not be
+     *  loaded
+     */
+    private static <T> T attemptLoad(
+            final Class<T> ofClass,
+            final String className) {
+        if (LOG.isLoggable(Level.FINEST)) {
+            LOG.finest("Attempting service load: " + className);
+        }
+        Level level;
+        Exception thrown;
+        try {
+            Class clazz = Class.forName(className);
+            if (!ofClass.isAssignableFrom(clazz)) {
+                if (LOG.isLoggable(Level.WARNING)) {
+                    LOG.warning(clazz.getName() + " is not assignable to "
+                            + ofClass.getName());
+                }
+                return null;
+            }
+            return ofClass.cast(clazz.newInstance());
+        } catch (ClassNotFoundException ex) {
+            level = Level.FINEST;
+            thrown = ex;
+        } catch (InstantiationException ex) {
+            level = Level.WARNING;
+            thrown = ex;
+        } catch (IllegalAccessException ex) {
+            level = Level.WARNING;
+            thrown = ex;
+        }
+        LOG.log(level,
+                "Could not load " + ofClass.getSimpleName()
+                + " instance: " + className,
+                thrown);
+        return null;
+    }
+
+    /**
+     * Check and close a closeable object, trapping and ignoring any
+     * exception that might result.
+     *
+     * @param closeMe the thing to close
+     */
+    private static void finalClose(final Closeable closeMe) {
+        if (closeMe != null) {
+            try {
+                closeMe.close();
+            } catch (IOException iox) {
+                LOG.log(Level.FINEST, "Could not close: " + closeMe, iox);
+            }
+        }
+    }
+
+}
diff --git a/src/com/kenai/jbosh/StaticBody.java b/src/com/kenai/jbosh/StaticBody.java
new file mode 100644
index 0000000..fe225fb
--- /dev/null
+++ b/src/com/kenai/jbosh/StaticBody.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Implementation of the {@code AbstractBody} class which allows for the
+ * definition of messages from pre-existing message content.  Instances of
+ * this class are based on the underlying data and therefore cannot be
+ * modified.  In order to obtain the wrapper element namespace and
+ * attribute information, the body content is partially parsed.
+ * <p/>
+ * This class does only minimal syntactic and semantic checking with respect
+ * to what the generated XML will look like.  It is up to the developer to
+ * protect against the definition of malformed XML messages when building
+ * instances of this class.
+ * <p/>
+ * Instances of this class are immutable and thread-safe.
+ */
+final class StaticBody extends AbstractBody {
+
+    /**
+     * Selected parser to be used to process raw XML messages.
+     */
+    private static final BodyParser PARSER =
+            new BodyParserXmlPull();
+
+    /**
+     * Size of the internal buffer when copying from a stream.
+     */
+    private static final int BUFFER_SIZE = 1024;
+
+    /**
+     * Map of all attributes to their values.
+     */
+    private final Map<BodyQName, String> attrs;
+
+    /**
+     * This body message in raw XML form.
+     */
+    private final String raw;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructors:
+
+    /**
+     * Prevent direct construction.
+     */
+    private StaticBody(
+            final Map<BodyQName, String> attrMap,
+            final String rawXML) {
+        attrs = attrMap;
+        raw = rawXML;
+    }
+
+    /**
+     * Creates an instance which is initialized by reading a body
+     * message from the provided stream.
+     *
+     * @param inStream stream to read message XML from
+     * @return body instance
+     * @throws BOSHException on parse error
+     */
+    public static StaticBody fromStream(
+            final InputStream inStream)
+            throws BOSHException {
+        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+        try {
+            byte[] buffer = new byte[BUFFER_SIZE];
+            int read;
+            do {
+                read = inStream.read(buffer);
+                if (read > 0) {
+                    byteOut.write(buffer, 0, read);
+                }
+            } while (read >= 0);
+        } catch (IOException iox) {
+            throw(new BOSHException(
+                    "Could not read body data", iox));
+        }
+        return fromString(byteOut.toString());
+    }
+
+    /**
+     * Creates an instance which is initialized by reading a body
+     * message from the provided raw XML string.
+     *
+     * @param rawXML raw message XML in string form
+     * @return body instance
+     * @throws BOSHException on parse error
+     */
+    public static StaticBody fromString(
+            final String rawXML)
+            throws BOSHException {
+        BodyParserResults results = PARSER.parse(rawXML);
+        return new StaticBody(results.getAttributes(), rawXML);
+    }
+
+
+    /**
+     * {@inheritDoc}
+     */
+    public Map<BodyQName, String> getAttributes() {
+        return Collections.unmodifiableMap(attrs);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String toXML() {
+        return raw;
+    }
+
+}
diff --git a/src/com/kenai/jbosh/TerminalBindingCondition.java b/src/com/kenai/jbosh/TerminalBindingCondition.java
new file mode 100644
index 0000000..0aecfd8
--- /dev/null
+++ b/src/com/kenai/jbosh/TerminalBindingCondition.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Terminal binding conditions and their associated messages.
+ */
+final class TerminalBindingCondition {
+
+    /**
+     * Map of condition names to condition instances.
+     */
+    private static final Map<String, TerminalBindingCondition>
+            COND_TO_INSTANCE = new HashMap<String, TerminalBindingCondition>();
+
+    /**
+     * Map of HTTP response codes to condition instances.
+     */
+    private static final Map<Integer, TerminalBindingCondition>
+            CODE_TO_INSTANCE = new HashMap<Integer, TerminalBindingCondition>();
+
+    static final TerminalBindingCondition BAD_REQUEST =
+            createWithCode("bad-request", "The format of an HTTP header or "
+            + "binding element received from the client is unacceptable "
+            + "(e.g., syntax error).", Integer.valueOf(400));
+
+    static final TerminalBindingCondition HOST_GONE =
+            create("host-gone", "The target domain specified in the 'to' "
+            + "attribute or the target host or port specified in the 'route' "
+            + "attribute is no longer serviced by the connection manager.");
+
+    static final TerminalBindingCondition HOST_UNKNOWN =
+            create("host-unknown", "The target domain specified in the 'to' "
+            + "attribute or the target host or port specified in the 'route' "
+            + "attribute is unknown to the connection manager.");
+
+    static final TerminalBindingCondition IMPROPER_ADDRESSING =
+            create("improper-addressing", "The initialization element lacks a "
+            + "'to' or 'route' attribute (or the attribute has no value) but "
+            + "the connection manager requires one.");
+
+    static final TerminalBindingCondition INTERNAL_SERVER_ERROR =
+            create("internal-server-error", "The connection manager has "
+            + "experienced an internal error that prevents it from servicing "
+            + "the request.");
+
+    static final TerminalBindingCondition ITEM_NOT_FOUND =
+            createWithCode("item-not-found", "(1) 'sid' is not valid, (2) "
+            + "'stream' is not valid, (3) 'rid' is larger than the upper limit "
+            + "of the expected window, (4) connection manager is unable to "
+            + "resend response, (5) 'key' sequence is invalid.",
+            Integer.valueOf(404));
+
+    static final TerminalBindingCondition OTHER_REQUEST =
+            create("other-request", "Another request being processed at the "
+            + "same time as this request caused the session to terminate.");
+
+    static final TerminalBindingCondition POLICY_VIOLATION =
+            createWithCode("policy-violation", "The client has broken the "
+            + "session rules (polling too frequently, requesting too "
+            + "frequently, sending too many simultaneous requests).",
+            Integer.valueOf(403));
+
+    static final TerminalBindingCondition REMOTE_CONNECTION_FAILED =
+            create("remote-connection-failed", "The connection manager was "
+            + "unable to connect to, or unable to connect securely to, or has "
+            + "lost its connection to, the server.");
+
+    static final TerminalBindingCondition REMOTE_STREAM_ERROR =
+            create("remote-stream-error", "Encapsulated transport protocol "
+            + "error.");
+
+    static final TerminalBindingCondition SEE_OTHER_URI =
+            create("see-other-uri", "The connection manager does not operate "
+            + "at this URI (e.g., the connection manager accepts only SSL or "
+            + "TLS connections at some https: URI rather than the http: URI "
+            + "requested by the client).");
+
+    static final TerminalBindingCondition SYSTEM_SHUTDOWN =
+            create("system-shutdown", "The connection manager is being shut "
+            + "down. All active HTTP sessions are being terminated. No new "
+            + "sessions can be created.");
+
+    static final TerminalBindingCondition UNDEFINED_CONDITION =
+            create("undefined-condition", "Unknown or undefined error "
+            + "condition.");
+
+    /**
+     * Condition name.
+     */
+    private final String cond;
+
+    /**
+     * Descriptive message.
+     */
+    private final String msg;
+
+    /**
+     * Private constructor to pre
+     */
+    private TerminalBindingCondition(
+            final String condition,
+            final String message) {
+        cond = condition;
+        msg = message;
+    }
+    
+    /**
+     * Helper method to call the helper method to add entries.
+     */
+    private static TerminalBindingCondition create(
+            final String condition,
+            final String message) {
+        return createWithCode(condition, message, null);
+    }
+    
+    /**
+     * Helper method to add entries.
+     */
+    private static TerminalBindingCondition createWithCode(
+            final String condition,
+            final String message,
+            final Integer code) {
+        if (condition == null) {
+            throw(new IllegalArgumentException(
+                    "condition may not be null"));
+        }
+        if (message == null) {
+            throw(new IllegalArgumentException(
+                    "message may not be null"));
+        }
+        if (COND_TO_INSTANCE.get(condition) != null) {
+            throw(new IllegalStateException(
+                    "Multiple definitions of condition: " + condition));
+        }
+        TerminalBindingCondition result =
+                new TerminalBindingCondition(condition, message);
+        COND_TO_INSTANCE.put(condition, result);
+        if (code != null) {
+            if (CODE_TO_INSTANCE.get(code) != null) {
+                throw(new IllegalStateException(
+                        "Multiple definitions of code: " + code));
+            }
+            CODE_TO_INSTANCE.put(code, result);
+        }
+        return result;
+    }
+
+    /**
+     * Lookup the terminal binding condition instance with the condition
+     * name specified.
+     *
+     * @param condStr condition name
+     * @return terminal binding condition instance, or {@code null} if no
+     *  instance is known by the name specified
+     */
+    static TerminalBindingCondition forString(final String condStr) {
+        return COND_TO_INSTANCE.get(condStr);
+    }
+
+    /**
+     * Lookup the terminal binding condition instance associated with the
+     * HTTP response code specified.
+     *
+     * @param httpRespCode HTTP response code
+     * @return terminal binding condition instance, or {@code null} if no
+     *  instance is known by the response code specified
+     */
+    static TerminalBindingCondition forHTTPResponseCode(final int httpRespCode) {
+        return CODE_TO_INSTANCE.get(Integer.valueOf(httpRespCode));
+    }
+
+    /**
+     * Get the name of the condition.
+     *
+     * @return condition name
+     */
+    String getCondition() {
+        return cond;
+    }
+
+    /**
+     * Get the human readable error message associated with this condition.
+     *
+     * @return error message
+     */
+    String getMessage() {
+        return msg;
+    }
+
+}
diff --git a/src/com/kenai/jbosh/ZLIBCodec.java b/src/com/kenai/jbosh/ZLIBCodec.java
new file mode 100644
index 0000000..20844ad
--- /dev/null
+++ b/src/com/kenai/jbosh/ZLIBCodec.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * Codec methods for compressing and uncompressing using ZLIB.
+ */
+final class ZLIBCodec {
+
+    /**
+     * Size of the internal buffer when decoding.
+     */
+    private static final int BUFFER_SIZE = 512;
+
+    ///////////////////////////////////////////////////////////////////////////
+    // Constructors:
+
+    /**
+     * Prevent construction.
+     */
+    private ZLIBCodec() {
+        // Empty
+    }
+
+    /**
+     * Returns the name of the codec.
+     *
+     * @return string name of the codec (i.e., "deflate")
+     */
+    public static String getID() {
+        return "deflate";
+    }
+
+    /**
+     * Compress/encode the data provided using the ZLIB format.
+     *
+     * @param data data to compress
+     * @return compressed data
+     * @throws IOException on compression failure
+     */
+    public static byte[] encode(final byte[] data) throws IOException {
+        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+        DeflaterOutputStream deflateOut = null;
+        try {
+            deflateOut = new DeflaterOutputStream(byteOut);
+            deflateOut.write(data);
+            deflateOut.close();
+            byteOut.close();
+            return byteOut.toByteArray();
+        } finally {
+            deflateOut.close();
+            byteOut.close();
+        }
+    }
+
+    /**
+     * Uncompress/decode the data provided using the ZLIB format.
+     *
+     * @param data data to uncompress
+     * @return uncompressed data
+     * @throws IOException on decompression failure
+     */
+    public static byte[] decode(final byte[] compressed) throws IOException {
+        ByteArrayInputStream byteIn = new ByteArrayInputStream(compressed);
+        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+        InflaterInputStream inflaterIn = null;
+        try {
+            inflaterIn = new InflaterInputStream(byteIn);
+            int read;
+            byte[] buffer = new byte[BUFFER_SIZE];
+            do {
+                read = inflaterIn.read(buffer);
+                if (read > 0) {
+                    byteOut.write(buffer, 0, read);
+                }
+            } while (read >= 0);
+            return byteOut.toByteArray();
+        } finally {
+            inflaterIn.close();
+            byteOut.close();
+        }
+    }
+
+}
diff --git a/src/com/kenai/jbosh/package.html b/src/com/kenai/jbosh/package.html
new file mode 100644
index 0000000..77a1924
--- /dev/null
+++ b/src/com/kenai/jbosh/package.html
@@ -0,0 +1,8 @@
+<html>
+    <body>
+        Core classes of the JBOSH API.
+        <p/>
+        Users of the client portion of the API should start by reading
+        up on the <code>BOSHClient</code> documentation.
+    </body>
+</html>
\ No newline at end of file
diff --git a/src/com/novell/sasl/client/DigestChallenge.java b/src/com/novell/sasl/client/DigestChallenge.java
new file mode 100644
index 0000000..90e6247
--- /dev/null
+++ b/src/com/novell/sasl/client/DigestChallenge.java
@@ -0,0 +1,393 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/DigestChallenge.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2003 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import java.util.*;
+import org.apache.harmony.javax.security.sasl.*;
+
+/**
+ * Implements the DigestChallenge class which will be used by the
+ * DigestMD5SaslClient class
+ */
+class DigestChallenge extends Object
+{
+    public static final int QOP_AUTH           =    0x01;
+    public static final int QOP_AUTH_INT       =    0x02;
+    public static final int QOP_AUTH_CONF       =    0x04;
+    public static final int QOP_UNRECOGNIZED   =    0x08;
+
+    private static final int CIPHER_3DES          = 0x01;
+    private static final int CIPHER_DES           = 0x02;
+    private static final int CIPHER_RC4_40        = 0x04;
+    private static final int CIPHER_RC4           = 0x08;
+    private static final int CIPHER_RC4_56        = 0x10;
+    private static final int CIPHER_UNRECOGNIZED  = 0x20;
+    private static final int CIPHER_RECOGNIZED_MASK =
+     CIPHER_3DES | CIPHER_DES | CIPHER_RC4_40 | CIPHER_RC4 | CIPHER_RC4_56;
+
+    private ArrayList m_realms;
+    private String    m_nonce;
+    private int       m_qop;
+    private boolean   m_staleFlag;
+    private int       m_maxBuf;
+    private String    m_characterSet;
+    private String    m_algorithm;
+    private int       m_cipherOptions;
+
+    DigestChallenge(
+        byte[] challenge)
+            throws SaslException
+    {
+        m_realms = new ArrayList(5);
+        m_nonce = null;
+        m_qop = 0;
+        m_staleFlag = false;
+        m_maxBuf = -1;
+        m_characterSet = null;
+        m_algorithm = null;
+        m_cipherOptions = 0;
+
+        DirectiveList dirList = new DirectiveList(challenge);
+        try
+        {
+            dirList.parseDirectives();
+            checkSemantics(dirList);
+        }
+        catch (SaslException e)
+        {
+        }
+    }
+
+    /**
+     * Checks the semantics of the directives in the directive list as parsed
+     * from the digest challenge byte array.
+     *
+     * @param dirList  the list of directives parsed from the digest challenge
+     *
+     * @exception SaslException   If a semantic error occurs
+     */
+    void checkSemantics(
+        DirectiveList dirList) throws SaslException
+    {
+    Iterator        directives = dirList.getIterator();
+    ParsedDirective directive;
+    String          name;
+
+    while (directives.hasNext())
+    {
+        directive = (ParsedDirective)directives.next();
+        name = directive.getName();
+        if (name.equals("realm"))
+            handleRealm(directive);
+        else if (name.equals("nonce"))
+            handleNonce(directive);
+        else if (name.equals("qop"))
+            handleQop(directive);
+        else if (name.equals("maxbuf"))
+            handleMaxbuf(directive);
+        else if (name.equals("charset"))
+            handleCharset(directive);
+        else if (name.equals("algorithm"))
+            handleAlgorithm(directive);
+        else if (name.equals("cipher"))
+            handleCipher(directive);
+        else if (name.equals("stale"))
+            handleStale(directive);
+    }
+
+    /* post semantic check */
+    if (-1 == m_maxBuf)
+        m_maxBuf = 65536;
+
+    if (m_qop == 0)
+        m_qop = QOP_AUTH;
+    else if ( (m_qop & QOP_AUTH) != QOP_AUTH )
+        throw new SaslException("Only qop-auth is supported by client");
+    else if ( ((m_qop & QOP_AUTH_CONF) == QOP_AUTH_CONF) &&
+              (0 == (m_cipherOptions & CIPHER_RECOGNIZED_MASK)) )
+        throw new SaslException("Invalid cipher options");
+    else if (null == m_nonce)
+        throw new SaslException("Missing nonce directive");
+    else if (m_staleFlag)
+        throw new SaslException("Unexpected stale flag");
+    else if ( null == m_algorithm )
+        throw new SaslException("Missing algorithm directive");
+    }
+
+    /**
+     * This function implements the semenatics of the nonce directive.
+     *
+     * @param      pd   ParsedDirective
+     *
+     * @exception  SaslException   If an error occurs due to too many nonce
+     *                             values
+     */
+    void handleNonce(
+        ParsedDirective  pd) throws SaslException
+    {
+        if (null != m_nonce)
+            throw new SaslException("Too many nonce values.");
+
+        m_nonce = pd.getValue();
+    }
+
+    /**
+     * This function implements the semenatics of the realm directive.
+     *
+     * @param      pd   ParsedDirective
+     */
+    void handleRealm(
+        ParsedDirective  pd)
+    {
+        m_realms.add(pd.getValue());
+    }
+
+    /**
+     * This function implements the semenatics of the qop (quality of protection)
+     * directive. The value of the qop directive is as defined below:
+     *      qop-options =     "qop" "=" <"> qop-list <">
+     *      qop-list    =     1#qop-value
+     *      qop-value    =     "auth" | "auth-int"  | "auth-conf" | token
+     *
+     * @param      pd   ParsedDirective
+     *
+     * @exception  SaslException   If an error occurs due to too many qop
+     *                             directives
+     */
+    void handleQop(
+        ParsedDirective  pd) throws SaslException
+    {
+        String       token;
+        TokenParser  parser;
+
+        if (m_qop != 0)
+            throw new SaslException("Too many qop directives.");
+
+        parser = new TokenParser(pd.getValue());
+        for (token = parser.parseToken();
+             token != null;
+             token = parser.parseToken())
+        {
+            if (token.equals("auth"))
+                  m_qop |= QOP_AUTH;
+              else if (token.equals("auth-int"))
+                  m_qop |= QOP_AUTH_INT;
+            else if (token.equals("auth-conf"))
+                m_qop |= QOP_AUTH_CONF;
+            else
+                m_qop |= QOP_UNRECOGNIZED;
+        }
+    }
+
+    /**
+     * This function implements the semenatics of the Maxbuf directive.
+     * the value is defined as: 1*DIGIT
+     *
+     * @param      pd   ParsedDirective
+     *
+     * @exception  SaslException If an error occur    
+     */
+    void handleMaxbuf(
+        ParsedDirective  pd) throws SaslException
+    {
+        if (-1 != m_maxBuf) /*it's initialized to -1 */
+            throw new SaslException("Too many maxBuf directives.");
+
+        m_maxBuf = Integer.parseInt(pd.getValue());
+
+        if (0 == m_maxBuf)
+            throw new SaslException("Max buf value must be greater than zero.");
+    }
+
+    /**
+     * This function implements the semenatics of the charset directive.
+     * the value is defined as: 1*DIGIT
+     *
+     * @param      pd   ParsedDirective
+     *
+     * @exception  SaslException If an error occurs dur to too many charset
+     *                           directives or Invalid character encoding
+     *                           directive
+     */
+    void handleCharset(
+        ParsedDirective  pd) throws SaslException
+    {
+        if (null != m_characterSet)
+            throw new SaslException("Too many charset directives.");
+
+        m_characterSet = pd.getValue();
+
+        if (!m_characterSet.equals("utf-8"))
+            throw new SaslException("Invalid character encoding directive");
+    }
+
+    /**
+     * This function implements the semenatics of the charset directive.
+     * the value is defined as: 1*DIGIT
+     *
+     * @param      pd   ParsedDirective
+     *
+     * @exception  SaslException If an error occurs due to too many algorith
+     *                           directive or Invalid algorithm directive
+     *                           value
+     */
+    void handleAlgorithm(
+        ParsedDirective  pd) throws SaslException
+    {
+        if (null != m_algorithm)
+            throw new SaslException("Too many algorithm directives.");
+
+          m_algorithm = pd.getValue();
+
+        if (!"md5-sess".equals(m_algorithm))
+            throw new SaslException("Invalid algorithm directive value: " +
+                                    m_algorithm);
+    }
+
+    /**
+     * This function implements the semenatics of the cipher-opts directive
+     * directive. The value of the qop directive is as defined below:
+     *      qop-options =     "qop" "=" <"> qop-list <">
+     *      qop-list    =     1#qop-value
+     *      qop-value    =     "auth" | "auth-int"  | "auth-conf" | token
+     *
+     * @param      pd   ParsedDirective
+     *
+     * @exception  SaslException If an error occurs due to Too many cipher
+     *                           directives 
+     */
+    void handleCipher(
+        ParsedDirective  pd) throws SaslException
+    {
+        String  token;
+        TokenParser parser;
+
+        if (0 != m_cipherOptions)
+            throw new SaslException("Too many cipher directives.");
+
+        parser = new TokenParser(pd.getValue());
+        token = parser.parseToken();
+        for (token = parser.parseToken();
+             token != null;
+             token = parser.parseToken())
+        {
+              if ("3des".equals(token))
+                  m_cipherOptions |= CIPHER_3DES;
+              else if ("des".equals(token))
+                  m_cipherOptions |= CIPHER_DES;
+            else if ("rc4-40".equals(token))
+                m_cipherOptions |= CIPHER_RC4_40;
+            else if ("rc4".equals(token))
+                m_cipherOptions |= CIPHER_RC4;
+            else if ("rc4-56".equals(token))
+                m_cipherOptions |= CIPHER_RC4_56;
+            else
+                m_cipherOptions |= CIPHER_UNRECOGNIZED;
+        }
+
+        if (m_cipherOptions == 0)
+            m_cipherOptions = CIPHER_UNRECOGNIZED;
+    }
+
+    /**
+     * This function implements the semenatics of the stale directive.
+     *
+     * @param      pd   ParsedDirective
+     *
+     * @exception  SaslException If an error occurs due to Too many stale
+     *                           directives or Invalid stale directive value
+     */
+    void handleStale(
+        ParsedDirective  pd) throws SaslException
+    {
+        if (false != m_staleFlag)
+            throw new SaslException("Too many stale directives.");
+
+        if ("true".equals(pd.getValue()))
+            m_staleFlag = true;
+        else
+            throw new SaslException("Invalid stale directive value: " +
+                                    pd.getValue());
+    }
+
+    /**
+     * Return the list of the All the Realms
+     *
+     * @return  List of all the realms 
+     */
+    public ArrayList getRealms()
+    {
+        return m_realms;
+    }
+
+    /**
+     * @return Returns the Nonce
+     */
+    public String getNonce()
+    {
+        return m_nonce;
+    }
+
+    /**
+     * Return the quality-of-protection
+     * 
+     * @return The quality-of-protection
+     */
+    public int getQop()
+    {
+        return m_qop;
+    }
+
+    /**
+     * @return The state of the Staleflag
+     */
+    public boolean getStaleFlag()
+    {
+        return m_staleFlag;
+    }
+
+    /**
+     * @return The Maximum Buffer value
+     */
+    public int getMaxBuf()
+    {
+        return m_maxBuf;
+    }
+
+    /**
+     * @return character set values as string
+     */
+    public String getCharacterSet()
+    {
+        return m_characterSet;
+    }
+
+    /**
+     * @return The String value of the algorithm
+     */
+    public String getAlgorithm()
+    {
+        return m_algorithm;
+    }
+
+    /**
+     * @return The cipher options
+     */
+    public int getCipherOptions()
+    {
+        return m_cipherOptions;
+    }
+}
+
diff --git a/src/com/novell/sasl/client/DigestMD5SaslClient.java b/src/com/novell/sasl/client/DigestMD5SaslClient.java
new file mode 100644
index 0000000..141c96b
--- /dev/null
+++ b/src/com/novell/sasl/client/DigestMD5SaslClient.java
@@ -0,0 +1,820 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/DigestMD5SaslClient.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2003 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import org.apache.harmony.javax.security.sasl.*;
+import org.apache.harmony.javax.security.auth.callback.*;
+import java.security.SecureRandom;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.io.UnsupportedEncodingException;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * Implements the Client portion of DigestMD5 Sasl mechanism.
+ */
+public class DigestMD5SaslClient implements SaslClient
+{
+    private String           m_authorizationId = "";
+    private String           m_protocol = "";
+    private String           m_serverName = "";
+    private Map              m_props;
+    private CallbackHandler  m_cbh;
+    private int              m_state;
+    private String           m_qopValue = "";
+    private char[]              m_HA1 = null;
+    private String           m_digestURI;
+    private DigestChallenge  m_dc;
+    private String           m_clientNonce = "";
+    private String           m_realm = "";
+    private String           m_name = "";
+
+    private static final int   STATE_INITIAL = 0;
+    private static final int   STATE_DIGEST_RESPONSE_SENT = 1;
+    private static final int   STATE_VALID_SERVER_RESPONSE = 2;
+    private static final int   STATE_INVALID_SERVER_RESPONSE = 3;
+    private static final int   STATE_DISPOSED = 4;
+
+    private static final int   NONCE_BYTE_COUNT = 32;
+    private static final int   NONCE_HEX_COUNT = 2*NONCE_BYTE_COUNT;
+
+    private static final String DIGEST_METHOD = "AUTHENTICATE";
+
+    /**
+     * Creates an DigestMD5SaslClient object using the parameters supplied.
+     * Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are
+     * contained in props
+     *
+     * @param authorizationId  The possibly null protocol-dependent
+     *                     identification to be used for authorization. If
+     *                     null or empty, the server derives an authorization
+     *                     ID from the client's authentication credentials.
+     *                     When the SASL authentication completes
+     *                     successfully, the specified entity is granted
+     *                     access.
+     *
+     * @param protocol     The non-null string name of the protocol for which
+     *                     the authentication is being performed (e.g. "ldap")
+     *
+     * @param serverName   The non-null fully qualified host name of the server
+     *                     to authenticate to
+     *
+     * @param props        The possibly null set of properties used to select
+     *                     the SASL mechanism and to configure the
+     *                     authentication exchange of the selected mechanism.
+     *                     See the Sasl class for a list of standard properties.
+     *                     Other, possibly mechanism-specific, properties can
+     *                     be included. Properties not relevant to the selected
+     *                     mechanism are ignored.
+     *
+     * @param cbh          The possibly null callback handler to used by the
+     *                     SASL mechanisms to get further information from the
+     *                     application/library to complete the authentication.
+     *                     For example, a SASL mechanism might require the
+     *                     authentication ID, password and realm from the
+     *                     caller. The authentication ID is requested by using
+     *                     a NameCallback. The password is requested by using
+     *                     a PasswordCallback. The realm is requested by using
+     *                     a RealmChoiceCallback if there is a list of realms
+     *                     to choose from, and by using a RealmCallback if the
+     *                     realm must be entered.
+     *
+     * @return            A possibly null SaslClient created using the
+     *                     parameters supplied. If null, this factory cannot
+     *                     produce a SaslClient using the parameters supplied.
+     *
+     * @exception SaslException  If a SaslClient instance cannot be created
+     *                     because of an error
+     */
+    public static SaslClient getClient(
+        String          authorizationId,
+        String          protocol,
+        String          serverName,
+        Map             props,
+        CallbackHandler cbh)
+    {
+        String desiredQOP = (String)props.get(Sasl.QOP);
+        String desiredStrength = (String)props.get(Sasl.STRENGTH);
+        String serverAuth = (String)props.get(Sasl.SERVER_AUTH);
+
+        //only support qop equal to auth
+        if ((desiredQOP != null) && !"auth".equals(desiredQOP))
+            return null;
+
+        //doesn't support server authentication
+        if ((serverAuth != null) && !"false".equals(serverAuth))
+            return null;
+
+        //need a callback handler to get the password
+        if (cbh == null)
+            return null;
+
+        return new DigestMD5SaslClient(authorizationId, protocol,
+                                       serverName, props, cbh);
+    }
+
+    /**
+     * Creates an DigestMD5SaslClient object using the parameters supplied.
+     * Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are
+     * contained in props
+     *
+     * @param authorizationId  The possibly null protocol-dependent
+     *                     identification to be used for authorization. If
+     *                     null or empty, the server derives an authorization
+     *                     ID from the client's authentication credentials.
+     *                     When the SASL authentication completes
+     *                     successfully, the specified entity is granted
+     *                     access.
+     *
+     * @param protocol     The non-null string name of the protocol for which
+     *                     the authentication is being performed (e.g. "ldap")
+     *
+     * @param serverName   The non-null fully qualified host name of the server
+     *                     to authenticate to
+     *
+     * @param props        The possibly null set of properties used to select
+     *                     the SASL mechanism and to configure the
+     *                     authentication exchange of the selected mechanism.
+     *                     See the Sasl class for a list of standard properties.
+     *                     Other, possibly mechanism-specific, properties can
+     *                     be included. Properties not relevant to the selected
+     *                     mechanism are ignored.
+     *
+     * @param cbh          The possibly null callback handler to used by the
+     *                     SASL mechanisms to get further information from the
+     *                     application/library to complete the authentication.
+     *                     For example, a SASL mechanism might require the
+     *                     authentication ID, password and realm from the
+     *                     caller. The authentication ID is requested by using
+     *                     a NameCallback. The password is requested by using
+     *                     a PasswordCallback. The realm is requested by using
+     *                     a RealmChoiceCallback if there is a list of realms
+     *                     to choose from, and by using a RealmCallback if the
+     *                     realm must be entered.
+     *
+     */
+    private  DigestMD5SaslClient(
+        String          authorizationId,
+        String          protocol,
+        String          serverName,
+        Map             props,
+        CallbackHandler cbh)
+    {
+        m_authorizationId = authorizationId;
+        m_protocol = protocol;
+        m_serverName = serverName;
+        m_props = props;
+        m_cbh = cbh;
+
+        m_state = STATE_INITIAL;
+    }
+
+    /**
+     * Determines if this mechanism has an optional initial response. If true,
+     * caller should call evaluateChallenge() with an empty array to get the
+     * initial response.
+     *
+     * @return  true if this mechanism has an initial response
+     */
+    public boolean hasInitialResponse()
+    {
+        return false;
+    }
+
+    /**
+     * Determines if the authentication exchange has completed. This method
+     * may be called at any time, but typically, it will not be called until
+     * the caller has received indication from the server (in a protocol-
+     * specific manner) that the exchange has completed.
+     *
+     * @return  true if the authentication exchange has completed;
+     *           false otherwise.
+     */
+    public boolean isComplete()
+    {
+        if ((m_state == STATE_VALID_SERVER_RESPONSE) ||
+            (m_state == STATE_INVALID_SERVER_RESPONSE) ||
+            (m_state == STATE_DISPOSED))
+            return true;
+        else
+            return false;
+    }
+
+    /**
+     * Unwraps a byte array received from the server. This method can be called
+     * only after the authentication exchange has completed (i.e., when
+     * isComplete() returns true) and only if the authentication exchange has
+     * negotiated integrity and/or privacy as the quality of protection;
+     * otherwise, an IllegalStateException is thrown.
+     *
+     * incoming is the contents of the SASL buffer as defined in RFC 2222
+     * without the leading four octet field that represents the length.
+     * offset and len specify the portion of incoming to use.
+     *
+     * @param incoming   A non-null byte array containing the encoded bytes
+     *                   from the server
+     * @param offset     The starting position at incoming of the bytes to use
+     *
+     * @param len        The number of bytes from incoming to use
+     *
+     * @return           A non-null byte array containing the decoded bytes
+     *
+     */
+    public byte[] unwrap(
+        byte[] incoming,
+        int    offset,
+        int    len)
+            throws SaslException
+    {
+        throw new IllegalStateException(
+         "unwrap: QOP has neither integrity nor privacy>");
+    }
+
+    /**
+     * Wraps a byte array to be sent to the server. This method can be called
+     * only after the authentication exchange has completed (i.e., when
+     * isComplete() returns true) and only if the authentication exchange has
+     * negotiated integrity and/or privacy as the quality of protection;
+     * otherwise, an IllegalStateException is thrown.
+     *
+     * The result of this method will make up the contents of the SASL buffer as
+     * defined in RFC 2222 without the leading four octet field that represents
+     * the length. offset and len specify the portion of outgoing to use.
+     *
+     * @param outgoing   A non-null byte array containing the bytes to encode
+     * @param offset     The starting position at outgoing of the bytes to use
+     * @param len        The number of bytes from outgoing to use
+     *
+     * @return A non-null byte array containing the encoded bytes
+     *
+     * @exception SaslException  if incoming cannot be successfully unwrapped.
+     *
+     * @exception IllegalStateException   if the authentication exchange has
+     *                   not completed, or if the negotiated quality of
+     *                   protection has neither integrity nor privacy.
+     */
+    public byte[] wrap(
+        byte[]  outgoing,
+        int     offset,
+        int     len)
+            throws SaslException
+    {
+        throw new IllegalStateException(
+         "wrap: QOP has neither integrity nor privacy>");
+    }
+
+    /**
+     * Retrieves the negotiated property. This method can be called only after
+     * the authentication exchange has completed (i.e., when isComplete()
+     * returns true); otherwise, an IllegalStateException is thrown.
+     *
+     * @param propName   The non-null property name
+     *
+     * @return  The value of the negotiated property. If null, the property was
+     *          not negotiated or is not applicable to this mechanism.
+     *
+     * @exception IllegalStateException   if this authentication exchange has
+     *                                    not completed
+     */
+    public Object getNegotiatedProperty(
+        String propName)
+    {
+        if (m_state != STATE_VALID_SERVER_RESPONSE)
+            throw new IllegalStateException(
+             "getNegotiatedProperty: authentication exchange not complete.");
+
+        if (Sasl.QOP.equals(propName))
+            return "auth";
+        else
+            return null;
+    }
+
+    /**
+     * Disposes of any system resources or security-sensitive information the
+     * SaslClient might be using. Invoking this method invalidates the
+     * SaslClient instance. This method is idempotent.
+     *
+     * @exception SaslException  if a problem was encountered while disposing
+     *                           of the resources
+     */
+    public void dispose()
+            throws SaslException
+    {
+        if (m_state != STATE_DISPOSED)
+        {
+            m_state = STATE_DISPOSED;
+        }
+    }
+
+    /**
+     * Evaluates the challenge data and generates a response. If a challenge
+     * is received from the server during the authentication process, this
+     * method is called to prepare an appropriate next response to submit to
+     * the server.
+     *
+     * @param challenge  The non-null challenge sent from the server. The
+     *                   challenge array may have zero length.
+     *
+     * @return    The possibly null reponse to send to the server. It is null
+     *            if the challenge accompanied a "SUCCESS" status and the
+     *            challenge only contains data for the client to update its
+     *            state and no response needs to be sent to the server.
+     *            The response is a zero-length byte array if the client is to
+     *            send a response with no data.
+     *
+     * @exception SaslException   If an error occurred while processing the
+     *                            challenge or generating a response.
+     */
+    public byte[] evaluateChallenge(
+        byte[] challenge)
+            throws SaslException
+    {
+        byte[] response = null;
+
+        //printState();
+        switch (m_state)
+        {
+        case STATE_INITIAL:
+            if (challenge.length == 0)
+                throw new SaslException("response = byte[0]");
+            else
+                try
+                {
+                    response = createDigestResponse(challenge).
+                                                           getBytes("UTF-8");
+                    m_state = STATE_DIGEST_RESPONSE_SENT;
+                }
+                catch (java.io.UnsupportedEncodingException e)
+                {
+                    throw new SaslException(
+                     "UTF-8 encoding not suppported by platform", e);
+                }
+            break;
+        case STATE_DIGEST_RESPONSE_SENT:
+            if (checkServerResponseAuth(challenge))
+                m_state = STATE_VALID_SERVER_RESPONSE;
+            else
+            {
+                m_state = STATE_INVALID_SERVER_RESPONSE;
+                throw new SaslException("Could not validate response-auth " +
+                                        "value from server");
+            }
+            break;
+        case STATE_VALID_SERVER_RESPONSE:
+        case STATE_INVALID_SERVER_RESPONSE:
+            throw new SaslException("Authentication sequence is complete");
+        case STATE_DISPOSED:
+            throw new SaslException("Client has been disposed");
+        default:
+            throw new SaslException("Unknown client state.");
+        }
+
+        return response;
+    }
+
+    /**
+     * This function takes a 16 byte binary md5-hash value and creates a 32
+     * character (plus    a terminating null character) hex-digit 
+     * representation of binary data.
+     *
+     * @param hash  16 byte binary md5-hash value in bytes
+     * 
+     * @return   32 character (plus    a terminating null character) hex-digit
+     *           representation of binary data.
+     */
+    char[] convertToHex(
+        byte[] hash)
+    {
+        int          i;
+        byte         j;
+        byte         fifteen = 15;
+        char[]      hex = new char[32];
+
+        for (i = 0; i < 16; i++)
+        {
+            //convert value of top 4 bits to hex char
+            hex[i*2] = getHexChar((byte)((hash[i] & 0xf0) >> 4));
+            //convert value of bottom 4 bits to hex char
+            hex[(i*2)+1] = getHexChar((byte)(hash[i] & 0x0f));
+        }
+
+        return hex;
+    }
+
+    /**
+     * Calculates the HA1 portion of the response
+     *
+     * @param  algorithm   Algorith to use.
+     * @param  userName    User being authenticated
+     * @param  realm       realm information
+     * @param  password    password of teh user
+     * @param  nonce       nonce value
+     * @param  clientNonce Clients Nonce value
+     *
+     * @return  HA1 portion of the response in a character array
+     *
+     * @exception SaslException  If an error occurs
+     */
+    char[] DigestCalcHA1(
+        String   algorithm,
+        String   userName,
+        String   realm,
+        String   password,
+        String   nonce,
+        String   clientNonce) throws SaslException
+    {
+        byte[]        hash;
+
+        try
+        {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+
+            md.update(userName.getBytes("UTF-8"));
+            md.update(":".getBytes("UTF-8"));
+            md.update(realm.getBytes("UTF-8"));
+            md.update(":".getBytes("UTF-8"));
+            md.update(password.getBytes("UTF-8"));
+            hash = md.digest();
+
+            if ("md5-sess".equals(algorithm))
+            {
+                md.update(hash);
+                md.update(":".getBytes("UTF-8"));
+                md.update(nonce.getBytes("UTF-8"));
+                md.update(":".getBytes("UTF-8"));
+                md.update(clientNonce.getBytes("UTF-8"));
+                hash = md.digest();
+            }
+        }
+        catch(NoSuchAlgorithmException e)
+        {
+            throw new SaslException("No provider found for MD5 hash", e);
+        }
+        catch(UnsupportedEncodingException e)
+        {
+            throw new SaslException(
+             "UTF-8 encoding not supported by platform.", e);
+        }
+
+        return convertToHex(hash);
+    }
+
+
+    /**
+     * This function calculates the response-value of the response directive of
+     * the digest-response as documented in RFC 2831
+     *
+     * @param  HA1           H(A1)
+     * @param  serverNonce   nonce from server
+     * @param  nonceCount    8 hex digits
+     * @param  clientNonce   client nonce 
+     * @param  qop           qop-value: "", "auth", "auth-int"
+     * @param  method        method from the request
+     * @param  digestUri     requested URL
+     * @param  clientResponseFlag request-digest or response-digest
+     *
+     * @return Response-value of the response directive of the digest-response
+     *
+     * @exception SaslException  If an error occurs
+     */
+    char[] DigestCalcResponse(
+        char[]      HA1,            /* H(A1) */
+        String      serverNonce,    /* nonce from server */
+        String      nonceCount,     /* 8 hex digits */
+        String      clientNonce,    /* client nonce */
+        String      qop,            /* qop-value: "", "auth", "auth-int" */
+        String      method,         /* method from the request */
+        String      digestUri,      /* requested URL */
+        boolean     clientResponseFlag) /* request-digest or response-digest */
+            throws SaslException
+    {
+        byte[]             HA2;
+        byte[]             respHash;
+        char[]             HA2Hex;
+
+        // calculate H(A2)
+        try
+        {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            if (clientResponseFlag)
+                  md.update(method.getBytes("UTF-8"));
+            md.update(":".getBytes("UTF-8"));
+            md.update(digestUri.getBytes("UTF-8"));
+            if ("auth-int".equals(qop))
+            {
+                md.update(":".getBytes("UTF-8"));
+                md.update("00000000000000000000000000000000".getBytes("UTF-8"));
+            }
+            HA2 = md.digest();
+            HA2Hex = convertToHex(HA2);
+
+            // calculate response
+            md.update(new String(HA1).getBytes("UTF-8"));
+            md.update(":".getBytes("UTF-8"));
+            md.update(serverNonce.getBytes("UTF-8"));
+            md.update(":".getBytes("UTF-8"));
+            if (qop.length() > 0)
+            {
+                md.update(nonceCount.getBytes("UTF-8"));
+                md.update(":".getBytes("UTF-8"));
+                md.update(clientNonce.getBytes("UTF-8"));
+                md.update(":".getBytes("UTF-8"));
+                md.update(qop.getBytes("UTF-8"));
+                md.update(":".getBytes("UTF-8"));
+            }
+            md.update(new String(HA2Hex).getBytes("UTF-8"));
+            respHash = md.digest();
+        }
+        catch(NoSuchAlgorithmException e)
+        {
+            throw new SaslException("No provider found for MD5 hash", e);
+        }
+        catch(UnsupportedEncodingException e)
+        {
+            throw new SaslException(
+             "UTF-8 encoding not supported by platform.", e);
+        }
+
+        return convertToHex(respHash);
+    }
+
+
+    /**
+     * Creates the intial response to be sent to the server.
+     *
+     * @param challenge  Challenge in bytes recived form the Server
+     *
+     * @return Initial response to be sent to the server
+     */
+    private String createDigestResponse(
+        byte[] challenge)
+            throws SaslException
+    {
+        char[]            response;
+        StringBuffer    digestResponse = new StringBuffer(512);
+        int             realmSize;
+
+        m_dc = new DigestChallenge(challenge);
+
+        m_digestURI = m_protocol + "/" + m_serverName;
+
+        if ((m_dc.getQop() & DigestChallenge.QOP_AUTH)
+            == DigestChallenge.QOP_AUTH )
+            m_qopValue = "auth";
+        else
+            throw new SaslException("Client only supports qop of 'auth'");
+
+        //get call back information
+        Callback[] callbacks = new Callback[3];
+        ArrayList realms = m_dc.getRealms();
+        realmSize = realms.size();
+        if (realmSize == 0)
+        {
+            callbacks[0] = new RealmCallback("Realm");
+        }
+        else if (realmSize == 1)
+        {
+            callbacks[0] = new RealmCallback("Realm", (String)realms.get(0));
+        }
+        else
+        {
+            callbacks[0] =
+             new RealmChoiceCallback(
+                         "Realm",
+                         (String[])realms.toArray(new String[realmSize]),
+                          0,      //the default choice index
+                          false); //no multiple selections
+        }
+
+        callbacks[1] = new PasswordCallback("Password", false); 
+        //false = no echo
+
+        if (m_authorizationId == null || m_authorizationId.length() == 0)
+            callbacks[2] = new NameCallback("Name");
+        else
+            callbacks[2] = new NameCallback("Name", m_authorizationId);
+
+        try
+        {
+            m_cbh.handle(callbacks);
+        }
+        catch(UnsupportedCallbackException e)
+        {
+            throw new SaslException("Handler does not support" +
+                                          " necessary callbacks",e);
+        }
+        catch(IOException e)
+        {
+            throw new SaslException("IO exception in CallbackHandler.", e);
+        }
+
+        if (realmSize > 1)
+        {
+            int[] selections =
+             ((RealmChoiceCallback)callbacks[0]).getSelectedIndexes();
+
+            if (selections.length > 0)
+                m_realm =
+                ((RealmChoiceCallback)callbacks[0]).getChoices()[selections[0]];
+            else
+                m_realm = ((RealmChoiceCallback)callbacks[0]).getChoices()[0];
+        }
+        else
+            m_realm = ((RealmCallback)callbacks[0]).getText();
+
+        m_clientNonce = getClientNonce();
+
+        m_name = ((NameCallback)callbacks[2]).getName();
+        if (m_name == null)
+            m_name = ((NameCallback)callbacks[2]).getDefaultName();
+        if (m_name == null)
+            throw new SaslException("No user name was specified.");
+
+        m_HA1 = DigestCalcHA1(
+                      m_dc.getAlgorithm(),
+                      m_name,
+                      m_realm,
+                      new String(((PasswordCallback)callbacks[1]).getPassword()),
+                      m_dc.getNonce(),
+                      m_clientNonce);
+
+        response = DigestCalcResponse(m_HA1,
+                                      m_dc.getNonce(),
+                                      "00000001",
+                                      m_clientNonce,
+                                      m_qopValue,
+                                      "AUTHENTICATE",
+                                      m_digestURI,
+                                      true);
+
+        digestResponse.append("username=\"");
+        digestResponse.append(m_authorizationId);
+        if (0 != m_realm.length())
+        {
+            digestResponse.append("\",realm=\"");
+            digestResponse.append(m_realm);
+        }
+        digestResponse.append("\",cnonce=\"");
+        digestResponse.append(m_clientNonce);
+        digestResponse.append("\",nc=");
+        digestResponse.append("00000001"); //nounce count
+        digestResponse.append(",qop=");
+        digestResponse.append(m_qopValue);
+        digestResponse.append(",digest-uri=\"");
+	digestResponse.append(m_digestURI);
+        digestResponse.append("\",response=");
+        digestResponse.append(response);
+        digestResponse.append(",charset=utf-8,nonce=\"");
+        digestResponse.append(m_dc.getNonce());
+        digestResponse.append("\"");
+
+        return digestResponse.toString();
+     }
+     
+     
+    /**
+     * This function validates the server response. This step performs a 
+     * modicum of mutual authentication by verifying that the server knows
+     * the user's password
+     *
+     * @param  serverResponse  Response recived form Server
+     *
+     * @return  true if the mutual authentication succeeds;
+     *          else return false
+     *
+     * @exception SaslException  If an error occurs
+     */
+    boolean checkServerResponseAuth(
+            byte[]  serverResponse) throws SaslException
+    {
+        char[]           response;
+        ResponseAuth  responseAuth = null;
+        String        responseStr;
+
+        responseAuth = new ResponseAuth(serverResponse);
+
+        response = DigestCalcResponse(m_HA1,
+                                  m_dc.getNonce(),
+                                  "00000001",
+                                  m_clientNonce,
+                                  m_qopValue,
+                                  DIGEST_METHOD,
+                                  m_digestURI,
+                                  false);
+
+        responseStr = new String(response);
+
+        return responseStr.equals(responseAuth.getResponseValue());
+    }
+
+
+    /**
+     * This function returns hex character representing the value of the input
+     * 
+     * @param value Input value in byte
+     *
+     * @return Hex value of the Input byte value
+     */
+    private static char getHexChar(
+        byte    value)
+    {
+        switch (value)
+        {
+        case 0:
+            return '0';
+        case 1:
+            return '1';
+        case 2:
+            return '2';
+        case 3:
+            return '3';
+        case 4:
+            return '4';
+        case 5:
+            return '5';
+        case 6:
+            return '6';
+        case 7:
+            return '7';
+        case 8:
+            return '8';
+        case 9:
+            return '9';
+        case 10:
+            return 'a';
+        case 11:
+            return 'b';
+        case 12:
+            return 'c';
+        case 13:
+            return 'd';
+        case 14:
+            return 'e';
+        case 15:
+            return 'f';
+        default:
+            return 'Z';
+        }
+    }
+
+    /**
+     * Calculates the Nonce value of the Client
+     * 
+     * @return   Nonce value of the client
+     *
+     * @exception   SaslException If an error Occurs
+     */
+    String getClientNonce() throws SaslException
+    {
+        byte[]          nonceBytes = new byte[NONCE_BYTE_COUNT];
+        SecureRandom    prng;
+        byte            nonceByte;
+        char[]          hexNonce = new char[NONCE_HEX_COUNT];
+
+        try
+        {
+            prng = SecureRandom.getInstance("SHA1PRNG");
+            prng.nextBytes(nonceBytes);
+            for(int i=0; i<NONCE_BYTE_COUNT; i++)
+            {
+                //low nibble
+                hexNonce[i*2] = getHexChar((byte)(nonceBytes[i] & 0x0f));
+                //high nibble
+                hexNonce[(i*2)+1] = getHexChar((byte)((nonceBytes[i] & 0xf0)
+                                                                      >> 4));
+            }
+            return new String(hexNonce);
+        }
+        catch(NoSuchAlgorithmException e)
+        {
+            throw new SaslException("No random number generator available", e);
+        }
+    }
+
+    /**
+     * Returns the IANA-registered mechanism name of this SASL client.
+     *  (e.g. "CRAM-MD5", "GSSAPI")
+     *
+     * @return  "DIGEST-MD5"the IANA-registered mechanism name of this SASL
+     *          client.
+     */
+    public String getMechanismName()
+    {
+        return "DIGEST-MD5";
+    }
+
+} //end class DigestMD5SaslClient
+
diff --git a/src/com/novell/sasl/client/DirectiveList.java b/src/com/novell/sasl/client/DirectiveList.java
new file mode 100644
index 0000000..fc26a6b
--- /dev/null
+++ b/src/com/novell/sasl/client/DirectiveList.java
@@ -0,0 +1,363 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/DirectiveList.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import java.util.*;
+import org.apache.harmony.javax.security.sasl.*;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Implements the DirectiveList class whihc will be used by the 
+ * DigestMD5SaslClient class
+ */
+class DirectiveList extends Object
+{
+    private static final int STATE_LOOKING_FOR_FIRST_DIRECTIVE  = 1;
+    private static final int STATE_LOOKING_FOR_DIRECTIVE        = 2;
+    private static final int STATE_SCANNING_NAME                = 3;
+    private static final int STATE_LOOKING_FOR_EQUALS            = 4;
+    private static final int STATE_LOOKING_FOR_VALUE            = 5;
+    private static final int STATE_LOOKING_FOR_COMMA            = 6;
+    private static final int STATE_SCANNING_QUOTED_STRING_VALUE    = 7;
+    private static final int STATE_SCANNING_TOKEN_VALUE            = 8;
+    private static final int STATE_NO_UTF8_SUPPORT              = 9;
+
+    private int        m_curPos;
+    private int        m_errorPos;
+    private String     m_directives;
+    private int        m_state;
+    private ArrayList  m_directiveList;
+    private String     m_curName;
+    private int        m_scanStart;
+
+    /**
+     *  Constructs a new DirectiveList.
+     */
+     DirectiveList(
+        byte[] directives)
+    {
+        m_curPos = 0;
+        m_state = STATE_LOOKING_FOR_FIRST_DIRECTIVE;
+        m_directiveList = new ArrayList(10);
+        m_scanStart = 0;
+        m_errorPos = -1;
+        try
+        {
+            m_directives = new String(directives, "UTF-8");
+        }
+        catch(UnsupportedEncodingException e)
+        {
+            m_state = STATE_NO_UTF8_SUPPORT;
+        }
+    }
+
+    /**
+     * This function takes a US-ASCII character string containing a list of comma
+     * separated directives, and parses the string into the individual directives
+     * and their values. A directive consists of a token specifying the directive
+     * name followed by an equal sign (=) and the directive value. The value is
+     * either a token or a quoted string
+     *
+     * @exception SaslException  If an error Occurs
+     */
+    void parseDirectives() throws SaslException
+    {
+        char        prevChar;
+        char        currChar;
+        int            rc = 0;
+        boolean        haveQuotedPair = false;
+        String      currentName = "<no name>";
+
+        if (m_state == STATE_NO_UTF8_SUPPORT)
+            throw new SaslException("No UTF-8 support on platform");
+
+        prevChar = 0;
+
+        while (m_curPos < m_directives.length())
+        {
+            currChar = m_directives.charAt(m_curPos);
+            switch (m_state)
+            {
+            case STATE_LOOKING_FOR_FIRST_DIRECTIVE:
+            case STATE_LOOKING_FOR_DIRECTIVE:
+                if (isWhiteSpace(currChar))
+                {
+                    break;
+                }
+                else if (isValidTokenChar(currChar))
+                {
+                    m_scanStart = m_curPos;
+                    m_state = STATE_SCANNING_NAME;
+                }
+                else
+                {
+                     m_errorPos = m_curPos;
+                    throw new SaslException("Parse error: Invalid name character");
+                }
+                break;
+
+            case STATE_SCANNING_NAME:
+                if (isValidTokenChar(currChar))
+                {
+                    break;
+                }
+                else if (isWhiteSpace(currChar))
+                {
+                    currentName = m_directives.substring(m_scanStart, m_curPos);
+                    m_state = STATE_LOOKING_FOR_EQUALS;
+                }
+                else if ('=' == currChar)
+                {
+                    currentName = m_directives.substring(m_scanStart, m_curPos);
+                    m_state = STATE_LOOKING_FOR_VALUE;
+                }
+                else
+                {
+                     m_errorPos = m_curPos;
+                    throw new SaslException("Parse error: Invalid name character");
+                }
+                break;
+
+            case STATE_LOOKING_FOR_EQUALS:
+                if (isWhiteSpace(currChar))
+                {
+                    break;
+                }
+                else if ('=' == currChar)
+                {
+                    m_state = STATE_LOOKING_FOR_VALUE;
+                }
+                else
+                {
+                    m_errorPos = m_curPos;
+                    throw new SaslException("Parse error: Expected equals sign '='.");
+                }
+                break;
+
+            case STATE_LOOKING_FOR_VALUE:
+                if (isWhiteSpace(currChar))
+                {
+                    break;
+                }
+                else if ('"' == currChar)
+                {
+                    m_scanStart = m_curPos+1; /* don't include the quote */
+                    m_state = STATE_SCANNING_QUOTED_STRING_VALUE;
+                }
+                else if (isValidTokenChar(currChar))
+                {
+                    m_scanStart = m_curPos;
+                    m_state = STATE_SCANNING_TOKEN_VALUE;
+                }
+                else
+                {
+                    m_errorPos = m_curPos;
+                    throw new SaslException("Parse error: Unexpected character");
+                }
+                break;
+
+            case STATE_SCANNING_TOKEN_VALUE:
+                if (isValidTokenChar(currChar))
+                {
+                    break;
+                }
+                else if (isWhiteSpace(currChar))
+                {
+                    addDirective(currentName, false);
+                    m_state = STATE_LOOKING_FOR_COMMA;
+                }
+                else if (',' == currChar)
+                {
+                    addDirective(currentName, false);
+                    m_state = STATE_LOOKING_FOR_DIRECTIVE;
+                }
+                else
+                {
+                     m_errorPos = m_curPos;
+                    throw new SaslException("Parse error: Invalid value character");
+                }
+                break;
+
+            case STATE_SCANNING_QUOTED_STRING_VALUE:
+                if ('\\' == currChar)
+                    haveQuotedPair = true;
+                if ( ('"' == currChar) &&
+                     ('\\' != prevChar) )
+                {
+                    addDirective(currentName, haveQuotedPair);
+                    haveQuotedPair = false;
+                    m_state = STATE_LOOKING_FOR_COMMA;
+                }
+                break;
+
+            case STATE_LOOKING_FOR_COMMA:
+                if (isWhiteSpace(currChar))
+                    break;
+                else if (currChar == ',')
+                    m_state = STATE_LOOKING_FOR_DIRECTIVE;
+                else
+                {
+                    m_errorPos = m_curPos;
+                    throw new SaslException("Parse error: Expected a comma.");
+                }
+                break;
+            }
+            if (0 != rc)
+                break;
+            prevChar = currChar;
+            m_curPos++;
+        } /* end while loop */
+
+
+        if (rc == 0)
+        {
+            /* check the ending state */
+            switch (m_state)
+            {
+            case STATE_SCANNING_TOKEN_VALUE:
+                addDirective(currentName, false);
+                break;
+
+            case STATE_LOOKING_FOR_FIRST_DIRECTIVE:
+            case STATE_LOOKING_FOR_COMMA:
+                break;
+
+            case STATE_LOOKING_FOR_DIRECTIVE:
+                    throw new SaslException("Parse error: Trailing comma.");
+
+            case STATE_SCANNING_NAME:
+            case STATE_LOOKING_FOR_EQUALS:
+            case STATE_LOOKING_FOR_VALUE:
+                    throw new SaslException("Parse error: Missing value.");
+
+            case STATE_SCANNING_QUOTED_STRING_VALUE:
+                    throw new SaslException("Parse error: Missing closing quote.");
+            }
+        }
+
+    }
+
+    /**
+     * This function returns TRUE if the character is a valid token character.
+     *
+     *     token          = 1*<any CHAR except CTLs or separators>
+     *
+     *      separators     = "(" | ")" | "<" | ">" | "@"
+     *                     | "," | ";" | ":" | "\" | <">
+     *                     | "/" | "[" | "]" | "?" | "="
+     *                     | "{" | "}" | SP | HT
+     *
+     *      CTL            = <any US-ASCII control character
+     *                       (octets 0 - 31) and DEL (127)>
+     *
+     *      CHAR           = <any US-ASCII character (octets 0 - 127)>
+     *
+     * @param c  character to be tested
+     *
+     * @return Returns TRUE if the character is a valid token character.
+     */
+    boolean isValidTokenChar(
+        char c)
+    {
+        if ( ( (c >= '\u0000') && (c <='\u0020') ) ||
+             ( (c >= '\u003a') && (c <= '\u0040') ) ||
+             ( (c >= '\u005b') && (c <= '\u005d') ) ||
+             ('\u002c' == c) ||
+             ('\u0025' == c) ||
+             ('\u0028' == c) ||
+             ('\u0029' == c) ||
+             ('\u007b' == c) ||
+             ('\u007d' == c) ||
+             ('\u007f' == c) )
+            return false;
+
+        return true;
+    }
+
+    /**
+     * This function returns TRUE if the character is linear white space (LWS).
+     *         LWS = [CRLF] 1*( SP | HT )
+     * @param c  Input charcter to be tested
+     *
+     * @return Returns TRUE if the character is linear white space (LWS)
+     */
+    boolean isWhiteSpace(
+        char c)
+    {
+        if ( ('\t' == c) ||  // HORIZONTAL TABULATION.
+             ('\n' == c) ||  // LINE FEED.
+             ('\r' == c) ||  // CARRIAGE RETURN.
+             ('\u0020' == c) )
+            return true;
+
+        return false;
+    }
+
+    /**
+     * This function creates a directive record and adds it to the list, the
+     * value will be added later after it is parsed.
+     *
+     * @param name  Name
+     * @param haveQuotedPair true if quoted pair is there else false
+     */
+    void addDirective(
+        String    name,
+        boolean   haveQuotedPair)
+    {
+        String value;
+        int    inputIndex;
+        int    valueIndex;
+        char   valueChar;
+        int    type;
+
+        if (!haveQuotedPair)
+        {
+            value = m_directives.substring(m_scanStart, m_curPos);
+        }
+        else
+        { //copy one character at a time skipping backslash excapes.
+            StringBuffer valueBuf = new StringBuffer(m_curPos - m_scanStart);
+            valueIndex = 0;
+            inputIndex = m_scanStart;
+            while (inputIndex < m_curPos)
+            {
+                if ('\\' == (valueChar = m_directives.charAt(inputIndex)))
+                    inputIndex++;
+                valueBuf.setCharAt(valueIndex, m_directives.charAt(inputIndex));
+                valueIndex++;
+                inputIndex++;
+            }
+            value = new String(valueBuf);
+        }
+
+        if (m_state == STATE_SCANNING_QUOTED_STRING_VALUE)
+            type = ParsedDirective.QUOTED_STRING_VALUE;
+        else
+            type = ParsedDirective.TOKEN_VALUE;
+        m_directiveList.add(new ParsedDirective(name, value, type));
+    }
+
+
+    /**
+     * Returns the List iterator.
+     *
+     * @return     Returns the Iterator Object for the List.
+     */
+    Iterator getIterator()
+    {
+        return m_directiveList.iterator();
+    }
+}
+
diff --git a/src/com/novell/sasl/client/ParsedDirective.java b/src/com/novell/sasl/client/ParsedDirective.java
new file mode 100644
index 0000000..17bf70e
--- /dev/null
+++ b/src/com/novell/sasl/client/ParsedDirective.java
@@ -0,0 +1,56 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/ParsedDirective.java,v 1.1 2003/08/21 10:06:26 kkanil Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+/**
+ * Implements the ParsedDirective class which will be used in the
+ * DigestMD5SaslClient mechanism.
+ */
+class ParsedDirective
+{
+    public static final int  QUOTED_STRING_VALUE = 1;
+    public static final int  TOKEN_VALUE         = 2;
+
+    private int     m_valueType;
+    private String  m_name;
+    private String  m_value;
+
+    ParsedDirective(
+        String  name,
+        String  value,
+        int     type)
+    {
+        m_name = name;
+        m_value = value;
+        m_valueType = type;
+    }
+
+    String getValue()
+    {
+        return m_value;
+    }
+
+    String getName()
+    {
+        return m_name;
+    }
+
+    int getValueType()
+    {
+        return m_valueType;
+    }
+
+}
+
diff --git a/src/com/novell/sasl/client/ResponseAuth.java b/src/com/novell/sasl/client/ResponseAuth.java
new file mode 100644
index 0000000..0aef955
--- /dev/null
+++ b/src/com/novell/sasl/client/ResponseAuth.java
@@ -0,0 +1,83 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/ResponseAuth.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import java.util.*;
+import org.apache.harmony.javax.security.sasl.*;
+
+/**
+ * Implements the ResponseAuth class used by the DigestMD5SaslClient mechanism
+ */
+class ResponseAuth
+{
+
+    private String m_responseValue;
+
+    ResponseAuth(
+        byte[] responseAuth)
+            throws SaslException
+    {
+        m_responseValue = null;
+
+        DirectiveList dirList = new DirectiveList(responseAuth);
+        try
+        {
+            dirList.parseDirectives();
+            checkSemantics(dirList);
+        }
+        catch (SaslException e)
+        {
+        }
+    }
+
+    /**
+     * Checks the semantics of the directives in the directive list as parsed
+     * from the digest challenge byte array.
+     *
+     * @param dirList  the list of directives parsed from the digest challenge
+     *
+     * @exception SaslException   If a semantic error occurs
+     */
+    void checkSemantics(
+        DirectiveList dirList) throws SaslException
+    {
+        Iterator        directives = dirList.getIterator();
+        ParsedDirective directive;
+        String          name;
+
+        while (directives.hasNext())
+        {
+            directive = (ParsedDirective)directives.next();
+            name = directive.getName();
+            if (name.equals("rspauth"))
+                m_responseValue = directive.getValue();
+        }
+
+        /* post semantic check */
+        if (m_responseValue == null)
+            throw new SaslException("Missing response-auth directive.");
+    }
+
+    /**
+     * returns the ResponseValue
+     *
+     * @return the ResponseValue as a String.
+     */
+    public String getResponseValue()
+    {
+        return m_responseValue;
+    }
+}
+
diff --git a/src/com/novell/sasl/client/TokenParser.java b/src/com/novell/sasl/client/TokenParser.java
new file mode 100644
index 0000000..3d3491d
--- /dev/null
+++ b/src/com/novell/sasl/client/TokenParser.java
@@ -0,0 +1,208 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/TokenParser.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import org.apache.harmony.javax.security.sasl.*;
+/**
+ * The TokenParser class will parse individual tokens from a list of tokens that
+ * are a directive value for a DigestMD5 authentication.The tokens are separated
+ * commas.
+ */
+class TokenParser extends Object
+{
+    private static final int STATE_LOOKING_FOR_FIRST_TOKEN = 1;
+    private static final int STATE_LOOKING_FOR_TOKEN       = 2;
+    private static final int STATE_SCANNING_TOKEN          = 3;
+    private static final int STATE_LOOKING_FOR_COMMA       = 4;
+    private static final int STATE_PARSING_ERROR           = 5;
+    private static final int STATE_DONE                    = 6;
+
+    private int        m_curPos;
+    private int     m_scanStart;
+    private int     m_state;
+    private String  m_tokens;
+
+
+    TokenParser(
+        String tokens)
+    {
+        m_tokens = tokens;
+        m_curPos = 0;
+        m_scanStart = 0;
+        m_state =  STATE_LOOKING_FOR_FIRST_TOKEN;
+    }
+
+    /**
+     * This function parses the next token from the tokens string and returns
+     * it as a string. If there are no more tokens a null reference is returned.
+     *
+     * @return  the parsed token or a null reference if there are no more
+     * tokens
+     *
+     * @exception  SASLException if an error occurs while parsing
+     */
+    String parseToken() throws SaslException
+    {
+        char    currChar;
+        String  token = null;
+
+
+        if (m_state == STATE_DONE)
+            return null;
+
+        while (m_curPos < m_tokens.length() && (token == null))
+        {
+            currChar = m_tokens.charAt(m_curPos);
+            switch (m_state)
+            {
+            case STATE_LOOKING_FOR_FIRST_TOKEN:
+            case STATE_LOOKING_FOR_TOKEN:
+                if (isWhiteSpace(currChar))
+                {
+                    break;
+                }
+                else if (isValidTokenChar(currChar))
+                {
+                    m_scanStart = m_curPos;
+                    m_state = STATE_SCANNING_TOKEN;
+                }
+                else
+                {
+                    m_state = STATE_PARSING_ERROR;
+                    throw new SaslException("Invalid token character at position " + m_curPos);
+                }
+                break;
+
+            case STATE_SCANNING_TOKEN:
+                if (isValidTokenChar(currChar))
+                {
+                    break;
+                }
+                else if (isWhiteSpace(currChar))
+                {
+                    token = m_tokens.substring(m_scanStart, m_curPos);
+                    m_state = STATE_LOOKING_FOR_COMMA;
+                }
+                else if (',' == currChar)
+                {
+                    token = m_tokens.substring(m_scanStart, m_curPos);
+                    m_state = STATE_LOOKING_FOR_TOKEN;
+                }
+                else
+                {
+                    m_state = STATE_PARSING_ERROR;
+                    throw new SaslException("Invalid token character at position " + m_curPos);
+                }
+                break;
+
+
+            case STATE_LOOKING_FOR_COMMA:
+                if (isWhiteSpace(currChar))
+                    break;
+                else if (currChar == ',')
+                    m_state = STATE_LOOKING_FOR_TOKEN;
+                else
+                {
+                    m_state = STATE_PARSING_ERROR;
+                    throw new SaslException("Expected a comma, found '" +
+                                            currChar + "' at postion " +
+                                            m_curPos);
+                }
+                break;
+            }
+            m_curPos++;
+        } /* end while loop */
+
+        if (token == null)
+        {    /* check the ending state */
+            switch (m_state)
+            {
+            case STATE_SCANNING_TOKEN:
+                token = m_tokens.substring(m_scanStart);
+                m_state = STATE_DONE;
+                break;
+
+            case STATE_LOOKING_FOR_FIRST_TOKEN:
+            case STATE_LOOKING_FOR_COMMA:
+                break;
+
+            case STATE_LOOKING_FOR_TOKEN:
+                throw new SaslException("Trialing comma");
+            }
+        }
+
+        return token;
+    }
+
+    /**
+     * This function returns TRUE if the character is a valid token character.
+     *
+     *     token          = 1*<any CHAR except CTLs or separators>
+     *
+     *      separators     = "(" | ")" | "<" | ">" | "@"
+     *                     | "," | ";" | ":" | "\" | <">
+     *                     | "/" | "[" | "]" | "?" | "="
+     *                     | "{" | "}" | SP | HT
+     *
+     *      CTL            = <any US-ASCII control character
+     *                       (octets 0 - 31) and DEL (127)>
+     *
+     *      CHAR           = <any US-ASCII character (octets 0 - 127)>
+     *
+     * @param c  character to be validated
+     *
+     * @return True if character is valid Token character else it returns 
+     * false
+     */
+    boolean isValidTokenChar(
+        char c)
+    {
+        if ( ( (c >= '\u0000') && (c <='\u0020') ) ||
+             ( (c >= '\u003a') && (c <= '\u0040') ) ||
+             ( (c >= '\u005b') && (c <= '\u005d') ) ||
+             ('\u002c' == c) ||
+             ('\u0025' == c) ||
+             ('\u0028' == c) ||
+             ('\u0029' == c) ||
+             ('\u007b' == c) ||
+             ('\u007d' == c) ||
+             ('\u007f' == c) )
+            return false;
+
+        return true;
+    }
+
+    /**
+     * This function returns TRUE if the character is linear white space (LWS).
+     *         LWS = [CRLF] 1*( SP | HT )
+     *
+     * @param c  character to be validated
+     *
+     * @return True if character is liner whitespace else it returns false
+     */
+    boolean isWhiteSpace(
+        char c)
+    {
+        if ( ('\t' == c) || // HORIZONTAL TABULATION.
+             ('\n' == c) || // LINE FEED.
+             ('\r' == c) || // CARRIAGE RETURN.
+             ('\u0020' == c) )
+            return true;
+
+        return false;
+    }
+
+}
+
diff --git a/src/de/measite/smack/AndroidDebugger.java b/src/de/measite/smack/AndroidDebugger.java
new file mode 100644
index 0000000..4dfc622
--- /dev/null
+++ b/src/de/measite/smack/AndroidDebugger.java
@@ -0,0 +1,185 @@
+package de.measite.smack;
+
+import org.jivesoftware.smack.debugger.SmackDebugger;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.*;
+
+import android.util.Log;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Very simple debugger that prints to the android log the sent and received stanzas. Use
+ * this debugger with caution since printing to the console is an expensive operation that may
+ * even block the thread since only one thread may print at a time.<p>
+ * <p/>
+ * It is possible to not only print the raw sent and received stanzas but also the interpreted
+ * packets by Smack. By default interpreted packets won't be printed. To enable this feature
+ * just change the <tt>printInterpreted</tt> static variable to <tt>true</tt>.
+ *
+ * @author Gaston Dombiak
+ */
+public class AndroidDebugger implements SmackDebugger {
+
+    public static boolean printInterpreted = false;
+    private SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa");
+
+    private Connection connection = null;
+
+    private PacketListener listener = null;
+    private ConnectionListener connListener = null;
+
+    private Writer writer;
+    private Reader reader;
+    private ReaderListener readerListener;
+    private WriterListener writerListener;
+
+    public AndroidDebugger(Connection connection, Writer writer, Reader reader) {
+        this.connection = connection;
+        this.writer = writer;
+        this.reader = reader;
+        createDebug();
+    }
+
+    /**
+     * Creates the listeners that will print in the console when new activity is detected.
+     */
+    private void createDebug() {
+        // Create a special Reader that wraps the main Reader and logs data to the GUI.
+        ObservableReader debugReader = new ObservableReader(reader);
+        readerListener = new ReaderListener() {
+            public void read(String str) {
+            	Log.d("SMACK",
+                        dateFormatter.format(new Date()) + " RCV  (" + connection.hashCode() +
+                        "): " +
+                        str);
+            }
+        };
+        debugReader.addReaderListener(readerListener);
+
+        // Create a special Writer that wraps the main Writer and logs data to the GUI.
+        ObservableWriter debugWriter = new ObservableWriter(writer);
+        writerListener = new WriterListener() {
+            public void write(String str) {
+            	Log.d("SMACK",
+                        dateFormatter.format(new Date()) + " SENT (" + connection.hashCode() +
+                        "): " +
+                        str);
+            }
+        };
+        debugWriter.addWriterListener(writerListener);
+
+        // Assign the reader/writer objects to use the debug versions. The packet reader
+        // and writer will use the debug versions when they are created.
+        reader = debugReader;
+        writer = debugWriter;
+
+        // Create a thread that will listen for all incoming packets and write them to
+        // the GUI. This is what we call "interpreted" packet data, since it's the packet
+        // data as Smack sees it and not as it's coming in as raw XML.
+        listener = new PacketListener() {
+            public void processPacket(Packet packet) {
+                if (printInterpreted) {
+                	Log.d("SMACK",
+                            dateFormatter.format(new Date()) + " RCV PKT (" +
+                            connection.hashCode() +
+                            "): " +
+                            packet.toXML());
+                }
+            }
+        };
+
+        connListener = new ConnectionListener() {
+            public void connectionClosed() {
+                Log.d("SMACK",
+                        dateFormatter.format(new Date()) + " Connection closed (" +
+                        connection.hashCode() +
+                        ")");
+            }
+
+            public void connectionClosedOnError(Exception e) {
+                Log.d("SMACK",
+                        dateFormatter.format(new Date()) +
+                        " Connection closed due to an exception (" +
+                        connection.hashCode() +
+                        ")");
+                e.printStackTrace();
+            }
+            public void reconnectionFailed(Exception e) {
+                Log.d("SMACK",
+                        dateFormatter.format(new Date()) +
+                        " Reconnection failed due to an exception (" +
+                        connection.hashCode() +
+                        ")");
+                e.printStackTrace();
+            }
+            public void reconnectionSuccessful() {
+                Log.d("SMACK",
+                        dateFormatter.format(new Date()) + " Connection reconnected (" +
+                        connection.hashCode() +
+                        ")");
+            }
+            public void reconnectingIn(int seconds) {
+                Log.d("SMACK",
+                        dateFormatter.format(new Date()) + " Connection (" +
+                        connection.hashCode() +
+                        ") will reconnect in " + seconds);
+            }
+        };
+    }
+
+    public Reader newConnectionReader(Reader newReader) {
+        ((ObservableReader)reader).removeReaderListener(readerListener);
+        ObservableReader debugReader = new ObservableReader(newReader);
+        debugReader.addReaderListener(readerListener);
+        reader = debugReader;
+        return reader;
+    }
+
+    public Writer newConnectionWriter(Writer newWriter) {
+        ((ObservableWriter)writer).removeWriterListener(writerListener);
+        ObservableWriter debugWriter = new ObservableWriter(newWriter);
+        debugWriter.addWriterListener(writerListener);
+        writer = debugWriter;
+        return writer;
+    }
+
+    public void userHasLogged(String user) {
+        boolean isAnonymous = "".equals(StringUtils.parseName(user));
+        String title =
+                "User logged (" + connection.hashCode() + "): "
+                + (isAnonymous ? "" : StringUtils.parseBareAddress(user))
+                + "@"
+                + connection.getServiceName()
+                + ":"
+                + connection.getPort();
+        title += "/" + StringUtils.parseResource(user);
+        Log.d("SMACK", title);
+        // Add the connection listener to the connection so that the debugger can be notified
+        // whenever the connection is closed.
+        connection.addConnectionListener(connListener);
+    }
+
+    public Reader getReader() {
+        return reader;
+    }
+
+    public Writer getWriter() {
+        return writer;
+    }
+
+    public PacketListener getReaderListener() {
+        return listener;
+    }
+
+    public PacketListener getWriterListener() {
+        return null;
+    }
+}
+
diff --git a/src/de/measite/smack/Sasl.java b/src/de/measite/smack/Sasl.java
new file mode 100644
index 0000000..a59135d
--- /dev/null
+++ b/src/de/measite/smack/Sasl.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2009 Rene Treffer
+ *
+ * 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 de.measite.smack;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslException;
+import org.apache.harmony.javax.security.sasl.SaslServer;
+import org.apache.harmony.javax.security.sasl.SaslServerFactory;
+
+public class Sasl {
+
+    // SaslClientFactory service name
+    private static final String CLIENTFACTORYSRV = "SaslClientFactory"; //$NON-NLS-1$
+
+    // SaslServerFactory service name
+    private static final String SERVERFACTORYSRV = "SaslServerFactory"; //$NON-NLS-1$
+
+    public static final String POLICY_NOPLAINTEXT = "javax.security.sasl.policy.noplaintext"; //$NON-NLS-1$
+
+    public static final String POLICY_NOACTIVE = "javax.security.sasl.policy.noactive"; //$NON-NLS-1$
+
+    public static final String POLICY_NODICTIONARY = "javax.security.sasl.policy.nodictionary"; //$NON-NLS-1$
+
+    public static final String POLICY_NOANONYMOUS = "javax.security.sasl.policy.noanonymous"; //$NON-NLS-1$
+
+    public static final String POLICY_FORWARD_SECRECY = "javax.security.sasl.policy.forward"; //$NON-NLS-1$
+
+    public static final String POLICY_PASS_CREDENTIALS = "javax.security.sasl.policy.credentials"; //$NON-NLS-1$
+
+    public static final String MAX_BUFFER = "javax.security.sasl.maxbuffer"; //$NON-NLS-1$
+
+    public static final String RAW_SEND_SIZE = "javax.security.sasl.rawsendsize"; //$NON-NLS-1$
+
+    public static final String REUSE = "javax.security.sasl.reuse"; //$NON-NLS-1$
+
+    public static final String QOP = "javax.security.sasl.qop"; //$NON-NLS-1$
+
+    public static final String STRENGTH = "javax.security.sasl.strength"; //$NON-NLS-1$
+
+    public static final String SERVER_AUTH = "javax.security.sasl.server.authentication"; //$NON-NLS-1$
+
+    public static Enumeration<SaslClientFactory> getSaslClientFactories() {
+        Hashtable<SaslClientFactory,Object> factories = new Hashtable<SaslClientFactory,Object>();
+        factories.put(new SaslClientFactory(), new Object());
+        return factories.keys();
+    }
+
+    public static Enumeration<SaslServerFactory> getSaslServerFactories() {
+        return org.apache.harmony.javax.security.sasl.Sasl.getSaslServerFactories();
+    }
+
+    public static SaslServer createSaslServer(String mechanism, String protocol,
+            String serverName, Map<String, ?> prop, CallbackHandler cbh) throws SaslException {
+        return org.apache.harmony.javax.security.sasl.Sasl.createSaslServer(mechanism, protocol, serverName, prop, cbh);
+    }
+
+    public static SaslClient createSaslClient(String[] mechanisms, String authanticationID,
+            String protocol, String serverName, Map<String, ?> prop, CallbackHandler cbh)
+            throws SaslException {
+        if (mechanisms == null) {
+            throw new NullPointerException("auth.33"); //$NON-NLS-1$
+        }
+        SaslClientFactory fact = getSaslClientFactories().nextElement();
+        String[] mech = fact.getMechanismNames(null);
+        boolean is = false;
+        if (mech != null) {
+            for (int j = 0; j < mech.length; j++) {
+                for (int n = 0; n < mechanisms.length; n++) {
+                    if (mech[j].equals(mechanisms[n])) {
+                        is = true;
+                        break;
+                    }
+                }
+            }
+        }
+        if (is) {
+            return fact.createSaslClient(
+                mechanisms,
+                authanticationID,
+                protocol,
+                serverName,
+                prop,
+                cbh
+            );
+        }
+        return null;
+    }
+
+}
diff --git a/src/de/measite/smack/SaslClientFactory.java b/src/de/measite/smack/SaslClientFactory.java
new file mode 100644
index 0000000..2fa1ebd
--- /dev/null
+++ b/src/de/measite/smack/SaslClientFactory.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2009 Rene Treffer
+ *
+ * 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 de.measite.smack;
+
+import java.util.Map;
+
+import com.novell.sasl.client.DigestMD5SaslClient;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslException;
+import org.apache.qpid.management.common.sasl.PlainSaslClient;
+
+public class SaslClientFactory implements
+		org.apache.harmony.javax.security.sasl.SaslClientFactory {
+
+	@Override
+	public SaslClient createSaslClient(String[] mechanisms,
+			String authorizationId, String protocol, String serverName,
+			Map<String, ?> props, CallbackHandler cbh) throws SaslException {
+		for (String mech: mechanisms) {
+			if ("PLAIN".equals(mech)) {
+				return new PlainSaslClient(authorizationId, cbh);
+			} else
+			if ("DIGEST-MD5".equals(mech)) {
+				return DigestMD5SaslClient.getClient(
+					authorizationId,
+					protocol,
+					serverName,
+					props,
+					cbh
+				);
+			}
+		}
+		return null;
+	}
+
+	@Override
+	public String[] getMechanismNames(Map<String, ?> props) {
+		return new String[]{
+			"PLAIN",
+			"DIGEST-MD5"
+		};
+	}
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/AuthPermission.java b/src/org/apache/harmony/javax/security/auth/AuthPermission.java
new file mode 100644
index 0000000..bb12554
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/AuthPermission.java
@@ -0,0 +1,97 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+import java.security.BasicPermission;
+
+
+
+/**
+ * Governs the use of methods in this package and also its subpackages. A
+ * <i>target name</i> of the permission specifies which methods are allowed
+ * without specifying the concrete action lists. Possible target names and
+ * associated authentication permissions are:
+ *
+ * <pre>
+ *    doAs                      invoke Subject.doAs methods.
+ *    doAsPrivileged            invoke the Subject.doAsPrivileged methods.
+ *    getSubject                invoke Subject.getSubject().
+ *    getSubjectFromDomainCombiner    invoke SubjectDomainCombiner.getSubject().
+ *    setReadOnly               invoke Subject.setReadonly().
+ *    modifyPrincipals          modify the set of principals
+ *                              associated with a Subject.
+ *    modifyPublicCredentials   modify the set of public credentials
+ *                              associated with a Subject.
+ *    modifyPrivateCredentials  modify the set of private credentials
+ *                              associated with a Subject.
+ *    refreshCredential         invoke the refresh method on a credential of a
+ *                              refreshable credential class.
+ *    destroyCredential         invoke the destroy method on a credential of a
+ *                              destroyable credential class.
+ *    createLoginContext.<i>name</i>   instantiate a LoginContext with the
+ *                              specified name. The wildcard name ('*')
+ *                              allows to a LoginContext of any name.
+ *    getLoginConfiguration     invoke the getConfiguration method of
+ *                              javax.security.auth.login.Configuration.
+ *    refreshLoginConfiguration Invoke the refresh method of
+ *                              javax.security.auth.login.Configuration.
+ * </pre>
+ */
+public final class AuthPermission extends BasicPermission {
+
+    private static final long serialVersionUID = 5806031445061587174L;
+
+    private static final String CREATE_LOGIN_CONTEXT = "createLoginContext"; //$NON-NLS-1$
+
+    private static final String CREATE_LOGIN_CONTEXT_ANY = "createLoginContext.*"; //$NON-NLS-1$
+
+    // inits permission name.
+    private static String init(String name) {
+
+        if (name == null) {
+            throw new NullPointerException("auth.13"); //$NON-NLS-1$
+        }
+
+        if (CREATE_LOGIN_CONTEXT.equals(name)) {
+            return CREATE_LOGIN_CONTEXT_ANY;
+        }
+        return name;
+    }
+
+    /**
+     * Creates an authentication permission with the specified target name.
+     *
+     * @param name
+     *            the target name of this authentication permission.
+     */
+    public AuthPermission(String name) {
+        super(init(name));
+    }
+
+    /**
+     * Creates an authentication permission with the specified target name.
+     *
+     * @param name
+     *            the target name of this authentication permission.
+     * @param actions
+     *            this parameter is ignored and should be {@code null}.
+     */
+    public AuthPermission(String name, String actions) {
+        super(init(name), actions);
+    }
+}
\ No newline at end of file
diff --git a/src/org/apache/harmony/javax/security/auth/DestroyFailedException.java b/src/org/apache/harmony/javax/security/auth/DestroyFailedException.java
new file mode 100644
index 0000000..7c7ea79
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/DestroyFailedException.java
@@ -0,0 +1,44 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+/**
+ * Signals that the {@link Destroyable#destroy()} method failed.
+ */
+public class DestroyFailedException extends Exception {
+
+    private static final long serialVersionUID = -7790152857282749162L;
+
+    /**
+     * Creates an exception of type {@code DestroyFailedException}.
+     */
+    public DestroyFailedException() {
+        super();
+    }
+
+    /**
+     * Creates an exception of type {@code DestroyFailedException}.
+     *
+     * @param message
+     *            A detail message that describes the reason for this exception.
+     */
+    public DestroyFailedException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/Destroyable.java b/src/org/apache/harmony/javax/security/auth/Destroyable.java
new file mode 100644
index 0000000..12a107b
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/Destroyable.java
@@ -0,0 +1,43 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+/**
+ * Allows for special treatment of sensitive information, when it comes to
+ * destroying or clearing of the data.
+ */
+public interface Destroyable {
+
+    /**
+     * Erases the sensitive information. Once an object is destroyed any calls
+     * to its methods will throw an {@code IllegalStateException}. If it does
+     * not succeed a DestroyFailedException is thrown.
+     *
+     * @throws DestroyFailedException
+     *             if the information cannot be erased.
+     */
+    void destroy() throws DestroyFailedException;
+
+    /**
+     * Returns {@code true} once an object has been safely destroyed.
+     *
+     * @return whether the object has been safely destroyed.
+     */
+    boolean isDestroyed();
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/PrivateCredentialPermission.java b/src/org/apache/harmony/javax/security/auth/PrivateCredentialPermission.java
new file mode 100644
index 0000000..d62bb24
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/PrivateCredentialPermission.java
@@ -0,0 +1,395 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.security.Permission;
+import java.security.PermissionCollection;
+import java.security.Principal;
+import java.util.Set;
+
+
+
+/**
+ * Protects private credential objects belonging to a {@code Subject}. It has
+ * only one action which is "read". The target name of this permission has a
+ * special syntax:
+ *
+ * <pre>
+ * targetName = CredentialClass {PrincipalClass &quot;PrincipalName&quot;}*
+ * </pre>
+ *
+ * First it states a credential class and is followed then by a list of one or
+ * more principals identifying the subject.
+ * <p>
+ * The principals on their part are specified as the name of the {@code
+ * Principal} class followed by the principal name in quotes. For example, the
+ * following file may define permission to read the private credentials of a
+ * principal named "Bob": "com.sun.PrivateCredential com.sun.Principal \"Bob\""
+ * <p>
+ * The syntax also allows the use of the wildcard "*" in place of {@code
+ * CredentialClass} or {@code PrincipalClass} and/or {@code PrincipalName}.
+ *
+ * @see Principal
+ */
+public final class PrivateCredentialPermission extends Permission {
+
+    private static final long serialVersionUID = 5284372143517237068L;
+
+    // allowed action
+    private static final String READ = "read"; //$NON-NLS-1$
+
+    private String credentialClass;
+
+    // current offset        
+    private transient int offset;
+
+    // owners set
+    private transient CredOwner[] set;
+    
+    /**
+     * Creates a new permission for private credentials specified by the target
+     * name {@code name} and an {@code action}. The action is always
+     * {@code "read"}.
+     *
+     * @param name
+     *            the target name of the permission.
+     * @param action
+     *            the action {@code "read"}.
+     */
+    public PrivateCredentialPermission(String name, String action) {
+        super(name);
+        if (READ.equalsIgnoreCase(action)) {
+            initTargetName(name);
+        } else {
+            throw new IllegalArgumentException("auth.11"); //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * Creates a {@code PrivateCredentialPermission} from the {@code Credential}
+     * class and set of principals.
+     * 
+     * @param credentialClass
+     *            the credential class name.
+     * @param principals
+     *            the set of principals.
+     */
+    PrivateCredentialPermission(String credentialClass, Set<Principal> principals) {
+        super(credentialClass);
+        this.credentialClass = credentialClass;
+
+        set = new CredOwner[principals.size()];
+        for (Principal p : principals) {
+            CredOwner element = new CredOwner(p.getClass().getName(), p.getName());
+            // check for duplicate elements
+            boolean found = false;
+            for (int ii = 0; ii < offset; ii++) {
+                if (set[ii].equals(element)) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                set[offset++] = element;
+            }
+        }
+    }
+
+    /**
+     * Initialize a PrivateCredentialPermission object and checks that a target
+     * name has a correct format: CredentialClass 1*(PrincipalClass
+     * "PrincipalName")
+     */
+    private void initTargetName(String name) {
+
+        if (name == null) {
+            throw new NullPointerException("auth.0E"); //$NON-NLS-1$
+        }
+
+        // check empty string
+        name = name.trim();
+        if (name.length() == 0) {
+            throw new IllegalArgumentException("auth.0F"); //$NON-NLS-1$
+        }
+
+        // get CredentialClass
+        int beg = name.indexOf(' ');
+        if (beg == -1) {
+            throw new IllegalArgumentException("auth.10"); //$NON-NLS-1$
+        }
+        credentialClass = name.substring(0, beg);
+
+        // get a number of pairs: PrincipalClass "PrincipalName"
+        beg++;
+        int count = 0;
+        int nameLength = name.length();
+        for (int i, j = 0; beg < nameLength; beg = j + 2, count++) {
+            i = name.indexOf(' ', beg);
+            j = name.indexOf('"', i + 2);
+
+            if (i == -1 || j == -1 || name.charAt(i + 1) != '"') {
+                throw new IllegalArgumentException("auth.10"); //$NON-NLS-1$
+            }
+        }
+
+        // name MUST have one pair at least
+        if (count < 1) {
+            throw new IllegalArgumentException("auth.10"); //$NON-NLS-1$
+        }
+
+        beg = name.indexOf(' ');
+        beg++;
+
+        // populate principal set with instances of CredOwner class
+        String principalClass;
+        String principalName;
+
+        set = new CredOwner[count];
+        for (int index = 0, i, j; index < count; beg = j + 2, index++) {
+            i = name.indexOf(' ', beg);
+            j = name.indexOf('"', i + 2);
+
+            principalClass = name.substring(beg, i);
+            principalName = name.substring(i + 2, j);
+
+            CredOwner element = new CredOwner(principalClass, principalName);
+            // check for duplicate elements
+            boolean found = false;
+            for (int ii = 0; ii < offset; ii++) {
+                if (set[ii].equals(element)) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                set[offset++] = element;
+            }
+        }
+    }
+
+    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
+        ois.defaultReadObject();
+        initTargetName(getName());
+    }
+
+    /**
+     * Returns the principal's classes and names associated with this {@code
+     * PrivateCredentialPermission} as a two dimensional array. The first
+     * dimension of the array corresponds to the number of principals. The
+     * second dimension defines either the name of the {@code PrincipalClass}
+     * [x][0] or the value of {@code PrincipalName} [x][1].
+     * <p>
+     * This corresponds to the the target name's syntax:
+     *
+     * <pre>
+     * targetName = CredentialClass {PrincipalClass &quot;PrincipalName&quot;}*
+     * </pre>
+     *
+     * @return the principal classes and names associated with this {@code
+     *         PrivateCredentialPermission}.
+     */
+    public String[][] getPrincipals() {
+
+        String[][] s = new String[offset][2];
+
+        for (int i = 0; i < s.length; i++) {
+            s[i][0] = set[i].principalClass;
+            s[i][1] = set[i].principalName;
+        }
+        return s;
+    }
+
+    @Override
+    public String getActions() {
+        return READ;
+    }
+
+    /**
+     * Returns the class name of the credential associated with this permission.
+     *
+     * @return the class name of the credential associated with this permission.
+     */
+    public String getCredentialClass() {
+        return credentialClass;
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 0;
+        for (int i = 0; i < offset; i++) {
+            hash = hash + set[i].hashCode();
+        }
+        return getCredentialClass().hashCode() + hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+
+        if (obj == null || this.getClass() != obj.getClass()) {
+            return false;
+        }
+
+        PrivateCredentialPermission that = (PrivateCredentialPermission) obj;
+
+        return credentialClass.equals(that.credentialClass) && (offset == that.offset)
+                && sameMembers(set, that.set, offset);
+    }
+
+    @Override
+    public boolean implies(Permission permission) {
+
+        if (permission == null || this.getClass() != permission.getClass()) {
+            return false;
+        }
+
+        PrivateCredentialPermission that = (PrivateCredentialPermission) permission;
+
+        if (!("*".equals(credentialClass) || credentialClass //$NON-NLS-1$
+                .equals(that.getCredentialClass()))) {
+            return false;
+        }
+
+        if (that.offset == 0) {
+            return true;
+        }
+
+        CredOwner[] thisCo = set;
+        CredOwner[] thatCo = that.set;
+        int thisPrincipalsSize = offset;
+        int thatPrincipalsSize = that.offset;
+        for (int i = 0, j; i < thisPrincipalsSize; i++) {
+            for (j = 0; j < thatPrincipalsSize; j++) {
+                if (thisCo[i].implies(thatCo[j])) {
+                    break;
+                }
+            }
+            if (j == thatCo.length) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public PermissionCollection newPermissionCollection() {
+        return null;
+    }
+
+    /**
+     * Returns true if the two arrays have the same length, and every member of
+     * one array is contained in another array
+     */
+    private boolean sameMembers(Object[] ar1, Object[] ar2, int length) {
+        if (ar1 == null && ar2 == null) {
+            return true;
+        }
+        if (ar1 == null || ar2 == null) {
+            return false;
+        }
+        boolean found;
+        for (int i = 0; i < length; i++) {
+            found = false;
+            for (int j = 0; j < length; j++) {
+                if (ar1[i].equals(ar2[j])) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static final class CredOwner implements Serializable {
+
+        private static final long serialVersionUID = -5607449830436408266L;
+
+        String principalClass;
+
+        String principalName;
+
+        // whether class name contains wildcards
+        private transient boolean isClassWildcard;
+
+        // whether pname contains wildcards
+        private transient boolean isPNameWildcard;
+
+        // Creates a new CredOwner with the specified Principal Class and Principal Name 
+        CredOwner(String principalClass, String principalName) {
+            super();
+            if ("*".equals(principalClass)) { //$NON-NLS-1$
+                isClassWildcard = true;
+            }
+
+            if ("*".equals(principalName)) { //$NON-NLS-1$
+                isPNameWildcard = true;
+            }
+
+            if (isClassWildcard && !isPNameWildcard) {
+                throw new IllegalArgumentException("auth.12"); //$NON-NLS-1$
+            }
+
+            this.principalClass = principalClass;
+            this.principalName = principalName;
+        }
+
+        // Checks if this CredOwner implies the specified Object. 
+        boolean implies(Object obj) {
+            if (obj == this) {
+                return true;
+            }
+
+            CredOwner co = (CredOwner) obj;
+
+            if (isClassWildcard || principalClass.equals(co.principalClass)) {
+                if (isPNameWildcard || principalName.equals(co.principalName)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        // Checks two CredOwner objects for equality. 
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == this) {
+                return true;
+            }
+            if (obj instanceof CredOwner) {
+                CredOwner that = (CredOwner) obj;
+                return principalClass.equals(that.principalClass)
+                    && principalName.equals(that.principalName);
+            }
+            return false;
+        }
+
+        // Returns the hash code value for this object.
+        @Override
+        public int hashCode() {
+            return principalClass.hashCode() + principalName.hashCode();
+        }
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/RefreshFailedException.java b/src/org/apache/harmony/javax/security/auth/RefreshFailedException.java
new file mode 100644
index 0000000..71bcc6b
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/RefreshFailedException.java
@@ -0,0 +1,31 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+public class RefreshFailedException extends Exception {
+
+    private static final long serialVersionUID = 5058444488565265840L;
+
+    public RefreshFailedException() {
+        super();
+    }
+
+    public RefreshFailedException(String message) {
+        super(message);
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/Refreshable.java b/src/org/apache/harmony/javax/security/auth/Refreshable.java
new file mode 100644
index 0000000..90b00cb
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/Refreshable.java
@@ -0,0 +1,26 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+public interface Refreshable {
+
+    void refresh() throws RefreshFailedException;
+
+    boolean isCurrent();
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/Subject.java b/src/org/apache/harmony/javax/security/auth/Subject.java
new file mode 100644
index 0000000..142686e
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/Subject.java
@@ -0,0 +1,782 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.DomainCombiner;
+import java.security.Permission;
+import java.security.Principal;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.security.ProtectionDomain;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Set;
+
+
+
+/**
+ * The central class of the {@code javax.security.auth} package representing an
+ * authenticated user or entity (both referred to as "subject"). IT defines also
+ * the static methods that allow code to be run, and do modifications according
+ * to the subject's permissions.
+ * <p>
+ * A subject has the following features:
+ * <ul>
+ * <li>A set of {@code Principal} objects specifying the identities bound to a
+ * {@code Subject} that distinguish it.</li>
+ * <li>Credentials (public and private) such as certificates, keys, or
+ * authentication proofs such as tickets</li>
+ * </ul>
+ */
+public final class Subject implements Serializable {
+
+    private static final long serialVersionUID = -8308522755600156056L;
+    
+    private static final AuthPermission _AS = new AuthPermission("doAs"); //$NON-NLS-1$
+
+    private static final AuthPermission _AS_PRIVILEGED = new AuthPermission(
+            "doAsPrivileged"); //$NON-NLS-1$
+
+    private static final AuthPermission _SUBJECT = new AuthPermission(
+            "getSubject"); //$NON-NLS-1$
+
+    private static final AuthPermission _PRINCIPALS = new AuthPermission(
+            "modifyPrincipals"); //$NON-NLS-1$
+
+    private static final AuthPermission _PRIVATE_CREDENTIALS = new AuthPermission(
+            "modifyPrivateCredentials"); //$NON-NLS-1$
+
+    private static final AuthPermission _PUBLIC_CREDENTIALS = new AuthPermission(
+            "modifyPublicCredentials"); //$NON-NLS-1$
+
+    private static final AuthPermission _READ_ONLY = new AuthPermission(
+            "setReadOnly"); //$NON-NLS-1$
+
+    private final Set<Principal> principals;
+
+    private boolean readOnly;
+    
+    // set of private credentials
+    private transient SecureSet<Object> privateCredentials;
+
+    // set of public credentials
+    private transient SecureSet<Object> publicCredentials;
+    
+    /**
+     * The default constructor initializing the sets of public and private
+     * credentials and principals with the empty set.
+     */
+    public Subject() {
+        super();
+        principals = new SecureSet<Principal>(_PRINCIPALS);
+        publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS);
+        privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS);
+
+        readOnly = false;
+    }
+
+    /**
+     * The constructor for the subject, setting its public and private
+     * credentials and principals according to the arguments.
+     *
+     * @param readOnly
+     *            {@code true} if this {@code Subject} is read-only, thus
+     *            preventing any modifications to be done.
+     * @param subjPrincipals
+     *            the set of Principals that are attributed to this {@code
+     *            Subject}.
+     * @param pubCredentials
+     *            the set of public credentials that distinguish this {@code
+     *            Subject}.
+     * @param privCredentials
+     *            the set of private credentials that distinguish this {@code
+     *            Subject}.
+     */
+    public Subject(boolean readOnly, Set<? extends Principal> subjPrincipals,
+            Set<?> pubCredentials, Set<?> privCredentials) {
+
+        if (subjPrincipals == null || pubCredentials == null || privCredentials == null) {
+            throw new NullPointerException();
+        }
+
+        principals = new SecureSet<Principal>(_PRINCIPALS, subjPrincipals);
+        publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS, pubCredentials);
+        privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS, privCredentials);
+
+        this.readOnly = readOnly;
+    }
+
+    /**
+     * Runs the code defined by {@code action} using the permissions granted to
+     * the {@code Subject} itself and to the code as well.
+     *
+     * @param subject
+     *            the distinguished {@code Subject}.
+     * @param action
+     *            the code to be run.
+     * @return the {@code Object} returned when running the {@code action}.
+     */
+    @SuppressWarnings("unchecked")
+    public static Object doAs(Subject subject, PrivilegedAction action) {
+
+        checkPermission(_AS);
+
+        return doAs_PrivilegedAction(subject, action, AccessController.getContext());
+    }
+
+    /**
+     * Run the code defined by {@code action} using the permissions granted to
+     * the {@code Subject} and to the code itself, additionally providing a more
+     * specific context.
+     *
+     * @param subject
+     *            the distinguished {@code Subject}.
+     * @param action
+     *            the code to be run.
+     * @param context
+     *            the specific context in which the {@code action} is invoked.
+     *            if {@code null} a new {@link AccessControlContext} is
+     *            instantiated.
+     * @return the {@code Object} returned when running the {@code action}.
+     */
+    @SuppressWarnings("unchecked")
+    public static Object doAsPrivileged(Subject subject, PrivilegedAction action,
+            AccessControlContext context) {
+
+        checkPermission(_AS_PRIVILEGED);
+
+        if (context == null) {
+            return doAs_PrivilegedAction(subject, action, new AccessControlContext(
+                    new ProtectionDomain[0]));
+        }
+        return doAs_PrivilegedAction(subject, action, context);
+    }
+
+    // instantiates a new context and passes it to AccessController
+    @SuppressWarnings("unchecked")
+    private static Object doAs_PrivilegedAction(Subject subject, PrivilegedAction action,
+            final AccessControlContext context) {
+
+        AccessControlContext newContext;
+
+        final SubjectDomainCombiner combiner;
+        if (subject == null) {
+            // performance optimization
+            // if subject is null there is nothing to combine
+            combiner = null;
+        } else {
+            combiner = new SubjectDomainCombiner(subject);
+        }
+
+        PrivilegedAction dccAction = new PrivilegedAction() {
+            public Object run() {
+
+                return new AccessControlContext(context, combiner);
+            }
+        };
+
+        newContext = (AccessControlContext) AccessController.doPrivileged(dccAction);
+
+        return AccessController.doPrivileged(action, newContext);
+    }
+
+    /**
+     * Runs the code defined by {@code action} using the permissions granted to
+     * the subject and to the code itself.
+     *
+     * @param subject
+     *            the distinguished {@code Subject}.
+     * @param action
+     *            the code to be run.
+     * @return the {@code Object} returned when running the {@code action}.
+     * @throws PrivilegedActionException
+     *             if running the {@code action} throws an exception.
+     */
+    @SuppressWarnings("unchecked")
+    public static Object doAs(Subject subject, PrivilegedExceptionAction action)
+            throws PrivilegedActionException {
+
+        checkPermission(_AS);
+
+        return doAs_PrivilegedExceptionAction(subject, action, AccessController.getContext());
+    }
+
+    /**
+     * Runs the code defined by {@code action} using the permissions granted to
+     * the subject and to the code itself, additionally providing a more
+     * specific context.
+     *
+     * @param subject
+     *            the distinguished {@code Subject}.
+     * @param action
+     *            the code to be run.
+     * @param context
+     *            the specific context in which the {@code action} is invoked.
+     *            if {@code null} a new {@link AccessControlContext} is
+     *            instantiated.
+     * @return the {@code Object} returned when running the {@code action}.
+     * @throws PrivilegedActionException
+     *             if running the {@code action} throws an exception.
+     */
+    @SuppressWarnings("unchecked")
+    public static Object doAsPrivileged(Subject subject,
+            PrivilegedExceptionAction action, AccessControlContext context)
+            throws PrivilegedActionException {
+
+        checkPermission(_AS_PRIVILEGED);
+
+        if (context == null) {
+            return doAs_PrivilegedExceptionAction(subject, action,
+                    new AccessControlContext(new ProtectionDomain[0]));
+        }
+        return doAs_PrivilegedExceptionAction(subject, action, context);
+    }
+
+    // instantiates a new context and passes it to AccessController
+    @SuppressWarnings("unchecked")
+    private static Object doAs_PrivilegedExceptionAction(Subject subject,
+            PrivilegedExceptionAction action, final AccessControlContext context)
+            throws PrivilegedActionException {
+
+        AccessControlContext newContext;
+
+        final SubjectDomainCombiner combiner;
+        if (subject == null) {
+            // performance optimization
+            // if subject is null there is nothing to combine
+            combiner = null;
+        } else {
+            combiner = new SubjectDomainCombiner(subject);
+        }
+
+        PrivilegedAction<AccessControlContext> dccAction = new PrivilegedAction<AccessControlContext>() {
+            public AccessControlContext run() {
+                return new AccessControlContext(context, combiner);
+            }
+        };
+
+        newContext = AccessController.doPrivileged(dccAction);
+
+        return AccessController.doPrivileged(action, newContext);
+    }
+
+    /**
+     * Checks two Subjects for equality. More specifically if the principals,
+     * public and private credentials are equal, equality for two {@code
+     * Subjects} is implied.
+     *
+     * @param obj
+     *            the {@code Object} checked for equality with this {@code
+     *            Subject}.
+     * @return {@code true} if the specified {@code Subject} is equal to this
+     *         one.
+     */
+    @Override
+    public boolean equals(Object obj) {
+
+        if (this == obj) {
+            return true;
+        }
+
+        if (obj == null || this.getClass() != obj.getClass()) {
+            return false;
+        }
+
+        Subject that = (Subject) obj;
+
+        if (principals.equals(that.principals)
+                && publicCredentials.equals(that.publicCredentials)
+                && privateCredentials.equals(that.privateCredentials)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns this {@code Subject}'s {@link Principal}.
+     *
+     * @return this {@code Subject}'s {@link Principal}.
+     */
+    public Set<Principal> getPrincipals() {
+        return principals;
+    }
+
+
+    /**
+     * Returns this {@code Subject}'s {@link Principal} which is a subclass of
+     * the {@code Class} provided.
+     *
+     * @param c
+     *            the {@code Class} as a criteria which the {@code Principal}
+     *            returned must satisfy.
+     * @return this {@code Subject}'s {@link Principal}. Modifications to the
+     *         returned set of {@code Principal}s do not affect this {@code
+     *         Subject}'s set.
+     */
+    public <T extends Principal> Set<T> getPrincipals(Class<T> c) {
+        return ((SecureSet<Principal>) principals).get(c);
+    }
+
+    /**
+     * Returns the private credentials associated with this {@code Subject}.
+     *
+     * @return the private credentials associated with this {@code Subject}.
+     */
+    public Set<Object> getPrivateCredentials() {
+        return privateCredentials;
+    }
+
+    /**
+     * Returns this {@code Subject}'s private credentials which are a subclass
+     * of the {@code Class} provided.
+     *
+     * @param c
+     *            the {@code Class} as a criteria which the private credentials
+     *            returned must satisfy.
+     * @return this {@code Subject}'s private credentials. Modifications to the
+     *         returned set of credentials do not affect this {@code Subject}'s
+     *         credentials.
+     */
+    public <T> Set<T> getPrivateCredentials(Class<T> c) {
+        return privateCredentials.get(c);
+    }
+
+    /**
+     * Returns the public credentials associated with this {@code Subject}.
+     *
+     * @return the public credentials associated with this {@code Subject}.
+     */
+    public Set<Object> getPublicCredentials() {
+        return publicCredentials;
+    }
+
+
+    /**
+     * Returns this {@code Subject}'s public credentials which are a subclass of
+     * the {@code Class} provided.
+     *
+     * @param c
+     *            the {@code Class} as a criteria which the public credentials
+     *            returned must satisfy.
+     * @return this {@code Subject}'s public credentials. Modifications to the
+     *         returned set of credentials do not affect this {@code Subject}'s
+     *         credentials.
+     */
+    public <T> Set<T> getPublicCredentials(Class<T> c) {
+        return publicCredentials.get(c);
+    }
+
+    /**
+     * Returns a hash code of this {@code Subject}.
+     *
+     * @return a hash code of this {@code Subject}.
+     */
+    @Override
+    public int hashCode() {
+        return principals.hashCode() + privateCredentials.hashCode()
+                + publicCredentials.hashCode();
+    }
+
+    /**
+     * Prevents from modifications being done to the credentials and {@link
+     * Principal} sets. After setting it to read-only this {@code Subject} can
+     * not be made writable again. The destroy method on the credentials still
+     * works though.
+     */
+    public void setReadOnly() {
+        checkPermission(_READ_ONLY);
+
+        readOnly = true;
+    }
+
+    /**
+     * Returns whether this {@code Subject} is read-only or not.
+     *
+     * @return whether this {@code Subject} is read-only or not.
+     */
+    public boolean isReadOnly() {
+        return readOnly;
+    }
+
+    /**
+     * Returns a {@code String} representation of this {@code Subject}.
+     *
+     * @return a {@code String} representation of this {@code Subject}.
+     */
+    @Override
+    public String toString() {
+
+        StringBuilder buf = new StringBuilder("Subject:\n"); //$NON-NLS-1$
+
+        Iterator<?> it = principals.iterator();
+        while (it.hasNext()) {
+            buf.append("\tPrincipal: "); //$NON-NLS-1$
+            buf.append(it.next());
+            buf.append('\n');
+        }
+
+        it = publicCredentials.iterator();
+        while (it.hasNext()) {
+            buf.append("\tPublic Credential: "); //$NON-NLS-1$
+            buf.append(it.next());
+            buf.append('\n');
+        }
+
+        int offset = buf.length() - 1;
+        it = privateCredentials.iterator();
+        try {
+            while (it.hasNext()) {
+                buf.append("\tPrivate Credential: "); //$NON-NLS-1$
+                buf.append(it.next());
+                buf.append('\n');
+            }
+        } catch (SecurityException e) {
+            buf.delete(offset, buf.length());
+            buf.append("\tPrivate Credentials: no accessible information\n"); //$NON-NLS-1$
+        }
+        return buf.toString();
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException,
+            ClassNotFoundException {
+
+        in.defaultReadObject();
+
+        publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS);
+        privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS);
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        out.defaultWriteObject();
+    }
+
+    /**
+     * Returns the {@code Subject} that was last associated with the {@code
+     * context} provided as argument.
+     *
+     * @param context
+     *            the {@code context} that was associated with the
+     *            {@code Subject}.
+     * @return the {@code Subject} that was last associated with the {@code
+     *         context} provided as argument.
+     */
+    public static Subject getSubject(final AccessControlContext context) {
+        checkPermission(_SUBJECT);
+        if (context == null) {
+            throw new NullPointerException("auth.09"); //$NON-NLS-1$
+        }
+        PrivilegedAction<DomainCombiner> action = new PrivilegedAction<DomainCombiner>() {
+            public DomainCombiner run() {
+                return context.getDomainCombiner();
+            }
+        };
+        DomainCombiner combiner = AccessController.doPrivileged(action);
+
+        if ((combiner == null) || !(combiner instanceof SubjectDomainCombiner)) {
+            return null;
+        }
+        return ((SubjectDomainCombiner) combiner).getSubject();
+    }
+
+    // checks passed permission
+    private static void checkPermission(Permission p) {
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            sm.checkPermission(p);
+        }
+    }
+
+    // FIXME is used only in two places. remove?
+    private void checkState() {
+        if (readOnly) {
+            throw new IllegalStateException("auth.0A"); //$NON-NLS-1$
+        }
+    }
+
+    private final class SecureSet<SST> extends AbstractSet<SST> implements Serializable {
+
+        /**
+         * Compatibility issue: see comments for setType variable
+         */
+        private static final long serialVersionUID = 7911754171111800359L;
+
+        private LinkedList<SST> elements;
+
+        /*
+         * Is used to define a set type for serialization.
+         * 
+         * A type can be principal, priv. or pub. credential set. The spec.
+         * doesn't clearly says that priv. and pub. credential sets can be
+         * serialized and what classes they are. It is only possible to figure
+         * out from writeObject method comments that priv. credential set is
+         * serializable and it is an instance of SecureSet class. So pub.
+         * credential was implemented by analogy
+         * 
+         * Compatibility issue: the class follows its specified serial form.
+         * Also according to the serialization spec. adding new field is a
+         * compatible change. So is ok for principal set (because the default
+         * value for integer is zero). But priv. or pub. credential set it is
+         * not compatible because most probably other implementations resolve
+         * this issue in other way
+         */
+        private int setType;
+
+        // Defines principal set for serialization.
+        private static final int SET_Principal = 0;
+
+        // Defines private credential set for serialization.
+        private static final int SET_PrivCred = 1;
+
+        // Defines public credential set for serialization.
+        private static final int SET_PubCred = 2;
+
+        // permission required to modify set
+        private transient AuthPermission permission;
+
+        protected SecureSet(AuthPermission perm) {
+            permission = perm;
+            elements = new LinkedList<SST>();
+        }
+
+        // creates set from specified collection with specified permission
+        // all collection elements are verified before adding
+        protected SecureSet(AuthPermission perm, Collection<? extends SST> s) {
+            this(perm);
+
+            // Subject's constructor receives a Set, we can trusts if a set is from bootclasspath,
+            // and not to check whether it contains duplicates or not
+            boolean trust = s.getClass().getClassLoader() == null; 
+            
+            Iterator<? extends SST> it = s.iterator();
+            while (it.hasNext()) {
+                SST o = it.next();
+                verifyElement(o);
+                if (trust || !elements.contains(o)) {
+                    elements.add(o);
+                }
+            }
+        }
+
+        // verifies new set element
+        private void verifyElement(Object o) {
+
+            if (o == null) {
+                throw new NullPointerException();
+            }
+            if (permission == _PRINCIPALS && !(Principal.class.isAssignableFrom(o.getClass()))) {
+                throw new IllegalArgumentException("auth.0B"); //$NON-NLS-1$
+            }
+        }
+
+        /*
+         * verifies specified element, checks set state, and security permission
+         * to modify set before adding new element
+         */
+        @Override
+        public boolean add(SST o) {
+
+            verifyElement(o);
+
+            checkState();
+            checkPermission(permission);
+
+            if (!elements.contains(o)) {
+                elements.add(o);
+                return true;
+            }
+            return false;
+        }
+
+        // returns an instance of SecureIterator
+        @Override
+        public Iterator<SST> iterator() {
+
+            if (permission == _PRIVATE_CREDENTIALS) {
+                /*
+                 * private credential set requires iterator with additional
+                 * security check (PrivateCredentialPermission)
+                 */
+                return new SecureIterator(elements.iterator()) {
+                    /*
+                     * checks permission to access next private credential moves
+                     * to the next element even SecurityException was thrown
+                     */
+                    @Override
+                    public SST next() {
+                        SST obj = iterator.next();
+                        checkPermission(new PrivateCredentialPermission(obj
+                                .getClass().getName(), principals));
+                        return obj;
+                    }
+                };
+            }
+            return new SecureIterator(elements.iterator());
+        }
+
+        @Override
+        public boolean retainAll(Collection<?> c) {
+
+            if (c == null) {
+                throw new NullPointerException();
+            }
+            return super.retainAll(c);
+        }
+
+        @Override
+        public int size() {
+            return elements.size();
+        }
+
+        /**
+         * return set with elements that are instances or subclasses of the
+         * specified class
+         */
+        protected final <E> Set<E> get(final Class<E> c) {
+
+            if (c == null) {
+                throw new NullPointerException();
+            }
+
+            AbstractSet<E> s = new AbstractSet<E>() {
+                private LinkedList<E> elements = new LinkedList<E>();
+
+                @Override
+                public boolean add(E o) {
+
+                    if (!c.isAssignableFrom(o.getClass())) {
+                        throw new IllegalArgumentException(
+                                "auth.0C " + c.getName()); //$NON-NLS-1$
+                    }
+
+                    if (elements.contains(o)) {
+                        return false;
+                    }
+                    elements.add(o);
+                    return true;
+                }
+
+                @Override
+                public Iterator<E> iterator() {
+                    return elements.iterator();
+                }
+
+                @Override
+                public boolean retainAll(Collection<?> c) {
+
+                    if (c == null) {
+                        throw new NullPointerException();
+                    }
+                    return super.retainAll(c);
+                }
+
+                @Override
+                public int size() {
+                    return elements.size();
+                }
+            };
+
+            // FIXME must have permissions for requested priv. credentials
+            for (Iterator<SST> it = iterator(); it.hasNext();) {
+                SST o = it.next();
+                if (c.isAssignableFrom(o.getClass())) {
+                    s.add(c.cast(o));
+                }
+            }
+            return s;
+        }
+
+        private void readObject(ObjectInputStream in) throws IOException,
+                ClassNotFoundException {
+            in.defaultReadObject();
+
+            switch (setType) {
+            case SET_Principal:
+                permission = _PRINCIPALS;
+                break;
+            case SET_PrivCred:
+                permission = _PRIVATE_CREDENTIALS;
+                break;
+            case SET_PubCred:
+                permission = _PUBLIC_CREDENTIALS;
+                break;
+            default:
+                throw new IllegalArgumentException();
+            }
+
+            Iterator<SST> it = elements.iterator();
+            while (it.hasNext()) {
+                verifyElement(it.next());
+            }
+        }
+
+        private void writeObject(ObjectOutputStream out) throws IOException {
+
+            if (permission == _PRIVATE_CREDENTIALS) {
+                // does security check for each private credential
+                for (Iterator<SST> it = iterator(); it.hasNext();) {
+                    it.next();
+                }
+                setType = SET_PrivCred;
+            } else if (permission == _PRINCIPALS) {
+                setType = SET_Principal;
+            } else {
+                setType = SET_PubCred;
+            }
+
+            out.defaultWriteObject();
+        }
+
+        /**
+         * Represents iterator for subject's secure set
+         */
+        private class SecureIterator implements Iterator<SST> {
+            protected Iterator<SST> iterator;
+
+            protected SecureIterator(Iterator<SST> iterator) {
+                this.iterator = iterator;
+            }
+
+            public boolean hasNext() {
+                return iterator.hasNext();
+            }
+
+            public SST next() {
+                return iterator.next();
+            }
+
+            /**
+             * checks set state, and security permission to modify set before
+             * removing current element
+             */
+            public void remove() {
+                checkState();
+                checkPermission(permission);
+                iterator.remove();
+            }
+        }
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/SubjectDomainCombiner.java b/src/org/apache/harmony/javax/security/auth/SubjectDomainCombiner.java
new file mode 100644
index 0000000..edbb672
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/SubjectDomainCombiner.java
@@ -0,0 +1,123 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+import java.security.DomainCombiner;
+import java.security.Principal;
+import java.security.ProtectionDomain;
+import java.util.Set;
+
+/**
+ * Merges permissions based on code source and code signers with permissions
+ * granted to the specified {@link Subject}.
+ */
+public class SubjectDomainCombiner implements DomainCombiner {
+
+    // subject to be associated
+    private Subject subject;
+
+    // permission required to get a subject object
+    private static final AuthPermission _GET = new AuthPermission(
+            "getSubjectFromDomainCombiner"); //$NON-NLS-1$
+
+    /**
+     * Creates a domain combiner for the entity provided in {@code subject}.
+     *
+     * @param subject
+     *            the entity to which this domain combiner is associated.
+     */
+    public SubjectDomainCombiner(Subject subject) {
+        super();
+        if (subject == null) {
+            throw new NullPointerException();
+        }
+        this.subject = subject;
+    }
+
+    /**
+     * Returns the entity to which this domain combiner is associated.
+     *
+     * @return the entity to which this domain combiner is associated.
+     */
+    public Subject getSubject() {
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            sm.checkPermission(_GET);
+        }
+
+        return subject;
+    }
+
+    /**
+     * Merges the {@code ProtectionDomain} with the {@code Principal}s
+     * associated with the subject of this {@code SubjectDomainCombiner}.
+     *
+     * @param currentDomains
+     *            the {@code ProtectionDomain}s associated with the context of
+     *            the current thread. The domains must be sorted according to
+     *            the execution order, the most recent residing at the
+     *            beginning.
+     * @param assignedDomains
+     *            the {@code ProtectionDomain}s from the parent thread based on
+     *            code source and signers.
+     * @return a single {@code ProtectionDomain} array computed from the two
+     *         provided arrays, or {@code null}.
+     * @see ProtectionDomain
+     */
+    public ProtectionDomain[] combine(ProtectionDomain[] currentDomains,
+            ProtectionDomain[] assignedDomains) {
+        // get array length for combining protection domains
+        int len = 0;
+        if (currentDomains != null) {
+            len += currentDomains.length;
+        }
+        if (assignedDomains != null) {
+            len += assignedDomains.length;
+        }
+        if (len == 0) {
+            return null;
+        }
+
+        ProtectionDomain[] pd = new ProtectionDomain[len];
+
+        // for each current domain substitute set of principal with subject's
+        int cur = 0;
+        if (currentDomains != null) {
+
+            Set<Principal> s = subject.getPrincipals();
+            Principal[] p = s.toArray(new Principal[s.size()]);
+
+            for (cur = 0; cur < currentDomains.length; cur++) {
+                if (currentDomains[cur] != null) {
+                    ProtectionDomain newPD;
+                    newPD = new ProtectionDomain(currentDomains[cur].getCodeSource(),
+                            currentDomains[cur].getPermissions(), currentDomains[cur]
+                                    .getClassLoader(), p);
+                    pd[cur] = newPD;
+                }
+            }
+        }
+
+        // copy assigned domains
+        if (assignedDomains != null) {
+            System.arraycopy(assignedDomains, 0, pd, cur, assignedDomains.length);
+        }
+
+        return pd;
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/Callback.java b/src/org/apache/harmony/javax/security/auth/callback/Callback.java
new file mode 100644
index 0000000..8fd745c
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/Callback.java
@@ -0,0 +1,25 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+/**
+ * Defines an empty base interface for all {@code Callback}s used during
+ * authentication.
+ */
+public interface Callback {
+}
\ No newline at end of file
diff --git a/src/org/apache/harmony/javax/security/auth/callback/CallbackHandler.java b/src/org/apache/harmony/javax/security/auth/callback/CallbackHandler.java
new file mode 100644
index 0000000..d09fafa
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/CallbackHandler.java
@@ -0,0 +1,54 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.IOException;
+
+/**
+ * Needs to be implemented by classes that want to handle authentication
+ * {@link Callback}s. A single method {@link #handle(Callback[])} must be
+ * provided that checks the type of the incoming {@code Callback}s and reacts
+ * accordingly. {@code CallbackHandler}s can be installed per application. It is
+ * also possible to configure a system-default {@code CallbackHandler} by
+ * setting the {@code auth.login.defaultCallbackHandler} property in the
+ * standard {@code security.properties} file.
+ */
+public interface CallbackHandler {
+
+    /**
+     * Handles the actual {@link Callback}. A {@code CallbackHandler} needs to
+     * implement this method. In the method, it is free to select which {@code
+     * Callback}s it actually wants to handle and in which way. For example, a
+     * console-based {@code CallbackHandler} might choose to sequentially ask
+     * the user for login and password, if it implements these {@code Callback}
+     * s, whereas a GUI-based one might open a single dialog window for both
+     * values. If a {@code CallbackHandler} is not able to handle a specific
+     * {@code Callback}, it needs to throw an
+     * {@link UnsupportedCallbackException}.
+     *
+     * @param callbacks
+     *            the array of {@code Callback}s that need handling
+     * @throws IOException
+     *             if an I/O related error occurs
+     * @throws UnsupportedCallbackException
+     *             if the {@code CallbackHandler} is not able to handle a
+     *             specific {@code Callback}
+     */
+    void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException;
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/ChoiceCallback.java b/src/org/apache/harmony/javax/security/auth/callback/ChoiceCallback.java
new file mode 100644
index 0000000..1e53fb6
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/ChoiceCallback.java
@@ -0,0 +1,109 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+
+
+
+public class ChoiceCallback implements Callback, Serializable {
+
+    private static final long serialVersionUID = -3975664071579892167L;
+
+    private int defaultChoice;
+
+    private String prompt;
+
+    private boolean multipleSelectionsAllowed;
+
+    private String[] choices;
+
+    private int[] selections;
+
+    private void setChoices(String[] choices) {
+        if (choices == null || choices.length == 0) {
+            throw new IllegalArgumentException("auth.1C"); //$NON-NLS-1$
+        }
+        for (int i = 0; i < choices.length; i++) {
+            if (choices[i] == null || choices[i].length() == 0) {
+                throw new IllegalArgumentException("auth.1C"); //$NON-NLS-1$
+            }
+        }
+        //FIXME: System.arraycopy(choices, 0 , new String[choices.length], 0, choices.length);
+        this.choices = choices;
+
+    }
+
+    private void setPrompt(String prompt) {
+        if (prompt == null || prompt.length() == 0) {
+            throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+        }
+        this.prompt = prompt;
+    }
+
+    private void setDefaultChoice(int defaultChoice) {
+        if (0 > defaultChoice || defaultChoice >= choices.length) {
+            throw new IllegalArgumentException("auth.1D"); //$NON-NLS-1$
+        }
+        this.defaultChoice = defaultChoice;
+    }
+
+    public ChoiceCallback(String prompt, String[] choices, int defaultChoice,
+            boolean multipleSelectionsAllowed) {
+        super();
+        setPrompt(prompt);
+        setChoices(choices);
+        setDefaultChoice(defaultChoice);
+        this.multipleSelectionsAllowed = multipleSelectionsAllowed;
+    }
+
+    public boolean allowMultipleSelections() {
+        return multipleSelectionsAllowed;
+    }
+
+    public String[] getChoices() {
+        return choices;
+    }
+
+    public int getDefaultChoice() {
+        return defaultChoice;
+    }
+
+    public String getPrompt() {
+        return prompt;
+    }
+
+    public int[] getSelectedIndexes() {
+        return selections;
+    }
+
+    public void setSelectedIndex(int selection) {
+        this.selections = new int[1];
+        this.selections[0] = selection;
+    }
+
+    public void setSelectedIndexes(int[] selections) {
+        if (!multipleSelectionsAllowed) {
+            throw new UnsupportedOperationException();
+        }
+        this.selections = selections;
+        //FIXME: 
+        // this.selections = new int[selections.length]
+        //System.arraycopy(selections, 0, this.selections, 0, this.selections.length);
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/ConfirmationCallback.java b/src/org/apache/harmony/javax/security/auth/callback/ConfirmationCallback.java
new file mode 100644
index 0000000..a1893f3
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/ConfirmationCallback.java
@@ -0,0 +1,234 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+
+
+
+public class ConfirmationCallback implements Callback, Serializable {
+
+    private static final long serialVersionUID = -9095656433782481624L;
+
+    public static final int YES = 0; // default options
+
+    public static final int NO = 1;
+
+    public static final int CANCEL = 2;
+
+    public static final int OK = 3;
+
+    public static final int YES_NO_OPTION = 0; // options type
+
+    public static final int YES_NO_CANCEL_OPTION = 1;
+
+    public static final int OK_CANCEL_OPTION = 2;
+
+    public static final int UNSPECIFIED_OPTION = -1;
+
+    public static final int INFORMATION = 0; // messages type
+
+    public static final int WARNING = 1;
+
+    public static final int ERROR = 2;
+
+    private String prompt;
+
+    private int messageType;
+
+    private int optionType = UNSPECIFIED_OPTION;
+
+    private int defaultOption;
+
+    private String[] options;
+
+    private int selection;
+
+    public ConfirmationCallback(int messageType, int optionType, int defaultOption) {
+        super();
+        if (messageType > ERROR || messageType < INFORMATION) {
+            throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$
+        }
+
+        switch (optionType) {
+            case YES_NO_OPTION:
+                if (defaultOption != YES && defaultOption != NO) {
+                    throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+                }
+                break;
+            case YES_NO_CANCEL_OPTION:
+                if (defaultOption != YES && defaultOption != NO && defaultOption != CANCEL) {
+                    throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+                }
+                break;
+            case OK_CANCEL_OPTION:
+                if (defaultOption != OK && defaultOption != CANCEL) {
+                    throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("auth.18"); //$NON-NLS-1$
+        }
+        this.messageType = messageType;
+        this.optionType = optionType;
+        this.defaultOption = defaultOption;
+    }
+
+    public ConfirmationCallback(int messageType, String[] options, int defaultOption) {
+        super();
+        if (messageType > ERROR || messageType < INFORMATION) {
+            throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$
+        }
+
+        if (options == null || options.length == 0) {
+            throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$
+        }
+        for (int i = 0; i < options.length; i++) {
+            if (options[i] == null || options[i].length() == 0) {
+                throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$
+            }
+        }
+        if (0 > defaultOption || defaultOption >= options.length) {
+            throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+        }
+        // FIXME:System.arraycopy(options, 0 , new String[this.options.length],
+        // 0, this.options.length);
+        this.options = options;
+        this.defaultOption = defaultOption;
+        this.messageType = messageType;
+    }
+
+    public ConfirmationCallback(String prompt, int messageType, int optionType,
+            int defaultOption) {
+        super();
+        if (prompt == null || prompt.length() == 0) {
+            throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+        }
+
+        if (messageType > ERROR || messageType < INFORMATION) {
+            throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$
+        }
+
+        switch (optionType) {
+            case YES_NO_OPTION:
+                if (defaultOption != YES && defaultOption != NO) {
+                    throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+                }
+                break;
+            case YES_NO_CANCEL_OPTION:
+                if (defaultOption != YES && defaultOption != NO && defaultOption != CANCEL) {
+                    throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+                }
+                break;
+            case OK_CANCEL_OPTION:
+                if (defaultOption != OK && defaultOption != CANCEL) {
+                    throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("auth.18"); //$NON-NLS-1$
+        }
+        this.prompt = prompt;
+        this.messageType = messageType;
+        this.optionType = optionType;
+        this.defaultOption = defaultOption;
+    }
+
+    public ConfirmationCallback(String prompt, int messageType, String[] options,
+            int defaultOption) {
+        super();
+        if (prompt == null || prompt.length() == 0) {
+            throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+        }
+
+        if (messageType > ERROR || messageType < INFORMATION) {
+            throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$
+        }
+
+        if (options == null || options.length == 0) {
+            throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$
+        }
+        for (int i = 0; i < options.length; i++) {
+            if (options[i] == null || options[i].length() == 0) {
+                throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$
+            }
+        }
+        if (0 > defaultOption || defaultOption >= options.length) {
+            throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+        }
+        // FIXME:System.arraycopy(options, 0 , new String[this.options.length],
+        // 0, this.options.length);
+        this.options = options;
+        this.defaultOption = defaultOption;
+        this.messageType = messageType;
+        this.prompt = prompt;
+    }
+
+    public String getPrompt() {
+        return prompt;
+    }
+
+    public int getMessageType() {
+        return messageType;
+    }
+
+    public int getDefaultOption() {
+        return defaultOption;
+    }
+
+    public String[] getOptions() {
+        return options;
+    }
+
+    public int getOptionType() {
+        return optionType;
+    }
+
+    public int getSelectedIndex() {
+        return selection;
+    }
+
+    public void setSelectedIndex(int selection) {
+        if (options != null) {
+            if (0 <= selection && selection <= options.length) {
+                this.selection = selection;
+            } else {
+                throw new ArrayIndexOutOfBoundsException("auth.1B"); //$NON-NLS-1$
+            }
+        } else {
+            switch (optionType) {
+                case YES_NO_OPTION:
+                    if (selection != YES && selection != NO) {
+                        throw new IllegalArgumentException("auth.19"); //$NON-NLS-1$
+                    }
+                    break;
+                case YES_NO_CANCEL_OPTION:
+                    if (selection != YES && selection != NO && selection != CANCEL) {
+                        throw new IllegalArgumentException("auth.19"); //$NON-NLS-1$
+                    }
+                    break;
+                case OK_CANCEL_OPTION:
+                    if (selection != OK && selection != CANCEL) {
+                        throw new IllegalArgumentException("auth.19"); //$NON-NLS-1$
+                    }
+                    break;
+            }
+            this.selection = selection;
+        }
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/LanguageCallback.java b/src/org/apache/harmony/javax/security/auth/callback/LanguageCallback.java
new file mode 100644
index 0000000..729bb49
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/LanguageCallback.java
@@ -0,0 +1,41 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+public class LanguageCallback implements Callback, Serializable {
+
+    private static final long serialVersionUID = 2019050433478903213L;
+
+    private Locale locale;
+
+    public LanguageCallback() {
+        super();
+    }
+
+    public Locale getLocale() {
+        return locale;
+    }
+
+    public void setLocale(Locale locale) {
+        this.locale = locale;
+    }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/NameCallback.java b/src/org/apache/harmony/javax/security/auth/callback/NameCallback.java
new file mode 100644
index 0000000..97264ef
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/NameCallback.java
@@ -0,0 +1,74 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+
+
+
+public class NameCallback implements Callback, Serializable {
+
+    private static final long serialVersionUID = 3770938795909392253L;
+
+    private String prompt;
+
+    private String defaultName;
+
+    private String inputName;
+
+    private void setPrompt(String prompt) {
+        if (prompt == null || prompt.length() == 0) {
+            throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+        }
+        this.prompt = prompt;
+    }
+
+    private void setDefaultName(String defaultName) {
+        if (defaultName == null || defaultName.length() == 0) {
+            throw new IllegalArgumentException("auth.1E"); //$NON-NLS-1$
+        }
+        this.defaultName = defaultName;
+    }
+
+    public NameCallback(String prompt) {
+        super();
+        setPrompt(prompt);
+    }
+
+    public NameCallback(String prompt, String defaultName) {
+        super();
+        setPrompt(prompt);
+        setDefaultName(defaultName);
+    }
+
+    public String getPrompt() {
+        return prompt;
+    }
+
+    public String getDefaultName() {
+        return defaultName;
+    }
+
+    public void setName(String name) {
+        this.inputName = name;
+    }
+
+    public String getName() {
+        return inputName;
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/PasswordCallback.java b/src/org/apache/harmony/javax/security/auth/callback/PasswordCallback.java
new file mode 100644
index 0000000..bd142fc
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/PasswordCallback.java
@@ -0,0 +1,124 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+
+
+/**
+ * Is used in conjunction with a {@link CallbackHandler} to retrieve a password
+ * when needed.
+ */
+public class PasswordCallback implements Callback, Serializable {
+
+    private static final long serialVersionUID = 2267422647454909926L;
+
+    private String prompt;
+
+    boolean echoOn;
+
+    private char[] inputPassword;
+
+    private void setPrompt(String prompt) throws IllegalArgumentException {
+        if (prompt == null || prompt.length() == 0) {
+            throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+        }
+        this.prompt = prompt;
+    }
+
+    /**
+     * Creates a new {@code PasswordCallback} instance.
+     *
+     * @param prompt
+     *            the message that should be displayed to the user
+     * @param echoOn
+     *            determines whether the user input should be echoed
+     */
+    public PasswordCallback(String prompt, boolean echoOn) {
+        super();
+        setPrompt(prompt);
+        this.echoOn = echoOn;
+    }
+
+    /**
+     * Returns the prompt that was specified when creating this {@code
+     * PasswordCallback}
+     *
+     * @return the prompt
+     */
+    public String getPrompt() {
+        return prompt;
+    }
+
+    /**
+     * Queries whether this {@code PasswordCallback} expects user input to be
+     * echoed, which is specified during the creation of the object.
+     *
+     * @return {@code true} if (and only if) user input should be echoed
+     */
+    public boolean isEchoOn() {
+        return echoOn;
+    }
+
+    /**
+     * Sets the password. The {@link CallbackHandler} that performs the actual
+     * provisioning or input of the password needs to call this method to hand
+     * back the password to the security service that requested it.
+     *
+     * @param password
+     *            the password. A copy of this is stored, so subsequent changes
+     *            to the input array do not affect the {@code PasswordCallback}.
+     */
+    public void setPassword(char[] password) {
+        if (password == null) {
+            this.inputPassword = password;
+        } else {
+            inputPassword = new char[password.length];
+            System.arraycopy(password, 0, inputPassword, 0, inputPassword.length);
+        }
+    }
+
+    /**
+     * Returns the password. The security service that needs the password
+     * usually calls this method once the {@link CallbackHandler} has finished
+     * its work.
+     *
+     * @return the password. A copy of the internal password is created and
+     *         returned, so subsequent changes to the internal password do not
+     *         affect the result.
+     */
+    public char[] getPassword() {
+        if (inputPassword != null) {
+            char[] tmp = new char[inputPassword.length];
+            System.arraycopy(inputPassword, 0, tmp, 0, tmp.length);
+            return tmp;
+        }
+        return null;
+    }
+
+    /**
+     * Clears the password stored in this {@code PasswordCallback}.
+     */
+    public void clearPassword() {
+        if (inputPassword != null) {
+            Arrays.fill(inputPassword, '\u0000');
+        }
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/TextInputCallback.java b/src/org/apache/harmony/javax/security/auth/callback/TextInputCallback.java
new file mode 100644
index 0000000..c7de222
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/TextInputCallback.java
@@ -0,0 +1,74 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+
+
+
+public class TextInputCallback implements Callback, Serializable {
+
+    private static final long serialVersionUID = -8064222478852811804L;
+
+    private String defaultText;
+
+    private String prompt;
+
+    private String inputText;
+
+    private void setPrompt(String prompt) {
+        if (prompt == null || prompt.length() == 0) {
+            throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+        }
+        this.prompt = prompt;
+    }
+
+    private void setDefaultText(String defaultText) {
+        if (defaultText == null || defaultText.length() == 0) {
+            throw new IllegalArgumentException("auth.15"); //$NON-NLS-1$
+        }
+        this.defaultText = defaultText;
+    }
+
+    public TextInputCallback(String prompt) {
+        super();
+        setPrompt(prompt);
+    }
+
+    public TextInputCallback(String prompt, String defaultText) {
+        super();
+        setPrompt(prompt);
+        setDefaultText(defaultText);
+    }
+
+    public String getDefaultText() {
+        return defaultText;
+    }
+
+    public String getPrompt() {
+        return prompt;
+    }
+
+    public String getText() {
+        return inputText;
+    }
+
+    public void setText(String text) {
+        this.inputText = text;
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/TextOutputCallback.java b/src/org/apache/harmony/javax/security/auth/callback/TextOutputCallback.java
new file mode 100644
index 0000000..23a72fa
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/TextOutputCallback.java
@@ -0,0 +1,56 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+
+
+
+public class TextOutputCallback implements Callback, Serializable {
+
+    private static final long serialVersionUID = 1689502495511663102L;
+
+    public static final int INFORMATION = 0;
+
+    public static final int WARNING = 1;
+
+    public static final int ERROR = 2;
+
+    private String message;
+
+    private int messageType;
+
+    public TextOutputCallback(int messageType, String message) {
+        if (messageType > ERROR || messageType < INFORMATION) {
+            throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$
+        }
+        if (message == null || message.length() == 0) {
+            throw new IllegalArgumentException("auth.1F"); //$NON-NLS-1$
+        }
+        this.messageType = messageType;
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public int getMessageType() {
+        return messageType;
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/UnsupportedCallbackException.java b/src/org/apache/harmony/javax/security/auth/callback/UnsupportedCallbackException.java
new file mode 100644
index 0000000..19f6e40
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/UnsupportedCallbackException.java
@@ -0,0 +1,64 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+/**
+ * Thrown when a {@link CallbackHandler} does not support a particular {@link
+ * Callback}.
+ */
+public class UnsupportedCallbackException extends Exception {
+
+    private static final long serialVersionUID = -6873556327655666839L;
+
+    private Callback callback;
+
+    /**
+     * Creates a new exception instance and initializes it with just the
+     * unsupported {@code Callback}, but no error message.
+     *
+     * @param callback
+     *            the {@code Callback}
+     */
+    public UnsupportedCallbackException(Callback callback) {
+        super();
+        this.callback = callback;
+    }
+
+    /**
+     * Creates a new exception instance and initializes it with both the
+     * unsupported {@code Callback} and an error message.
+     *
+     * @param callback
+     *            the {@code Callback}
+     * @param message
+     *            the error message
+     */
+    public UnsupportedCallbackException(Callback callback, String message) {
+        super(message);
+        this.callback = callback;
+    }
+
+    /**
+     * Returns the unsupported {@code Callback} that triggered this exception.
+     *
+     * @return the {@code Callback}
+     */
+    public Callback getCallback() {
+        return callback;
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/AccountException.java b/src/org/apache/harmony/javax/security/auth/login/AccountException.java
new file mode 100644
index 0000000..c86e801
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/AccountException.java
@@ -0,0 +1,31 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class AccountException extends LoginException {
+
+    private static final long serialVersionUID = -2112878680072211787L;
+
+    public AccountException() {
+        super();
+    }
+
+    public AccountException(String message) {
+        super(message);
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/AccountExpiredException.java b/src/org/apache/harmony/javax/security/auth/login/AccountExpiredException.java
new file mode 100644
index 0000000..1e0ce4d
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/AccountExpiredException.java
@@ -0,0 +1,31 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class AccountExpiredException extends AccountException {
+
+    private static final long serialVersionUID = -6064064890162661560L;
+
+    public AccountExpiredException() {
+        super();
+    }
+
+    public AccountExpiredException(String message) {
+        super(message);
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/AccountLockedException.java b/src/org/apache/harmony/javax/security/auth/login/AccountLockedException.java
new file mode 100644
index 0000000..47913a5
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/AccountLockedException.java
@@ -0,0 +1,32 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class AccountLockedException extends AccountException {
+
+    private static final long serialVersionUID = 8280345554014066334L;
+
+    public AccountLockedException() {
+        super();
+    }
+
+    public AccountLockedException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/AccountNotFoundException.java b/src/org/apache/harmony/javax/security/auth/login/AccountNotFoundException.java
new file mode 100644
index 0000000..8ca9b96
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/AccountNotFoundException.java
@@ -0,0 +1,32 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class AccountNotFoundException extends AccountException {
+
+    private static final long serialVersionUID = 1498349563916294614L;
+
+    public AccountNotFoundException() {
+        super();
+    }
+
+    public AccountNotFoundException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/AppConfigurationEntry.java b/src/org/apache/harmony/javax/security/auth/login/AppConfigurationEntry.java
new file mode 100644
index 0000000..2a735dc
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/AppConfigurationEntry.java
@@ -0,0 +1,95 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+import java.util.Collections;
+import java.util.Map;
+
+
+
+public class AppConfigurationEntry {
+
+    // the login module options
+    private final Map<String, ?> options;
+
+    // the control flag
+    private final AppConfigurationEntry.LoginModuleControlFlag controlFlag;
+
+    // the login module name 
+    private final String loginModuleName;
+
+    public AppConfigurationEntry(String loginModuleName,
+            AppConfigurationEntry.LoginModuleControlFlag controlFlag, Map<String, ?> options) {
+
+        if (loginModuleName == null || loginModuleName.length() == 0) {
+            throw new IllegalArgumentException("auth.26"); //$NON-NLS-1$
+        }
+
+        if (controlFlag == null) {
+            throw new IllegalArgumentException("auth.27"); //$NON-NLS-1$
+        }
+
+        if (options == null) {
+            throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$
+        }
+
+        this.loginModuleName = loginModuleName;
+        this.controlFlag = controlFlag;
+        this.options = Collections.unmodifiableMap(options);
+    }
+
+    public String getLoginModuleName() {
+        return loginModuleName;
+    }
+
+    public LoginModuleControlFlag getControlFlag() {
+        return controlFlag;
+    }
+
+    public Map<java.lang.String, ?> getOptions() {
+        return options;
+    }
+
+    public static class LoginModuleControlFlag {
+
+        // the control flag
+        private final String flag;
+
+        public static final LoginModuleControlFlag REQUIRED = new LoginModuleControlFlag(
+                "LoginModuleControlFlag: required"); //$NON-NLS-1$
+
+        public static final LoginModuleControlFlag REQUISITE = new LoginModuleControlFlag(
+                "LoginModuleControlFlag: requisite"); //$NON-NLS-1$
+
+        public static final LoginModuleControlFlag OPTIONAL = new LoginModuleControlFlag(
+                "LoginModuleControlFlag: optional"); //$NON-NLS-1$
+
+        public static final LoginModuleControlFlag SUFFICIENT = new LoginModuleControlFlag(
+                "LoginModuleControlFlag: sufficient"); //$NON-NLS-1$
+
+        // Creates the LoginModuleControlFlag object with specified a flag
+        private LoginModuleControlFlag(String flag) {
+            this.flag = flag;
+        }
+
+        @Override
+        public String toString() {
+            return flag;
+        }
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/Configuration.java b/src/org/apache/harmony/javax/security/auth/login/Configuration.java
new file mode 100644
index 0000000..74c371f
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/Configuration.java
@@ -0,0 +1,102 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+import java.security.AccessController;
+import org.apache.harmony.javax.security.auth.AuthPermission;
+
+public abstract class Configuration {
+
+    // the current configuration 
+    private static Configuration configuration;
+
+    // creates a AuthPermission object with a specify property
+    private static final AuthPermission GET_LOGIN_CONFIGURATION = new AuthPermission(
+            "getLoginConfiguration"); //$NON-NLS-1$
+
+    // creates a AuthPermission object with a specify property
+    private static final AuthPermission SET_LOGIN_CONFIGURATION = new AuthPermission(
+            "setLoginConfiguration"); //$NON-NLS-1$
+
+    // Key to security properties, defining default configuration provider.
+    private static final String LOGIN_CONFIGURATION_PROVIDER = "login.configuration.provider"; //$NON-NLS-1$
+
+    protected Configuration() {
+        super();
+    }
+
+    public static Configuration getConfiguration() {
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            sm.checkPermission(GET_LOGIN_CONFIGURATION);
+        }
+        return getAccessibleConfiguration();
+    }
+
+    /**
+     * Reads name of default configuration provider from security.properties,
+     * loads the class and instantiates the provider.<br> In case of any
+     * exception, wraps it with SecurityException and throws further.
+     */
+    private static final Configuration getDefaultProvider() {
+        return new Configuration() {
+			
+			@Override
+			public void refresh() {
+			}
+			
+			@Override
+			public AppConfigurationEntry[] getAppConfigurationEntry(
+					String applicationName) {
+				return new AppConfigurationEntry[0];
+			}
+		};
+    }
+
+    /**
+     * Shortcut accessor for friendly classes, to skip security checks.
+     * If active configuration was set to <code>null</code>, tries to load a default 
+     * provider, so this method never returns <code>null</code>. <br>
+     * This method is synchronized with setConfiguration()
+     */
+    static Configuration getAccessibleConfiguration() {
+        Configuration current = configuration;
+        if (current == null) {
+            synchronized (Configuration.class) {
+                if (configuration == null) {
+                    configuration = getDefaultProvider();
+                }
+                return configuration;
+            }
+        }
+        return current;
+    }
+
+    public static void setConfiguration(Configuration configuration) {
+        SecurityManager sm = System.getSecurityManager();
+        if (sm != null) {
+            sm.checkPermission(SET_LOGIN_CONFIGURATION);
+        }
+        Configuration.configuration = configuration;
+    }
+
+    public abstract AppConfigurationEntry[] getAppConfigurationEntry(String applicationName);
+
+    public abstract void refresh();
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/CredentialException.java b/src/org/apache/harmony/javax/security/auth/login/CredentialException.java
new file mode 100644
index 0000000..e74e866
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/CredentialException.java
@@ -0,0 +1,32 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class CredentialException extends LoginException {
+
+    private static final long serialVersionUID = -4772893876810601859L;
+
+    public CredentialException() {
+        super();
+    }
+
+    public CredentialException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/CredentialExpiredException.java b/src/org/apache/harmony/javax/security/auth/login/CredentialExpiredException.java
new file mode 100644
index 0000000..3ca3ad7
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/CredentialExpiredException.java
@@ -0,0 +1,32 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class CredentialExpiredException extends CredentialException {
+
+    private static final long serialVersionUID = -5344739593859737937L;
+
+    public CredentialExpiredException() {
+        super();
+    }
+
+    public CredentialExpiredException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/CredentialNotFoundException.java b/src/org/apache/harmony/javax/security/auth/login/CredentialNotFoundException.java
new file mode 100644
index 0000000..ffd529f
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/CredentialNotFoundException.java
@@ -0,0 +1,32 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class CredentialNotFoundException extends CredentialException {
+
+    private static final long serialVersionUID = -7779934467214319475L;
+
+    public CredentialNotFoundException() {
+        super();
+    }
+
+    public CredentialNotFoundException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/FailedLoginException.java b/src/org/apache/harmony/javax/security/auth/login/FailedLoginException.java
new file mode 100644
index 0000000..f689d99
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/FailedLoginException.java
@@ -0,0 +1,32 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class FailedLoginException extends LoginException {
+
+    private static final long serialVersionUID = 802556922354616286L;
+
+    public FailedLoginException() {
+        super();
+    }
+
+    public FailedLoginException(String message) {
+        super(message);
+    }
+
+}
\ No newline at end of file
diff --git a/src/org/apache/harmony/javax/security/auth/login/LoginContext.java b/src/org/apache/harmony/javax/security/auth/login/LoginContext.java
new file mode 100644
index 0000000..7d46278
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/LoginContext.java
@@ -0,0 +1,548 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+import java.io.IOException;
+import java.security.AccessController;
+import java.security.AccessControlContext;
+import java.security.PrivilegedExceptionAction;
+import java.security.PrivilegedActionException;
+
+import java.security.Security;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.harmony.javax.security.auth.Subject;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
+import org.apache.harmony.javax.security.auth.spi.LoginModule;
+import org.apache.harmony.javax.security.auth.AuthPermission;
+
+import org.apache.harmony.javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
+
+
+
+public class LoginContext {
+
+    private static final String DEFAULT_CALLBACK_HANDLER_PROPERTY = "auth.login.defaultCallbackHandler"; //$NON-NLS-1$
+
+    /*
+     * Integer constants which serve as a replacement for the corresponding
+     * LoginModuleControlFlag.* constants. These integers are used later as
+     * index in the arrays - see loginImpl() and logoutImpl() methods
+     */
+    private static final int OPTIONAL = 0;
+
+    private static final int REQUIRED = 1;
+
+    private static final int REQUISITE = 2;
+
+    private static final int SUFFICIENT = 3;
+
+    // Subject to be used for this LoginContext's operations
+    private Subject subject;
+
+    /*
+     * Shows whether the subject was specified by user (true) or was created by
+     * this LoginContext itself (false).
+     */
+    private boolean userProvidedSubject;
+
+    // Shows whether we use installed or user-provided Configuration
+    private boolean userProvidedConfig;
+
+    // An user's AccessControlContext, used when user specifies 
+    private AccessControlContext userContext;
+
+    /*
+     * Either a callback handler passed by the user or a wrapper for the user's
+     * specified handler - see init() below.
+     */
+    private CallbackHandler callbackHandler;
+
+    /*
+     * An array which keeps the instantiated and init()-ialized login modules
+     * and their states
+     */
+    private Module[] modules;
+
+    // Stores a shared state
+    private Map<String, ?> sharedState;
+
+    // A context class loader used to load [mainly] LoginModules
+    private ClassLoader contextClassLoader;
+
+    // Shows overall status - whether this LoginContext was successfully logged 
+    private boolean loggedIn;
+
+    public LoginContext(String name) throws LoginException {
+        super();
+        init(name, null, null, null);
+    }
+
+    public LoginContext(String name, CallbackHandler cbHandler) throws LoginException {
+        super();
+        if (cbHandler == null) {
+            throw new LoginException("auth.34"); //$NON-NLS-1$
+        }
+        init(name, null, cbHandler, null);
+    }
+
+    public LoginContext(String name, Subject subject) throws LoginException {
+        super();
+        if (subject == null) {
+            throw new LoginException("auth.03"); //$NON-NLS-1$
+        }
+        init(name, subject, null, null);
+    }
+
+    public LoginContext(String name, Subject subject, CallbackHandler cbHandler)
+            throws LoginException {
+        super();
+        if (subject == null) {
+            throw new LoginException("auth.03"); //$NON-NLS-1$
+        }
+        if (cbHandler == null) {
+            throw new LoginException("auth.34"); //$NON-NLS-1$
+        }
+        init(name, subject, cbHandler, null);
+    }
+
+    public LoginContext(String name, Subject subject, CallbackHandler cbHandler,
+            Configuration config) throws LoginException {
+        super();
+        init(name, subject, cbHandler, config);
+    }
+
+    // Does all the machinery needed for the initialization.
+    private void init(String name, Subject subject, final CallbackHandler cbHandler,
+            Configuration config) throws LoginException {
+        userProvidedSubject = (this.subject = subject) != null;
+
+        //
+        // Set config
+        //
+        if (name == null) {
+            throw new LoginException("auth.00"); //$NON-NLS-1$
+        }
+
+        if (config == null) {
+            config = Configuration.getAccessibleConfiguration();
+        } else {
+            userProvidedConfig = true;
+        }
+
+        SecurityManager sm = System.getSecurityManager();
+
+        if (sm != null && !userProvidedConfig) {
+            sm.checkPermission(new AuthPermission("createLoginContext." + name));//$NON-NLS-1$
+        }
+
+        AppConfigurationEntry[] entries = config.getAppConfigurationEntry(name);
+        if (entries == null) {
+            if (sm != null && !userProvidedConfig) {
+                sm.checkPermission(new AuthPermission("createLoginContext.other")); //$NON-NLS-1$
+            }
+            entries = config.getAppConfigurationEntry("other"); //$NON-NLS-1$
+            if (entries == null) {
+                throw new LoginException("auth.35 " + name); //$NON-NLS-1$
+            }
+        }
+
+        modules = new Module[entries.length];
+        for (int i = 0; i < modules.length; i++) {
+            modules[i] = new Module(entries[i]);
+        }
+        //
+        // Set CallbackHandler and this.contextClassLoader
+        //
+
+        /*
+         * as some of the operations to be executed (i.e. get*ClassLoader,
+         * getProperty, class loading) are security-checked, then combine all of
+         * them into a single doPrivileged() call.
+         */
+        try {
+            AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
+                public Void run() throws Exception {
+                    // First, set the 'contextClassLoader'
+                    contextClassLoader = Thread.currentThread().getContextClassLoader();
+                    if (contextClassLoader == null) {
+                        contextClassLoader = ClassLoader.getSystemClassLoader();
+                    }
+                    // then, checks whether the cbHandler is set
+                    if (cbHandler == null) {
+                        // well, let's try to find it
+                        String klassName = Security
+                                .getProperty(DEFAULT_CALLBACK_HANDLER_PROPERTY);
+                        if (klassName == null || klassName.length() == 0) {
+                            return null;
+                        }
+                        Class<?> klass = Class.forName(klassName, true, contextClassLoader);
+                        callbackHandler = (CallbackHandler) klass.newInstance();
+                    } else {
+                        callbackHandler = cbHandler;
+                    }
+                    return null;
+                }
+            });
+        } catch (PrivilegedActionException ex) {
+            Throwable cause = ex.getCause();
+            throw (LoginException) new LoginException("auth.36").initCause(cause);//$NON-NLS-1$
+        }
+
+        if (userProvidedConfig) {
+            userContext = AccessController.getContext();
+        } else if (callbackHandler != null) {
+            userContext = AccessController.getContext();
+            callbackHandler = new ContextedCallbackHandler(callbackHandler);
+        }
+    }
+
+    public Subject getSubject() {
+        if (userProvidedSubject || loggedIn) {
+            return subject;
+        }
+        return null;
+    }
+
+    /**
+     * Warning: calling the method more than once may result in undefined
+     * behaviour if logout() method is not invoked before.
+     */
+    public void login() throws LoginException {
+        PrivilegedExceptionAction<Void> action = new PrivilegedExceptionAction<Void>() {
+            public Void run() throws LoginException {
+                loginImpl();
+                return null;
+            }
+        };
+        try {
+            if (userProvidedConfig) {
+                AccessController.doPrivileged(action, userContext);
+            } else {
+                AccessController.doPrivileged(action);
+            }
+        } catch (PrivilegedActionException ex) {
+            throw (LoginException) ex.getException();
+        }
+    }
+
+    /**
+     * The real implementation of login() method whose calls are wrapped into
+     * appropriate doPrivileged calls in login().
+     */
+    private void loginImpl() throws LoginException {
+        if (subject == null) {
+            subject = new Subject();
+        }
+
+        if (sharedState == null) {
+            sharedState = new HashMap<String, Object>();
+        }
+
+        // PHASE 1: Calling login()-s
+        Throwable firstProblem = null;
+
+        int[] logged = new int[4];
+        int[] total = new int[4];
+
+        for (Module module : modules) {
+            try {
+                // if a module fails during Class.forName(), then it breaks overall 
+                // attempt - see catch() below
+                module.create(subject, callbackHandler, sharedState);
+
+                if (module.module.login()) {
+                    ++total[module.getFlag()];
+                    ++logged[module.getFlag()];
+                    if (module.getFlag() == SUFFICIENT) {
+                        break;
+                    }
+                }
+            } catch (Throwable ex) {
+                if (firstProblem == null) {
+                    firstProblem = ex;
+                }
+                if (module.klass == null) {
+                    /*
+                     * an exception occurred during class lookup - overall
+                     * attempt must fail a little trick: increase the REQUIRED's
+                     * number - this will look like a failed REQUIRED module
+                     * later, so overall attempt will fail
+                     */
+                    ++total[REQUIRED];
+                    break;
+                }
+                ++total[module.getFlag()];
+                // something happened after the class was loaded
+                if (module.getFlag() == REQUISITE) {
+                    // ... and no need to walk down anymore
+                    break;
+                }
+            }
+        }
+        // end of PHASE1, 
+
+        // Let's decide whether we have either overall success or a total failure
+        boolean fail = true;
+
+        /*
+         * Note: 'failed[xxx]!=0' is not enough to check.
+         * 
+         * Use 'logged[xx] != total[xx]' instead. This is because some modules
+         * might not be counted as 'failed' if an exception occurred during
+         * preload()/Class.forName()-ing. But, such modules still get counted in
+         * the total[].
+         */
+
+        // if any REQ* module failed - then it's failure
+        if (logged[REQUIRED] != total[REQUIRED] || logged[REQUISITE] != total[REQUISITE]) {
+            // fail = true;
+        } else {
+            if (total[REQUIRED] == 0 && total[REQUISITE] == 0) {
+                // neither REQUIRED nor REQUISITE was configured.
+                // must have at least one SUFFICIENT or OPTIONAL
+                if (logged[OPTIONAL] != 0 || logged[SUFFICIENT] != 0) {
+                    fail = false;
+                }
+                //else { fail = true; }
+            } else {
+                fail = false;
+            }
+        }
+
+        int commited[] = new int[4];
+        // clear it
+        total[0] = total[1] = total[2] = total[3] = 0;
+        if (!fail) {
+            // PHASE 2: 
+
+            for (Module module : modules) {
+                if (module.klass != null) {
+                    ++total[module.getFlag()];
+                    try {
+                        module.module.commit();
+                        ++commited[module.getFlag()];
+                    } catch (Throwable ex) {
+                        if (firstProblem == null) {
+                            firstProblem = ex;
+                        }
+                    }
+                }
+            }
+        }
+
+        // need to decide once again
+        fail = true;
+        if (commited[REQUIRED] != total[REQUIRED] || commited[REQUISITE] != total[REQUISITE]) {
+            //fail = true;
+        } else {
+            if (total[REQUIRED] == 0 && total[REQUISITE] == 0) {
+                /*
+                 * neither REQUIRED nor REQUISITE was configured. must have at
+                 * least one SUFFICIENT or OPTIONAL
+                 */
+                if (commited[OPTIONAL] != 0 || commited[SUFFICIENT] != 0) {
+                    fail = false;
+                } else {
+                    //fail = true;
+                }
+            } else {
+                fail = false;
+            }
+        }
+
+        if (fail) {
+            // either login() or commit() failed. aborting...
+
+            for (Module module : modules) {
+                try {
+                    module.module.abort();
+                } catch ( /*LoginException*/Throwable ex) {
+                    if (firstProblem == null) {
+                        firstProblem = ex;
+                    }
+                }
+            }
+            if (firstProblem instanceof PrivilegedActionException
+                    && firstProblem.getCause() != null) {
+                firstProblem = firstProblem.getCause();
+            }
+            if (firstProblem instanceof LoginException) {
+                throw (LoginException) firstProblem;
+            }
+            throw (LoginException) new LoginException("auth.37").initCause(firstProblem); //$NON-NLS-1$
+        }
+        loggedIn = true;
+    }
+
+    public void logout() throws LoginException {
+        PrivilegedExceptionAction<Void> action = new PrivilegedExceptionAction<Void>() {
+            public Void run() throws LoginException {
+                logoutImpl();
+                return null;
+            }
+        };
+        try {
+            if (userProvidedConfig) {
+                AccessController.doPrivileged(action, userContext);
+            } else {
+                AccessController.doPrivileged(action);
+            }
+        } catch (PrivilegedActionException ex) {
+            throw (LoginException) ex.getException();
+        }
+    }
+
+    /**
+     * The real implementation of logout() method whose calls are wrapped into
+     * appropriate doPrivileged calls in logout().
+     */
+    private void logoutImpl() throws LoginException {
+        if (subject == null) {
+            throw new LoginException("auth.38"); //$NON-NLS-1$
+        }
+        loggedIn = false;
+        Throwable firstProblem = null;
+        int total = 0;
+        for (Module module : modules) {
+            try {
+                module.module.logout();
+                ++total;
+            } catch (Throwable ex) {
+                if (firstProblem == null) {
+                    firstProblem = ex;
+                }
+            }
+        }
+        if (firstProblem != null || total == 0) {
+            if (firstProblem instanceof PrivilegedActionException
+                    && firstProblem.getCause() != null) {
+                firstProblem = firstProblem.getCause();
+            }
+            if (firstProblem instanceof LoginException) {
+                throw (LoginException) firstProblem;
+            }
+            throw (LoginException) new LoginException("auth.37").initCause(firstProblem); //$NON-NLS-1$
+        }
+    }
+
+    /**
+     * <p>A class that servers as a wrapper for the CallbackHandler when we use
+     * installed Configuration, but not a passed one. See API docs on the
+     * LoginContext.</p>
+     * 
+     * <p>Simply invokes the given handler with the given AccessControlContext.</p>
+     */
+    private class ContextedCallbackHandler implements CallbackHandler {
+        private final CallbackHandler hiddenHandlerRef;
+
+        ContextedCallbackHandler(CallbackHandler handler) {
+            super();
+            this.hiddenHandlerRef = handler;
+        }
+
+        public void handle(final Callback[] callbacks) throws IOException,
+                UnsupportedCallbackException {
+            try {
+                AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
+                    public Void run() throws IOException, UnsupportedCallbackException {
+                        hiddenHandlerRef.handle(callbacks);
+                        return null;
+                    }
+                }, userContext);
+            } catch (PrivilegedActionException ex) {
+                if (ex.getCause() instanceof UnsupportedCallbackException) {
+                    throw (UnsupportedCallbackException) ex.getCause();
+                }
+                throw (IOException) ex.getCause();
+            }
+        }
+    }
+
+    /** 
+     * A private class that stores an instantiated LoginModule.
+     */
+    private final class Module {
+
+        // An initial info about the module to be used
+        AppConfigurationEntry entry;
+
+        // A mapping of LoginModuleControlFlag onto a simple int constant
+        int flag;
+
+        // The LoginModule itself 
+        LoginModule module;
+
+        // A class of the module
+        Class<?> klass;
+
+        Module(AppConfigurationEntry entry) {
+            this.entry = entry;
+            LoginModuleControlFlag flg = entry.getControlFlag();
+            if (flg == LoginModuleControlFlag.OPTIONAL) {
+                flag = OPTIONAL;
+            } else if (flg == LoginModuleControlFlag.REQUISITE) {
+                flag = REQUISITE;
+            } else if (flg == LoginModuleControlFlag.SUFFICIENT) {
+                flag = SUFFICIENT;
+            } else {
+                flag = REQUIRED;
+                //if(flg!=LoginModuleControlFlag.REQUIRED) throw new Error()
+            }
+        }
+
+        int getFlag() {
+            return flag;
+        }
+
+        /**
+         * Loads class of the LoginModule, instantiates it and then calls
+         * initialize().
+         */
+        void create(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState)
+                throws LoginException {
+            String klassName = entry.getLoginModuleName();
+            if (klass == null) {
+                try {
+                    klass = Class.forName(klassName, false, contextClassLoader);
+                } catch (ClassNotFoundException ex) {
+                    throw (LoginException) new LoginException(
+                            "auth.39 " + klassName).initCause(ex); //$NON-NLS-1$
+                }
+            }
+
+            if (module == null) {
+                try {
+                    module = (LoginModule) klass.newInstance();
+                } catch (IllegalAccessException ex) {
+                    throw (LoginException) new LoginException(
+                            "auth.3A " + klassName) //$NON-NLS-1$
+                            .initCause(ex);
+                } catch (InstantiationException ex) {
+                    throw (LoginException) new LoginException(
+                            "auth.3A" + klassName) //$NON-NLS-1$
+                            .initCause(ex);
+                }
+                module.initialize(subject, callbackHandler, sharedState, entry.getOptions());
+            }
+        }
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/LoginException.java b/src/org/apache/harmony/javax/security/auth/login/LoginException.java
new file mode 100644
index 0000000..e9ea566
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/LoginException.java
@@ -0,0 +1,45 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Base class for exceptions that are thrown when a login error occurs.
+ */
+public class LoginException extends GeneralSecurityException {
+
+    private static final long serialVersionUID = -4679091624035232488L;
+
+    /**
+     * Creates a new exception instance and initializes it with default values.
+     */
+    public LoginException() {
+        super();
+    }
+
+    /**
+     * Creates a new exception instance and initializes it with a given message.
+     *
+     * @param message the error message
+     */
+    public LoginException(String message) {
+        super(message);
+    }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/spi/LoginModule.java b/src/org/apache/harmony/javax/security/auth/spi/LoginModule.java
new file mode 100644
index 0000000..3ed9eb2
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/spi/LoginModule.java
@@ -0,0 +1,38 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.spi;
+
+import java.util.Map;
+
+import org.apache.harmony.javax.security.auth.Subject;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.login.LoginException;
+
+public interface LoginModule {
+
+    void initialize(Subject subject, CallbackHandler callbackHandler,
+            Map<String, ?> sharedState, Map<String, ?> options);
+
+    boolean login() throws LoginException;
+
+    boolean commit() throws LoginException;
+
+    boolean abort() throws LoginException;
+
+    boolean logout() throws LoginException;
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/AuthenticationException.java b/src/org/apache/harmony/javax/security/sasl/AuthenticationException.java
new file mode 100644
index 0000000..38703ef
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/AuthenticationException.java
@@ -0,0 +1,35 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+public class AuthenticationException extends SaslException {
+
+    private static final long serialVersionUID = -3579708765071815007L;
+
+    public AuthenticationException() {
+        super();
+    }
+
+    public AuthenticationException(String detail) {
+        super(detail);
+    }
+
+    public AuthenticationException(String detail, Throwable ex) {
+        super(detail, ex);
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/AuthorizeCallback.java b/src/org/apache/harmony/javax/security/sasl/AuthorizeCallback.java
new file mode 100644
index 0000000..2ba90a2
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/AuthorizeCallback.java
@@ -0,0 +1,79 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import java.io.Serializable;
+import org.apache.harmony.javax.security.auth.callback.Callback;
+
+public class AuthorizeCallback implements Callback, Serializable {
+
+    private static final long serialVersionUID = -2353344186490470805L;
+
+    /**
+     * Serialized field for storing authenticationID.
+     */
+    private final String authenticationID;
+
+    /**
+     * Serialized field for storing authorizationID.
+     */
+    private final String authorizationID;
+
+    /**
+     * Serialized field for storing authorizedID.
+     */
+    private String authorizedID;
+
+    /**
+     * Store authorized Serialized field.
+     */
+    private boolean authorized;
+
+    public AuthorizeCallback(String authnID, String authzID) {
+        super();
+        authenticationID = authnID;
+        authorizationID = authzID;
+        authorizedID = authzID;
+    }
+
+    public String getAuthenticationID() {
+        return authenticationID;
+    }
+
+    public String getAuthorizationID() {
+        return authorizationID;
+    }
+
+    public String getAuthorizedID() {
+        return (authorized ? authorizedID : null);
+    }
+
+    public boolean isAuthorized() {
+        return authorized;
+    }
+
+    public void setAuthorized(boolean ok) {
+        authorized = ok;
+    }
+
+    public void setAuthorizedID(String id) {
+        if (id != null) {
+            authorizedID = id;
+        }
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/RealmCallback.java b/src/org/apache/harmony/javax/security/sasl/RealmCallback.java
new file mode 100644
index 0000000..65b5d15
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/RealmCallback.java
@@ -0,0 +1,33 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.TextInputCallback;
+
+public class RealmCallback extends TextInputCallback {
+
+    private static final long serialVersionUID = -4342673378785456908L;
+
+    public RealmCallback(String prompt) {
+        super(prompt);
+    }
+
+    public RealmCallback(String prompt, String defaultRealmInfo) {
+        super(prompt, defaultRealmInfo);
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/RealmChoiceCallback.java b/src/org/apache/harmony/javax/security/sasl/RealmChoiceCallback.java
new file mode 100644
index 0000000..079ea07
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/RealmChoiceCallback.java
@@ -0,0 +1,30 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.ChoiceCallback;
+
+public class RealmChoiceCallback extends ChoiceCallback {
+
+    private static final long serialVersionUID = -8588141348846281332L;
+
+    public RealmChoiceCallback(String prompt, String[] choices, int defaultChoice,
+            boolean multiple) {
+        super(prompt, choices, defaultChoice, multiple);
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/Sasl.java b/src/org/apache/harmony/javax/security/sasl/Sasl.java
new file mode 100644
index 0000000..4d827f8
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/Sasl.java
@@ -0,0 +1,204 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import java.security.Provider;
+import java.security.Security;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+
+
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.HashSet;
+import java.util.Iterator;
+
+public class Sasl {
+    // SaslClientFactory service name
+    private static final String CLIENTFACTORYSRV = "SaslClientFactory"; //$NON-NLS-1$
+
+    // SaslServerFactory service name
+    private static final String SERVERFACTORYSRV = "SaslServerFactory"; //$NON-NLS-1$
+
+    public static final String POLICY_NOPLAINTEXT = "javax.security.sasl.policy.noplaintext"; //$NON-NLS-1$
+
+    public static final String POLICY_NOACTIVE = "javax.security.sasl.policy.noactive"; //$NON-NLS-1$
+
+    public static final String POLICY_NODICTIONARY = "javax.security.sasl.policy.nodictionary"; //$NON-NLS-1$
+
+    public static final String POLICY_NOANONYMOUS = "javax.security.sasl.policy.noanonymous"; //$NON-NLS-1$
+
+    public static final String POLICY_FORWARD_SECRECY = "javax.security.sasl.policy.forward"; //$NON-NLS-1$
+
+    public static final String POLICY_PASS_CREDENTIALS = "javax.security.sasl.policy.credentials"; //$NON-NLS-1$
+
+    public static final String MAX_BUFFER = "javax.security.sasl.maxbuffer"; //$NON-NLS-1$
+
+    public static final String RAW_SEND_SIZE = "javax.security.sasl.rawsendsize"; //$NON-NLS-1$
+
+    public static final String REUSE = "javax.security.sasl.reuse"; //$NON-NLS-1$
+
+    public static final String QOP = "javax.security.sasl.qop"; //$NON-NLS-1$
+
+    public static final String STRENGTH = "javax.security.sasl.strength"; //$NON-NLS-1$
+
+    public static final String SERVER_AUTH = "javax.security.sasl.server.authentication"; //$NON-NLS-1$
+
+    // Default public constructor is overridden
+    private Sasl() {
+        super();
+    }
+
+    // Forms new instance of factory
+    private static Object newInstance(String factoryName, Provider prv) throws SaslException {
+        String msg = "auth.31"; //$NON-NLS-1$
+        Object factory;
+        ClassLoader cl = prv.getClass().getClassLoader();
+        if (cl == null) {
+            cl = ClassLoader.getSystemClassLoader();
+        }
+        try {
+            factory = (Class.forName(factoryName, true, cl)).newInstance();
+            return factory;
+        } catch (IllegalAccessException e) {
+            throw new SaslException(msg + factoryName, e);
+        } catch (ClassNotFoundException e) {
+            throw new SaslException(msg + factoryName, e);
+        } catch (InstantiationException e) {
+            throw new SaslException(msg + factoryName, e);
+        }
+    }
+
+    /**
+     * This method forms the list of SaslClient/SaslServer factories which are
+     * implemented in used providers
+     */
+    private static Collection<?> findFactories(String service) {
+        HashSet<Object> fact = new HashSet<Object>();
+        Provider[] pp = Security.getProviders();
+        if ((pp == null) || (pp.length == 0)) {
+            return fact;
+        }
+        HashSet<String> props = new HashSet<String>();
+        for (int i = 0; i < pp.length; i++) {
+            String prName = pp[i].getName();
+            Enumeration<Object> keys = pp[i].keys();
+            while (keys.hasMoreElements()) {
+                String s = (String) keys.nextElement();
+                if (s.startsWith(service)) {
+                    String prop = pp[i].getProperty(s);
+                    try {
+                        if (props.add(prName.concat(prop))) {
+                            fact.add(newInstance(prop, pp[i]));
+                        }
+                    } catch (SaslException e) {
+                        // ignore this factory
+                        e.printStackTrace();
+                    }
+                }
+            }
+        }
+        return fact;
+    }
+
+    @SuppressWarnings("unchecked")
+    public static Enumeration<SaslClientFactory> getSaslClientFactories() {
+        Collection<SaslClientFactory> res = (Collection<SaslClientFactory>) findFactories(CLIENTFACTORYSRV);
+        return Collections.enumeration(res);
+
+    }
+
+    @SuppressWarnings("unchecked")
+    public static Enumeration<SaslServerFactory> getSaslServerFactories() {
+        Collection<SaslServerFactory> res = (Collection<SaslServerFactory>) findFactories(SERVERFACTORYSRV);
+        return Collections.enumeration(res);
+    }
+
+    public static SaslServer createSaslServer(String mechanism, String protocol,
+            String serverName, Map<String, ?> prop, CallbackHandler cbh) throws SaslException {
+        if (mechanism == null) {
+            throw new NullPointerException("auth.32"); //$NON-NLS-1$
+        }
+        Collection<?> res = findFactories(SERVERFACTORYSRV);
+        if (res.isEmpty()) {
+            return null;
+        }
+
+        Iterator<?> iter = res.iterator();
+        while (iter.hasNext()) {
+            SaslServerFactory fact = (SaslServerFactory) iter.next();
+            String[] mech = fact.getMechanismNames(null);
+            boolean is = false;
+            if (mech != null) {
+                for (int j = 0; j < mech.length; j++) {
+                    if (mech[j].equals(mechanism)) {
+                        is = true;
+                        break;
+                    }
+                }
+            }
+            if (is) {
+                SaslServer saslS = fact.createSaslServer(mechanism, protocol, serverName, prop,
+                        cbh);
+                if (saslS != null) {
+                    return saslS;
+                }
+            }
+        }
+        return null;
+    }
+
+    public static SaslClient createSaslClient(String[] mechanisms, String authanticationID,
+            String protocol, String serverName, Map<String, ?> prop, CallbackHandler cbh)
+            throws SaslException {
+        if (mechanisms == null) {
+            throw new NullPointerException("auth.33"); //$NON-NLS-1$
+        }
+        Collection<?> res = findFactories(CLIENTFACTORYSRV);
+        if (res.isEmpty()) {
+            return null;
+        }
+
+        Iterator<?> iter = res.iterator();
+        while (iter.hasNext()) {
+            SaslClientFactory fact = (SaslClientFactory) iter.next();
+            String[] mech = fact.getMechanismNames(null);
+            boolean is = false;
+            if (mech != null) {
+                for (int j = 0; j < mech.length; j++) {
+                    for (int n = 0; n < mechanisms.length; n++) {
+                        if (mech[j].equals(mechanisms[n])) {
+                            is = true;
+                            break;
+                        }
+                    }
+                }
+            }
+            if (is) {
+                SaslClient saslC = fact.createSaslClient(mechanisms, authanticationID,
+                        protocol, serverName, prop, cbh);
+                if (saslC != null) {
+                    return saslC;
+                }
+            }
+        }
+        return null;
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/SaslClient.java b/src/org/apache/harmony/javax/security/sasl/SaslClient.java
new file mode 100644
index 0000000..e07ff53
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/SaslClient.java
@@ -0,0 +1,37 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+public interface SaslClient {
+
+    void dispose() throws SaslException;
+
+    byte[] evaluateChallenge(byte[] challenge) throws SaslException;
+
+    String getMechanismName();
+
+    Object getNegotiatedProperty(String propName);
+
+    boolean hasInitialResponse();
+
+    boolean isComplete();
+
+    byte[] unwrap(byte[] incoming, int offset, int len) throws SaslException;
+
+    byte[] wrap(byte[] outgoing, int offset, int len) throws SaslException;
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/SaslClientFactory.java b/src/org/apache/harmony/javax/security/sasl/SaslClientFactory.java
new file mode 100644
index 0000000..e567ed3
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/SaslClientFactory.java
@@ -0,0 +1,30 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import java.util.Map;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+
+public interface SaslClientFactory {
+
+    SaslClient createSaslClient(String[] mechanisms, String authorizationId, String protocol,
+            String serverName, Map<String, ?> props, CallbackHandler cbh) throws SaslException;
+
+    String[] getMechanismNames(Map<String, ?> props);
+
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/SaslException.java b/src/org/apache/harmony/javax/security/sasl/SaslException.java
new file mode 100644
index 0000000..1ab7b12
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/SaslException.java
@@ -0,0 +1,69 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import java.io.IOException;
+
+public class SaslException extends IOException {
+
+    private static final long serialVersionUID = 4579784287983423626L;
+
+    /**
+     * Serialized field for storing initial cause
+     */
+    private Throwable _exception;
+
+    public SaslException() {
+        super();
+    }
+
+    public SaslException(String detail) {
+        super(detail);
+    }
+
+    public SaslException(String detail, Throwable ex) {
+        super(detail);
+        if (ex != null) {
+            super.initCause(ex);
+            _exception = ex;
+        }
+    }
+
+    @Override
+    public Throwable getCause() {
+        return _exception;
+    }
+
+    @Override
+    public Throwable initCause(Throwable cause) {
+        super.initCause(cause);
+        _exception = cause;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        if (_exception == null) {
+            return super.toString();
+        }
+        StringBuilder sb = new StringBuilder(super.toString());
+        sb.append(", caused by: "); //$NON-NLS-1$
+        sb.append(_exception.toString());
+        return sb.toString();
+    }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/SaslServer.java b/src/org/apache/harmony/javax/security/sasl/SaslServer.java
new file mode 100644
index 0000000..f057a4b
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/SaslServer.java
@@ -0,0 +1,37 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+public interface SaslServer {
+
+    void dispose() throws SaslException;
+
+    byte[] evaluateResponse(byte[] response) throws SaslException;
+
+    String getAuthorizationID();
+
+    String getMechanismName();
+
+    Object getNegotiatedProperty(String propName);
+
+    boolean isComplete();
+
+    byte[] unwrap(byte[] incoming, int offset, int len) throws SaslException;
+
+    byte[] wrap(byte[] outgoing, int offset, int len) throws SaslException;
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/SaslServerFactory.java b/src/org/apache/harmony/javax/security/sasl/SaslServerFactory.java
new file mode 100644
index 0000000..d59530e
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/SaslServerFactory.java
@@ -0,0 +1,30 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import java.util.Map;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+
+public interface SaslServerFactory {
+
+    SaslServer createSaslServer(String mechanisms, String protocol, String serverName,
+            Map<String, ?> props, CallbackHandler cbh) throws SaslException;
+
+    String[] getMechanismNames(Map<String, ?> props);
+
+}
diff --git a/src/org/apache/qpid/management/common/sasl/CRAMMD5HashedSaslClientFactory.java b/src/org/apache/qpid/management/common/sasl/CRAMMD5HashedSaslClientFactory.java
new file mode 100644
index 0000000..5c33e40
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/CRAMMD5HashedSaslClientFactory.java
@@ -0,0 +1,59 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import de.measite.smack.Sasl;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslClientFactory;
+import org.apache.harmony.javax.security.sasl.SaslException;
+import java.util.Map;
+
+public class CRAMMD5HashedSaslClientFactory implements SaslClientFactory
+{
+    /** The name of this mechanism */
+    public static final String MECHANISM = "CRAM-MD5-HASHED";
+
+    public SaslClient createSaslClient(String[] mechanisms, String authorizationId, String protocol,
+                                       String serverName, Map<String, ?> props, CallbackHandler cbh)
+    throws SaslException
+    {
+        for (int i = 0; i < mechanisms.length; i++)
+        {
+            if (mechanisms[i].equals(MECHANISM))
+            {
+                if (cbh == null)
+                {
+                    throw new SaslException("CallbackHandler must not be null");
+                }
+
+                String[] mechs = {"CRAM-MD5"};
+                return Sasl.createSaslClient(mechs, authorizationId, protocol, serverName, props, cbh);
+            }
+        }
+        return null;
+    }
+
+    public String[] getMechanismNames(Map props)
+    { 
+        return new String[]{MECHANISM};
+    }
+}
diff --git a/src/org/apache/qpid/management/common/sasl/ClientSaslFactory.java b/src/org/apache/qpid/management/common/sasl/ClientSaslFactory.java
new file mode 100644
index 0000000..19162d8
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/ClientSaslFactory.java
@@ -0,0 +1,53 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslClientFactory;
+import org.apache.harmony.javax.security.sasl.SaslException;
+import java.util.Map;
+
+public class ClientSaslFactory implements SaslClientFactory
+{
+    public SaslClient createSaslClient(String[] mechs, String authorizationId, String protocol,
+                                       String serverName, Map props, CallbackHandler cbh)
+    throws SaslException 
+    {
+        for (int i = 0; i < mechs.length; i++)
+        {
+            if (mechs[i].equals("PLAIN"))
+            {
+                return new PlainSaslClient(authorizationId, cbh);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Simple-minded implementation that ignores props
+     */
+    public String[] getMechanismNames(Map props)
+    {
+        return new String[]{"PLAIN"};
+    }
+
+}
diff --git a/src/org/apache/qpid/management/common/sasl/Constants.java b/src/org/apache/qpid/management/common/sasl/Constants.java
new file mode 100644
index 0000000..31010ba
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/Constants.java
@@ -0,0 +1,33 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+
+package org.apache.qpid.management.common.sasl;
+
+public class Constants
+{
+
+    public final static String MECH_CRAMMD5 = "CRAM-MD5";
+    public final static String MECH_PLAIN = "PLAIN";
+    public final static String SASL_CRAMMD5 = "SASL/CRAM-MD5";
+    public final static String SASL_PLAIN = "SASL/PLAIN";
+
+}
+
diff --git a/src/org/apache/qpid/management/common/sasl/JCAProvider.java b/src/org/apache/qpid/management/common/sasl/JCAProvider.java
new file mode 100644
index 0000000..5793dae
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/JCAProvider.java
@@ -0,0 +1,55 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.sasl.SaslClientFactory;
+import java.security.Provider;
+import java.util.Map;
+
+public class JCAProvider extends Provider
+{
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Creates the security provider with a map from SASL mechanisms to implementing factories.
+     *
+     * @param providerMap The map from SASL mechanims to implementing factory classes.
+     */
+    public JCAProvider(Map<String, Class<? extends SaslClientFactory>> providerMap)
+    {
+        super("AMQSASLProvider", 1.0, "A JCA provider that registers all "
+              + "AMQ SASL providers that want to be registered");
+        register(providerMap);
+    }
+
+    /**
+     * Registers client factory classes for a map of mechanism names to client factory classes.
+     *
+     * @param providerMap The map from SASL mechanims to implementing factory classes.
+     */
+    private void register(Map<String, Class<? extends SaslClientFactory>> providerMap)
+    {
+        for (Map.Entry<String, Class<? extends SaslClientFactory>> me : providerMap.entrySet())
+        {
+            put("SaslClientFactory." + me.getKey(), me.getValue().getName());
+        }
+    }
+}
diff --git a/src/org/apache/qpid/management/common/sasl/PlainSaslClient.java b/src/org/apache/qpid/management/common/sasl/PlainSaslClient.java
new file mode 100644
index 0000000..99a1d43
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/PlainSaslClient.java
@@ -0,0 +1,210 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.NameCallback;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
+import de.measite.smack.Sasl;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+public class PlainSaslClient implements SaslClient
+{
+
+    private boolean completed;
+    private CallbackHandler cbh;
+    private String authorizationID;
+    private String authenticationID;
+    private byte password[];
+    private static byte SEPARATOR = 0;
+    
+    public PlainSaslClient(String authorizationID, CallbackHandler cbh) throws SaslException
+    {
+        completed = false;
+        this.cbh = cbh;
+        Object[] userInfo = getUserInfo();
+        this.authorizationID = authorizationID;
+        this.authenticationID = (String) userInfo[0];
+        this.password = (byte[]) userInfo[1];
+        if (authenticationID == null || password == null)
+        {
+            throw new SaslException("PLAIN: authenticationID and password must be specified");
+        }
+    }
+
+    public byte[] evaluateChallenge(byte[] challenge) throws SaslException
+    {
+        if (completed)
+        {
+            throw new IllegalStateException("PLAIN: authentication already " +
+            "completed");
+        }
+        completed = true;
+        try 
+        {
+            byte authzid[] =
+                authorizationID == null ? null : authorizationID.getBytes("UTF8");
+            byte authnid[] = authenticationID.getBytes("UTF8");
+            byte response[] =
+                new byte[
+                         password.length +
+                         authnid.length +
+                         2 + // SEPARATOR
+                         (authzid != null ? authzid.length : 0)
+                         ];
+            int size = 0;
+            if (authzid != null) {
+                System.arraycopy(authzid, 0, response, 0, authzid.length);
+                size = authzid.length;
+            }
+            response[size++] = SEPARATOR;
+            System.arraycopy(authnid, 0, response, size, authnid.length);
+            size += authnid.length;
+            response[size++] = SEPARATOR;
+            System.arraycopy(password, 0, response, size, password.length);
+            clearPassword();
+            return response;
+        } catch (UnsupportedEncodingException e) {
+            throw new SaslException("PLAIN: Cannot get UTF-8 encoding of ids",
+                    e);
+        }
+    }
+
+    public String getMechanismName()
+    {
+        return "PLAIN";
+    }
+
+    public boolean hasInitialResponse()
+    {
+        return true;
+    }
+
+    public boolean isComplete()
+    {
+        return completed;
+    }
+
+    public byte[] unwrap(byte[] incoming, int offset, int len) throws SaslException
+    {
+        if (completed) {
+            throw new IllegalStateException("PLAIN: this mechanism supports " +
+            "neither integrity nor privacy");
+        } else {
+            throw new IllegalStateException("PLAIN: authentication not " +
+            "completed");
+        }
+    }
+
+    public byte[] wrap(byte[] outgoing, int offset, int len) throws SaslException
+    {
+        if (completed)
+        {
+            throw new IllegalStateException("PLAIN: this mechanism supports " +
+            "neither integrity nor privacy");
+        }
+        else
+        {
+            throw new IllegalStateException("PLAIN: authentication not " +
+            "completed");
+        }
+    }
+
+    public Object getNegotiatedProperty(String propName)
+    {
+        if (completed)
+        {
+            if (propName.equals(Sasl.QOP))
+            {
+                return "auth";
+            }
+            else
+            {
+                return null;
+            }
+        }
+        else 
+        {
+            throw new IllegalStateException("PLAIN: authentication not " +
+            "completed");
+        }
+    }
+
+    private void clearPassword()
+    {
+        if (password != null)
+        {
+            for (int i = 0 ; i < password.length ; i++)
+            {
+                password[i] = 0;
+            }
+            password = null;
+        }
+    }
+
+    public void dispose() throws SaslException
+    {
+        clearPassword();
+    }
+
+    protected void finalize()
+    {
+        clearPassword();
+    }
+
+    private Object[] getUserInfo() throws SaslException
+    {
+        try
+        {
+            final String userPrompt = "PLAIN authentication id: ";
+            final String pwPrompt = "PLAIN password: ";
+            NameCallback nameCb = new NameCallback(userPrompt);
+            PasswordCallback passwordCb = new PasswordCallback(pwPrompt, false);
+            cbh.handle(new Callback[] { nameCb, passwordCb });
+            String userid = nameCb.getName();
+            char pwchars[] = passwordCb.getPassword();
+            byte pwbytes[];
+            if (pwchars != null)
+            {
+                pwbytes = (new String(pwchars)).getBytes("UTF8");
+                passwordCb.clearPassword();
+            }
+            else 
+            {
+                pwbytes = null;
+            }
+            return (new Object[] { userid, pwbytes });
+        } 
+        catch (IOException e)
+        {
+            throw new SaslException("Cannot get password", e);
+        } 
+        catch (UnsupportedCallbackException e)
+        {
+            throw new SaslException("Cannot get userid/password", e);
+        }
+    }
+}
diff --git a/src/org/apache/qpid/management/common/sasl/SaslProvider.java b/src/org/apache/qpid/management/common/sasl/SaslProvider.java
new file mode 100644
index 0000000..1eb44e3
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/SaslProvider.java
@@ -0,0 +1,35 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import java.security.Provider;
+
+public class SaslProvider extends Provider
+{
+    private static final long serialVersionUID = -6978096016899676466L;
+
+    public SaslProvider()
+    {
+        super("SaslClientFactory", 1.0, "SASL PLAIN CLIENT MECHANISM");
+        put("SaslClientFactory.PLAIN", "ClientSaslFactory");
+    }
+
+}
diff --git a/src/org/apache/qpid/management/common/sasl/UserPasswordCallbackHandler.java b/src/org/apache/qpid/management/common/sasl/UserPasswordCallbackHandler.java
new file mode 100644
index 0000000..a7886cf
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/UserPasswordCallbackHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.NameCallback;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
+import java.io.IOException;
+
+public class UserPasswordCallbackHandler implements CallbackHandler
+{
+    private String user;
+    private char[] pwchars;
+    
+    public UserPasswordCallbackHandler(String user, String password)
+    {
+        this.user = user;
+        this.pwchars = password.toCharArray();
+    }
+
+    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
+    {
+        for (int i = 0; i < callbacks.length; i++)
+        {
+            if (callbacks[i] instanceof NameCallback)
+            {
+                NameCallback ncb = (NameCallback) callbacks[i];
+                ncb.setName(user);
+            } 
+            else if (callbacks[i] instanceof PasswordCallback)
+            {
+                PasswordCallback pcb = (PasswordCallback) callbacks[i];
+                pcb.setPassword(pwchars);
+            } 
+            else
+            {
+                throw new UnsupportedCallbackException(callbacks[i]);
+            }
+        }
+    }
+
+    private void clearPassword()
+    {
+        if (pwchars != null) 
+        {
+            for (int i = 0 ; i < pwchars.length ; i++)
+            {
+                pwchars[i] = 0;
+            }
+            pwchars = null;
+        }
+    }
+
+    protected void finalize()
+    {
+        clearPassword();
+    }
+}
diff --git a/src/org/apache/qpid/management/common/sasl/UsernameHashedPasswordCallbackHandler.java b/src/org/apache/qpid/management/common/sasl/UsernameHashedPasswordCallbackHandler.java
new file mode 100644
index 0000000..54d7374
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/UsernameHashedPasswordCallbackHandler.java
@@ -0,0 +1,107 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.NameCallback;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+
+public class UsernameHashedPasswordCallbackHandler implements CallbackHandler
+{
+    private String user;
+    private char[] pwchars;
+    
+    public UsernameHashedPasswordCallbackHandler(String user, String password) throws Exception
+    {
+        this.user = user;
+        this.pwchars = getHash(password);
+    }
+
+    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
+    {
+        for (int i = 0; i < callbacks.length; i++)
+        {
+            if (callbacks[i] instanceof NameCallback)
+            {
+                NameCallback ncb = (NameCallback) callbacks[i];
+                ncb.setName(user);
+            } 
+            else if (callbacks[i] instanceof PasswordCallback)
+            {
+                PasswordCallback pcb = (PasswordCallback) callbacks[i];
+                pcb.setPassword(pwchars);
+            } 
+            else
+            {
+                throw new UnsupportedCallbackException(callbacks[i]);
+            }
+        }
+    }
+
+    
+    private void clearPassword()
+    {
+        if (pwchars != null) 
+        {
+            for (int i = 0 ; i < pwchars.length ; i++)
+            {
+                pwchars[i] = 0;
+            }
+            pwchars = null;
+        }
+    }
+
+    protected void finalize()
+    {
+        clearPassword();
+    }
+    
+    public static char[] getHash(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException
+    {
+        byte[] data = text.getBytes("utf-8");
+
+        MessageDigest md = MessageDigest.getInstance("MD5");
+
+        for (byte b : data)
+        {
+            md.update(b);
+        }
+
+        byte[] digest = md.digest();
+
+        char[] hash = new char[digest.length ];
+
+        int index = 0;
+        for (byte b : digest)
+        {            
+            hash[index++] = (char) b;
+        }
+
+        return hash;
+    }
+}
diff --git a/src/org/jivesoftware/smack/AbstractConnectionListener.java b/src/org/jivesoftware/smack/AbstractConnectionListener.java
new file mode 100644
index 0000000..69acf90
--- /dev/null
+++ b/src/org/jivesoftware/smack/AbstractConnectionListener.java
@@ -0,0 +1,46 @@
+/**

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

+

+/**

+ * The AbstractConnectionListener class provides an empty implementation for all

+ * methods defined by the {@link ConnectionListener} interface. This is a

+ * convenience class which should be used in case you do not need to implement

+ * all methods.

+ * 

+ * @author Henning Staib

+ */

+public class AbstractConnectionListener implements ConnectionListener {

+

+    public void connectionClosed() {

+        // do nothing

+    }

+

+    public void connectionClosedOnError(Exception e) {

+        // do nothing

+    }

+

+    public void reconnectingIn(int seconds) {

+        // do nothing

+    }

+

+    public void reconnectionFailed(Exception e) {

+        // do nothing

+    }

+

+    public void reconnectionSuccessful() {

+        // do nothing

+    }

+

+}

diff --git a/src/org/jivesoftware/smack/AccountManager.java b/src/org/jivesoftware/smack/AccountManager.java
new file mode 100644
index 0000000..4d9faa5
--- /dev/null
+++ b/src/org/jivesoftware/smack/AccountManager.java
@@ -0,0 +1,337 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Registration;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Allows creation and management of accounts on an XMPP server.
+ *
+ * @see Connection#getAccountManager()
+ * @author Matt Tucker
+ */
+public class AccountManager {
+
+    private Connection connection;
+    private Registration info = null;
+
+    /**
+     * Flag that indicates whether the server supports In-Band Registration.
+     * In-Band Registration may be advertised as a stream feature. If no stream feature
+     * was advertised from the server then try sending an IQ packet to discover if In-Band
+     * Registration is available.
+     */
+    private boolean accountCreationSupported = false;
+
+    /**
+     * Creates a new AccountManager instance.
+     *
+     * @param connection a connection to a XMPP server.
+     */
+    public AccountManager(Connection connection) {
+        this.connection = connection;
+    }
+
+    /**
+     * Sets whether the server supports In-Band Registration. In-Band Registration may be
+     * advertised as a stream feature. If no stream feature was advertised from the server
+     * then try sending an IQ packet to discover if In-Band Registration is available.
+     *
+     * @param accountCreationSupported true if the server supports In-Band Registration.
+     */
+    void setSupportsAccountCreation(boolean accountCreationSupported) {
+        this.accountCreationSupported = accountCreationSupported;
+    }
+
+    /**
+     * Returns true if the server supports creating new accounts. Many servers require
+     * that you not be currently authenticated when creating new accounts, so the safest
+     * behavior is to only create new accounts before having logged in to a server.
+     *
+     * @return true if the server support creating new accounts.
+     */
+    public boolean supportsAccountCreation() {
+        // Check if we already know that the server supports creating new accounts
+        if (accountCreationSupported) {
+            return true;
+        }
+        // No information is known yet (e.g. no stream feature was received from the server
+        // indicating that it supports creating new accounts) so send an IQ packet as a way
+        // to discover if this feature is supported
+        try {
+            if (info == null) {
+                getRegistrationInfo();
+                accountCreationSupported = info.getType() != IQ.Type.ERROR;
+            }
+            return accountCreationSupported;
+        }
+        catch (XMPPException xe) {
+            return false;
+        }
+    }
+
+    /**
+     * Returns an unmodifiable collection of the names of the required account attributes.
+     * All attributes must be set when creating new accounts. The standard set of possible
+     * attributes are as follows: <ul>
+     *      <li>name -- the user's name.
+     *      <li>first -- the user's first name.
+     *      <li>last -- the user's last name.
+     *      <li>email -- the user's email address.
+     *      <li>city -- the user's city.
+     *      <li>state -- the user's state.
+     *      <li>zip -- the user's ZIP code.
+     *      <li>phone -- the user's phone number.
+     *      <li>url -- the user's website.
+     *      <li>date -- the date the registration took place.
+     *      <li>misc -- other miscellaneous information to associate with the account.
+     *      <li>text -- textual information to associate with the account.
+     *      <li>remove -- empty flag to remove account.
+     * </ul><p>
+     *
+     * Typically, servers require no attributes when creating new accounts, or just
+     * the user's email address.
+     *
+     * @return the required account attributes.
+     */
+    public Collection<String> getAccountAttributes() {
+        try {
+            if (info == null) {
+                getRegistrationInfo();
+            }
+            Map<String, String> attributes = info.getAttributes();
+            if (attributes != null) {
+                return Collections.unmodifiableSet(attributes.keySet());
+            }
+        }
+        catch (XMPPException xe) {
+            xe.printStackTrace();
+        }
+        return Collections.emptySet();
+    }
+
+    /**
+     * Returns the value of a given account attribute or <tt>null</tt> if the account
+     * attribute wasn't found.
+     *
+     * @param name the name of the account attribute to return its value.
+     * @return the value of the account attribute or <tt>null</tt> if an account
+     * attribute wasn't found for the requested name.
+     */
+    public String getAccountAttribute(String name) {
+        try {
+            if (info == null) {
+                getRegistrationInfo();
+            }
+            return info.getAttributes().get(name);
+        }
+        catch (XMPPException xe) {
+            xe.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * Returns the instructions for creating a new account, or <tt>null</tt> if there
+     * are no instructions. If present, instructions should be displayed to the end-user
+     * that will complete the registration process.
+     *
+     * @return the account creation instructions, or <tt>null</tt> if there are none.
+     */
+    public String getAccountInstructions() {
+        try {
+            if (info == null) {
+                getRegistrationInfo();
+            }
+            return info.getInstructions();
+        }
+        catch (XMPPException xe) {
+            return null;
+        }
+    }
+
+    /**
+     * Creates a new account using the specified username and password. The server may
+     * require a number of extra account attributes such as an email address and phone
+     * number. In that case, Smack will attempt to automatically set all required
+     * attributes with blank values, which may or may not be accepted by the server.
+     * Therefore, it's recommended to check the required account attributes and to let
+     * the end-user populate them with real values instead.
+     *
+     * @param username the username.
+     * @param password the password.
+     * @throws XMPPException if an error occurs creating the account.
+     */
+    public void createAccount(String username, String password) throws XMPPException {
+        if (!supportsAccountCreation()) {
+            throw new XMPPException("Server does not support account creation.");
+        }
+        // Create a map for all the required attributes, but give them blank values.
+        Map<String, String> attributes = new HashMap<String, String>();
+        for (String attributeName : getAccountAttributes()) {
+            attributes.put(attributeName, "");
+        }
+        createAccount(username, password, attributes);
+    }
+
+    /**
+     * Creates a new account using the specified username, password and account attributes.
+     * The attributes Map must contain only String name/value pairs and must also have values
+     * for all required attributes.
+     *
+     * @param username the username.
+     * @param password the password.
+     * @param attributes the account attributes.
+     * @throws XMPPException if an error occurs creating the account.
+     * @see #getAccountAttributes()
+     */
+    public void createAccount(String username, String password, Map<String, String> attributes)
+            throws XMPPException
+    {
+        if (!supportsAccountCreation()) {
+            throw new XMPPException("Server does not support account creation.");
+        }
+        Registration reg = new Registration();
+        reg.setType(IQ.Type.SET);
+        reg.setTo(connection.getServiceName());
+        attributes.put("username",username);
+        attributes.put("password",password);
+        reg.setAttributes(attributes);
+        PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+                new PacketTypeFilter(IQ.class));
+        PacketCollector collector = connection.createPacketCollector(filter);
+        connection.sendPacket(reg);
+        IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        collector.cancel();
+        if (result == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (result.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(result.getError());
+        }
+    }
+
+    /**
+     * Changes the password of the currently logged-in account. This operation can only
+     * be performed after a successful login operation has been completed. Not all servers
+     * support changing passwords; an XMPPException will be thrown when that is the case.
+     *
+     * @throws IllegalStateException if not currently logged-in to the server.
+     * @throws XMPPException if an error occurs when changing the password.
+     */
+    public void changePassword(String newPassword) throws XMPPException {
+        Registration reg = new Registration();
+        reg.setType(IQ.Type.SET);
+        reg.setTo(connection.getServiceName());
+        Map<String, String> map = new HashMap<String, String>();
+        map.put("username",StringUtils.parseName(connection.getUser()));
+        map.put("password",newPassword);
+        reg.setAttributes(map);
+        PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+                new PacketTypeFilter(IQ.class));
+        PacketCollector collector = connection.createPacketCollector(filter);
+        connection.sendPacket(reg);
+        IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        collector.cancel();
+        if (result == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (result.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(result.getError());
+        }
+    }
+
+    /**
+     * Deletes the currently logged-in account from the server. This operation can only
+     * be performed after a successful login operation has been completed. Not all servers
+     * support deleting accounts; an XMPPException will be thrown when that is the case.
+     *
+     * @throws IllegalStateException if not currently logged-in to the server.
+     * @throws XMPPException if an error occurs when deleting the account.
+     */
+    public void deleteAccount() throws XMPPException {
+        if (!connection.isAuthenticated()) {
+            throw new IllegalStateException("Must be logged in to delete a account.");
+        }
+        Registration reg = new Registration();
+        reg.setType(IQ.Type.SET);
+        reg.setTo(connection.getServiceName());
+        Map<String, String> attributes = new HashMap<String, String>();
+        // To delete an account, we add a single attribute, "remove", that is blank.
+        attributes.put("remove", "");
+        reg.setAttributes(attributes);
+        PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+                new PacketTypeFilter(IQ.class));
+        PacketCollector collector = connection.createPacketCollector(filter);
+        connection.sendPacket(reg);
+        IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        collector.cancel();
+        if (result == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (result.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(result.getError());
+        }
+    }
+
+    /**
+     * Gets the account registration info from the server.
+     *
+     * @throws XMPPException if an error occurs.
+     */
+    private synchronized void getRegistrationInfo() throws XMPPException {
+        Registration reg = new Registration();
+        reg.setTo(connection.getServiceName());
+        PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+                new PacketTypeFilter(IQ.class));
+        PacketCollector collector = connection.createPacketCollector(filter);
+        connection.sendPacket(reg);
+        IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        collector.cancel();
+        if (result == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (result.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(result.getError());
+        }
+        else {
+            info = (Registration)result;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/AndroidConnectionConfiguration.java b/src/org/jivesoftware/smack/AndroidConnectionConfiguration.java
new file mode 100644
index 0000000..6ec05e0
--- /dev/null
+++ b/src/org/jivesoftware/smack/AndroidConnectionConfiguration.java
@@ -0,0 +1,113 @@
+package org.jivesoftware.smack;
+
+import java.io.File;
+
+import android.os.Build;
+
+import org.jivesoftware.smack.proxy.ProxyInfo;
+import org.jivesoftware.smack.util.DNSUtil;
+import org.jivesoftware.smack.util.dns.HostAddress;
+
+import java.util.List;
+
+/**
+ * This class wraps DNS SRV lookups for a new ConnectionConfiguration in a 
+ * new thread, since Android API >= 11 (Honeycomb) does not allow network 
+ * activity in the main thread. 
+ * 
+ * @author Florian Schmaus fschmaus@gmail.com
+ *
+ */
+public class AndroidConnectionConfiguration extends ConnectionConfiguration {
+    private static final int DEFAULT_TIMEOUT = 10000;
+    
+    /**
+     * Creates a new ConnectionConfiguration for the specified service name.
+     * A DNS SRV lookup will be performed to find out the actual host address
+     * and port to use for the connection.
+     *
+     * @param serviceName the name of the service provided by an XMPP server.
+     */
+    public AndroidConnectionConfiguration(String serviceName) throws XMPPException {
+        super();
+        AndroidInit(serviceName, DEFAULT_TIMEOUT);
+    }
+    
+    /**
+     * 
+     * @param serviceName
+     * @param timeout
+     * @throws XMPPException
+     */
+    public AndroidConnectionConfiguration(String serviceName, int timeout) throws XMPPException {
+        super();
+        AndroidInit(serviceName, timeout);
+    }
+
+    public AndroidConnectionConfiguration(String host, int port, String name) {
+	super(host, port, name);
+	AndroidInit();
+    }
+
+    private void AndroidInit() {
+    	// API 14 is Ice Cream Sandwich
+	if (Build.VERSION.SDK_INT >= 14) {
+	    setTruststoreType("AndroidCAStore");
+	    setTruststorePassword(null);
+	    setTruststorePath(null);
+	} else {
+	    setTruststoreType("BKS");
+	    String path = System.getProperty("javax.net.ssl.trustStore");
+	    if (path == null)
+		path = System.getProperty("java.home") + File.separator + "etc"
+		    + File.separator + "security" + File.separator
+		    + "cacerts.bks";
+	    setTruststorePath(path);
+	}
+    }
+
+    /**
+     * 
+     * @param serviceName
+     * @param timeout
+     * @throws XMPPException
+     */
+    private void AndroidInit(String serviceName, int timeout) throws XMPPException {
+	AndroidInit();
+        class DnsSrvLookupRunnable implements Runnable {
+            String serviceName;
+            List<HostAddress> addresses;
+
+            public DnsSrvLookupRunnable(String serviceName) {
+                this.serviceName = serviceName;
+            }
+
+            @Override
+            public void run() {
+                addresses = DNSUtil.resolveXMPPDomain(serviceName);
+            }
+
+            public List<HostAddress> getHostAddresses() {
+                return addresses;
+            }
+        }
+
+        DnsSrvLookupRunnable dnsSrv = new DnsSrvLookupRunnable(serviceName);
+        Thread t = new Thread(dnsSrv, "dns-srv-lookup");
+        t.start();
+        try {
+            t.join(timeout);
+        } catch (InterruptedException e) {
+            throw new XMPPException("DNS lookup timeout after " + timeout + "ms", e);
+        }
+
+        hostAddresses = dnsSrv.getHostAddresses();
+        if (hostAddresses == null) {
+        	throw new XMPPException("DNS lookup failure");
+        }
+
+        ProxyInfo proxy = ProxyInfo.forDefaultProxy();
+
+        init(serviceName, proxy);
+    }
+}
diff --git a/src/org/jivesoftware/smack/BOSHConfiguration.java b/src/org/jivesoftware/smack/BOSHConfiguration.java
new file mode 100644
index 0000000..0b033b4
--- /dev/null
+++ b/src/org/jivesoftware/smack/BOSHConfiguration.java
@@ -0,0 +1,124 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.proxy.ProxyInfo;
+
+/**
+ * Configuration to use while establishing the connection to the XMPP server via
+ * HTTP binding.
+ * 
+ * @see BOSHConnection
+ * @author Guenther Niess
+ */
+public class BOSHConfiguration extends ConnectionConfiguration {
+
+    private boolean ssl;
+    private String file;
+
+    public BOSHConfiguration(String xmppDomain) {
+        super(xmppDomain, 7070);
+        setSASLAuthenticationEnabled(true);
+        ssl = false;
+        file = "/http-bind/";
+    }
+
+    public BOSHConfiguration(String xmppDomain, int port) {
+        super(xmppDomain, port);
+        setSASLAuthenticationEnabled(true);
+        ssl = false;
+        file = "/http-bind/";
+    }
+
+    /**
+     * Create a HTTP Binding configuration.
+     * 
+     * @param https true if you want to use SSL
+     *             (e.g. false for http://domain.lt:7070/http-bind).
+     * @param host the hostname or IP address of the connection manager
+     *             (e.g. domain.lt for http://domain.lt:7070/http-bind).
+     * @param port the port of the connection manager
+     *             (e.g. 7070 for http://domain.lt:7070/http-bind).
+     * @param filePath the file which is described by the URL
+     *             (e.g. /http-bind for http://domain.lt:7070/http-bind).
+     * @param xmppDomain the XMPP service name
+     *             (e.g. domain.lt for the user alice@domain.lt)
+     */
+    public BOSHConfiguration(boolean https, String host, int port, String filePath, String xmppDomain) {
+        super(host, port, xmppDomain);
+        setSASLAuthenticationEnabled(true);
+        ssl = https;
+        file = (filePath != null ? filePath : "/");
+    }
+
+    /**
+     * Create a HTTP Binding configuration.
+     * 
+     * @param https true if you want to use SSL
+     *             (e.g. false for http://domain.lt:7070/http-bind).
+     * @param host the hostname or IP address of the connection manager
+     *             (e.g. domain.lt for http://domain.lt:7070/http-bind).
+     * @param port the port of the connection manager
+     *             (e.g. 7070 for http://domain.lt:7070/http-bind).
+     * @param filePath the file which is described by the URL
+     *             (e.g. /http-bind for http://domain.lt:7070/http-bind).
+     * @param proxy the configuration of a proxy server.
+     * @param xmppDomain the XMPP service name
+     *             (e.g. domain.lt for the user alice@domain.lt)
+     */
+    public BOSHConfiguration(boolean https, String host, int port, String filePath, ProxyInfo proxy, String xmppDomain) {
+        super(host, port, xmppDomain, proxy);
+        setSASLAuthenticationEnabled(true);
+        ssl = https;
+        file = (filePath != null ? filePath : "/");
+    }
+
+    public boolean isProxyEnabled() {
+        return (proxy != null && proxy.getProxyType() != ProxyInfo.ProxyType.NONE);
+    }
+
+    public ProxyInfo getProxyInfo() {
+        return proxy;
+    }
+
+    public String getProxyAddress() {
+        return (proxy != null ? proxy.getProxyAddress() : null);
+    }
+
+    public int getProxyPort() {
+        return (proxy != null ? proxy.getProxyPort() : 8080);
+    }
+
+    public boolean isUsingSSL() {
+        return ssl;
+    }
+
+    public URI getURI() throws URISyntaxException {
+        if (file.charAt(0) != '/') {
+            file = '/' + file;
+        }
+        return new URI((ssl ? "https://" : "http://") + getHost() + ":" + getPort() + file);
+    }
+}
diff --git a/src/org/jivesoftware/smack/BOSHConnection.java b/src/org/jivesoftware/smack/BOSHConnection.java
new file mode 100644
index 0000000..594cf9d
--- /dev/null
+++ b/src/org/jivesoftware/smack/BOSHConnection.java
@@ -0,0 +1,779 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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;
+
+import java.io.IOException;
+import java.io.PipedReader;
+import java.io.PipedWriter;
+import java.io.Writer;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.Roster;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+
+import com.kenai.jbosh.BOSHClient;
+import com.kenai.jbosh.BOSHClientConfig;
+import com.kenai.jbosh.BOSHClientConnEvent;
+import com.kenai.jbosh.BOSHClientConnListener;
+import com.kenai.jbosh.BOSHClientRequestListener;
+import com.kenai.jbosh.BOSHClientResponseListener;
+import com.kenai.jbosh.BOSHException;
+import com.kenai.jbosh.BOSHMessageEvent;
+import com.kenai.jbosh.BodyQName;
+import com.kenai.jbosh.ComposableBody;
+
+/**
+ * Creates a connection to a XMPP server via HTTP binding.
+ * This is specified in the XEP-0206: XMPP Over BOSH.
+ * 
+ * @see Connection
+ * @author Guenther Niess
+ */
+public class BOSHConnection extends Connection {
+
+    /**
+     * The XMPP Over Bosh namespace.
+     */
+    public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh";
+
+    /**
+     * The BOSH namespace from XEP-0124.
+     */
+    public static final String BOSH_URI = "http://jabber.org/protocol/httpbind";
+
+    /**
+     * The used BOSH client from the jbosh library.
+     */
+    private BOSHClient client;
+
+    /**
+     * Holds the initial configuration used while creating the connection.
+     */
+    private final BOSHConfiguration config;
+
+    // Some flags which provides some info about the current state.
+    private boolean connected = false;
+    private boolean authenticated = false;
+    private boolean anonymous = false;
+    private boolean isFirstInitialization = true;
+    private boolean wasAuthenticated = false;
+    private boolean done = false;
+
+    /**
+     * The Thread environment for sending packet listeners.
+     */
+    private ExecutorService listenerExecutor;
+
+    // The readerPipe and consumer thread are used for the debugger.
+    private PipedWriter readerPipe;
+    private Thread readerConsumer;
+
+    /**
+     * The BOSH equivalent of the stream ID which is used for DIGEST authentication.
+     */
+    protected String authID = null;
+
+    /**
+     * The session ID for the BOSH session with the connection manager.
+     */
+    protected String sessionID = null;
+
+    /**
+     * The full JID of the authenticated user.
+     */
+    private String user = null;
+
+    /**
+     * The roster maybe also called buddy list holds the list of the users contacts.
+     */
+    private Roster roster = null;
+
+
+    /**
+     * Create a HTTP Binding connection to a XMPP server.
+     * 
+     * @param https true if you want to use SSL
+     *             (e.g. false for http://domain.lt:7070/http-bind).
+     * @param host the hostname or IP address of the connection manager
+     *             (e.g. domain.lt for http://domain.lt:7070/http-bind).
+     * @param port the port of the connection manager
+     *             (e.g. 7070 for http://domain.lt:7070/http-bind).
+     * @param filePath the file which is described by the URL
+     *             (e.g. /http-bind for http://domain.lt:7070/http-bind).
+     * @param xmppDomain the XMPP service name
+     *             (e.g. domain.lt for the user alice@domain.lt)
+     */
+    public BOSHConnection(boolean https, String host, int port, String filePath, String xmppDomain) {
+        super(new BOSHConfiguration(https, host, port, filePath, xmppDomain));
+        this.config = (BOSHConfiguration) getConfiguration();
+    }
+
+    /**
+     * Create a HTTP Binding connection to a XMPP server.
+     * 
+     * @param config The configuration which is used for this connection.
+     */
+    public BOSHConnection(BOSHConfiguration config) {
+        super(config);
+        this.config = config;
+    }
+
+    public void connect() throws XMPPException {
+        if (connected) {
+            throw new IllegalStateException("Already connected to a server.");
+        }
+        done = false;
+        try {
+            // Ensure a clean starting state
+            if (client != null) {
+                client.close();
+                client = null;
+            }
+            saslAuthentication.init();
+            sessionID = null;
+            authID = null;
+
+            // Initialize BOSH client
+            BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder
+                    .create(config.getURI(), config.getServiceName());
+            if (config.isProxyEnabled()) {
+                cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
+            }
+            client = BOSHClient.create(cfgBuilder.build());
+
+            // Create an executor to deliver incoming packets to listeners.
+            // We'll use a single thread with an unbounded queue.
+            listenerExecutor = Executors
+                    .newSingleThreadExecutor(new ThreadFactory() {
+                        public Thread newThread(Runnable runnable) {
+                            Thread thread = new Thread(runnable,
+                                    "Smack Listener Processor ("
+                                            + connectionCounterValue + ")");
+                            thread.setDaemon(true);
+                            return thread;
+                        }
+                    });
+            client.addBOSHClientConnListener(new BOSHConnectionListener(this));
+            client.addBOSHClientResponseListener(new BOSHPacketReader(this));
+
+            // Initialize the debugger
+            if (config.isDebuggerEnabled()) {
+                initDebugger();
+                if (isFirstInitialization) {
+                    if (debugger.getReaderListener() != null) {
+                        addPacketListener(debugger.getReaderListener(), null);
+                    }
+                    if (debugger.getWriterListener() != null) {
+                        addPacketSendingListener(debugger.getWriterListener(), null);
+                    }
+                }
+            }
+
+            // Send the session creation request
+            client.send(ComposableBody.builder()
+                    .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
+                    .setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0")
+                    .build());
+        } catch (Exception e) {
+            throw new XMPPException("Can't connect to " + getServiceName(), e);
+        }
+
+        // Wait for the response from the server
+        synchronized (this) {
+            long endTime = System.currentTimeMillis() +
+                           SmackConfiguration.getPacketReplyTimeout() * 6;
+            while ((!connected) && (System.currentTimeMillis() < endTime)) {
+                try {
+                    wait(Math.abs(endTime - System.currentTimeMillis()));
+                }
+                catch (InterruptedException e) {}
+            }
+        }
+
+        // If there is no feedback, throw an remote server timeout error
+        if (!connected && !done) {
+            done = true;
+            String errorMessage = "Timeout reached for the connection to " 
+                    + getHost() + ":" + getPort() + ".";
+            throw new XMPPException(
+                    errorMessage,
+                    new XMPPError(XMPPError.Condition.remote_server_timeout, errorMessage));
+        }
+    }
+
+    public String getConnectionID() {
+        if (!connected) {
+            return null;
+        } else if (authID != null) {
+            return authID;
+        } else {
+            return sessionID;
+        }
+    }
+
+    public Roster getRoster() {
+        if (roster == null) {
+            return null;
+        }
+        if (!config.isRosterLoadedAtLogin()) {
+            roster.reload();
+        }
+        // If this is the first time the user has asked for the roster after calling
+        // login, we want to wait for the server to send back the user's roster.
+        // This behavior shields API users from having to worry about the fact that
+        // roster operations are asynchronous, although they'll still have to listen
+        // for changes to the roster. Note: because of this waiting logic, internal
+        // Smack code should be wary about calling the getRoster method, and may
+        // need to access the roster object directly.
+        if (!roster.rosterInitialized) {
+            try {
+                synchronized (roster) {
+                    long waitTime = SmackConfiguration.getPacketReplyTimeout();
+                    long start = System.currentTimeMillis();
+                    while (!roster.rosterInitialized) {
+                        if (waitTime <= 0) {
+                            break;
+                        }
+                        roster.wait(waitTime);
+                        long now = System.currentTimeMillis();
+                        waitTime -= now - start;
+                        start = now;
+                    }
+                }
+            } catch (InterruptedException ie) {
+                // Ignore.
+            }
+        }
+        return roster;
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public boolean isAnonymous() {
+        return anonymous;
+    }
+
+    public boolean isAuthenticated() {
+        return authenticated;
+    }
+
+    public boolean isConnected() {
+        return connected;
+    }
+
+    public boolean isSecureConnection() {
+        // TODO: Implement SSL usage
+        return false;
+    }
+
+    public boolean isUsingCompression() {
+        // TODO: Implement compression
+        return false;
+    }
+
+    public void login(String username, String password, String resource)
+            throws XMPPException {
+        if (!isConnected()) {
+            throw new IllegalStateException("Not connected to server.");
+        }
+        if (authenticated) {
+            throw new IllegalStateException("Already logged in to server.");
+        }
+        // Do partial version of nameprep on the username.
+        username = username.toLowerCase().trim();
+
+        String response;
+        if (config.isSASLAuthenticationEnabled()
+                && saslAuthentication.hasNonAnonymousAuthentication()) {
+            // Authenticate using SASL
+            if (password != null) {
+                response = saslAuthentication.authenticate(username, password, resource);
+            } else {
+                response = saslAuthentication.authenticate(username, resource, config.getCallbackHandler());
+            }
+        } else {
+            // Authenticate using Non-SASL
+            response = new NonSASLAuthentication(this).authenticate(username, password, resource);
+        }
+
+        // Set the user.
+        if (response != null) {
+            this.user = response;
+            // Update the serviceName with the one returned by the server
+            config.setServiceName(StringUtils.parseServer(response));
+        } else {
+            this.user = username + "@" + getServiceName();
+            if (resource != null) {
+                this.user += "/" + resource;
+            }
+        }
+
+        // Create the roster if it is not a reconnection.
+        if (this.roster == null) {
+            if (this.rosterStorage == null) {
+                this.roster = new Roster(this);
+            } else {
+                this.roster = new Roster(this, rosterStorage);
+            }
+        }
+
+        // Set presence to online.
+        if (config.isSendPresence()) {
+            sendPacket(new Presence(Presence.Type.available));
+        }
+
+        // Indicate that we're now authenticated.
+        authenticated = true;
+        anonymous = false;
+
+        if (config.isRosterLoadedAtLogin()) {
+            this.roster.reload();
+        }
+        // Stores the autentication for future reconnection
+        config.setLoginInfo(username, password, resource);
+
+        // If debugging is enabled, change the the debug window title to include
+        // the
+        // name we are now logged-in as.l
+        if (config.isDebuggerEnabled() && debugger != null) {
+            debugger.userHasLogged(user);
+        }
+    }
+
+    public void loginAnonymously() throws XMPPException {
+    	if (!isConnected()) {
+            throw new IllegalStateException("Not connected to server.");
+        }
+        if (authenticated) {
+            throw new IllegalStateException("Already logged in to server.");
+        }
+
+        String response;
+        if (config.isSASLAuthenticationEnabled() &&
+                saslAuthentication.hasAnonymousAuthentication()) {
+            response = saslAuthentication.authenticateAnonymously();
+        }
+        else {
+            // Authenticate using Non-SASL
+            response = new NonSASLAuthentication(this).authenticateAnonymously();
+        }
+
+        // Set the user value.
+        this.user = response;
+        // Update the serviceName with the one returned by the server
+        config.setServiceName(StringUtils.parseServer(response));
+
+        // Anonymous users can't have a roster.
+        roster = null;
+
+        // Set presence to online.
+        if (config.isSendPresence()) {
+            sendPacket(new Presence(Presence.Type.available));
+        }
+
+        // Indicate that we're now authenticated.
+        authenticated = true;
+        anonymous = true;
+
+        // If debugging is enabled, change the the debug window title to include the
+        // name we are now logged-in as.
+        // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
+        // will be null
+        if (config.isDebuggerEnabled() && debugger != null) {
+            debugger.userHasLogged(user);
+        }
+    }
+
+    public void sendPacket(Packet packet) {
+        if (!isConnected()) {
+            throw new IllegalStateException("Not connected to server.");
+        }
+        if (packet == null) {
+            throw new NullPointerException("Packet is null.");
+        }
+        if (!done) {
+            // Invoke interceptors for the new packet that is about to be sent.
+            // Interceptors
+            // may modify the content of the packet.
+            firePacketInterceptors(packet);
+
+            try {
+                send(ComposableBody.builder().setPayloadXML(packet.toXML())
+                        .build());
+            } catch (BOSHException e) {
+                e.printStackTrace();
+                return;
+            }
+
+            // Process packet writer listeners. Note that we're using the
+            // sending
+            // thread so it's expected that listeners are fast.
+            firePacketSendingListeners(packet);
+        }
+    }
+
+    public void disconnect(Presence unavailablePresence) {
+        if (!connected) {
+            return;
+        }
+        shutdown(unavailablePresence);
+
+        // Cleanup
+        if (roster != null) {
+            roster.cleanup();
+            roster = null;
+        }
+        sendListeners.clear();
+        recvListeners.clear();
+        collectors.clear();
+        interceptors.clear();
+
+        // Reset the connection flags
+        wasAuthenticated = false;
+        isFirstInitialization = true;
+
+        // Notify connection listeners of the connection closing if done hasn't already been set.
+        for (ConnectionListener listener : getConnectionListeners()) {
+            try {
+                listener.connectionClosed();
+            }
+            catch (Exception e) {
+                // Catch and print any exception so we can recover
+                // from a faulty listener and finish the shutdown process
+                e.printStackTrace();
+            }
+        }
+    }
+
+    /**
+     * Closes the connection by setting presence to unavailable and closing the 
+     * HTTP client. The shutdown logic will be used during a planned disconnection or when
+     * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
+     * BOSH packet reader and {@link Roster} will not be removed; thus
+     * connection's state is kept.
+     *
+     * @param unavailablePresence the presence packet to send during shutdown.
+     */
+    protected void shutdown(Presence unavailablePresence) {
+        setWasAuthenticated(authenticated);
+        authID = null;
+        sessionID = null;
+        done = true;
+        authenticated = false;
+        connected = false;
+        isFirstInitialization = false;
+
+        try {
+            client.disconnect(ComposableBody.builder()
+                    .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
+                    .setPayloadXML(unavailablePresence.toXML())
+                    .build());
+            // Wait 150 ms for processes to clean-up, then shutdown.
+            Thread.sleep(150);
+        }
+        catch (Exception e) {
+            // Ignore.
+        }
+
+        // Close down the readers and writers.
+        if (readerPipe != null) {
+            try {
+                readerPipe.close();
+            }
+            catch (Throwable ignore) { /* ignore */ }
+            reader = null;
+        }
+        if (reader != null) {
+            try {
+                reader.close();
+            }
+            catch (Throwable ignore) { /* ignore */ }
+            reader = null;
+        }
+        if (writer != null) {
+            try {
+                writer.close();
+            }
+            catch (Throwable ignore) { /* ignore */ }
+            writer = null;
+        }
+
+        // Shut down the listener executor.
+        if (listenerExecutor != null) {
+            listenerExecutor.shutdown();
+        }
+        readerConsumer = null;
+    }
+
+    /**
+     * Sets whether the connection has already logged in the server.
+     *
+     * @param wasAuthenticated true if the connection has already been authenticated.
+     */
+    private void setWasAuthenticated(boolean wasAuthenticated) {
+        if (!this.wasAuthenticated) {
+            this.wasAuthenticated = wasAuthenticated;
+        }
+    }
+
+    /**
+     * Send a HTTP request to the connection manager with the provided body element.
+     * 
+     * @param body the body which will be sent.
+     */
+    protected void send(ComposableBody body) throws BOSHException {
+        if (!connected) {
+            throw new IllegalStateException("Not connected to a server!");
+        }
+        if (body == null) {
+            throw new NullPointerException("Body mustn't be null!");
+        }
+        if (sessionID != null) {
+            body = body.rebuild().setAttribute(
+                    BodyQName.create(BOSH_URI, "sid"), sessionID).build();
+        }
+        client.send(body);
+    }
+
+    /**
+     * Processes a packet after it's been fully parsed by looping through the
+     * installed packet collectors and listeners and letting them examine the
+     * packet to see if they are a match with the filter.
+     * 
+     * @param packet the packet to process.
+     */
+    protected void processPacket(Packet packet) {
+        if (packet == null) {
+            return;
+        }
+
+        // Loop through all collectors and notify the appropriate ones.
+        for (PacketCollector collector : getPacketCollectors()) {
+            collector.processPacket(packet);
+        }
+
+        // Deliver the incoming packet to listeners.
+        listenerExecutor.submit(new ListenerNotification(packet));
+    }
+
+    /**
+     * Initialize the SmackDebugger which allows to log and debug XML traffic.
+     */
+    protected void initDebugger() {
+        // TODO: Maybe we want to extend the SmackDebugger for simplification
+        //       and a performance boost.
+
+        // Initialize a empty writer which discards all data.
+        writer = new Writer() {
+                public void write(char[] cbuf, int off, int len) { /* ignore */}
+                public void close() { /* ignore */ }
+                public void flush() { /* ignore */ }
+            };
+
+        // Initialize a pipe for received raw data.
+        try {
+            readerPipe = new PipedWriter();
+            reader = new PipedReader(readerPipe);
+        }
+        catch (IOException e) {
+            // Ignore
+        }
+
+        // Call the method from the parent class which initializes the debugger.
+        super.initDebugger();
+
+        // Add listeners for the received and sent raw data.
+        client.addBOSHClientResponseListener(new BOSHClientResponseListener() {
+            public void responseReceived(BOSHMessageEvent event) {
+                if (event.getBody() != null) {
+                    try {
+                        readerPipe.write(event.getBody().toXML());
+                        readerPipe.flush();
+                    } catch (Exception e) {
+                        // Ignore
+                    }
+                }
+            }
+        });
+        client.addBOSHClientRequestListener(new BOSHClientRequestListener() {
+            public void requestSent(BOSHMessageEvent event) {
+                if (event.getBody() != null) {
+                    try {
+                        writer.write(event.getBody().toXML());
+                    } catch (Exception e) {
+                        // Ignore
+                    }
+                }
+            }
+        });
+
+        // Create and start a thread which discards all read data.
+        readerConsumer = new Thread() {
+            private Thread thread = this;
+            private int bufferLength = 1024;
+
+            public void run() {
+                try {
+                    char[] cbuf = new char[bufferLength];
+                    while (readerConsumer == thread && !done) {
+                        reader.read(cbuf, 0, bufferLength);
+                    }
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }
+        };
+        readerConsumer.setDaemon(true);
+        readerConsumer.start();
+    }
+
+    /**
+     * Sends out a notification that there was an error with the connection
+     * and closes the connection.
+     *
+     * @param e the exception that causes the connection close event.
+     */
+    protected void notifyConnectionError(Exception e) {
+        // Closes the connection temporary. A reconnection is possible
+        shutdown(new Presence(Presence.Type.unavailable));
+        // Print the stack trace to help catch the problem
+        e.printStackTrace();
+        // Notify connection listeners of the error.
+        for (ConnectionListener listener : getConnectionListeners()) {
+            try {
+                listener.connectionClosedOnError(e);
+            }
+            catch (Exception e2) {
+                // Catch and print any exception so we can recover
+                // from a faulty listener
+                e2.printStackTrace();
+            }
+        }
+    }
+
+
+    /**
+     * A listener class which listen for a successfully established connection
+     * and connection errors and notifies the BOSHConnection.
+     * 
+     * @author Guenther Niess
+     */
+    private class BOSHConnectionListener implements BOSHClientConnListener {
+
+        private final BOSHConnection connection;
+
+        public BOSHConnectionListener(BOSHConnection connection) {
+            this.connection = connection;
+        }
+
+        /**
+         * Notify the BOSHConnection about connection state changes.
+         * Process the connection listeners and try to login if the
+         * connection was formerly authenticated and is now reconnected.
+         */
+        public void connectionEvent(BOSHClientConnEvent connEvent) {
+            try {
+                if (connEvent.isConnected()) {
+                    connected = true;
+                    if (isFirstInitialization) {
+                        isFirstInitialization = false;
+                        for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
+                            listener.connectionCreated(connection);
+                        }
+                    }
+                    else {
+                        try {
+                            if (wasAuthenticated) {
+                                connection.login(
+                                        config.getUsername(),
+                                        config.getPassword(),
+                                        config.getResource());
+                            }
+                            for (ConnectionListener listener : getConnectionListeners()) {
+                                 listener.reconnectionSuccessful();
+                            }
+                        }
+                        catch (XMPPException e) {
+                            for (ConnectionListener listener : getConnectionListeners()) {
+                                listener.reconnectionFailed(e);
+                           }
+                        }
+                    }
+                }
+                else {
+                    if (connEvent.isError()) {
+                        try {
+                            connEvent.getCause();
+                        }
+                        catch (Exception e) {
+                            notifyConnectionError(e);
+                        }
+                    }
+                    connected = false;
+                }
+            }
+            finally {
+                synchronized (connection) {
+                    connection.notifyAll();
+                }
+            }
+        }
+    }
+
+    /**
+     * This class notifies all listeners that a packet was received.
+     */
+    private class ListenerNotification implements Runnable {
+
+        private Packet packet;
+
+        public ListenerNotification(Packet packet) {
+            this.packet = packet;
+        }
+
+        public void run() {
+            for (ListenerWrapper listenerWrapper : recvListeners.values()) {
+                listenerWrapper.notifyListener(packet);
+            }
+        }
+    }
+
+	@Override
+	public void setRosterStorage(RosterStorage storage)
+			throws IllegalStateException {
+		if(this.roster!=null){
+			throw new IllegalStateException("Roster is already initialized");
+		}
+		this.rosterStorage = storage;
+	}
+}
diff --git a/src/org/jivesoftware/smack/BOSHPacketReader.java b/src/org/jivesoftware/smack/BOSHPacketReader.java
new file mode 100644
index 0000000..c86d756
--- /dev/null
+++ b/src/org/jivesoftware/smack/BOSHPacketReader.java
@@ -0,0 +1,169 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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;
+
+import java.io.StringReader;
+
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smack.sasl.SASLMechanism.Challenge;
+import org.jivesoftware.smack.sasl.SASLMechanism.Failure;
+import org.jivesoftware.smack.sasl.SASLMechanism.Success;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlPullParser;
+
+import com.kenai.jbosh.AbstractBody;
+import com.kenai.jbosh.BOSHClientResponseListener;
+import com.kenai.jbosh.BOSHMessageEvent;
+import com.kenai.jbosh.BodyQName;
+import com.kenai.jbosh.ComposableBody;
+
+/**
+ * Listens for XML traffic from the BOSH connection manager and parses it into
+ * packet objects.
+ * 
+ * @author Guenther Niess
+ */
+public class BOSHPacketReader implements BOSHClientResponseListener {
+
+    private BOSHConnection connection;
+
+    /**
+     * Create a packet reader which listen on a BOSHConnection for received
+     * HTTP responses, parse the packets and notifies the connection.
+     * 
+     * @param connection the corresponding connection for the received packets.
+     */
+    public BOSHPacketReader(BOSHConnection connection) {
+        this.connection = connection;
+    }
+
+    /**
+     * Parse the received packets and notify the corresponding connection.
+     * 
+     * @param event the BOSH client response which includes the received packet.
+     */
+    public void responseReceived(BOSHMessageEvent event) {
+        AbstractBody body = event.getBody();
+        if (body != null) {
+            try {
+                if (connection.sessionID == null) {
+                    connection.sessionID = body.getAttribute(BodyQName.create(BOSHConnection.BOSH_URI, "sid"));
+                }
+                if (connection.authID == null) {
+                    connection.authID = body.getAttribute(BodyQName.create(BOSHConnection.BOSH_URI, "authid"));
+                }
+                final XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+                parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,
+                        true);
+                parser.setInput(new StringReader(body.toXML()));
+                int eventType = parser.getEventType();
+                do {
+                    eventType = parser.next();
+                    if (eventType == XmlPullParser.START_TAG) {
+                        if (parser.getName().equals("body")) {
+                            // ignore the container root element
+                        } else if (parser.getName().equals("message")) {
+                            connection.processPacket(PacketParserUtils.parseMessage(parser));
+                        } else if (parser.getName().equals("iq")) {
+                            connection.processPacket(PacketParserUtils.parseIQ(parser, connection));
+                        } else if (parser.getName().equals("presence")) {
+                            connection.processPacket(PacketParserUtils.parsePresence(parser));
+                        } else if (parser.getName().equals("challenge")) {
+                            // The server is challenging the SASL authentication
+                            // made by the client
+                            final String challengeData = parser.nextText();
+                            connection.getSASLAuthentication()
+                                    .challengeReceived(challengeData);
+                            connection.processPacket(new Challenge(
+                                    challengeData));
+                        } else if (parser.getName().equals("success")) {
+                            connection.send(ComposableBody.builder()
+                                    .setNamespaceDefinition("xmpp", BOSHConnection.XMPP_BOSH_NS)
+                                    .setAttribute(
+                                            BodyQName.createWithPrefix(BOSHConnection.XMPP_BOSH_NS, "restart", "xmpp"),
+                                            "true")
+                                    .setAttribute(
+                                            BodyQName.create(BOSHConnection.BOSH_URI, "to"),
+                                            connection.getServiceName())
+                                    .build());
+                            connection.getSASLAuthentication().authenticated();
+                            connection.processPacket(new Success(parser.nextText()));
+                        } else if (parser.getName().equals("features")) {
+                            parseFeatures(parser);
+                        } else if (parser.getName().equals("failure")) {
+                            if ("urn:ietf:params:xml:ns:xmpp-sasl".equals(parser.getNamespace(null))) {
+                                final Failure failure = PacketParserUtils.parseSASLFailure(parser);
+                                connection.getSASLAuthentication().authenticationFailed();
+                                connection.processPacket(failure);
+                            }
+                        } else if (parser.getName().equals("error")) {
+                            throw new XMPPException(PacketParserUtils.parseStreamError(parser));
+                        }
+                    }
+                } while (eventType != XmlPullParser.END_DOCUMENT);
+            }
+            catch (Exception e) {
+                if (connection.isConnected()) {
+                    connection.notifyConnectionError(e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Parse and setup the XML stream features.
+     * 
+     * @param parser the XML parser, positioned at the start of a message packet.
+     * @throws Exception if an exception occurs while parsing the packet.
+     */
+    private void parseFeatures(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        while (!done) {
+            int eventType = parser.next();
+
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("mechanisms")) {
+                    // The server is reporting available SASL mechanisms. Store
+                    // this information
+                    // which will be used later while logging (i.e.
+                    // authenticating) into
+                    // the server
+                    connection.getSASLAuthentication().setAvailableSASLMethods(
+                            PacketParserUtils.parseMechanisms(parser));
+                } else if (parser.getName().equals("bind")) {
+                    // The server requires the client to bind a resource to the
+                    // stream
+                    connection.getSASLAuthentication().bindingRequired();
+                } else if (parser.getName().equals("session")) {
+                    // The server supports sessions
+                    connection.getSASLAuthentication().sessionsSupported();
+                } else if (parser.getName().equals("register")) {
+                    connection.getAccountManager().setSupportsAccountCreation(
+                            true);
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("features")) {
+                    done = true;
+                }
+            }
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/Chat.java b/src/org/jivesoftware/smack/Chat.java
new file mode 100644
index 0000000..66f5a54
--- /dev/null
+++ b/src/org/jivesoftware/smack/Chat.java
@@ -0,0 +1,180 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.packet.Message;
+
+import java.util.Set;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * A chat is a series of messages sent between two users. Each chat has a unique
+ * thread ID, which is used to track which messages are part of a particular
+ * conversation. Some messages are sent without a thread ID, and some clients
+ * don't send thread IDs at all. Therefore, if a message without a thread ID
+ * arrives it is routed to the most recently created Chat with the message
+ * sender.
+ * 
+ * @author Matt Tucker
+ */
+public class Chat {
+
+    private ChatManager chatManager;
+    private String threadID;
+    private String participant;
+    private final Set<MessageListener> listeners = new CopyOnWriteArraySet<MessageListener>();
+
+    /**
+     * Creates a new chat with the specified user and thread ID.
+     *
+     * @param chatManager the chatManager the chat will use.
+     * @param participant the user to chat with.
+     * @param threadID the thread ID to use.
+     */
+    Chat(ChatManager chatManager, String participant, String threadID) {
+        this.chatManager = chatManager;
+        this.participant = participant;
+        this.threadID = threadID;
+    }
+
+    /**
+     * Returns the thread id associated with this chat, which corresponds to the
+     * <tt>thread</tt> field of XMPP messages. This method may return <tt>null</tt>
+     * if there is no thread ID is associated with this Chat.
+     *
+     * @return the thread ID of this chat.
+     */
+    public String getThreadID() {
+        return threadID;
+    }
+
+    /**
+     * Returns the name of the user the chat is with.
+     *
+     * @return the name of the user the chat is occuring with.
+     */
+    public String getParticipant() {
+        return participant;
+    }
+
+    /**
+     * Sends the specified text as a message to the other chat participant.
+     * This is a convenience method for:
+     *
+     * <pre>
+     *     Message message = chat.createMessage();
+     *     message.setBody(messageText);
+     *     chat.sendMessage(message);
+     * </pre>
+     *
+     * @param text the text to send.
+     * @throws XMPPException if sending the message fails.
+     */
+    public void sendMessage(String text) throws XMPPException {
+        Message message = new Message(participant, Message.Type.chat);
+        message.setThread(threadID);
+        message.setBody(text);
+        chatManager.sendMessage(this, message);
+    }
+
+    /**
+     * Sends a message to the other chat participant. The thread ID, recipient,
+     * and message type of the message will automatically set to those of this chat.
+     *
+     * @param message the message to send.
+     * @throws XMPPException if an error occurs sending the message.
+     */
+    public void sendMessage(Message message) throws XMPPException {
+        // Force the recipient, message type, and thread ID since the user elected
+        // to send the message through this chat object.
+        message.setTo(participant);
+        message.setType(Message.Type.chat);
+        message.setThread(threadID);
+        chatManager.sendMessage(this, message);
+    }
+
+    /**
+     * Adds a packet listener that will be notified of any new messages in the
+     * chat.
+     *
+     * @param listener a packet listener.
+     */
+    public void addMessageListener(MessageListener listener) {
+        if(listener == null) {
+            return;
+        }
+        // TODO these references should be weak.
+        listeners.add(listener);
+    }
+
+    public void removeMessageListener(MessageListener listener) {
+        listeners.remove(listener);
+    }
+
+    /**
+     * Returns an unmodifiable collection of all of the listeners registered with this chat.
+     *
+     * @return an unmodifiable collection of all of the listeners registered with this chat.
+     */
+    public Collection<MessageListener> getListeners() {
+        return Collections.unmodifiableCollection(listeners);
+    }
+
+    /**
+     * Creates a {@link org.jivesoftware.smack.PacketCollector} which will accumulate the Messages
+     * for this chat. Always cancel PacketCollectors when finished with them as they will accumulate
+     * messages indefinitely.
+     *
+     * @return the PacketCollector which returns Messages for this chat.
+     */
+    public PacketCollector createCollector() {
+        return chatManager.createPacketCollector(this);
+    }
+
+    /**
+     * Delivers a message directly to this chat, which will add the message
+     * to the collector and deliver it to all listeners registered with the
+     * Chat. This is used by the Connection class to deliver messages
+     * without a thread ID.
+     *
+     * @param message the message.
+     */
+    void deliver(Message message) {
+        // Because the collector and listeners are expecting a thread ID with
+        // a specific value, set the thread ID on the message even though it
+        // probably never had one.
+        message.setThread(threadID);
+
+        for (MessageListener listener : listeners) {
+            listener.processMessage(this, message);
+        }
+    }
+
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof Chat
+                && threadID.equals(((Chat)obj).getThreadID())
+                && participant.equals(((Chat)obj).getParticipant());
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/ChatManager.java b/src/org/jivesoftware/smack/ChatManager.java
new file mode 100644
index 0000000..22dc3f9
--- /dev/null
+++ b/src/org/jivesoftware/smack/ChatManager.java
@@ -0,0 +1,284 @@
+/**
+ * $RCSfile$
+ * $Revision: 2407 $
+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
+ *
+ * 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;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.FromContainsFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.ThreadFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smack.util.collections.ReferenceMap;
+
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * The chat manager keeps track of references to all current chats. It will not hold any references
+ * in memory on its own so it is neccesary to keep a reference to the chat object itself. To be
+ * made aware of new chats, register a listener by calling {@link #addChatListener(ChatManagerListener)}.
+ *
+ * @author Alexander Wenckus
+ */
+public class ChatManager {
+
+    /**
+     * Returns the next unique id. Each id made up of a short alphanumeric
+     * prefix along with a unique numeric value.
+     *
+     * @return the next id.
+     */
+    private static synchronized String nextID() {
+        return prefix + Long.toString(id++);
+    }
+
+    /**
+     * A prefix helps to make sure that ID's are unique across mutliple instances.
+     */
+    private static String prefix = StringUtils.randomString(5);
+
+    /**
+     * Keeps track of the current increment, which is appended to the prefix to
+     * forum a unique ID.
+     */
+    private static long id = 0;
+
+    /**
+     * Maps thread ID to chat.
+     */
+    private Map<String, Chat> threadChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
+            ReferenceMap.WEAK));
+
+    /**
+     * Maps jids to chats
+     */
+    private Map<String, Chat> jidChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
+            ReferenceMap.WEAK));
+
+    /**
+     * Maps base jids to chats
+     */
+    private Map<String, Chat> baseJidChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
+	    ReferenceMap.WEAK));
+
+    private Set<ChatManagerListener> chatManagerListeners
+            = new CopyOnWriteArraySet<ChatManagerListener>();
+
+    private Map<PacketInterceptor, PacketFilter> interceptors
+            = new WeakHashMap<PacketInterceptor, PacketFilter>();
+
+    private Connection connection;
+
+    ChatManager(Connection connection) {
+        this.connection = connection;
+
+        PacketFilter filter = new PacketFilter() {
+            public boolean accept(Packet packet) {
+                if (!(packet instanceof Message)) {
+                    return false;
+                }
+                Message.Type messageType = ((Message) packet).getType();
+                return messageType != Message.Type.groupchat &&
+                        messageType != Message.Type.headline;
+            }
+        };
+        // Add a listener for all message packets so that we can deliver errant
+        // messages to the best Chat instance available.
+        connection.addPacketListener(new PacketListener() {
+            public void processPacket(Packet packet) {
+                Message message = (Message) packet;
+                Chat chat;
+                if (message.getThread() == null) {
+                	chat = getUserChat(message.getFrom());
+                }
+                else {
+                    chat = getThreadChat(message.getThread());
+                    if (chat == null) {
+                        // Try to locate the chat based on the sender of the message
+                    	chat = getUserChat(message.getFrom());
+                    }
+                }
+
+                if(chat == null) {
+                    chat = createChat(message);
+                }
+                deliverMessage(chat, message);
+            }
+        }, filter);
+    }
+
+    /**
+     * Creates a new chat and returns it.
+     *
+     * @param userJID the user this chat is with.
+     * @param listener the listener which will listen for new messages from this chat.
+     * @return the created chat.
+     */
+    public Chat createChat(String userJID, MessageListener listener) {
+        String threadID;
+        do  {
+            threadID = nextID();
+        } while (threadChats.get(threadID) != null);
+
+        return createChat(userJID, threadID, listener);
+    }
+
+    /**
+     * Creates a new chat using the specified thread ID, then returns it.
+     * 
+     * @param userJID the jid of the user this chat is with
+     * @param thread the thread of the created chat.
+     * @param listener the listener to add to the chat
+     * @return the created chat.
+     */
+    public Chat createChat(String userJID, String thread, MessageListener listener) {
+        if(thread == null) {
+            thread = nextID();
+        }
+        Chat chat = threadChats.get(thread);
+        if(chat != null) {
+            throw new IllegalArgumentException("ThreadID is already used");
+        }
+        chat = createChat(userJID, thread, true);
+        chat.addMessageListener(listener);
+        return chat;
+    }
+
+    private Chat createChat(String userJID, String threadID, boolean createdLocally) {
+        Chat chat = new Chat(this, userJID, threadID);
+        threadChats.put(threadID, chat);
+        jidChats.put(userJID, chat);
+        baseJidChats.put(StringUtils.parseBareAddress(userJID), chat);
+
+        for(ChatManagerListener listener : chatManagerListeners) {
+            listener.chatCreated(chat, createdLocally);
+        }
+
+        return chat;
+    }
+
+    private Chat createChat(Message message) {
+        String threadID = message.getThread();
+        if(threadID == null) {
+            threadID = nextID();
+        }
+        String userJID = message.getFrom();
+
+        return createChat(userJID, threadID, false);
+    }
+
+    /**
+     * Try to get a matching chat for the given user JID.  Try the full
+     * JID map first, the try to match on the base JID if no match is
+     * found.
+     * 
+     * @param userJID
+     * @return
+     */
+    private Chat getUserChat(String userJID) {
+	Chat match = jidChats.get(userJID);
+	
+	if (match == null) {
+	    match = baseJidChats.get(StringUtils.parseBareAddress(userJID));
+	}
+	return match;
+    }
+
+    public Chat getThreadChat(String thread) {
+        return threadChats.get(thread);
+    }
+
+    /**
+     * Register a new listener with the ChatManager to recieve events related to chats.
+     *
+     * @param listener the listener.
+     */
+    public void addChatListener(ChatManagerListener listener) {
+        chatManagerListeners.add(listener);
+    }
+
+    /**
+     * Removes a listener, it will no longer be notified of new events related to chats.
+     *
+     * @param listener the listener that is being removed
+     */
+    public void removeChatListener(ChatManagerListener listener) {
+        chatManagerListeners.remove(listener);
+    }
+
+    /**
+     * Returns an unmodifiable collection of all chat listeners currently registered with this
+     * manager.
+     *
+     * @return an unmodifiable collection of all chat listeners currently registered with this
+     * manager.
+     */
+    public Collection<ChatManagerListener> getChatListeners() {
+        return Collections.unmodifiableCollection(chatManagerListeners);
+    }
+
+    private void deliverMessage(Chat chat, Message message) {
+        // Here we will run any interceptors
+        chat.deliver(message);
+    }
+
+    void sendMessage(Chat chat, Message message) {
+        for(Map.Entry<PacketInterceptor, PacketFilter> interceptor : interceptors.entrySet()) {
+            PacketFilter filter = interceptor.getValue();
+            if(filter != null && filter.accept(message)) {
+                interceptor.getKey().interceptPacket(message);
+            }
+        }
+        // Ensure that messages being sent have a proper FROM value
+        if (message.getFrom() == null) {
+            message.setFrom(connection.getUser());
+        }
+        connection.sendPacket(message);
+    }
+
+    PacketCollector createPacketCollector(Chat chat) {
+        return connection.createPacketCollector(new AndFilter(new ThreadFilter(chat.getThreadID()), 
+                new FromContainsFilter(chat.getParticipant())));
+    }
+
+    /**
+     * Adds an interceptor which intercepts any messages sent through chats.
+     *
+     * @param packetInterceptor the interceptor.
+     */
+    public void addOutgoingMessageInterceptor(PacketInterceptor packetInterceptor) {
+        addOutgoingMessageInterceptor(packetInterceptor, null);
+    }
+
+    public void addOutgoingMessageInterceptor(PacketInterceptor packetInterceptor, PacketFilter filter) {
+        if (packetInterceptor != null) {
+            interceptors.put(packetInterceptor, filter);
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/ChatManagerListener.java b/src/org/jivesoftware/smack/ChatManagerListener.java
new file mode 100644
index 0000000..d7d5ab7
--- /dev/null
+++ b/src/org/jivesoftware/smack/ChatManagerListener.java
@@ -0,0 +1,37 @@
+/**
+ * $RCSfile$
+ * $Revision: 2407 $
+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
+ *
+ * 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;
+
+/**
+ * A listener for chat related events.
+ *
+ * @author Alexander Wenckus
+ */
+public interface ChatManagerListener {
+
+    /**
+     * Event fired when a new chat is created.
+     *
+     * @param chat the chat that was created.
+     * @param createdLocally true if the chat was created by the local user and false if it wasn't.
+     */
+    void chatCreated(Chat chat, boolean createdLocally);
+}
diff --git a/src/org/jivesoftware/smack/Connection.java b/src/org/jivesoftware/smack/Connection.java
new file mode 100644
index 0000000..c6b4b1c
--- /dev/null
+++ b/src/org/jivesoftware/smack/Connection.java
@@ -0,0 +1,920 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jivesoftware.smack.compression.JzlibInputOutputStream;
+import org.jivesoftware.smack.compression.XMPPInputOutputStream;
+import org.jivesoftware.smack.compression.Java7ZlibInputOutputStream;
+import org.jivesoftware.smack.debugger.SmackDebugger;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+
+/**
+ * The abstract Connection class provides an interface for connections to a
+ * XMPP server and implements shared methods which are used by the
+ * different types of connections (e.g. XMPPConnection or BoshConnection).
+ * 
+ * To create a connection to a XMPP server a simple usage of this API might
+ * look like the following:
+ * <pre>
+ * // Create a connection to the igniterealtime.org XMPP server.
+ * Connection con = new XMPPConnection("igniterealtime.org");
+ * // Connect to the server
+ * con.connect();
+ * // Most servers require you to login before performing other tasks.
+ * con.login("jsmith", "mypass");
+ * // Start a new conversation with John Doe and send him a message.
+ * Chat chat = connection.getChatManager().createChat("jdoe@igniterealtime.org"</font>, new MessageListener() {
+ * <p/>
+ *     public void processMessage(Chat chat, Message message) {
+ *         // Print out any messages we get back to standard out.
+ *         System.out.println(<font color="green">"Received message: "</font> + message);
+ *     }
+ * });
+ * chat.sendMessage(<font color="green">"Howdy!"</font>);
+ * // Disconnect from the server
+ * con.disconnect();
+ * </pre>
+ * <p/>
+ * Connections can be reused between connections. This means that an Connection
+ * may be connected, disconnected and then connected again. Listeners of the Connection
+ * will be retained accross connections.<p>
+ * <p/>
+ * If a connected Connection gets disconnected abruptly then it will try to reconnect
+ * again. To stop the reconnection process, use {@link #disconnect()}. Once stopped
+ * you can use {@link #connect()} to manually connect to the server.
+ * 
+ * @see XMPPConnection
+ * @author Matt Tucker
+ * @author Guenther Niess
+ */
+public abstract class Connection {
+
+    /** 
+     * Counter to uniquely identify connections that are created.
+     */
+    private final static AtomicInteger connectionCounter = new AtomicInteger(0);
+
+    /**
+     * A set of listeners which will be invoked if a new connection is created.
+     */
+    private final static Set<ConnectionCreationListener> connectionEstablishedListeners =
+            new CopyOnWriteArraySet<ConnectionCreationListener>();
+
+    protected final static List<XMPPInputOutputStream> compressionHandlers = new ArrayList<XMPPInputOutputStream>(2);
+
+    /**
+     * Value that indicates whether debugging is enabled. When enabled, a debug
+     * window will apear for each new connection that will contain the following
+     * information:<ul>
+     * <li> Client Traffic -- raw XML traffic generated by Smack and sent to the server.
+     * <li> Server Traffic -- raw XML traffic sent by the server to the client.
+     * <li> Interpreted Packets -- shows XML packets from the server as parsed by Smack.
+     * </ul>
+     * <p/>
+     * Debugging can be enabled by setting this field to true, or by setting the Java system
+     * property <tt>smack.debugEnabled</tt> to true. The system property can be set on the
+     * command line such as "java SomeApp -Dsmack.debugEnabled=true".
+     */
+    public static boolean DEBUG_ENABLED = false;
+
+    static {
+        // Use try block since we may not have permission to get a system
+        // property (for example, when an applet).
+        try {
+            DEBUG_ENABLED = Boolean.getBoolean("smack.debugEnabled");
+        }
+        catch (Exception e) {
+            // Ignore.
+        }
+        // Ensure the SmackConfiguration class is loaded by calling a method in it.
+        SmackConfiguration.getVersion();
+        // Add the Java7 compression handler first, since it's preferred
+        compressionHandlers.add(new Java7ZlibInputOutputStream());
+        // If we don't have access to the Java7 API use the JZlib compression handler
+        compressionHandlers.add(new JzlibInputOutputStream());
+    }
+
+    /**
+     * A collection of ConnectionListeners which listen for connection closing
+     * and reconnection events.
+     */
+    protected final Collection<ConnectionListener> connectionListeners =
+            new CopyOnWriteArrayList<ConnectionListener>();
+
+    /**
+     * A collection of PacketCollectors which collects packets for a specified filter
+     * and perform blocking and polling operations on the result queue.
+     */
+    protected final Collection<PacketCollector> collectors = new ConcurrentLinkedQueue<PacketCollector>();
+
+    /**
+     * List of PacketListeners that will be notified when a new packet was received.
+     */
+    protected final Map<PacketListener, ListenerWrapper> recvListeners =
+            new ConcurrentHashMap<PacketListener, ListenerWrapper>();
+
+    /**
+     * List of PacketListeners that will be notified when a new packet was sent.
+     */
+    protected final Map<PacketListener, ListenerWrapper> sendListeners =
+            new ConcurrentHashMap<PacketListener, ListenerWrapper>();
+
+    /**
+     * List of PacketInterceptors that will be notified when a new packet is about to be
+     * sent to the server. These interceptors may modify the packet before it is being
+     * actually sent to the server.
+     */
+    protected final Map<PacketInterceptor, InterceptorWrapper> interceptors =
+            new ConcurrentHashMap<PacketInterceptor, InterceptorWrapper>();
+
+    /**
+     * The AccountManager allows creation and management of accounts on an XMPP server.
+     */
+    private AccountManager accountManager = null;
+
+    /**
+     * The ChatManager keeps track of references to all current chats.
+     */
+    protected ChatManager chatManager = null;
+
+    /**
+     * The SmackDebugger allows to log and debug XML traffic.
+     */
+    protected SmackDebugger debugger = null;
+
+    /**
+     * The Reader which is used for the {@see debugger}.
+     */
+    protected Reader reader;
+
+    /**
+     * The Writer which is used for the {@see debugger}.
+     */
+    protected Writer writer;
+    
+    /**
+     * The permanent storage for the roster
+     */
+    protected RosterStorage rosterStorage;
+
+
+    /**
+     * The SASLAuthentication manager that is responsible for authenticating with the server.
+     */
+    protected SASLAuthentication saslAuthentication = new SASLAuthentication(this);
+
+    /**
+     * A number to uniquely identify connections that are created. This is distinct from the
+     * connection ID, which is a value sent by the server once a connection is made.
+     */
+    protected final int connectionCounterValue = connectionCounter.getAndIncrement();
+
+    /**
+     * Holds the initial configuration used while creating the connection.
+     */
+    protected final ConnectionConfiguration config;
+
+    /**
+     * Holds the Caps Node information for the used XMPP service (i.e. the XMPP server)
+     */
+    private String serviceCapsNode;
+
+    protected XMPPInputOutputStream compressionHandler;
+
+    /**
+     * Create a new Connection to a XMPP server.
+     * 
+     * @param configuration The configuration which is used to establish the connection.
+     */
+    protected Connection(ConnectionConfiguration configuration) {
+        config = configuration;
+    }
+
+    /**
+     * Returns the configuration used to connect to the server.
+     * 
+     * @return the configuration used to connect to the server.
+     */
+    protected ConnectionConfiguration getConfiguration() {
+        return config;
+    }
+
+    /**
+     * Returns the name of the service provided by the XMPP server for this connection.
+     * This is also called XMPP domain of the connected server. After
+     * authenticating with the server the returned value may be different.
+     * 
+     * @return the name of the service provided by the XMPP server.
+     */
+    public String getServiceName() {
+        return config.getServiceName();
+    }
+
+    /**
+     * Returns the host name of the server where the XMPP server is running. This would be the
+     * IP address of the server or a name that may be resolved by a DNS server.
+     * 
+     * @return the host name of the server where the XMPP server is running.
+     */
+    public String getHost() {
+        return config.getHost();
+    }
+
+    /**
+     * Returns the port number of the XMPP server for this connection. The default port
+     * for normal connections is 5222. The default port for SSL connections is 5223.
+     * 
+     * @return the port number of the XMPP server.
+     */
+    public int getPort() {
+        return config.getPort();
+    }
+
+    /**
+     * Returns the full XMPP address of the user that is logged in to the connection or
+     * <tt>null</tt> if not logged in yet. An XMPP address is in the form
+     * username@server/resource.
+     * 
+     * @return the full XMPP address of the user logged in.
+     */
+    public abstract String getUser();
+
+    /**
+     * Returns the connection ID for this connection, which is the value set by the server
+     * when opening a XMPP stream. If the server does not set a connection ID, this value
+     * will be null. This value will be <tt>null</tt> if not connected to the server.
+     * 
+     * @return the ID of this connection returned from the XMPP server or <tt>null</tt> if
+     *      not connected to the server.
+     */
+    public abstract String getConnectionID();
+
+    /**
+     * Returns true if currently connected to the XMPP server.
+     * 
+     * @return true if connected.
+     */
+    public abstract boolean isConnected();
+
+    /**
+     * Returns true if currently authenticated by successfully calling the login method.
+     * 
+     * @return true if authenticated.
+     */
+    public abstract boolean isAuthenticated();
+
+    /**
+     * Returns true if currently authenticated anonymously.
+     * 
+     * @return true if authenticated anonymously.
+     */
+    public abstract boolean isAnonymous();
+
+    /**
+     * Returns true if the connection to the server has successfully negotiated encryption. 
+     * 
+     * @return true if a secure connection to the server.
+     */
+    public abstract boolean isSecureConnection();
+
+    /**
+     * Returns if the reconnection mechanism is allowed to be used. By default
+     * reconnection is allowed.
+     * 
+     * @return true if the reconnection mechanism is allowed to be used.
+     */
+    protected boolean isReconnectionAllowed() {
+        return config.isReconnectionAllowed();
+    }
+
+    /**
+     * Returns true if network traffic is being compressed. When using stream compression network
+     * traffic can be reduced up to 90%. Therefore, stream compression is ideal when using a slow
+     * speed network connection. However, the server will need to use more CPU time in order to
+     * un/compress network data so under high load the server performance might be affected.
+     * 
+     * @return true if network traffic is being compressed.
+     */
+    public abstract boolean isUsingCompression();
+
+    /**
+     * Establishes a connection to the XMPP server and performs an automatic login
+     * only if the previous connection state was logged (authenticated). It basically
+     * creates and maintains a connection to the server.<p>
+     * <p/>
+     * Listeners will be preserved from a previous connection if the reconnection
+     * occurs after an abrupt termination.
+     * 
+     * @throws XMPPException if an error occurs while trying to establish the connection.
+     */
+    public abstract void connect() throws XMPPException;
+
+    /**
+     * Logs in to the server using the strongest authentication mode supported by
+     * the server, then sets presence to available. If the server supports SASL authentication 
+     * then the user will be authenticated using SASL if not Non-SASL authentication will 
+     * be tried. If more than five seconds (default timeout) elapses in each step of the 
+     * authentication process without a response from the server, or if an error occurs, a 
+     * XMPPException will be thrown.<p>
+     * 
+     * Before logging in (i.e. authenticate) to the server the connection must be connected.
+     * 
+     * It is possible to log in without sending an initial available presence by using
+     * {@link ConnectionConfiguration#setSendPresence(boolean)}. If this connection is
+     * not interested in loading its roster upon login then use
+     * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}.
+     * Finally, if you want to not pass a password and instead use a more advanced mechanism
+     * while using SASL then you may be interested in using
+     * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}.
+     * For more advanced login settings see {@link ConnectionConfiguration}.
+     * 
+     * @param username the username.
+     * @param password the password or <tt>null</tt> if using a CallbackHandler.
+     * @throws XMPPException if an error occurs.
+     */
+    public void login(String username, String password) throws XMPPException {
+        login(username, password, "Smack");
+    }
+
+    /**
+     * Logs in to the server using the strongest authentication mode supported by
+     * the server, then sets presence to available. If the server supports SASL authentication 
+     * then the user will be authenticated using SASL if not Non-SASL authentication will 
+     * be tried. If more than five seconds (default timeout) elapses in each step of the 
+     * authentication process without a response from the server, or if an error occurs, a 
+     * XMPPException will be thrown.<p>
+     * 
+     * Before logging in (i.e. authenticate) to the server the connection must be connected.
+     * 
+     * It is possible to log in without sending an initial available presence by using
+     * {@link ConnectionConfiguration#setSendPresence(boolean)}. If this connection is
+     * not interested in loading its roster upon login then use
+     * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}.
+     * Finally, if you want to not pass a password and instead use a more advanced mechanism
+     * while using SASL then you may be interested in using
+     * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}.
+     * For more advanced login settings see {@link ConnectionConfiguration}.
+     * 
+     * @param username the username.
+     * @param password the password or <tt>null</tt> if using a CallbackHandler.
+     * @param resource the resource.
+     * @throws XMPPException if an error occurs.
+     * @throws IllegalStateException if not connected to the server, or already logged in
+     *      to the serrver.
+     */
+    public abstract void login(String username, String password, String resource) throws XMPPException;
+
+    /**
+     * Logs in to the server anonymously. Very few servers are configured to support anonymous
+     * authentication, so it's fairly likely logging in anonymously will fail. If anonymous login
+     * does succeed, your XMPP address will likely be in the form "123ABC@server/789XYZ" or
+     * "server/123ABC" (where "123ABC" and "789XYZ" is a random value generated by the server).
+     * 
+     * @throws XMPPException if an error occurs or anonymous logins are not supported by the server.
+     * @throws IllegalStateException if not connected to the server, or already logged in
+     *      to the serrver.
+     */
+    public abstract void loginAnonymously() throws XMPPException;
+
+    /**
+     * Sends the specified packet to the server.
+     * 
+     * @param packet the packet to send.
+     */
+    public abstract void sendPacket(Packet packet);
+
+    /**
+     * Returns an account manager instance for this connection.
+     * 
+     * @return an account manager for this connection.
+     */
+    public AccountManager getAccountManager() {
+        if (accountManager == null) {
+            accountManager = new AccountManager(this);
+        }
+        return accountManager;
+    }
+
+    /**
+     * Returns a chat manager instance for this connection. The ChatManager manages all incoming and
+     * outgoing chats on the current connection.
+     * 
+     * @return a chat manager instance for this connection.
+     */
+    public synchronized ChatManager getChatManager() {
+        if (this.chatManager == null) {
+            this.chatManager = new ChatManager(this);
+        }
+        return this.chatManager;
+    }
+
+    /**
+     * Returns the roster for the user.
+     * <p>
+     * This method will never return <code>null</code>, instead if the user has not yet logged into
+     * the server or is logged in anonymously all modifying methods of the returned roster object
+     * like {@link Roster#createEntry(String, String, String[])},
+     * {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing
+     * {@link RosterListener}s will throw an IllegalStateException.
+     * 
+     * @return the user's roster.
+     */
+    public abstract Roster getRoster();
+    
+    /**
+     * Set the store for the roster of this connection. If you set the roster storage
+     * of a connection you enable support for XEP-0237 (RosterVersioning)
+     * @param store the store used for roster versioning
+     * @throws IllegalStateException if you add a roster store when roster is initializied
+     */
+    public abstract void setRosterStorage(RosterStorage storage) throws IllegalStateException;
+    
+    /**
+     * Returns the SASLAuthentication manager that is responsible for authenticating with
+     * the server.
+     * 
+     * @return the SASLAuthentication manager that is responsible for authenticating with
+     *         the server.
+     */
+    public SASLAuthentication getSASLAuthentication() {
+        return saslAuthentication;
+    }
+
+    /**
+     * Closes the connection by setting presence to unavailable then closing the connection to
+     * the XMPP server. The Connection can still be used for connecting to the server
+     * again.<p>
+     * <p/>
+     * This method cleans up all resources used by the connection. Therefore, the roster,
+     * listeners and other stateful objects cannot be re-used by simply calling connect()
+     * on this connection again. This is unlike the behavior during unexpected disconnects
+     * (and subsequent connections). In that case, all state is preserved to allow for
+     * more seamless error recovery.
+     */
+    public void disconnect() {
+        disconnect(new Presence(Presence.Type.unavailable));
+    }
+
+    /**
+     * Closes the connection. A custom unavailable presence is sent to the server, followed
+     * by closing the stream. The Connection can still be used for connecting to the server
+     * again. A custom unavilable presence is useful for communicating offline presence
+     * information such as "On vacation". Typically, just the status text of the presence
+     * packet is set with online information, but most XMPP servers will deliver the full
+     * presence packet with whatever data is set.<p>
+     * <p/>
+     * This method cleans up all resources used by the connection. Therefore, the roster,
+     * listeners and other stateful objects cannot be re-used by simply calling connect()
+     * on this connection again. This is unlike the behavior during unexpected disconnects
+     * (and subsequent connections). In that case, all state is preserved to allow for
+     * more seamless error recovery.
+     * 
+     * @param unavailablePresence the presence packet to send during shutdown.
+     */
+    public abstract void disconnect(Presence unavailablePresence);
+
+    /**
+     * Adds a new listener that will be notified when new Connections are created. Note
+     * that newly created connections will not be actually connected to the server.
+     * 
+     * @param connectionCreationListener a listener interested on new connections.
+     */
+    public static void addConnectionCreationListener(
+            ConnectionCreationListener connectionCreationListener) {
+        connectionEstablishedListeners.add(connectionCreationListener);
+    }
+
+    /**
+     * Removes a listener that was interested in connection creation events.
+     * 
+     * @param connectionCreationListener a listener interested on new connections.
+     */
+    public static void removeConnectionCreationListener(
+            ConnectionCreationListener connectionCreationListener) {
+        connectionEstablishedListeners.remove(connectionCreationListener);
+    }
+
+    /**
+     * Get the collection of listeners that are interested in connection creation events.
+     * 
+     * @return a collection of listeners interested on new connections.
+     */
+    protected static Collection<ConnectionCreationListener> getConnectionCreationListeners() {
+        return Collections.unmodifiableCollection(connectionEstablishedListeners);
+    }
+
+    /**
+     * Adds a connection listener to this connection that will be notified when
+     * the connection closes or fails. The connection needs to already be connected
+     * or otherwise an IllegalStateException will be thrown.
+     * 
+     * @param connectionListener a connection listener.
+     */
+    public void addConnectionListener(ConnectionListener connectionListener) {
+        if (!isConnected()) {
+            throw new IllegalStateException("Not connected to server.");
+        }
+        if (connectionListener == null) {
+            return;
+        }
+        if (!connectionListeners.contains(connectionListener)) {
+            connectionListeners.add(connectionListener);
+        }
+    }
+
+    /**
+     * Removes a connection listener from this connection.
+     * 
+     * @param connectionListener a connection listener.
+     */
+    public void removeConnectionListener(ConnectionListener connectionListener) {
+        connectionListeners.remove(connectionListener);
+    }
+
+    /**
+     * Get the collection of listeners that are interested in connection events.
+     * 
+     * @return a collection of listeners interested on connection events.
+     */
+    protected Collection<ConnectionListener> getConnectionListeners() {
+        return connectionListeners;
+    }
+
+    /**
+     * Creates a new packet collector for this connection. A packet filter determines
+     * which packets will be accumulated by the collector. A PacketCollector is
+     * more suitable to use than a {@link PacketListener} when you need to wait for
+     * a specific result.
+     * 
+     * @param packetFilter the packet filter to use.
+     * @return a new packet collector.
+     */
+    public PacketCollector createPacketCollector(PacketFilter packetFilter) {
+        PacketCollector collector = new PacketCollector(this, packetFilter);
+        // Add the collector to the list of active collectors.
+        collectors.add(collector);
+        return collector;
+    }
+
+    /**
+     * Remove a packet collector of this connection.
+     * 
+     * @param collector a packet collectors which was created for this connection.
+     */
+    protected void removePacketCollector(PacketCollector collector) {
+        collectors.remove(collector);
+    }
+
+    /**
+     * Get the collection of all packet collectors for this connection.
+     * 
+     * @return a collection of packet collectors for this connection.
+     */
+    protected Collection<PacketCollector> getPacketCollectors() {
+        return collectors;
+    }
+
+    /**
+     * Registers a packet listener with this connection. A packet filter determines
+     * which packets will be delivered to the listener. If the same packet listener
+     * is added again with a different filter, only the new filter will be used.
+     * 
+     * @param packetListener the packet listener to notify of new received packets.
+     * @param packetFilter   the packet filter to use.
+     */
+    public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) {
+        if (packetListener == null) {
+            throw new NullPointerException("Packet listener is null.");
+        }
+        ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter);
+        recvListeners.put(packetListener, wrapper);
+    }
+
+    /**
+     * Removes a packet listener for received packets from this connection.
+     * 
+     * @param packetListener the packet listener to remove.
+     */
+    public void removePacketListener(PacketListener packetListener) {
+        recvListeners.remove(packetListener);
+    }
+
+    /**
+     * Get a map of all packet listeners for received packets of this connection.
+     * 
+     * @return a map of all packet listeners for received packets.
+     */
+    protected Map<PacketListener, ListenerWrapper> getPacketListeners() {
+        return recvListeners;
+    }
+
+    /**
+     * Registers a packet listener with this connection. The listener will be
+     * notified of every packet that this connection sends. A packet filter determines
+     * which packets will be delivered to the listener. Note that the thread
+     * that writes packets will be used to invoke the listeners. Therefore, each
+     * packet listener should complete all operations quickly or use a different
+     * thread for processing.
+     * 
+     * @param packetListener the packet listener to notify of sent packets.
+     * @param packetFilter   the packet filter to use.
+     */
+    public void addPacketSendingListener(PacketListener packetListener, PacketFilter packetFilter) {
+        if (packetListener == null) {
+            throw new NullPointerException("Packet listener is null.");
+        }
+        ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter);
+        sendListeners.put(packetListener, wrapper);
+    }
+
+    /**
+     * Removes a packet listener for sending packets from this connection.
+     * 
+     * @param packetListener the packet listener to remove.
+     */
+    public void removePacketSendingListener(PacketListener packetListener) {
+        sendListeners.remove(packetListener);
+    }
+
+    /**
+     * Get a map of all packet listeners for sending packets of this connection.
+     * 
+     * @return a map of all packet listeners for sent packets.
+     */
+    protected Map<PacketListener, ListenerWrapper> getPacketSendingListeners() {
+        return sendListeners;
+    }
+
+
+    /**
+     * Process all packet listeners for sending packets.
+     * 
+     * @param packet the packet to process.
+     */
+    protected void firePacketSendingListeners(Packet packet) {
+        // Notify the listeners of the new sent packet
+        for (ListenerWrapper listenerWrapper : sendListeners.values()) {
+            listenerWrapper.notifyListener(packet);
+        }
+    }
+
+    /**
+     * Registers a packet interceptor with this connection. The interceptor will be
+     * invoked every time a packet is about to be sent by this connection. Interceptors
+     * may modify the packet to be sent. A packet filter determines which packets
+     * will be delivered to the interceptor.
+     *
+     * @param packetInterceptor the packet interceptor to notify of packets about to be sent.
+     * @param packetFilter      the packet filter to use.
+     */
+    public void addPacketInterceptor(PacketInterceptor packetInterceptor,
+            PacketFilter packetFilter) {
+        if (packetInterceptor == null) {
+            throw new NullPointerException("Packet interceptor is null.");
+        }
+        interceptors.put(packetInterceptor, new InterceptorWrapper(packetInterceptor, packetFilter));
+    }
+
+    /**
+     * Removes a packet interceptor.
+     *
+     * @param packetInterceptor the packet interceptor to remove.
+     */
+    public void removePacketInterceptor(PacketInterceptor packetInterceptor) {
+        interceptors.remove(packetInterceptor);
+    }
+    
+    public boolean isSendPresence() {
+        return config.isSendPresence();
+    }
+
+    /**
+     * Get a map of all packet interceptors for sending packets of this connection.
+     * 
+     * @return a map of all packet interceptors for sending packets.
+     */
+    protected Map<PacketInterceptor, InterceptorWrapper> getPacketInterceptors() {
+        return interceptors;
+    }
+
+    /**
+     * Process interceptors. Interceptors may modify the packet that is about to be sent.
+     * Since the thread that requested to send the packet will invoke all interceptors, it
+     * is important that interceptors perform their work as soon as possible so that the
+     * thread does not remain blocked for a long period.
+     * 
+     * @param packet the packet that is going to be sent to the server
+     */
+    protected void firePacketInterceptors(Packet packet) {
+        if (packet != null) {
+            for (InterceptorWrapper interceptorWrapper : interceptors.values()) {
+                interceptorWrapper.notifyListener(packet);
+            }
+        }
+    }
+
+    /**
+     * Initialize the {@link #debugger}. You can specify a customized {@link SmackDebugger}
+     * by setup the system property <code>smack.debuggerClass</code> to the implementation.
+     * 
+     * @throws IllegalStateException if the reader or writer isn't yet initialized.
+     * @throws IllegalArgumentException if the SmackDebugger can't be loaded.
+     */
+    protected void initDebugger() {
+        if (reader == null || writer == null) {
+            throw new NullPointerException("Reader or writer isn't initialized.");
+        }
+        // If debugging is enabled, we open a window and write out all network traffic.
+        if (config.isDebuggerEnabled()) {
+            if (debugger == null) {
+                // Detect the debugger class to use.
+                String className = null;
+                // Use try block since we may not have permission to get a system
+                // property (for example, when an applet).
+                try {
+                    className = System.getProperty("smack.debuggerClass");
+                }
+                catch (Throwable t) {
+                    // Ignore.
+                }
+                Class<?> debuggerClass = null;
+                if (className != null) {
+                    try {
+                        debuggerClass = Class.forName(className);
+                    }
+                    catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                }
+                if (debuggerClass == null) {
+                    try {
+                        debuggerClass =
+                                Class.forName("de.measite.smack.AndroidDebugger");
+                    }
+                    catch (Exception ex) {
+                        try {
+                            debuggerClass =
+                                    Class.forName("org.jivesoftware.smack.debugger.ConsoleDebugger");
+                        }
+                        catch (Exception ex2) {
+                            ex2.printStackTrace();
+                        }
+                    }
+                }
+                // Create a new debugger instance. If an exception occurs then disable the debugging
+                // option
+                try {
+                    Constructor<?> constructor = debuggerClass
+                            .getConstructor(Connection.class, Writer.class, Reader.class);
+                    debugger = (SmackDebugger) constructor.newInstance(this, writer, reader);
+                    reader = debugger.getReader();
+                    writer = debugger.getWriter();
+                }
+                catch (Exception e) {
+                    throw new IllegalArgumentException("Can't initialize the configured debugger!", e);
+                }
+            }
+            else {
+                // Obtain new reader and writer from the existing debugger
+                reader = debugger.newConnectionReader(reader);
+                writer = debugger.newConnectionWriter(writer);
+            }
+        }
+        
+    }
+
+    /**
+     * Set the servers Entity Caps node
+     * 
+     * Connection holds this information in order to avoid a dependency to
+     * smackx where EntityCapsManager lives from smack.
+     * 
+     * @param node
+     */
+    protected void setServiceCapsNode(String node) {
+        serviceCapsNode = node;
+    }
+
+    /**
+     * Retrieve the servers Entity Caps node
+     * 
+     * Connection holds this information in order to avoid a dependency to
+     * smackx where EntityCapsManager lives from smack.
+     * 
+     * @return
+     */
+    public String getServiceCapsNode() {
+        return serviceCapsNode;
+    }
+
+    /**
+     * A wrapper class to associate a packet filter with a listener.
+     */
+    protected static class ListenerWrapper {
+
+        private PacketListener packetListener;
+        private PacketFilter packetFilter;
+
+        /**
+         * Create a class which associates a packet filter with a listener.
+         * 
+         * @param packetListener the packet listener.
+         * @param packetFilter the associated filter or null if it listen for all packets.
+         */
+        public ListenerWrapper(PacketListener packetListener, PacketFilter packetFilter) {
+            this.packetListener = packetListener;
+            this.packetFilter = packetFilter;
+        }
+
+        /**
+         * Notify and process the packet listener if the filter matches the packet.
+         * 
+         * @param packet the packet which was sent or received.
+         */
+        public void notifyListener(Packet packet) {
+            if (packetFilter == null || packetFilter.accept(packet)) {
+                packetListener.processPacket(packet);
+            }
+        }
+    }
+
+    /**
+     * A wrapper class to associate a packet filter with an interceptor.
+     */
+    protected static class InterceptorWrapper {
+
+        private PacketInterceptor packetInterceptor;
+        private PacketFilter packetFilter;
+
+        /**
+         * Create a class which associates a packet filter with an interceptor.
+         * 
+         * @param packetInterceptor the interceptor.
+         * @param packetFilter the associated filter or null if it intercepts all packets.
+         */
+        public InterceptorWrapper(PacketInterceptor packetInterceptor, PacketFilter packetFilter) {
+            this.packetInterceptor = packetInterceptor;
+            this.packetFilter = packetFilter;
+        }
+
+        public boolean equals(Object object) {
+            if (object == null) {
+                return false;
+            }
+            if (object instanceof InterceptorWrapper) {
+                return ((InterceptorWrapper) object).packetInterceptor
+                        .equals(this.packetInterceptor);
+            }
+            else if (object instanceof PacketInterceptor) {
+                return object.equals(this.packetInterceptor);
+            }
+            return false;
+        }
+
+        /**
+         * Notify and process the packet interceptor if the filter matches the packet.
+         * 
+         * @param packet the packet which will be sent.
+         */
+        public void notifyListener(Packet packet) {
+            if (packetFilter == null || packetFilter.accept(packet)) {
+                packetInterceptor.interceptPacket(packet);
+            }
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/Connection.java.orig b/src/org/jivesoftware/smack/Connection.java.orig
new file mode 100644
index 0000000..6c70a82
--- /dev/null
+++ b/src/org/jivesoftware/smack/Connection.java.orig
@@ -0,0 +1,920 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.Constructor;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.jivesoftware.smack.compression.JzlibInputOutputStream;
+import org.jivesoftware.smack.compression.XMPPInputOutputStream;
+import org.jivesoftware.smack.compression.Java7ZlibInputOutputStream;
+import org.jivesoftware.smack.debugger.SmackDebugger;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+
+/**
+ * The abstract Connection class provides an interface for connections to a
+ * XMPP server and implements shared methods which are used by the
+ * different types of connections (e.g. XMPPConnection or BoshConnection).
+ * 
+ * To create a connection to a XMPP server a simple usage of this API might
+ * look like the following:
+ * <pre>
+ * // Create a connection to the igniterealtime.org XMPP server.
+ * Connection con = new XMPPConnection("igniterealtime.org");
+ * // Connect to the server
+ * con.connect();
+ * // Most servers require you to login before performing other tasks.
+ * con.login("jsmith", "mypass");
+ * // Start a new conversation with John Doe and send him a message.
+ * Chat chat = connection.getChatManager().createChat("jdoe@igniterealtime.org"</font>, new MessageListener() {
+ * <p/>
+ *     public void processMessage(Chat chat, Message message) {
+ *         // Print out any messages we get back to standard out.
+ *         System.out.println(<font color="green">"Received message: "</font> + message);
+ *     }
+ * });
+ * chat.sendMessage(<font color="green">"Howdy!"</font>);
+ * // Disconnect from the server
+ * con.disconnect();
+ * </pre>
+ * <p/>
+ * Connections can be reused between connections. This means that an Connection
+ * may be connected, disconnected and then connected again. Listeners of the Connection
+ * will be retained accross connections.<p>
+ * <p/>
+ * If a connected Connection gets disconnected abruptly then it will try to reconnect
+ * again. To stop the reconnection process, use {@link #disconnect()}. Once stopped
+ * you can use {@link #connect()} to manually connect to the server.
+ * 
+ * @see XMPPConnection
+ * @author Matt Tucker
+ * @author Guenther Niess
+ */
+public abstract class Connection {
+
+    /** 
+     * Counter to uniquely identify connections that are created.
+     */
+    private final static AtomicInteger connectionCounter = new AtomicInteger(0);
+
+    /**
+     * A set of listeners which will be invoked if a new connection is created.
+     */
+    private final static Set<ConnectionCreationListener> connectionEstablishedListeners =
+            new CopyOnWriteArraySet<ConnectionCreationListener>();
+
+    protected final static List<XMPPInputOutputStream> compressionHandlers = new ArrayList<XMPPInputOutputStream>(2);
+
+    /**
+     * Value that indicates whether debugging is enabled. When enabled, a debug
+     * window will apear for each new connection that will contain the following
+     * information:<ul>
+     * <li> Client Traffic -- raw XML traffic generated by Smack and sent to the server.
+     * <li> Server Traffic -- raw XML traffic sent by the server to the client.
+     * <li> Interpreted Packets -- shows XML packets from the server as parsed by Smack.
+     * </ul>
+     * <p/>
+     * Debugging can be enabled by setting this field to true, or by setting the Java system
+     * property <tt>smack.debugEnabled</tt> to true. The system property can be set on the
+     * command line such as "java SomeApp -Dsmack.debugEnabled=true".
+     */
+    public static boolean DEBUG_ENABLED = false;
+
+    static {
+        // Use try block since we may not have permission to get a system
+        // property (for example, when an applet).
+        try {
+            DEBUG_ENABLED = Boolean.getBoolean("smack.debugEnabled");
+        }
+        catch (Exception e) {
+            // Ignore.
+        }
+        // Ensure the SmackConfiguration class is loaded by calling a method in it.
+        SmackConfiguration.getVersion();
+        // Add the Java7 compression handler first, since it's preferred
+        compressionHandlers.add(new Java7ZlibInputOutputStream());
+        // If we don't have access to the Java7 API use the JZlib compression handler
+        compressionHandlers.add(new JzlibInputOutputStream());
+    }
+
+    /**
+     * A collection of ConnectionListeners which listen for connection closing
+     * and reconnection events.
+     */
+    protected final Collection<ConnectionListener> connectionListeners =
+            new CopyOnWriteArrayList<ConnectionListener>();
+
+    /**
+     * A collection of PacketCollectors which collects packets for a specified filter
+     * and perform blocking and polling operations on the result queue.
+     */
+    protected final Collection<PacketCollector> collectors = new ConcurrentLinkedQueue<PacketCollector>();
+
+    /**
+     * List of PacketListeners that will be notified when a new packet was received.
+     */
+    protected final Map<PacketListener, ListenerWrapper> recvListeners =
+            new ConcurrentHashMap<PacketListener, ListenerWrapper>();
+
+    /**
+     * List of PacketListeners that will be notified when a new packet was sent.
+     */
+    protected final Map<PacketListener, ListenerWrapper> sendListeners =
+            new ConcurrentHashMap<PacketListener, ListenerWrapper>();
+
+    /**
+     * List of PacketInterceptors that will be notified when a new packet is about to be
+     * sent to the server. These interceptors may modify the packet before it is being
+     * actually sent to the server.
+     */
+    protected final Map<PacketInterceptor, InterceptorWrapper> interceptors =
+            new ConcurrentHashMap<PacketInterceptor, InterceptorWrapper>();
+
+    /**
+     * The AccountManager allows creation and management of accounts on an XMPP server.
+     */
+    private AccountManager accountManager = null;
+
+    /**
+     * The ChatManager keeps track of references to all current chats.
+     */
+    protected ChatManager chatManager = null;
+
+    /**
+     * The SmackDebugger allows to log and debug XML traffic.
+     */
+    protected SmackDebugger debugger = null;
+
+    /**
+     * The Reader which is used for the {@see debugger}.
+     */
+    protected Reader reader;
+
+    /**
+     * The Writer which is used for the {@see debugger}.
+     */
+    protected Writer writer;
+    
+    /**
+     * The permanent storage for the roster
+     */
+    protected RosterStorage rosterStorage;
+
+
+    /**
+     * The SASLAuthentication manager that is responsible for authenticating with the server.
+     */
+    protected SASLAuthentication saslAuthentication = new SASLAuthentication(this);
+
+    /**
+     * A number to uniquely identify connections that are created. This is distinct from the
+     * connection ID, which is a value sent by the server once a connection is made.
+     */
+    protected final int connectionCounterValue = connectionCounter.getAndIncrement();
+
+    /**
+     * Holds the initial configuration used while creating the connection.
+     */
+    protected final ConnectionConfiguration config;
+
+    /**
+     * Holds the Caps Node information for the used XMPP service (i.e. the XMPP server)
+     */
+    private String serviceCapsNode;
+
+    protected XMPPInputOutputStream compressionHandler;
+
+    /**
+     * Create a new Connection to a XMPP server.
+     * 
+     * @param configuration The configuration which is used to establish the connection.
+     */
+    protected Connection(ConnectionConfiguration configuration) {
+        config = configuration;
+    }
+
+    /**
+     * Returns the configuration used to connect to the server.
+     * 
+     * @return the configuration used to connect to the server.
+     */
+    protected ConnectionConfiguration getConfiguration() {
+        return config;
+    }
+
+    /**
+     * Returns the name of the service provided by the XMPP server for this connection.
+     * This is also called XMPP domain of the connected server. After
+     * authenticating with the server the returned value may be different.
+     * 
+     * @return the name of the service provided by the XMPP server.
+     */
+    public String getServiceName() {
+        return config.getServiceName();
+    }
+
+    /**
+     * Returns the host name of the server where the XMPP server is running. This would be the
+     * IP address of the server or a name that may be resolved by a DNS server.
+     * 
+     * @return the host name of the server where the XMPP server is running.
+     */
+    public String getHost() {
+        return config.getHost();
+    }
+
+    /**
+     * Returns the port number of the XMPP server for this connection. The default port
+     * for normal connections is 5222. The default port for SSL connections is 5223.
+     * 
+     * @return the port number of the XMPP server.
+     */
+    public int getPort() {
+        return config.getPort();
+    }
+
+    /**
+     * Returns the full XMPP address of the user that is logged in to the connection or
+     * <tt>null</tt> if not logged in yet. An XMPP address is in the form
+     * username@server/resource.
+     * 
+     * @return the full XMPP address of the user logged in.
+     */
+    public abstract String getUser();
+
+    /**
+     * Returns the connection ID for this connection, which is the value set by the server
+     * when opening a XMPP stream. If the server does not set a connection ID, this value
+     * will be null. This value will be <tt>null</tt> if not connected to the server.
+     * 
+     * @return the ID of this connection returned from the XMPP server or <tt>null</tt> if
+     *      not connected to the server.
+     */
+    public abstract String getConnectionID();
+
+    /**
+     * Returns true if currently connected to the XMPP server.
+     * 
+     * @return true if connected.
+     */
+    public abstract boolean isConnected();
+
+    /**
+     * Returns true if currently authenticated by successfully calling the login method.
+     * 
+     * @return true if authenticated.
+     */
+    public abstract boolean isAuthenticated();
+
+    /**
+     * Returns true if currently authenticated anonymously.
+     * 
+     * @return true if authenticated anonymously.
+     */
+    public abstract boolean isAnonymous();
+
+    /**
+     * Returns true if the connection to the server has successfully negotiated encryption. 
+     * 
+     * @return true if a secure connection to the server.
+     */
+    public abstract boolean isSecureConnection();
+
+    /**
+     * Returns if the reconnection mechanism is allowed to be used. By default
+     * reconnection is allowed.
+     * 
+     * @return true if the reconnection mechanism is allowed to be used.
+     */
+    protected boolean isReconnectionAllowed() {
+        return config.isReconnectionAllowed();
+    }
+
+    /**
+     * Returns true if network traffic is being compressed. When using stream compression network
+     * traffic can be reduced up to 90%. Therefore, stream compression is ideal when using a slow
+     * speed network connection. However, the server will need to use more CPU time in order to
+     * un/compress network data so under high load the server performance might be affected.
+     * 
+     * @return true if network traffic is being compressed.
+     */
+    public abstract boolean isUsingCompression();
+
+    /**
+     * Establishes a connection to the XMPP server and performs an automatic login
+     * only if the previous connection state was logged (authenticated). It basically
+     * creates and maintains a connection to the server.<p>
+     * <p/>
+     * Listeners will be preserved from a previous connection if the reconnection
+     * occurs after an abrupt termination.
+     * 
+     * @throws XMPPException if an error occurs while trying to establish the connection.
+     */
+    public abstract void connect() throws XMPPException;
+
+    /**
+     * Logs in to the server using the strongest authentication mode supported by
+     * the server, then sets presence to available. If the server supports SASL authentication 
+     * then the user will be authenticated using SASL if not Non-SASL authentication will 
+     * be tried. If more than five seconds (default timeout) elapses in each step of the 
+     * authentication process without a response from the server, or if an error occurs, a 
+     * XMPPException will be thrown.<p>
+     * 
+     * Before logging in (i.e. authenticate) to the server the connection must be connected.
+     * 
+     * It is possible to log in without sending an initial available presence by using
+     * {@link ConnectionConfiguration#setSendPresence(boolean)}. If this connection is
+     * not interested in loading its roster upon login then use
+     * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}.
+     * Finally, if you want to not pass a password and instead use a more advanced mechanism
+     * while using SASL then you may be interested in using
+     * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}.
+     * For more advanced login settings see {@link ConnectionConfiguration}.
+     * 
+     * @param username the username.
+     * @param password the password or <tt>null</tt> if using a CallbackHandler.
+     * @throws XMPPException if an error occurs.
+     */
+    public void login(String username, String password) throws XMPPException {
+        login(username, password, "Smack");
+    }
+
+    /**
+     * Logs in to the server using the strongest authentication mode supported by
+     * the server, then sets presence to available. If the server supports SASL authentication 
+     * then the user will be authenticated using SASL if not Non-SASL authentication will 
+     * be tried. If more than five seconds (default timeout) elapses in each step of the 
+     * authentication process without a response from the server, or if an error occurs, a 
+     * XMPPException will be thrown.<p>
+     * 
+     * Before logging in (i.e. authenticate) to the server the connection must be connected.
+     * 
+     * It is possible to log in without sending an initial available presence by using
+     * {@link ConnectionConfiguration#setSendPresence(boolean)}. If this connection is
+     * not interested in loading its roster upon login then use
+     * {@link ConnectionConfiguration#setRosterLoadedAtLogin(boolean)}.
+     * Finally, if you want to not pass a password and instead use a more advanced mechanism
+     * while using SASL then you may be interested in using
+     * {@link ConnectionConfiguration#setCallbackHandler(javax.security.auth.callback.CallbackHandler)}.
+     * For more advanced login settings see {@link ConnectionConfiguration}.
+     * 
+     * @param username the username.
+     * @param password the password or <tt>null</tt> if using a CallbackHandler.
+     * @param resource the resource.
+     * @throws XMPPException if an error occurs.
+     * @throws IllegalStateException if not connected to the server, or already logged in
+     *      to the serrver.
+     */
+    public abstract void login(String username, String password, String resource) throws XMPPException;
+
+    /**
+     * Logs in to the server anonymously. Very few servers are configured to support anonymous
+     * authentication, so it's fairly likely logging in anonymously will fail. If anonymous login
+     * does succeed, your XMPP address will likely be in the form "123ABC@server/789XYZ" or
+     * "server/123ABC" (where "123ABC" and "789XYZ" is a random value generated by the server).
+     * 
+     * @throws XMPPException if an error occurs or anonymous logins are not supported by the server.
+     * @throws IllegalStateException if not connected to the server, or already logged in
+     *      to the serrver.
+     */
+    public abstract void loginAnonymously() throws XMPPException;
+
+    /**
+     * Sends the specified packet to the server.
+     * 
+     * @param packet the packet to send.
+     */
+    public abstract void sendPacket(Packet packet);
+
+    /**
+     * Returns an account manager instance for this connection.
+     * 
+     * @return an account manager for this connection.
+     */
+    public AccountManager getAccountManager() {
+        if (accountManager == null) {
+            accountManager = new AccountManager(this);
+        }
+        return accountManager;
+    }
+
+    /**
+     * Returns a chat manager instance for this connection. The ChatManager manages all incoming and
+     * outgoing chats on the current connection.
+     * 
+     * @return a chat manager instance for this connection.
+     */
+    public synchronized ChatManager getChatManager() {
+        if (this.chatManager == null) {
+            this.chatManager = new ChatManager(this);
+        }
+        return this.chatManager;
+    }
+
+    /**
+     * Returns the roster for the user.
+     * <p>
+     * This method will never return <code>null</code>, instead if the user has not yet logged into
+     * the server or is logged in anonymously all modifying methods of the returned roster object
+     * like {@link Roster#createEntry(String, String, String[])},
+     * {@link Roster#removeEntry(RosterEntry)} , etc. except adding or removing
+     * {@link RosterListener}s will throw an IllegalStateException.
+     * 
+     * @return the user's roster.
+     */
+    public abstract Roster getRoster();
+    
+    /**
+     * Set the store for the roster of this connection. If you set the roster storage
+     * of a connection you enable support for XEP-0237 (RosterVersioning)
+     * @param store the store used for roster versioning
+     * @throws IllegalStateException if you add a roster store when roster is initializied
+     */
+    public abstract void setRosterStorage(RosterStorage storage) throws IllegalStateException;
+    
+    /**
+     * Returns the SASLAuthentication manager that is responsible for authenticating with
+     * the server.
+     * 
+     * @return the SASLAuthentication manager that is responsible for authenticating with
+     *         the server.
+     */
+    public SASLAuthentication getSASLAuthentication() {
+        return saslAuthentication;
+    }
+
+    /**
+     * Closes the connection by setting presence to unavailable then closing the connection to
+     * the XMPP server. The Connection can still be used for connecting to the server
+     * again.<p>
+     * <p/>
+     * This method cleans up all resources used by the connection. Therefore, the roster,
+     * listeners and other stateful objects cannot be re-used by simply calling connect()
+     * on this connection again. This is unlike the behavior during unexpected disconnects
+     * (and subsequent connections). In that case, all state is preserved to allow for
+     * more seamless error recovery.
+     */
+    public void disconnect() {
+        disconnect(new Presence(Presence.Type.unavailable));
+    }
+
+    /**
+     * Closes the connection. A custom unavailable presence is sent to the server, followed
+     * by closing the stream. The Connection can still be used for connecting to the server
+     * again. A custom unavilable presence is useful for communicating offline presence
+     * information such as "On vacation". Typically, just the status text of the presence
+     * packet is set with online information, but most XMPP servers will deliver the full
+     * presence packet with whatever data is set.<p>
+     * <p/>
+     * This method cleans up all resources used by the connection. Therefore, the roster,
+     * listeners and other stateful objects cannot be re-used by simply calling connect()
+     * on this connection again. This is unlike the behavior during unexpected disconnects
+     * (and subsequent connections). In that case, all state is preserved to allow for
+     * more seamless error recovery.
+     * 
+     * @param unavailablePresence the presence packet to send during shutdown.
+     */
+    public abstract void disconnect(Presence unavailablePresence);
+
+    /**
+     * Adds a new listener that will be notified when new Connections are created. Note
+     * that newly created connections will not be actually connected to the server.
+     * 
+     * @param connectionCreationListener a listener interested on new connections.
+     */
+    public static void addConnectionCreationListener(
+            ConnectionCreationListener connectionCreationListener) {
+        connectionEstablishedListeners.add(connectionCreationListener);
+    }
+
+    /**
+     * Removes a listener that was interested in connection creation events.
+     * 
+     * @param connectionCreationListener a listener interested on new connections.
+     */
+    public static void removeConnectionCreationListener(
+            ConnectionCreationListener connectionCreationListener) {
+        connectionEstablishedListeners.remove(connectionCreationListener);
+    }
+
+    /**
+     * Get the collection of listeners that are interested in connection creation events.
+     * 
+     * @return a collection of listeners interested on new connections.
+     */
+    protected static Collection<ConnectionCreationListener> getConnectionCreationListeners() {
+        return Collections.unmodifiableCollection(connectionEstablishedListeners);
+    }
+
+    /**
+     * Adds a connection listener to this connection that will be notified when
+     * the connection closes or fails. The connection needs to already be connected
+     * or otherwise an IllegalStateException will be thrown.
+     * 
+     * @param connectionListener a connection listener.
+     */
+    public void addConnectionListener(ConnectionListener connectionListener) {
+        if (!isConnected()) {
+            throw new IllegalStateException("Not connected to server.");
+        }
+        if (connectionListener == null) {
+            return;
+        }
+        if (!connectionListeners.contains(connectionListener)) {
+            connectionListeners.add(connectionListener);
+        }
+    }
+
+    /**
+     * Removes a connection listener from this connection.
+     * 
+     * @param connectionListener a connection listener.
+     */
+    public void removeConnectionListener(ConnectionListener connectionListener) {
+        connectionListeners.remove(connectionListener);
+    }
+
+    /**
+     * Get the collection of listeners that are interested in connection events.
+     * 
+     * @return a collection of listeners interested on connection events.
+     */
+    protected Collection<ConnectionListener> getConnectionListeners() {
+        return connectionListeners;
+    }
+
+    /**
+     * Creates a new packet collector for this connection. A packet filter determines
+     * which packets will be accumulated by the collector. A PacketCollector is
+     * more suitable to use than a {@link PacketListener} when you need to wait for
+     * a specific result.
+     * 
+     * @param packetFilter the packet filter to use.
+     * @return a new packet collector.
+     */
+    public PacketCollector createPacketCollector(PacketFilter packetFilter) {
+        PacketCollector collector = new PacketCollector(this, packetFilter);
+        // Add the collector to the list of active collectors.
+        collectors.add(collector);
+        return collector;
+    }
+
+    /**
+     * Remove a packet collector of this connection.
+     * 
+     * @param collector a packet collectors which was created for this connection.
+     */
+    protected void removePacketCollector(PacketCollector collector) {
+        collectors.remove(collector);
+    }
+
+    /**
+     * Get the collection of all packet collectors for this connection.
+     * 
+     * @return a collection of packet collectors for this connection.
+     */
+    protected Collection<PacketCollector> getPacketCollectors() {
+        return collectors;
+    }
+
+    /**
+     * Registers a packet listener with this connection. A packet filter determines
+     * which packets will be delivered to the listener. If the same packet listener
+     * is added again with a different filter, only the new filter will be used.
+     * 
+     * @param packetListener the packet listener to notify of new received packets.
+     * @param packetFilter   the packet filter to use.
+     */
+    public void addPacketListener(PacketListener packetListener, PacketFilter packetFilter) {
+        if (packetListener == null) {
+            throw new NullPointerException("Packet listener is null.");
+        }
+        ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter);
+        recvListeners.put(packetListener, wrapper);
+    }
+
+    /**
+     * Removes a packet listener for received packets from this connection.
+     * 
+     * @param packetListener the packet listener to remove.
+     */
+    public void removePacketListener(PacketListener packetListener) {
+        recvListeners.remove(packetListener);
+    }
+
+    /**
+     * Get a map of all packet listeners for received packets of this connection.
+     * 
+     * @return a map of all packet listeners for received packets.
+     */
+    protected Map<PacketListener, ListenerWrapper> getPacketListeners() {
+        return recvListeners;
+    }
+
+    /**
+     * Registers a packet listener with this connection. The listener will be
+     * notified of every packet that this connection sends. A packet filter determines
+     * which packets will be delivered to the listener. Note that the thread
+     * that writes packets will be used to invoke the listeners. Therefore, each
+     * packet listener should complete all operations quickly or use a different
+     * thread for processing.
+     * 
+     * @param packetListener the packet listener to notify of sent packets.
+     * @param packetFilter   the packet filter to use.
+     */
+    public void addPacketSendingListener(PacketListener packetListener, PacketFilter packetFilter) {
+        if (packetListener == null) {
+            throw new NullPointerException("Packet listener is null.");
+        }
+        ListenerWrapper wrapper = new ListenerWrapper(packetListener, packetFilter);
+        sendListeners.put(packetListener, wrapper);
+    }
+
+    /**
+     * Removes a packet listener for sending packets from this connection.
+     * 
+     * @param packetListener the packet listener to remove.
+     */
+    public void removePacketSendingListener(PacketListener packetListener) {
+        sendListeners.remove(packetListener);
+    }
+
+    /**
+     * Get a map of all packet listeners for sending packets of this connection.
+     * 
+     * @return a map of all packet listeners for sent packets.
+     */
+    protected Map<PacketListener, ListenerWrapper> getPacketSendingListeners() {
+        return sendListeners;
+    }
+
+
+    /**
+     * Process all packet listeners for sending packets.
+     * 
+     * @param packet the packet to process.
+     */
+    protected void firePacketSendingListeners(Packet packet) {
+        // Notify the listeners of the new sent packet
+        for (ListenerWrapper listenerWrapper : sendListeners.values()) {
+            listenerWrapper.notifyListener(packet);
+        }
+    }
+
+    /**
+     * Registers a packet interceptor with this connection. The interceptor will be
+     * invoked every time a packet is about to be sent by this connection. Interceptors
+     * may modify the packet to be sent. A packet filter determines which packets
+     * will be delivered to the interceptor.
+     *
+     * @param packetInterceptor the packet interceptor to notify of packets about to be sent.
+     * @param packetFilter      the packet filter to use.
+     */
+    public void addPacketInterceptor(PacketInterceptor packetInterceptor,
+            PacketFilter packetFilter) {
+        if (packetInterceptor == null) {
+            throw new NullPointerException("Packet interceptor is null.");
+        }
+        interceptors.put(packetInterceptor, new InterceptorWrapper(packetInterceptor, packetFilter));
+    }
+
+    /**
+     * Removes a packet interceptor.
+     *
+     * @param packetInterceptor the packet interceptor to remove.
+     */
+    public void removePacketInterceptor(PacketInterceptor packetInterceptor) {
+        interceptors.remove(packetInterceptor);
+    }
+    
+    public boolean isSendPresence() {
+        return config.isSendPresence();
+    }
+
+    /**
+     * Get a map of all packet interceptors for sending packets of this connection.
+     * 
+     * @return a map of all packet interceptors for sending packets.
+     */
+    protected Map<PacketInterceptor, InterceptorWrapper> getPacketInterceptors() {
+        return interceptors;
+    }
+
+    /**
+     * Process interceptors. Interceptors may modify the packet that is about to be sent.
+     * Since the thread that requested to send the packet will invoke all interceptors, it
+     * is important that interceptors perform their work as soon as possible so that the
+     * thread does not remain blocked for a long period.
+     * 
+     * @param packet the packet that is going to be sent to the server
+     */
+    protected void firePacketInterceptors(Packet packet) {
+        if (packet != null) {
+            for (InterceptorWrapper interceptorWrapper : interceptors.values()) {
+                interceptorWrapper.notifyListener(packet);
+            }
+        }
+    }
+
+    /**
+     * Initialize the {@link #debugger}. You can specify a customized {@link SmackDebugger}
+     * by setup the system property <code>smack.debuggerClass</code> to the implementation.
+     * 
+     * @throws IllegalStateException if the reader or writer isn't yet initialized.
+     * @throws IllegalArgumentException if the SmackDebugger can't be loaded.
+     */
+    protected void initDebugger() {
+        if (reader == null || writer == null) {
+            throw new NullPointerException("Reader or writer isn't initialized.");
+        }
+        // If debugging is enabled, we open a window and write out all network traffic.
+        if (config.isDebuggerEnabled()) {
+            if (debugger == null) {
+                // Detect the debugger class to use.
+                String className = null;
+                // Use try block since we may not have permission to get a system
+                // property (for example, when an applet).
+                try {
+                    className = System.getProperty("smack.debuggerClass");
+                }
+                catch (Throwable t) {
+                    // Ignore.
+                }
+                Class<?> debuggerClass = null;
+                if (className != null) {
+                    try {
+                        debuggerClass = Class.forName(className);
+                    }
+                    catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                }
+                if (debuggerClass == null) {
+                    try {
+                        debuggerClass =
+                                Class.forName("org.jivesoftware.smackx.debugger.EnhancedDebugger");
+                    }
+                    catch (Exception ex) {
+                        try {
+                            debuggerClass =
+                                    Class.forName("org.jivesoftware.smack.debugger.LiteDebugger");
+                        }
+                        catch (Exception ex2) {
+                            ex2.printStackTrace();
+                        }
+                    }
+                }
+                // Create a new debugger instance. If an exception occurs then disable the debugging
+                // option
+                try {
+                    Constructor<?> constructor = debuggerClass
+                            .getConstructor(Connection.class, Writer.class, Reader.class);
+                    debugger = (SmackDebugger) constructor.newInstance(this, writer, reader);
+                    reader = debugger.getReader();
+                    writer = debugger.getWriter();
+                }
+                catch (Exception e) {
+                    throw new IllegalArgumentException("Can't initialize the configured debugger!", e);
+                }
+            }
+            else {
+                // Obtain new reader and writer from the existing debugger
+                reader = debugger.newConnectionReader(reader);
+                writer = debugger.newConnectionWriter(writer);
+            }
+        }
+        
+    }
+
+    /**
+     * Set the servers Entity Caps node
+     * 
+     * Connection holds this information in order to avoid a dependency to
+     * smackx where EntityCapsManager lives from smack.
+     * 
+     * @param node
+     */
+    protected void setServiceCapsNode(String node) {
+        serviceCapsNode = node;
+    }
+
+    /**
+     * Retrieve the servers Entity Caps node
+     * 
+     * Connection holds this information in order to avoid a dependency to
+     * smackx where EntityCapsManager lives from smack.
+     * 
+     * @return
+     */
+    public String getServiceCapsNode() {
+        return serviceCapsNode;
+    }
+
+    /**
+     * A wrapper class to associate a packet filter with a listener.
+     */
+    protected static class ListenerWrapper {
+
+        private PacketListener packetListener;
+        private PacketFilter packetFilter;
+
+        /**
+         * Create a class which associates a packet filter with a listener.
+         * 
+         * @param packetListener the packet listener.
+         * @param packetFilter the associated filter or null if it listen for all packets.
+         */
+        public ListenerWrapper(PacketListener packetListener, PacketFilter packetFilter) {
+            this.packetListener = packetListener;
+            this.packetFilter = packetFilter;
+        }
+
+        /**
+         * Notify and process the packet listener if the filter matches the packet.
+         * 
+         * @param packet the packet which was sent or received.
+         */
+        public void notifyListener(Packet packet) {
+            if (packetFilter == null || packetFilter.accept(packet)) {
+                packetListener.processPacket(packet);
+            }
+        }
+    }
+
+    /**
+     * A wrapper class to associate a packet filter with an interceptor.
+     */
+    protected static class InterceptorWrapper {
+
+        private PacketInterceptor packetInterceptor;
+        private PacketFilter packetFilter;
+
+        /**
+         * Create a class which associates a packet filter with an interceptor.
+         * 
+         * @param packetInterceptor the interceptor.
+         * @param packetFilter the associated filter or null if it intercepts all packets.
+         */
+        public InterceptorWrapper(PacketInterceptor packetInterceptor, PacketFilter packetFilter) {
+            this.packetInterceptor = packetInterceptor;
+            this.packetFilter = packetFilter;
+        }
+
+        public boolean equals(Object object) {
+            if (object == null) {
+                return false;
+            }
+            if (object instanceof InterceptorWrapper) {
+                return ((InterceptorWrapper) object).packetInterceptor
+                        .equals(this.packetInterceptor);
+            }
+            else if (object instanceof PacketInterceptor) {
+                return object.equals(this.packetInterceptor);
+            }
+            return false;
+        }
+
+        /**
+         * Notify and process the packet interceptor if the filter matches the packet.
+         * 
+         * @param packet the packet which will be sent.
+         */
+        public void notifyListener(Packet packet) {
+            if (packetFilter == null || packetFilter.accept(packet)) {
+                packetInterceptor.interceptPacket(packet);
+            }
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/ConnectionConfiguration.java b/src/org/jivesoftware/smack/ConnectionConfiguration.java
new file mode 100644
index 0000000..d9108d5
--- /dev/null
+++ b/src/org/jivesoftware/smack/ConnectionConfiguration.java
@@ -0,0 +1,787 @@
+/**
+ * $RCSfile$
+ * $Revision: 3306 $
+ * $Date: 2006-01-16 14:34:56 -0300 (Mon, 16 Jan 2006) $
+ *
+ * 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;
+
+import org.jivesoftware.smack.proxy.ProxyInfo;
+import org.jivesoftware.smack.util.DNSUtil;
+import org.jivesoftware.smack.util.dns.HostAddress;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Configuration to use while establishing the connection to the server. It is possible to
+ * configure the path to the trustore file that keeps the trusted CA root certificates and
+ * enable or disable all or some of the checkings done while verifying server certificates.<p>
+ *
+ * It is also possible to configure if TLS, SASL, and compression are used or not.
+ *
+ * @author Gaston Dombiak
+ */
+public class ConnectionConfiguration implements Cloneable {
+
+    /**
+     * Hostname of the XMPP server. Usually servers use the same service name as the name
+     * of the server. However, there are some servers like google where host would be
+     * talk.google.com and the serviceName would be gmail.com.
+     */
+    private String serviceName;
+
+    private String host;
+    private int port;
+    protected List<HostAddress> hostAddresses;
+
+    private String truststorePath;
+    private String truststoreType;
+    private String truststorePassword;
+    private String keystorePath;
+    private String keystoreType;
+    private String pkcs11Library;
+    private boolean verifyChainEnabled = false;
+    private boolean verifyRootCAEnabled = false;
+    private boolean selfSignedCertificateEnabled = false;
+    private boolean expiredCertificatesCheckEnabled = false;
+    private boolean notMatchingDomainCheckEnabled = false;
+    private boolean isRosterVersioningAvailable = false;
+    private SSLContext customSSLContext;
+
+    private boolean compressionEnabled = false;
+
+    private boolean saslAuthenticationEnabled = true;
+    /**
+     * Used to get information from the user
+     */
+    private CallbackHandler callbackHandler;
+
+    private boolean debuggerEnabled = Connection.DEBUG_ENABLED;
+
+    // Flag that indicates if a reconnection should be attempted when abruptly disconnected
+    private boolean reconnectionAllowed = true;
+    
+    // Holds the socket factory that is used to generate the socket in the connection
+    private SocketFactory socketFactory;
+    
+    // Holds the authentication information for future reconnections
+    private String username;
+    private String password;
+    private String resource;
+    private boolean sendPresence = true;
+    private boolean rosterLoadedAtLogin = true;
+    private SecurityMode securityMode = SecurityMode.enabled;
+	
+	// Holds the proxy information (such as proxyhost, proxyport, username, password etc)
+    protected ProxyInfo proxy;
+
+    /**
+     * Creates a new ConnectionConfiguration for the specified service name.
+     * A DNS SRV lookup will be performed to find out the actual host address
+     * and port to use for the connection.
+     *
+     * @param serviceName the name of the service provided by an XMPP server.
+     */
+    public ConnectionConfiguration(String serviceName) {
+        // Perform DNS lookup to get host and port to use
+        hostAddresses = DNSUtil.resolveXMPPDomain(serviceName);
+        init(serviceName, ProxyInfo.forDefaultProxy());
+    }
+
+    /**
+     * 
+     */
+    protected ConnectionConfiguration() {
+      /* Does nothing */	
+    }
+
+    /**
+     * Creates a new ConnectionConfiguration for the specified service name 
+     * with specified proxy.
+     * A DNS SRV lookup will be performed to find out the actual host address
+     * and port to use for the connection.
+     *
+     * @param serviceName the name of the service provided by an XMPP server.
+     * @param proxy the proxy through which XMPP is to be connected
+     */
+    public ConnectionConfiguration(String serviceName,ProxyInfo proxy) {
+        // Perform DNS lookup to get host and port to use
+        hostAddresses = DNSUtil.resolveXMPPDomain(serviceName);
+        init(serviceName, proxy);
+    }
+
+    /**
+     * Creates a new ConnectionConfiguration using the specified host, port and
+     * service name. This is useful for manually overriding the DNS SRV lookup
+     * process that's used with the {@link #ConnectionConfiguration(String)}
+     * constructor. For example, say that an XMPP server is running at localhost
+     * in an internal network on port 5222 but is configured to think that it's
+     * "example.com" for testing purposes. This constructor is necessary to connect
+     * to the server in that case since a DNS SRV lookup for example.com would not
+     * point to the local testing server.
+     *
+     * @param host the host where the XMPP server is running.
+     * @param port the port where the XMPP is listening.
+     * @param serviceName the name of the service provided by an XMPP server.
+     */
+    public ConnectionConfiguration(String host, int port, String serviceName) {
+        initHostAddresses(host, port);
+        init(serviceName, ProxyInfo.forDefaultProxy());
+    }
+	
+	/**
+     * Creates a new ConnectionConfiguration using the specified host, port and
+     * service name. This is useful for manually overriding the DNS SRV lookup
+     * process that's used with the {@link #ConnectionConfiguration(String)}
+     * constructor. For example, say that an XMPP server is running at localhost
+     * in an internal network on port 5222 but is configured to think that it's
+     * "example.com" for testing purposes. This constructor is necessary to connect
+     * to the server in that case since a DNS SRV lookup for example.com would not
+     * point to the local testing server.
+     *
+     * @param host the host where the XMPP server is running.
+     * @param port the port where the XMPP is listening.
+     * @param serviceName the name of the service provided by an XMPP server.
+     * @param proxy the proxy through which XMPP is to be connected
+     */
+    public ConnectionConfiguration(String host, int port, String serviceName, ProxyInfo proxy) {
+        initHostAddresses(host, port);
+        init(serviceName, proxy);
+    }
+
+    /**
+     * Creates a new ConnectionConfiguration for a connection that will connect
+     * to the desired host and port.
+     *
+     * @param host the host where the XMPP server is running.
+     * @param port the port where the XMPP is listening.
+     */
+    public ConnectionConfiguration(String host, int port) {
+        initHostAddresses(host, port);
+        init(host, ProxyInfo.forDefaultProxy());
+    }
+	
+	/**
+     * Creates a new ConnectionConfiguration for a connection that will connect
+     * to the desired host and port with desired proxy.
+     *
+     * @param host the host where the XMPP server is running.
+     * @param port the port where the XMPP is listening.
+     * @param proxy the proxy through which XMPP is to be connected
+     */
+    public ConnectionConfiguration(String host, int port, ProxyInfo proxy) {
+        initHostAddresses(host, port);
+        init(host, proxy);
+    }
+
+    protected void init(String serviceName, ProxyInfo proxy) {
+        this.serviceName = serviceName;
+        this.proxy = proxy;
+
+        // Build the default path to the cacert truststore file. By default we are
+        // going to use the file located in $JREHOME/lib/security/cacerts.
+        String javaHome = System.getProperty("java.home");
+        StringBuilder buffer = new StringBuilder();
+        buffer.append(javaHome).append(File.separator).append("lib");
+        buffer.append(File.separator).append("security");
+        buffer.append(File.separator).append("cacerts");
+        truststorePath = buffer.toString();
+        // Set the default store type
+        truststoreType = "jks";
+        // Set the default password of the cacert file that is "changeit"
+        truststorePassword = "changeit";
+        keystorePath = System.getProperty("javax.net.ssl.keyStore");
+        keystoreType = "jks";
+        pkcs11Library = "pkcs11.config";
+		
+		//Setting the SocketFactory according to proxy supplied
+        socketFactory = proxy.getSocketFactory();
+    }
+
+    /**
+     * Sets the server name, also known as XMPP domain of the target server.
+     *
+     * @param serviceName the XMPP domain of the target server.
+     */
+    public void setServiceName(String serviceName) {
+        this.serviceName = serviceName;
+    }
+
+    /**
+     * Returns the server name of the target server.
+     *
+     * @return the server name of the target server.
+     */
+    public String getServiceName() {
+        return serviceName;
+    }
+
+    /**
+     * Returns the host to use when establishing the connection. The host and port to use
+     * might have been resolved by a DNS lookup as specified by the XMPP spec (and therefore
+     * may not match the {@link #getServiceName service name}.
+     *
+     * @return the host to use when establishing the connection.
+     */
+    public String getHost() {
+        return host;
+    }
+
+    /**
+     * Returns the port to use when establishing the connection. The host and port to use
+     * might have been resolved by a DNS lookup as specified by the XMPP spec.
+     *
+     * @return the port to use when establishing the connection.
+     */
+    public int getPort() {
+        return port;
+    }
+
+    public void setUsedHostAddress(HostAddress hostAddress) {
+        this.host = hostAddress.getFQDN();
+        this.port = hostAddress.getPort();
+    }
+
+    /**
+     * Returns the TLS security mode used when making the connection. By default,
+     * the mode is {@link SecurityMode#enabled}.
+     *
+     * @return the security mode.
+     */
+    public SecurityMode getSecurityMode() {
+        return securityMode;
+    }
+
+    /**
+     * Sets the TLS security mode used when making the connection. By default,
+     * the mode is {@link SecurityMode#enabled}.
+     *
+     * @param securityMode the security mode.
+     */
+    public void setSecurityMode(SecurityMode securityMode) {
+        this.securityMode = securityMode;
+    }
+
+    /**
+     * Retuns the path to the trust store file. The trust store file contains the root
+     * certificates of several well known CAs. By default, will attempt to use the
+     * the file located in $JREHOME/lib/security/cacerts.
+     *
+     * @return the path to the truststore file.
+     */
+    public String getTruststorePath() {
+        return truststorePath;
+    }
+
+    /**
+     * Sets the path to the trust store file. The truststore file contains the root
+     * certificates of several well?known CAs. By default Smack is going to use
+     * the file located in $JREHOME/lib/security/cacerts.
+     *
+     * @param truststorePath the path to the truststore file.
+     */
+    public void setTruststorePath(String truststorePath) {
+        this.truststorePath = truststorePath;
+    }
+
+    /**
+     * Returns the trust store type, or <tt>null</tt> if it's not set.
+     *
+     * @return the trust store type.
+     */
+    public String getTruststoreType() {
+        return truststoreType;
+    }
+
+    /**
+     * Sets the trust store type.
+     *
+     * @param truststoreType the trust store type.
+     */
+    public void setTruststoreType(String truststoreType) {
+        this.truststoreType = truststoreType;
+    }
+
+    /**
+     * Returns the password to use to access the trust store file. It is assumed that all
+     * certificates share the same password in the trust store.
+     *
+     * @return the password to use to access the truststore file.
+     */
+    public String getTruststorePassword() {
+        return truststorePassword;
+    }
+
+    /**
+     * Sets the password to use to access the trust store file. It is assumed that all
+     * certificates share the same password in the trust store.
+     *
+     * @param truststorePassword the password to use to access the truststore file.
+     */
+    public void setTruststorePassword(String truststorePassword) {
+        this.truststorePassword = truststorePassword;
+    }
+
+    /**
+     * Retuns the path to the keystore file. The key store file contains the 
+     * certificates that may be used to authenticate the client to the server,
+     * in the event the server requests or requires it.
+     *
+     * @return the path to the keystore file.
+     */
+    public String getKeystorePath() {
+        return keystorePath;
+    }
+
+    /**
+     * Sets the path to the keystore file. The key store file contains the 
+     * certificates that may be used to authenticate the client to the server,
+     * in the event the server requests or requires it.
+     *
+     * @param keystorePath the path to the keystore file.
+     */
+    public void setKeystorePath(String keystorePath) {
+        this.keystorePath = keystorePath;
+    }
+
+    /**
+     * Returns the keystore type, or <tt>null</tt> if it's not set.
+     *
+     * @return the keystore type.
+     */
+    public String getKeystoreType() {
+        return keystoreType;
+    }
+
+    /**
+     * Sets the keystore type.
+     *
+     * @param keystoreType the keystore type.
+     */
+    public void setKeystoreType(String keystoreType) {
+        this.keystoreType = keystoreType;
+    }
+
+
+    /**
+     * Returns the PKCS11 library file location, needed when the
+     * Keystore type is PKCS11.
+     *
+     * @return the path to the PKCS11 library file
+     */
+    public String getPKCS11Library() {
+        return pkcs11Library;
+    }
+
+    /**
+     * Sets the PKCS11 library file location, needed when the
+     * Keystore type is PKCS11
+     *
+     * @param pkcs11Library the path to the PKCS11 library file
+     */
+    public void setPKCS11Library(String pkcs11Library) {
+        this.pkcs11Library = pkcs11Library;
+    }
+
+    /**
+     * Returns true if the whole chain of certificates presented by the server are going to
+     * be checked. By default the certificate chain is not verified.
+     *
+     * @return true if the whole chaing of certificates presented by the server are going to
+     *         be checked.
+     */
+    public boolean isVerifyChainEnabled() {
+        return verifyChainEnabled;
+    }
+
+    /**
+     * Sets if the whole chain of certificates presented by the server are going to
+     * be checked. By default the certificate chain is not verified.
+     *
+     * @param verifyChainEnabled if the whole chaing of certificates presented by the server
+     *        are going to be checked.
+     */
+    public void setVerifyChainEnabled(boolean verifyChainEnabled) {
+        this.verifyChainEnabled = verifyChainEnabled;
+    }
+
+    /**
+     * Returns true if root CA checking is going to be done. By default checking is disabled.
+     *
+     * @return true if root CA checking is going to be done.
+     */
+    public boolean isVerifyRootCAEnabled() {
+        return verifyRootCAEnabled;
+    }
+
+    /**
+     * Sets if root CA checking is going to be done. By default checking is disabled.
+     *
+     * @param verifyRootCAEnabled if root CA checking is going to be done.
+     */
+    public void setVerifyRootCAEnabled(boolean verifyRootCAEnabled) {
+        this.verifyRootCAEnabled = verifyRootCAEnabled;
+    }
+
+    /**
+     * Returns true if self-signed certificates are going to be accepted. By default
+     * this option is disabled.
+     *
+     * @return true if self-signed certificates are going to be accepted.
+     */
+    public boolean isSelfSignedCertificateEnabled() {
+        return selfSignedCertificateEnabled;
+    }
+
+    /**
+     * Sets if self-signed certificates are going to be accepted. By default
+     * this option is disabled.
+     *
+     * @param selfSignedCertificateEnabled if self-signed certificates are going to be accepted.
+     */
+    public void setSelfSignedCertificateEnabled(boolean selfSignedCertificateEnabled) {
+        this.selfSignedCertificateEnabled = selfSignedCertificateEnabled;
+    }
+
+    /**
+     * Returns true if certificates presented by the server are going to be checked for their
+     * validity. By default certificates are not verified.
+     *
+     * @return true if certificates presented by the server are going to be checked for their
+     *         validity.
+     */
+    public boolean isExpiredCertificatesCheckEnabled() {
+        return expiredCertificatesCheckEnabled;
+    }
+
+    /**
+     * Sets if certificates presented by the server are going to be checked for their
+     * validity. By default certificates are not verified.
+     *
+     * @param expiredCertificatesCheckEnabled if certificates presented by the server are going
+     *        to be checked for their validity.
+     */
+    public void setExpiredCertificatesCheckEnabled(boolean expiredCertificatesCheckEnabled) {
+        this.expiredCertificatesCheckEnabled = expiredCertificatesCheckEnabled;
+    }
+
+    /**
+     * Returns true if certificates presented by the server are going to be checked for their
+     * domain. By default certificates are not verified.
+     *
+     * @return true if certificates presented by the server are going to be checked for their
+     *         domain.
+     */
+    public boolean isNotMatchingDomainCheckEnabled() {
+        return notMatchingDomainCheckEnabled;
+    }
+
+    /**
+     * Sets if certificates presented by the server are going to be checked for their
+     * domain. By default certificates are not verified.
+     *
+     * @param notMatchingDomainCheckEnabled if certificates presented by the server are going
+     *        to be checked for their domain.
+     */
+    public void setNotMatchingDomainCheckEnabled(boolean notMatchingDomainCheckEnabled) {
+        this.notMatchingDomainCheckEnabled = notMatchingDomainCheckEnabled;
+    }
+
+    /**
+     * Gets the custom SSLContext for SSL sockets. This is null by default.
+     *
+     * @return the SSLContext previously set with setCustomSSLContext() or null.
+     */
+    public SSLContext getCustomSSLContext() {
+        return this.customSSLContext;
+    }
+
+    /**
+     * Sets a custom SSLContext for creating SSL sockets. A custom Context causes all other
+     * SSL/TLS realted settings to be ignored.
+     *
+     * @param context the custom SSLContext for new sockets; null to reset default behavior.
+     */
+    public void setCustomSSLContext(SSLContext context) {
+        this.customSSLContext = context;
+    }
+
+    /**
+     * Returns true if the connection is going to use stream compression. Stream compression
+     * will be requested after TLS was established (if TLS was enabled) and only if the server
+     * offered stream compression. With stream compression network traffic can be reduced
+     * up to 90%. By default compression is disabled.
+     *
+     * @return true if the connection is going to use stream compression.
+     */
+    public boolean isCompressionEnabled() {
+        return compressionEnabled;
+    }
+
+    /**
+     * Sets if the connection is going to use stream compression. Stream compression
+     * will be requested after TLS was established (if TLS was enabled) and only if the server
+     * offered stream compression. With stream compression network traffic can be reduced
+     * up to 90%. By default compression is disabled.
+     *
+     * @param compressionEnabled if the connection is going to use stream compression.
+     */
+    public void setCompressionEnabled(boolean compressionEnabled) {
+        this.compressionEnabled = compressionEnabled;
+    }
+
+    /**
+     * Returns true if the client is going to use SASL authentication when logging into the
+     * server. If SASL authenticatin fails then the client will try to use non-sasl authentication.
+     * By default SASL is enabled.
+     *
+     * @return true if the client is going to use SASL authentication when logging into the
+     *         server.
+     */
+    public boolean isSASLAuthenticationEnabled() {
+        return saslAuthenticationEnabled;
+    }
+
+    /**
+     * Sets whether the client will use SASL authentication when logging into the
+     * server. If SASL authenticatin fails then the client will try to use non-sasl authentication.
+     * By default, SASL is enabled.
+     *
+     * @param saslAuthenticationEnabled if the client is going to use SASL authentication when
+     *        logging into the server.
+     */
+    public void setSASLAuthenticationEnabled(boolean saslAuthenticationEnabled) {
+        this.saslAuthenticationEnabled = saslAuthenticationEnabled;
+    }
+
+    /**
+     * Returns true if the new connection about to be establish is going to be debugged. By
+     * default the value of {@link Connection#DEBUG_ENABLED} is used.
+     *
+     * @return true if the new connection about to be establish is going to be debugged.
+     */
+    public boolean isDebuggerEnabled() {
+        return debuggerEnabled;
+    }
+
+    /**
+     * Sets if the new connection about to be establish is going to be debugged. By
+     * default the value of {@link Connection#DEBUG_ENABLED} is used.
+     *
+     * @param debuggerEnabled if the new connection about to be establish is going to be debugged.
+     */
+    public void setDebuggerEnabled(boolean debuggerEnabled) {
+        this.debuggerEnabled = debuggerEnabled;
+    }
+    
+    /**
+     * Sets if the reconnection mechanism is allowed to be used. By default
+     * reconnection is allowed.
+     * 
+     * @param isAllowed if the reconnection mechanism is allowed to use.
+     */
+    public void setReconnectionAllowed(boolean isAllowed) {
+        this.reconnectionAllowed = isAllowed;
+    }
+    /**
+     * Returns if the reconnection mechanism is allowed to be used. By default
+     * reconnection is allowed.
+     *
+     * @return if the reconnection mechanism is allowed to be used.
+     */
+    public boolean isReconnectionAllowed() {
+        return this.reconnectionAllowed;
+    }
+    
+    /**
+     * Sets the socket factory used to create new xmppConnection sockets.
+     * This is useful when connecting through SOCKS5 proxies.
+     *
+     * @param socketFactory used to create new sockets.
+     */
+    public void setSocketFactory(SocketFactory socketFactory) {
+        this.socketFactory = socketFactory;
+    }
+
+    /**
+     * Sets if an initial available presence will be sent to the server. By default
+     * an available presence will be sent to the server indicating that this presence
+     * is not online and available to receive messages. If you want to log in without
+     * being 'noticed' then pass a <tt>false</tt> value.
+     *
+     * @param sendPresence true if an initial available presence will be sent while logging in.
+     */
+    public void setSendPresence(boolean sendPresence) {
+        this.sendPresence = sendPresence;
+    }
+
+    /**
+     * Returns true if the roster will be loaded from the server when logging in. This
+     * is the common behaviour for clients but sometimes clients may want to differ this
+     * or just never do it if not interested in rosters.
+     *
+     * @return true if the roster will be loaded from the server when logging in.
+     */
+    public boolean isRosterLoadedAtLogin() {
+        return rosterLoadedAtLogin;
+    }
+
+    /**
+     * Sets if the roster will be loaded from the server when logging in. This
+     * is the common behaviour for clients but sometimes clients may want to differ this
+     * or just never do it if not interested in rosters.
+     *
+     * @param rosterLoadedAtLogin if the roster will be loaded from the server when logging in.
+     */
+    public void setRosterLoadedAtLogin(boolean rosterLoadedAtLogin) {
+        this.rosterLoadedAtLogin = rosterLoadedAtLogin;
+    }
+
+    /**
+     * Returns a CallbackHandler to obtain information, such as the password or
+     * principal information during the SASL authentication. A CallbackHandler
+     * will be used <b>ONLY</b> if no password was specified during the login while
+     * using SASL authentication.
+     *
+     * @return a CallbackHandler to obtain information, such as the password or
+     * principal information during the SASL authentication.
+     */
+    public CallbackHandler getCallbackHandler() {
+        return callbackHandler;
+    }
+
+    /**
+     * Sets a CallbackHandler to obtain information, such as the password or
+     * principal information during the SASL authentication. A CallbackHandler
+     * will be used <b>ONLY</b> if no password was specified during the login while
+     * using SASL authentication.
+     *
+     * @param callbackHandler to obtain information, such as the password or
+     * principal information during the SASL authentication.
+     */
+    public void setCallbackHandler(CallbackHandler callbackHandler) {
+        this.callbackHandler = callbackHandler;
+    }
+
+    /**
+     * Returns the socket factory used to create new xmppConnection sockets.
+     * This is useful when connecting through SOCKS5 proxies.
+     * 
+     * @return socketFactory used to create new sockets.
+     */
+    public SocketFactory getSocketFactory() {
+        return this.socketFactory;
+    }
+
+    public List<HostAddress> getHostAddresses() {
+        return Collections.unmodifiableList(hostAddresses);
+    }
+
+    /**
+     * An enumeration for TLS security modes that are available when making a connection
+     * to the XMPP server.
+     */
+    public static enum SecurityMode {
+
+        /**
+         * Securirty via TLS encryption is required in order to connect. If the server
+         * does not offer TLS or if the TLS negotiaton fails, the connection to the server
+         * will fail.
+         */
+        required,
+
+        /**
+         * Security via TLS encryption is used whenever it's available. This is the
+         * default setting.
+         */
+        enabled,
+
+        /**
+         * Security via TLS encryption is disabled and only un-encrypted connections will
+         * be used. If only TLS encryption is available from the server, the connection
+         * will fail.
+         */
+        disabled
+    }
+
+    /**
+     * Returns the username to use when trying to reconnect to the server.
+     *
+     * @return the username to use when trying to reconnect to the server.
+     */
+    String getUsername() {
+        return this.username;
+    }
+
+    /**
+     * Returns the password to use when trying to reconnect to the server.
+     *
+     * @return the password to use when trying to reconnect to the server.
+     */
+    String getPassword() {
+        return this.password;
+    }
+
+    /**
+     * Returns the resource to use when trying to reconnect to the server.
+     *
+     * @return the resource to use when trying to reconnect to the server.
+     */
+    String getResource() {
+        return resource;
+    }
+    
+    boolean isRosterVersioningAvailable(){
+    	return isRosterVersioningAvailable;
+    }
+    
+    void setRosterVersioningAvailable(boolean enabled){
+    	isRosterVersioningAvailable = enabled;
+    }
+
+    /**
+     * Returns true if an available presence should be sent when logging in while reconnecting.
+     *
+     * @return true if an available presence should be sent when logging in while reconnecting
+     */
+    boolean isSendPresence() {
+        return sendPresence;
+    }
+
+    void setLoginInfo(String username, String password, String resource) {
+        this.username = username;
+        this.password = password;
+        this.resource = resource;
+    }
+
+    private void initHostAddresses(String host, int port) {
+        hostAddresses = new ArrayList<HostAddress>(1);
+        HostAddress hostAddress;
+        try {
+             hostAddress = new HostAddress(host, port);
+        } catch (Exception e) {
+            throw new IllegalStateException(e);
+        }
+        hostAddresses.add(hostAddress);
+    }
+}
diff --git a/src/org/jivesoftware/smack/ConnectionCreationListener.java b/src/org/jivesoftware/smack/ConnectionCreationListener.java
new file mode 100644
index 0000000..7cbda18
--- /dev/null
+++ b/src/org/jivesoftware/smack/ConnectionCreationListener.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;
+
+/**
+ * Implementors of this interface will be notified when a new {@link Connection}
+ * has been created. The newly created connection will not be actually connected to
+ * the server. Use {@link Connection#addConnectionCreationListener(ConnectionCreationListener)}
+ * to add new listeners.
+ *
+ * @author Gaston Dombiak
+ */
+public interface ConnectionCreationListener {
+
+    /**
+     * Notification that a new connection has been created. The new connection
+     * will not yet be connected to the server.
+     * 
+     * @param connection the newly created connection.
+     */
+    public void connectionCreated(Connection connection);
+
+}
diff --git a/src/org/jivesoftware/smack/ConnectionListener.java b/src/org/jivesoftware/smack/ConnectionListener.java
new file mode 100644
index 0000000..a7ceef1
--- /dev/null
+++ b/src/org/jivesoftware/smack/ConnectionListener.java
@@ -0,0 +1,69 @@
+/**
+ * $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;
+
+/**
+ * Interface that allows for implementing classes to listen for connection closing
+ * and reconnection events. Listeners are registered with Connection objects.
+ *
+ * @see Connection#addConnectionListener
+ * @see Connection#removeConnectionListener
+ * 
+ * @author Matt Tucker
+ */
+public interface ConnectionListener {
+
+    /**
+     * Notification that the connection was closed normally or that the reconnection
+     * process has been aborted.
+     */
+    public void connectionClosed();
+
+    /**
+     * Notification that the connection was closed due to an exception. When
+     * abruptly disconnected it is possible for the connection to try reconnecting
+     * to the server.
+     *
+     * @param e the exception.
+     */
+    public void connectionClosedOnError(Exception e);
+    
+    /**
+     * The connection will retry to reconnect in the specified number of seconds.
+     * 
+     * @param seconds remaining seconds before attempting a reconnection.
+     */
+    public void reconnectingIn(int seconds);
+    
+    /**
+     * The connection has reconnected successfully to the server. Connections will
+     * reconnect to the server when the previous socket connection was abruptly closed.
+     */
+    public void reconnectionSuccessful();
+    
+    /**
+     * An attempt to connect to the server has failed. The connection will keep trying
+     * reconnecting to the server in a moment.
+     *
+     * @param e the exception that caused the reconnection to fail.
+     */
+    public void reconnectionFailed(Exception e);
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/MessageListener.java b/src/org/jivesoftware/smack/MessageListener.java
new file mode 100644
index 0000000..187c56c
--- /dev/null
+++ b/src/org/jivesoftware/smack/MessageListener.java
@@ -0,0 +1,30 @@
+/**
+ * $RCSfile$
+ * $Revision: 2407 $
+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
+ *
+ * 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;
+
+import org.jivesoftware.smack.packet.Message;
+
+/**
+ *
+ */
+public interface MessageListener {
+    void processMessage(Chat chat, Message message);
+}
diff --git a/src/org/jivesoftware/smack/NonSASLAuthentication.java b/src/org/jivesoftware/smack/NonSASLAuthentication.java
new file mode 100644
index 0000000..88b91ce
--- /dev/null
+++ b/src/org/jivesoftware/smack/NonSASLAuthentication.java
@@ -0,0 +1,143 @@
+/**

+ * $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;

+

+import org.jivesoftware.smack.filter.PacketIDFilter;

+import org.jivesoftware.smack.packet.Authentication;

+import org.jivesoftware.smack.packet.IQ;

+

+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;

+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;

+import org.apache.harmony.javax.security.auth.callback.Callback;

+

+/**

+ * Implementation of JEP-0078: Non-SASL Authentication. Follow the following

+ * <a href=http://www.jabber.org/jeps/jep-0078.html>link</a> to obtain more

+ * information about the JEP.

+ *

+ * @author Gaston Dombiak

+ */

+class NonSASLAuthentication implements UserAuthentication {

+

+    private Connection connection;

+

+    public NonSASLAuthentication(Connection connection) {

+        super();

+        this.connection = connection;

+    }

+

+    public String authenticate(String username, String resource, CallbackHandler cbh) throws XMPPException {

+        //Use the callback handler to determine the password, and continue on.

+        PasswordCallback pcb = new PasswordCallback("Password: ",false);

+        try {

+            cbh.handle(new Callback[]{pcb});

+            return authenticate(username, String.valueOf(pcb.getPassword()),resource);

+        } catch (Exception e) {

+            throw new XMPPException("Unable to determine password.",e);

+        }   

+    }

+

+    public String authenticate(String username, String password, String resource) throws

+            XMPPException {

+        // If we send an authentication packet in "get" mode with just the username,

+        // the server will return the list of authentication protocols it supports.

+        Authentication discoveryAuth = new Authentication();

+        discoveryAuth.setType(IQ.Type.GET);

+        discoveryAuth.setUsername(username);

+

+        PacketCollector collector =

+            connection.createPacketCollector(new PacketIDFilter(discoveryAuth.getPacketID()));

+        // Send the packet

+        connection.sendPacket(discoveryAuth);

+        // Wait up to a certain number of seconds for a response from the server.

+        IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+        if (response == null) {

+            throw new XMPPException("No response from the server.");

+        }

+        // If the server replied with an error, throw an exception.

+        else if (response.getType() == IQ.Type.ERROR) {

+            throw new XMPPException(response.getError());

+        }

+        // Otherwise, no error so continue processing.

+        Authentication authTypes = (Authentication) response;

+        collector.cancel();

+

+        // Now, create the authentication packet we'll send to the server.

+        Authentication auth = new Authentication();

+        auth.setUsername(username);

+

+        // Figure out if we should use digest or plain text authentication.

+        if (authTypes.getDigest() != null) {

+            auth.setDigest(connection.getConnectionID(), password);

+        }

+        else if (authTypes.getPassword() != null) {

+            auth.setPassword(password);

+        }

+        else {

+            throw new XMPPException("Server does not support compatible authentication mechanism.");

+        }

+

+        auth.setResource(resource);

+

+        collector = connection.createPacketCollector(new PacketIDFilter(auth.getPacketID()));

+        // Send the packet.

+        connection.sendPacket(auth);

+        // Wait up to a certain number of seconds for a response from the server.

+        response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+        if (response == null) {

+            throw new XMPPException("Authentication failed.");

+        }

+        else if (response.getType() == IQ.Type.ERROR) {

+            throw new XMPPException(response.getError());

+        }

+        // We're done with the collector, so explicitly cancel it.

+        collector.cancel();

+

+        return response.getTo();

+    }

+

+    public String authenticateAnonymously() throws XMPPException {

+        // Create the authentication packet we'll send to the server.

+        Authentication auth = new Authentication();

+

+        PacketCollector collector =

+            connection.createPacketCollector(new PacketIDFilter(auth.getPacketID()));

+        // Send the packet.

+        connection.sendPacket(auth);

+        // Wait up to a certain number of seconds for a response from the server.

+        IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+        if (response == null) {

+            throw new XMPPException("Anonymous login failed.");

+        }

+        else if (response.getType() == IQ.Type.ERROR) {

+            throw new XMPPException(response.getError());

+        }

+        // We're done with the collector, so explicitly cancel it.

+        collector.cancel();

+

+        if (response.getTo() != null) {

+            return response.getTo();

+        }

+        else {

+            return connection.getServiceName() + "/" + ((Authentication) response).getResource();

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smack/OpenTrustManager.java b/src/org/jivesoftware/smack/OpenTrustManager.java
new file mode 100644
index 0000000..61ed8c6
--- /dev/null
+++ b/src/org/jivesoftware/smack/OpenTrustManager.java
@@ -0,0 +1,49 @@
+/**

+ * $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;

+

+import javax.net.ssl.X509TrustManager;

+import java.security.cert.CertificateException;

+import java.security.cert.X509Certificate;

+

+/**

+ * Dummy trust manager that trust all certificates presented by the server. This class

+ * is used during old SSL connections.

+ *

+ * @author Gaston Dombiak

+ */

+class OpenTrustManager implements X509TrustManager {

+

+    public OpenTrustManager() {

+    }

+

+    public X509Certificate[] getAcceptedIssuers() {

+        return new X509Certificate[0];

+    }

+

+    public void checkClientTrusted(X509Certificate[] arg0, String arg1)

+            throws CertificateException {

+    }

+

+    public void checkServerTrusted(X509Certificate[] arg0, String arg1)

+            throws CertificateException {

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/PacketCollector.java b/src/org/jivesoftware/smack/PacketCollector.java
new file mode 100644
index 0000000..9b4b4ae
--- /dev/null
+++ b/src/org/jivesoftware/smack/PacketCollector.java
@@ -0,0 +1,160 @@
+/**
+ * $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;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Provides a mechanism to collect packets into a result queue that pass a
+ * specified filter. The collector lets you perform blocking and polling
+ * operations on the result queue. So, a PacketCollector is more suitable to
+ * use than a {@link PacketListener} when you need to wait for a specific
+ * result.<p>
+ *
+ * Each packet collector will queue up a configured number of packets for processing before
+ * older packets are automatically dropped.  The default number is retrieved by 
+ * {@link SmackConfiguration#getPacketCollectorSize()}.
+ *
+ * @see Connection#createPacketCollector(PacketFilter)
+ * @author Matt Tucker
+ */
+public class PacketCollector {
+
+    private PacketFilter packetFilter;
+    private ArrayBlockingQueue<Packet> resultQueue;
+    private Connection connection;
+    private boolean cancelled = false;
+
+    /**
+     * Creates a new packet collector. If the packet filter is <tt>null</tt>, then
+     * all packets will match this collector.
+     *
+     * @param conection the connection the collector is tied to.
+     * @param packetFilter determines which packets will be returned by this collector.
+     */
+    protected PacketCollector(Connection conection, PacketFilter packetFilter) {
+    	this(conection, packetFilter, SmackConfiguration.getPacketCollectorSize());
+    }
+
+    /**
+     * Creates a new packet collector. If the packet filter is <tt>null</tt>, then
+     * all packets will match this collector.
+     *
+     * @param conection the connection the collector is tied to.
+     * @param packetFilter determines which packets will be returned by this collector.
+     * @param maxSize the maximum number of packets that will be stored in the collector.
+     */
+    protected PacketCollector(Connection conection, PacketFilter packetFilter, int maxSize) {
+        this.connection = conection;
+        this.packetFilter = packetFilter;
+        this.resultQueue = new ArrayBlockingQueue<Packet>(maxSize);
+    }
+
+    /**
+     * Explicitly cancels the packet collector so that no more results are
+     * queued up. Once a packet collector has been cancelled, it cannot be
+     * re-enabled. Instead, a new packet collector must be created.
+     */
+    public void cancel() {
+        // If the packet collector has already been cancelled, do nothing.
+        if (!cancelled) {
+            cancelled = true;
+            connection.removePacketCollector(this);
+        }
+    }
+
+    /**
+     * Returns the packet filter associated with this packet collector. The packet
+     * filter is used to determine what packets are queued as results.
+     *
+     * @return the packet filter.
+     */
+    public PacketFilter getPacketFilter() {
+        return packetFilter;
+    }
+
+    /**
+     * Polls to see if a packet is currently available and returns it, or
+     * immediately returns <tt>null</tt> if no packets are currently in the
+     * result queue.
+     *
+     * @return the next packet result, or <tt>null</tt> if there are no more
+     *      results.
+     */
+    public Packet pollResult() {
+    	return resultQueue.poll();
+    }
+
+    /**
+     * Returns the next available packet. The method call will block (not return)
+     * until a packet is available.
+     *
+     * @return the next available packet.
+     */
+    public Packet nextResult() {
+        try {
+			return resultQueue.take();
+		}
+		catch (InterruptedException e) {
+			throw new RuntimeException(e);
+		}
+    }
+
+    /**
+     * Returns the next available packet. The method call will block (not return)
+     * until a packet is available or the <tt>timeout</tt> has elapased. If the
+     * timeout elapses without a result, <tt>null</tt> will be returned.
+     *
+     * @param timeout the amount of time to wait for the next packet (in milleseconds).
+     * @return the next available packet.
+     */
+    public Packet nextResult(long timeout) {
+    	try {
+			return resultQueue.poll(timeout, TimeUnit.MILLISECONDS);
+		}
+		catch (InterruptedException e) {
+			throw new RuntimeException(e);
+		}
+    }
+
+    /**
+     * Processes a packet to see if it meets the criteria for this packet collector.
+     * If so, the packet is added to the result queue.
+     *
+     * @param packet the packet to process.
+     */
+    protected void processPacket(Packet packet) {
+        if (packet == null) {
+            return;
+        }
+        
+        if (packetFilter == null || packetFilter.accept(packet)) {
+        	while (!resultQueue.offer(packet)) {
+        		// Since we know the queue is full, this poll should never actually block.
+        		resultQueue.poll();
+        	}
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/PacketInterceptor.java b/src/org/jivesoftware/smack/PacketInterceptor.java
new file mode 100644
index 0000000..bd89031
--- /dev/null
+++ b/src/org/jivesoftware/smack/PacketInterceptor.java
@@ -0,0 +1,49 @@
+/**

+ * $Revision: 2408 $

+ * $Date: 2004-11-02 20:53:30 -0300 (Tue, 02 Nov 2004) $

+ *

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

+

+import org.jivesoftware.smack.packet.Packet;

+

+/**

+ * Provides a mechanism to intercept and modify packets that are going to be

+ * sent to the server. PacketInterceptors are added to the {@link Connection}

+ * together with a {@link org.jivesoftware.smack.filter.PacketFilter} so that only

+ * certain packets are intercepted and processed by the interceptor.<p>

+ *

+ * This allows event-style programming -- every time a new packet is found,

+ * the {@link #interceptPacket(Packet)} method will be called.

+ *

+ * @see Connection#addPacketInterceptor(PacketInterceptor, org.jivesoftware.smack.filter.PacketFilter)

+ * @author Gaston Dombiak

+ */

+public interface PacketInterceptor {

+

+    /**

+     * Process the packet that is about to be sent to the server. The intercepted

+     * packet can be modified by the interceptor.<p>

+     *

+     * Interceptors are invoked using the same thread that requested the packet

+     * to be sent, so it's very important that implementations of this method

+     * not block for any extended period of time.

+     *

+     * @param packet the packet to is going to be sent to the server.

+     */

+    public void interceptPacket(Packet packet);

+}

diff --git a/src/org/jivesoftware/smack/PacketListener.java b/src/org/jivesoftware/smack/PacketListener.java
new file mode 100644
index 0000000..4bc83aa
--- /dev/null
+++ b/src/org/jivesoftware/smack/PacketListener.java
@@ -0,0 +1,48 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Provides a mechanism to listen for packets that pass a specified filter.
+ * This allows event-style programming -- every time a new packet is found,
+ * the {@link #processPacket(Packet)} method will be called. This is the
+ * opposite approach to the functionality provided by a {@link PacketCollector}
+ * which lets you block while waiting for results.
+ *
+ * @see Connection#addPacketListener(PacketListener, org.jivesoftware.smack.filter.PacketFilter)
+ * @author Matt Tucker
+ */
+public interface PacketListener {
+
+    /**
+     * Process the next packet sent to this packet listener.<p>
+     *
+     * A single thread is responsible for invoking all listeners, so
+     * it's very important that implementations of this method not block
+     * for any extended period of time.
+     *
+     * @param packet the packet to process.
+     */
+    public void processPacket(Packet packet);
+
+}
diff --git a/src/org/jivesoftware/smack/PacketReader.java b/src/org/jivesoftware/smack/PacketReader.java
new file mode 100644
index 0000000..05ffc67
--- /dev/null
+++ b/src/org/jivesoftware/smack/PacketReader.java
@@ -0,0 +1,429 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.Connection.ListenerWrapper;
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.sasl.SASLMechanism.Challenge;
+import org.jivesoftware.smack.sasl.SASLMechanism.Failure;
+import org.jivesoftware.smack.sasl.SASLMechanism.Success;
+import org.jivesoftware.smack.util.PacketParserUtils;
+
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.util.concurrent.*;
+
+/**
+ * Listens for XML traffic from the XMPP server and parses it into packet objects.
+ * The packet reader also invokes all packet listeners and collectors.<p>
+ *
+ * @see Connection#createPacketCollector
+ * @see Connection#addPacketListener
+ * @author Matt Tucker
+ */
+class PacketReader {
+
+    private Thread readerThread;
+    private ExecutorService listenerExecutor;
+
+    private XMPPConnection connection;
+    private XmlPullParser parser;
+    volatile boolean done;
+
+    private String connectionID = null;
+
+    protected PacketReader(final XMPPConnection connection) {
+        this.connection = connection;
+        this.init();
+    }
+
+    /**
+     * Initializes the reader in order to be used. The reader is initialized during the
+     * first connection and when reconnecting due to an abruptly disconnection.
+     */
+    protected void init() {
+        done = false;
+        connectionID = null;
+
+        readerThread = new Thread() {
+            public void run() {
+                parsePackets(this);
+            }
+        };
+        readerThread.setName("Smack Packet Reader (" + connection.connectionCounterValue + ")");
+        readerThread.setDaemon(true);
+
+        // Create an executor to deliver incoming packets to listeners. We'll use a single
+        // thread with an unbounded queue.
+        listenerExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
+
+            public Thread newThread(Runnable runnable) {
+                Thread thread = new Thread(runnable,
+                        "Smack Listener Processor (" + connection.connectionCounterValue + ")");
+                thread.setDaemon(true);
+                return thread;
+            }
+        });
+
+        resetParser();
+    }
+
+    /**
+     * Starts the packet reader thread and returns once a connection to the server
+     * has been established. A connection will be attempted for a maximum of five
+     * seconds. An XMPPException will be thrown if the connection fails.
+     *
+     * @throws XMPPException if the server fails to send an opening stream back
+     *      for more than five seconds.
+     */
+    synchronized public void startup() throws XMPPException {
+        readerThread.start();
+        // Wait for stream tag before returning. We'll wait a couple of seconds before
+        // giving up and throwing an error.
+        try {
+            // A waiting thread may be woken up before the wait time or a notify
+            // (although this is a rare thing). Therefore, we continue waiting
+            // until either a connectionID has been set (and hence a notify was
+            // made) or the total wait time has elapsed.
+            int waitTime = SmackConfiguration.getPacketReplyTimeout();
+            wait(3 * waitTime);
+        }
+        catch (InterruptedException ie) {
+            // Ignore.
+        }
+        if (connectionID == null) {
+            throw new XMPPException("Connection failed. No response from server.");
+        }
+        else {
+            connection.connectionID = connectionID;
+        }
+    }
+
+    /**
+     * Shuts the packet reader down.
+     */
+    public void shutdown() {
+        // Notify connection listeners of the connection closing if done hasn't already been set.
+        if (!done) {
+            for (ConnectionListener listener : connection.getConnectionListeners()) {
+                try {
+                    listener.connectionClosed();
+                }
+                catch (Exception e) {
+                    // Catch and print any exception so we can recover
+                    // from a faulty listener and finish the shutdown process
+                    e.printStackTrace();
+                }
+            }
+        }
+        done = true;
+
+        // Shut down the listener executor.
+        listenerExecutor.shutdown();
+    }
+
+    /**
+     * Cleans up all resources used by the packet reader.
+     */
+    void cleanup() {
+        connection.recvListeners.clear();
+        connection.collectors.clear();
+    }
+
+    /**
+     * Resets the parser using the latest connection's reader. Reseting the parser is necessary
+     * when the plain connection has been secured or when a new opening stream element is going
+     * to be sent by the server.
+     */
+    private void resetParser() {
+        try {
+            parser = XmlPullParserFactory.newInstance().newPullParser();
+            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+            parser.setInput(connection.reader);
+        }
+        catch (XmlPullParserException xppe) {
+            xppe.printStackTrace();
+        }
+    }
+
+    /**
+     * Parse top-level packets in order to process them further.
+     *
+     * @param thread the thread that is being used by the reader to parse incoming packets.
+     */
+    private void parsePackets(Thread thread) {
+        try {
+            int eventType = parser.getEventType();
+            do {
+                if (eventType == XmlPullParser.START_TAG) {
+                    if (parser.getName().equals("message")) {
+                        processPacket(PacketParserUtils.parseMessage(parser));
+                    }
+                    else if (parser.getName().equals("iq")) {
+                        processPacket(PacketParserUtils.parseIQ(parser, connection));
+                    }
+                    else if (parser.getName().equals("presence")) {
+                        processPacket(PacketParserUtils.parsePresence(parser));
+                    }
+                    // We found an opening stream. Record information about it, then notify
+                    // the connectionID lock so that the packet reader startup can finish.
+                    else if (parser.getName().equals("stream")) {
+                        // Ensure the correct jabber:client namespace is being used.
+                        if ("jabber:client".equals(parser.getNamespace(null))) {
+                            // Get the connection id.
+                            for (int i=0; i<parser.getAttributeCount(); i++) {
+                                if (parser.getAttributeName(i).equals("id")) {
+                                    // Save the connectionID
+                                    connectionID = parser.getAttributeValue(i);
+                                    if (!"1.0".equals(parser.getAttributeValue("", "version"))) {
+                                        // Notify that a stream has been opened if the
+                                        // server is not XMPP 1.0 compliant otherwise make the
+                                        // notification after TLS has been negotiated or if TLS
+                                        // is not supported
+                                        releaseConnectionIDLock();
+                                    }
+                                }
+                                else if (parser.getAttributeName(i).equals("from")) {
+                                    // Use the server name that the server says that it is.
+                                    connection.config.setServiceName(parser.getAttributeValue(i));
+                                }
+                            }
+                        }
+                    }
+                    else if (parser.getName().equals("error")) {
+                        throw new XMPPException(PacketParserUtils.parseStreamError(parser));
+                    }
+                    else if (parser.getName().equals("features")) {
+                        parseFeatures(parser);
+                    }
+                    else if (parser.getName().equals("proceed")) {
+                        // Secure the connection by negotiating TLS
+                        connection.proceedTLSReceived();
+                        // Reset the state of the parser since a new stream element is going
+                        // to be sent by the server
+                        resetParser();
+                    }
+                    else if (parser.getName().equals("failure")) {
+                        String namespace = parser.getNamespace(null);
+                        if ("urn:ietf:params:xml:ns:xmpp-tls".equals(namespace)) {
+                            // TLS negotiation has failed. The server will close the connection
+                            throw new Exception("TLS negotiation has failed");
+                        }
+                        else if ("http://jabber.org/protocol/compress".equals(namespace)) {
+                            // Stream compression has been denied. This is a recoverable
+                            // situation. It is still possible to authenticate and
+                            // use the connection but using an uncompressed connection
+                            connection.streamCompressionDenied();
+                        }
+                        else {
+                            // SASL authentication has failed. The server may close the connection
+                            // depending on the number of retries
+                            final Failure failure = PacketParserUtils.parseSASLFailure(parser);
+                            processPacket(failure);
+                            connection.getSASLAuthentication().authenticationFailed();
+                        }
+                    }
+                    else if (parser.getName().equals("challenge")) {
+                        // The server is challenging the SASL authentication made by the client
+                        String challengeData = parser.nextText();
+                        processPacket(new Challenge(challengeData));
+                        connection.getSASLAuthentication().challengeReceived(challengeData);
+                    }
+                    else if (parser.getName().equals("success")) {
+                        processPacket(new Success(parser.nextText()));
+                        // We now need to bind a resource for the connection
+                        // Open a new stream and wait for the response
+                        connection.packetWriter.openStream();
+                        // Reset the state of the parser since a new stream element is going
+                        // to be sent by the server
+                        resetParser();
+                        // The SASL authentication with the server was successful. The next step
+                        // will be to bind the resource
+                        connection.getSASLAuthentication().authenticated();
+                    }
+                    else if (parser.getName().equals("compressed")) {
+                        // Server confirmed that it's possible to use stream compression. Start
+                        // stream compression
+                        connection.startStreamCompression();
+                        // Reset the state of the parser since a new stream element is going
+                        // to be sent by the server
+                        resetParser();
+                    }
+                }
+                else if (eventType == XmlPullParser.END_TAG) {
+                    if (parser.getName().equals("stream")) {
+                        // Disconnect the connection
+                        connection.disconnect();
+                    }
+                }
+                eventType = parser.next();
+            } while (!done && eventType != XmlPullParser.END_DOCUMENT && thread == readerThread);
+        }
+        catch (Exception e) {
+            // The exception can be ignored if the the connection is 'done'
+            // or if the it was caused because the socket got closed
+            if (!(done || connection.isSocketClosed())) {
+                // Close the connection and notify connection listeners of the
+                // error.
+                connection.notifyConnectionError(e);
+            }
+        }
+    }
+
+    /**
+     * Releases the connection ID lock so that the thread that was waiting can resume. The
+     * lock will be released when one of the following three conditions is met:<p>
+     *
+     * 1) An opening stream was sent from a non XMPP 1.0 compliant server
+     * 2) Stream features were received from an XMPP 1.0 compliant server that does not support TLS
+     * 3) TLS negotiation was successful
+     *
+     */
+    synchronized private void releaseConnectionIDLock() {
+        notify();
+    }
+
+    /**
+     * Processes a packet after it's been fully parsed by looping through the installed
+     * packet collectors and listeners and letting them examine the packet to see if
+     * they are a match with the filter.
+     *
+     * @param packet the packet to process.
+     */
+    private void processPacket(Packet packet) {
+        if (packet == null) {
+            return;
+        }
+
+        // Loop through all collectors and notify the appropriate ones.
+        for (PacketCollector collector: connection.getPacketCollectors()) {
+            collector.processPacket(packet);
+        }
+
+        // Deliver the incoming packet to listeners.
+        listenerExecutor.submit(new ListenerNotification(packet));
+    }
+
+    private void parseFeatures(XmlPullParser parser) throws Exception {
+        boolean startTLSReceived = false;
+        boolean startTLSRequired = false;
+        boolean done = false;
+        while (!done) {
+            int eventType = parser.next();
+
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("starttls")) {
+                    startTLSReceived = true;
+                }
+                else if (parser.getName().equals("mechanisms")) {
+                    // The server is reporting available SASL mechanisms. Store this information
+                    // which will be used later while logging (i.e. authenticating) into
+                    // the server
+                    connection.getSASLAuthentication()
+                            .setAvailableSASLMethods(PacketParserUtils.parseMechanisms(parser));
+                }
+                else if (parser.getName().equals("bind")) {
+                    // The server requires the client to bind a resource to the stream
+                    connection.getSASLAuthentication().bindingRequired();
+                }
+                else if(parser.getName().equals("ver")){
+                	connection.getConfiguration().setRosterVersioningAvailable(true);
+                }
+                // Set the entity caps node for the server if one is send
+                // See http://xmpp.org/extensions/xep-0115.html#stream
+                else if (parser.getName().equals("c")) {
+                    String node = parser.getAttributeValue(null, "node");
+                    String ver = parser.getAttributeValue(null, "ver");
+                    if (ver != null && node != null) {
+                        String capsNode = node + "#" + ver;
+                        // In order to avoid a dependency from smack to smackx
+                        // we have to set the services caps node in the connection
+                        // and not directly in the EntityCapsManager
+                        connection.setServiceCapsNode(capsNode);
+                    }
+                }
+                else if (parser.getName().equals("session")) {
+                    // The server supports sessions
+                    connection.getSASLAuthentication().sessionsSupported();
+                }
+                else if (parser.getName().equals("compression")) {
+                    // The server supports stream compression
+                    connection.setAvailableCompressionMethods(PacketParserUtils.parseCompressionMethods(parser));
+                }
+                else if (parser.getName().equals("register")) {
+                    connection.getAccountManager().setSupportsAccountCreation(true);
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("starttls")) {
+                    // Confirm the server that we want to use TLS
+                    connection.startTLSReceived(startTLSRequired);
+                }
+                else if (parser.getName().equals("required") && startTLSReceived) {
+                    startTLSRequired = true;
+                }
+                else if (parser.getName().equals("features")) {
+                    done = true;
+                }
+            }
+        }
+
+        // If TLS is required but the server doesn't offer it, disconnect
+        // from the server and throw an error. First check if we've already negotiated TLS
+        // and are secure, however (features get parsed a second time after TLS is established).
+        if (!connection.isSecureConnection()) {
+            if (!startTLSReceived && connection.getConfiguration().getSecurityMode() ==
+                    ConnectionConfiguration.SecurityMode.required)
+            {
+                throw new XMPPException("Server does not support security (TLS), " +
+                        "but security required by connection configuration.",
+                        new XMPPError(XMPPError.Condition.forbidden));
+            }
+        }
+        
+        // Release the lock after TLS has been negotiated or we are not insterested in TLS
+        if (!startTLSReceived || connection.getConfiguration().getSecurityMode() ==
+                ConnectionConfiguration.SecurityMode.disabled)
+        {
+            releaseConnectionIDLock();
+        }
+    }
+
+    /**
+     * A runnable to notify all listeners of a packet.
+     */
+    private class ListenerNotification implements Runnable {
+
+        private Packet packet;
+
+        public ListenerNotification(Packet packet) {
+            this.packet = packet;
+        }
+
+        public void run() {
+            for (ListenerWrapper listenerWrapper : connection.recvListeners.values()) {
+                listenerWrapper.notifyListener(packet);
+            }
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/PacketWriter.java b/src/org/jivesoftware/smack/PacketWriter.java
new file mode 100644
index 0000000..675af25
--- /dev/null
+++ b/src/org/jivesoftware/smack/PacketWriter.java
@@ -0,0 +1,240 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.packet.Packet;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+
+/**
+ * Writes packets to a XMPP server. Packets are sent using a dedicated thread. Packet
+ * interceptors can be registered to dynamically modify packets before they're actually
+ * sent. Packet listeners can be registered to listen for all outgoing packets.
+ *
+ * @see Connection#addPacketInterceptor
+ * @see Connection#addPacketSendingListener
+ *
+ * @author Matt Tucker
+ */
+class PacketWriter {
+
+    private Thread writerThread;
+    private Thread keepAliveThread;
+    private Writer writer;
+    private XMPPConnection connection;
+    private final BlockingQueue<Packet> queue;
+    volatile boolean done;
+
+    /**
+     * Creates a new packet writer with the specified connection.
+     *
+     * @param connection the connection.
+     */
+    protected PacketWriter(XMPPConnection connection) {
+        this.queue = new ArrayBlockingQueue<Packet>(500, true);
+        this.connection = connection;
+        init();
+    }
+
+    /** 
+    * Initializes the writer in order to be used. It is called at the first connection and also 
+    * is invoked if the connection is disconnected by an error.
+    */ 
+    protected void init() {
+        this.writer = connection.writer;
+        done = false;
+
+        writerThread = new Thread() {
+            public void run() {
+                writePackets(this);
+            }
+        };
+        writerThread.setName("Smack Packet Writer (" + connection.connectionCounterValue + ")");
+        writerThread.setDaemon(true);
+    }
+    
+    /**
+     * Sends the specified packet to the server.
+     *
+     * @param packet the packet to send.
+     */
+    public void sendPacket(Packet packet) {
+        if (!done) {
+            // Invoke interceptors for the new packet that is about to be sent. Interceptors
+            // may modify the content of the packet.
+            connection.firePacketInterceptors(packet);
+
+            try {
+                queue.put(packet);
+            }
+            catch (InterruptedException ie) {
+                ie.printStackTrace();
+                return;
+            }
+            synchronized (queue) {
+                queue.notifyAll();
+            }
+
+            // Process packet writer listeners. Note that we're using the sending
+            // thread so it's expected that listeners are fast.
+            connection.firePacketSendingListeners(packet);
+        }
+    }
+
+    /**
+     * Starts the packet writer thread and opens a connection to the server. The
+     * packet writer will continue writing packets until {@link #shutdown} or an
+     * error occurs.
+     */
+    public void startup() {
+        writerThread.start();
+    }
+
+    void setWriter(Writer writer) {
+        this.writer = writer;
+    }
+
+    /**
+     * Shuts down the packet writer. Once this method has been called, no further
+     * packets will be written to the server.
+     */
+    public void shutdown() {
+        done = true;
+        synchronized (queue) {
+            queue.notifyAll();
+        }
+        // Interrupt the keep alive thread if one was created
+        if (keepAliveThread != null)
+                keepAliveThread.interrupt();
+    }
+
+    /**
+     * Cleans up all resources used by the packet writer.
+     */
+    void cleanup() {
+        connection.interceptors.clear();
+        connection.sendListeners.clear();
+    }
+
+    /**
+     * Returns the next available packet from the queue for writing.
+     *
+     * @return the next packet for writing.
+     */
+    private Packet nextPacket() {
+        Packet packet = null;
+        // Wait until there's a packet or we're done.
+        while (!done && (packet = queue.poll()) == null) {
+            try {
+                synchronized (queue) {
+                    queue.wait();
+                }
+            }
+            catch (InterruptedException ie) {
+                // Do nothing
+            }
+        }
+        return packet;
+    }
+
+    private void writePackets(Thread thisThread) {
+        try {
+            // Open the stream.
+            openStream();
+            // Write out packets from the queue.
+            while (!done && (writerThread == thisThread)) {
+                Packet packet = nextPacket();
+                if (packet != null) {
+                    writer.write(packet.toXML());
+                    if (queue.isEmpty()) {
+                        writer.flush();
+                    }
+                }
+            }
+            // Flush out the rest of the queue. If the queue is extremely large, it's possible
+            // we won't have time to entirely flush it before the socket is forced closed
+            // by the shutdown process.
+            try {
+                while (!queue.isEmpty()) {
+                    Packet packet = queue.remove();
+                    writer.write(packet.toXML());
+                }
+                writer.flush();
+            }
+            catch (Exception e) {
+                e.printStackTrace();
+            }
+
+            // Delete the queue contents (hopefully nothing is left).
+            queue.clear();
+
+            // Close the stream.
+            try {
+                writer.write("</stream:stream>");
+                writer.flush();
+            }
+            catch (Exception e) {
+                // Do nothing
+            }
+            finally {
+                try {
+                    writer.close();
+                }
+                catch (Exception e) {
+                    // Do nothing
+                }
+            }
+        }
+        catch (IOException ioe) {
+            // The exception can be ignored if the the connection is 'done'
+            // or if the it was caused because the socket got closed
+            if (!(done || connection.isSocketClosed())) {
+                done = true;
+                // packetReader could be set to null by an concurrent disconnect() call.
+                // Therefore Prevent NPE exceptions by checking packetReader.
+                if (connection.packetReader != null) {
+                        connection.notifyConnectionError(ioe);
+                }
+            }
+        }
+    }
+
+    /**
+     * Sends to the server a new stream element. This operation may be requested several times
+     * so we need to encapsulate the logic in one place. This message will be sent while doing
+     * TLS, SASL and resource binding.
+     *
+     * @throws IOException If an error occurs while sending the stanza to the server.
+     */
+    void openStream() throws IOException {
+        StringBuilder stream = new StringBuilder();
+        stream.append("<stream:stream");
+        stream.append(" to=\"").append(connection.getServiceName()).append("\"");
+        stream.append(" xmlns=\"jabber:client\"");
+        stream.append(" xmlns:stream=\"http://etherx.jabber.org/streams\"");
+        stream.append(" version=\"1.0\">");
+        writer.write(stream.toString());
+        writer.flush();
+    }
+}
diff --git a/src/org/jivesoftware/smack/PrivacyList.java b/src/org/jivesoftware/smack/PrivacyList.java
new file mode 100644
index 0000000..67d731d
--- /dev/null
+++ b/src/org/jivesoftware/smack/PrivacyList.java
@@ -0,0 +1,74 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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;

+

+import org.jivesoftware.smack.packet.PrivacyItem;

+

+import java.util.List;

+

+/**

+ * A privacy list represents a list of contacts that is a read only class used to represent a set of allowed or blocked communications. 

+ * Basically it can:<ul>

+ *

+ *      <li>Handle many {@link org.jivesoftware.smack.packet.PrivacyItem}.</li>

+ *      <li>Answer if it is the default list.</li>

+ *      <li>Answer if it is the active list.</li>

+ * </ul>

+ *

+ * {@link PrivacyItem Privacy Items} can handle different kind of blocking communications based on JID, group,

+ * subscription type or globally.

+ * 

+ * @author Francisco Vives

+ */

+public class PrivacyList {

+

+    /** Holds if it is an active list or not **/

+    private boolean isActiveList;

+    /** Holds if it is an default list or not **/

+    private boolean isDefaultList;

+    /** Holds the list name used to print **/

+    private String listName;

+    /** Holds the list of {@see PrivacyItem} **/

+    private List<PrivacyItem> items;

+    

+    protected PrivacyList(boolean isActiveList, boolean isDefaultList,

+            String listName, List<PrivacyItem> privacyItems) {

+        super();

+        this.isActiveList = isActiveList;

+        this.isDefaultList = isDefaultList;

+        this.listName = listName;

+        this.items = privacyItems;

+    }

+

+    public boolean isActiveList() {

+        return isActiveList;

+    }

+

+    public boolean isDefaultList() {

+        return isDefaultList;

+    }

+

+    public List<PrivacyItem> getItems() {

+        return items;

+    }

+

+    public String toString() {

+        return listName;

+    }

+

+}

diff --git a/src/org/jivesoftware/smack/PrivacyListListener.java b/src/org/jivesoftware/smack/PrivacyListListener.java
new file mode 100644
index 0000000..5644ed7
--- /dev/null
+++ b/src/org/jivesoftware/smack/PrivacyListListener.java
@@ -0,0 +1,51 @@
+/**

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2006-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;

+

+import org.jivesoftware.smack.packet.PrivacyItem;

+

+import java.util.List;

+

+/**

+ * Interface to implement classes to listen for server events about privacy communication. 

+ * Listeners are registered with the {@link PrivacyListManager}.

+ *

+ * @see PrivacyListManager#addListener

+ * 

+ * @author Francisco Vives

+ */

+public interface PrivacyListListener {

+

+    /**

+     * Set or update a privacy list with PrivacyItem.

+     *

+     * @param listName the name of the new or updated privacy list.

+     * @param listItem the PrivacyItems that rules the list.

+     */

+    public void setPrivacyList(String listName, List<PrivacyItem> listItem);

+

+    /**

+     * A privacy list has been modified by another. It gets notified.

+     *

+     * @param listName the name of the updated privacy list.

+     */

+    public void updatedPrivacyList(String listName);

+

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/PrivacyListManager.java b/src/org/jivesoftware/smack/PrivacyListManager.java
new file mode 100644
index 0000000..4dcc9e1
--- /dev/null
+++ b/src/org/jivesoftware/smack/PrivacyListManager.java
@@ -0,0 +1,467 @@
+/**

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2006-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;

+

+import org.jivesoftware.smack.filter.*;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.Privacy;

+import org.jivesoftware.smack.packet.PrivacyItem;

+

+import java.util.*;

+

+/**

+ * A PrivacyListManager is used by XMPP clients to block or allow communications from other

+ * users. Use the manager to: <ul>

+ *      <li>Retrieve privacy lists.

+ *      <li>Add, remove, and edit privacy lists.

+ *      <li>Set, change, or decline active lists.

+ *      <li>Set, change, or decline the default list (i.e., the list that is active by default).

+ * </ul>

+ * Privacy Items can handle different kind of permission communications based on JID, group, 

+ * subscription type or globally (@see PrivacyItem).

+ * 

+ * @author Francisco Vives

+ */

+public class PrivacyListManager {

+

+    // Keep the list of instances of this class.

+    private static Map<Connection, PrivacyListManager> instances = Collections

+            .synchronizedMap(new WeakHashMap<Connection, PrivacyListManager>());

+

+	private Connection connection;

+	private final List<PrivacyListListener> listeners = new ArrayList<PrivacyListListener>();

+	PacketFilter packetFilter = new AndFilter(new IQTypeFilter(IQ.Type.SET),

+    		new PacketExtensionFilter("query", "jabber:iq:privacy"));

+

+    static {

+        // Create a new PrivacyListManager on every established connection. In the init()

+        // method of PrivacyListManager, we'll add a listener that will delete the

+        // instance when the connection is closed.

+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {

+            public void connectionCreated(Connection connection) {

+                new PrivacyListManager(connection);

+            }

+        });

+    }

+    /**

+     * Creates a new privacy manager to maintain the communication privacy. Note: no

+     * information is sent to or received from the server until you attempt to 

+     * get or set the privacy communication.<p>

+     *

+     * @param connection the XMPP connection.

+     */

+	private PrivacyListManager(Connection connection) {

+        this.connection = connection;

+        this.init();

+    }

+

+	/** Answer the connection userJID that owns the privacy.

+	 * @return the userJID that owns the privacy

+	 */

+	private String getUser() {

+		return connection.getUser();

+	}

+

+    /**

+     * Initializes the packet listeners of the connection that will notify for any set privacy 

+     * package. 

+     */

+    private void init() {

+        // Register the new instance and associate it with the connection 

+        instances.put(connection, this);

+        // Add a listener to the connection that removes the registered instance when

+        // the connection is closed

+        connection.addConnectionListener(new ConnectionListener() {

+            public void connectionClosed() {

+                // Unregister this instance since the connection has been closed

+                instances.remove(connection);

+            }

+

+            public void connectionClosedOnError(Exception e) {

+                // ignore

+            }

+

+            public void reconnectionFailed(Exception e) {

+                // ignore

+            }

+

+            public void reconnectingIn(int seconds) {

+                // ignore

+            }

+

+            public void reconnectionSuccessful() {

+                // ignore

+            }

+        });

+

+        connection.addPacketListener(new PacketListener() {

+            public void processPacket(Packet packet) {

+

+                if (packet == null || packet.getError() != null) {

+                    return;

+                }

+                // The packet is correct.

+                Privacy privacy = (Privacy) packet;

+                

+                // Notifies the event to the listeners.

+                synchronized (listeners) {

+                    for (PrivacyListListener listener : listeners) {

+                        // Notifies the created or updated privacy lists

+                        for (Map.Entry<String,List<PrivacyItem>> entry : privacy.getItemLists().entrySet()) {

+                            String listName = entry.getKey();

+                            List<PrivacyItem> items = entry.getValue();

+                            if (items.isEmpty()) {

+                                listener.updatedPrivacyList(listName);

+                            } else {

+                                listener.setPrivacyList(listName, items);

+                            }

+                        }

+                    }

+                }

+                

+                // Send a result package acknowledging the reception of a privacy package.

+                

+                // Prepare the IQ packet to send

+                IQ iq = new IQ() {

+                    public String getChildElementXML() {

+                        return "";

+                    }

+                };

+                iq.setType(IQ.Type.RESULT);

+                iq.setFrom(packet.getFrom());

+                iq.setPacketID(packet.getPacketID());

+

+                // Send create & join packet.

+                connection.sendPacket(iq);

+            }

+        }, packetFilter);

+    }

+

+    /**

+     * Returns the PrivacyListManager instance associated with a given Connection.

+     * 

+     * @param connection the connection used to look for the proper PrivacyListManager.

+     * @return the PrivacyListManager associated with a given Connection.

+     */

+    public static PrivacyListManager getInstanceFor(Connection connection) {

+        return instances.get(connection);

+    }

+    

+	/**

+	 * Send the {@link Privacy} packet to the server in order to know some privacy content and then 

+	 * waits for the answer.

+	 * 

+	 * @param requestPrivacy is the {@link Privacy} packet configured properly whose XML

+     *      will be sent to the server.

+	 * @return a new {@link Privacy} with the data received from the server.

+	 * @exception XMPPException if the request or the answer failed, it raises an exception.

+	 */ 

+	private Privacy getRequest(Privacy requestPrivacy) throws XMPPException {

+		// The request is a get iq type

+		requestPrivacy.setType(Privacy.Type.GET);

+		requestPrivacy.setFrom(this.getUser());

+		

+		// Filter packets looking for an answer from the server.

+		PacketFilter responseFilter = new PacketIDFilter(requestPrivacy.getPacketID());

+        PacketCollector response = connection.createPacketCollector(responseFilter);

+        

+        // Send create & join packet.

+        connection.sendPacket(requestPrivacy);

+        

+        // Wait up to a certain number of seconds for a reply.

+        Privacy privacyAnswer =

+            (Privacy) response.nextResult(SmackConfiguration.getPacketReplyTimeout());

+        

+        // Stop queuing results

+        response.cancel();

+

+        // Interprete the result and answer the privacy only if it is valid

+        if (privacyAnswer == null) {

+            throw new XMPPException("No response from server.");

+        }

+        else if (privacyAnswer.getError() != null) {

+            throw new XMPPException(privacyAnswer.getError());

+        }

+        return privacyAnswer;

+	}

+	

+	/**

+	 * Send the {@link Privacy} packet to the server in order to modify the server privacy and 

+	 * waits for the answer.

+	 * 

+	 * @param requestPrivacy is the {@link Privacy} packet configured properly whose xml will be sent

+	 * to the server.

+	 * @return a new {@link Privacy} with the data received from the server.

+	 * @exception XMPPException if the request or the answer failed, it raises an exception.

+	 */ 

+	private Packet setRequest(Privacy requestPrivacy) throws XMPPException {

+		

+		// The request is a get iq type

+		requestPrivacy.setType(Privacy.Type.SET);

+		requestPrivacy.setFrom(this.getUser());

+		

+		// Filter packets looking for an answer from the server.

+		PacketFilter responseFilter = new PacketIDFilter(requestPrivacy.getPacketID());

+        PacketCollector response = connection.createPacketCollector(responseFilter);

+        

+        // Send create & join packet.

+        connection.sendPacket(requestPrivacy);

+        

+        // Wait up to a certain number of seconds for a reply.

+        Packet privacyAnswer = response.nextResult(SmackConfiguration.getPacketReplyTimeout());

+        

+        // Stop queuing results

+        response.cancel();

+

+        // Interprete the result and answer the privacy only if it is valid

+        if (privacyAnswer == null) {

+            throw new XMPPException("No response from server.");

+        } else if (privacyAnswer.getError() != null) {

+            throw new XMPPException(privacyAnswer.getError());

+        }

+        return privacyAnswer;

+	}

+

+	/**

+	 * Answer a privacy containing the list structre without {@link PrivacyItem}.

+	 * 

+	 * @return a Privacy with the list names.

+     * @throws XMPPException if an error occurs.

+	 */ 

+	private Privacy getPrivacyWithListNames() throws XMPPException {

+		

+		// The request of the list is an empty privacy message

+		Privacy request = new Privacy();

+		

+		// Send the package to the server and get the answer

+		return getRequest(request);

+	}

+	

+    /**

+     * Answer the active privacy list.

+     * 

+     * @return the privacy list of the active list.

+     * @throws XMPPException if an error occurs.

+     */ 

+    public PrivacyList getActiveList() throws XMPPException {

+        Privacy privacyAnswer = this.getPrivacyWithListNames();

+        String listName = privacyAnswer.getActiveName();

+        boolean isDefaultAndActive = privacyAnswer.getActiveName() != null

+                && privacyAnswer.getDefaultName() != null

+                && privacyAnswer.getActiveName().equals(

+                privacyAnswer.getDefaultName());

+        return new PrivacyList(true, isDefaultAndActive, listName, getPrivacyListItems(listName));

+    }

+    

+    /**

+     * Answer the default privacy list.

+     * 

+     * @return the privacy list of the default list.

+     * @throws XMPPException if an error occurs.

+     */ 

+    public PrivacyList getDefaultList() throws XMPPException {

+        Privacy privacyAnswer = this.getPrivacyWithListNames();

+        String listName = privacyAnswer.getDefaultName();

+        boolean isDefaultAndActive = privacyAnswer.getActiveName() != null

+                && privacyAnswer.getDefaultName() != null

+                && privacyAnswer.getActiveName().equals(

+                privacyAnswer.getDefaultName());

+        return new PrivacyList(isDefaultAndActive, true, listName, getPrivacyListItems(listName));

+    }

+    

+    /**

+     * Answer the privacy list items under listName with the allowed and blocked permissions.

+     * 

+     * @param listName the name of the list to get the allowed and blocked permissions.

+     * @return a list of privacy items under the list listName.

+     * @throws XMPPException if an error occurs.

+     */ 

+    private List<PrivacyItem> getPrivacyListItems(String listName) throws XMPPException {

+        

+        // The request of the list is an privacy message with an empty list

+        Privacy request = new Privacy();

+        request.setPrivacyList(listName, new ArrayList<PrivacyItem>());

+        

+        // Send the package to the server and get the answer

+        Privacy privacyAnswer = getRequest(request);

+        

+        return privacyAnswer.getPrivacyList(listName);

+    }

+    

+	/**

+	 * Answer the privacy list items under listName with the allowed and blocked permissions.

+	 * 

+	 * @param listName the name of the list to get the allowed and blocked permissions.

+	 * @return a privacy list under the list listName.

+     * @throws XMPPException if an error occurs.

+	 */ 

+	public PrivacyList getPrivacyList(String listName) throws XMPPException {

+		

+        return new PrivacyList(false, false, listName, getPrivacyListItems(listName));

+	}

+	

+    /**

+     * Answer every privacy list with the allowed and blocked permissions.

+     * 

+     * @return an array of privacy lists.

+     * @throws XMPPException if an error occurs.

+     */ 

+    public PrivacyList[] getPrivacyLists() throws XMPPException {

+        Privacy privacyAnswer = this.getPrivacyWithListNames();

+        Set<String> names = privacyAnswer.getPrivacyListNames();

+        PrivacyList[] lists = new PrivacyList[names.size()];

+        boolean isActiveList;

+        boolean isDefaultList;

+        int index=0;

+        for (String listName : names) {

+            isActiveList = listName.equals(privacyAnswer.getActiveName());

+            isDefaultList = listName.equals(privacyAnswer.getDefaultName());

+            lists[index] = new PrivacyList(isActiveList, isDefaultList,

+                    listName, getPrivacyListItems(listName));

+            index = index + 1;

+        }

+        return lists;

+    }

+

+    

+	/**

+	 * Set or change the active list to listName.

+	 * 

+	 * @param listName the list name to set as the active one.

+	 * @exception XMPPException if the request or the answer failed, it raises an exception.

+	 */ 

+	public void setActiveListName(String listName) throws XMPPException {

+		

+		// The request of the list is an privacy message with an empty list

+		Privacy request = new Privacy();

+		request.setActiveName(listName);

+		

+		// Send the package to the server

+		setRequest(request);

+	}

+

+	/**

+	 * Client declines the use of active lists.

+     *

+     * @throws XMPPException if an error occurs.

+	 */ 

+	public void declineActiveList() throws XMPPException {

+		

+		// The request of the list is an privacy message with an empty list

+		Privacy request = new Privacy();

+		request.setDeclineActiveList(true);

+		

+		// Send the package to the server

+		setRequest(request);

+	}

+

+	/**

+	 * Set or change the default list to listName.

+	 * 

+	 * @param listName the list name to set as the default one.

+	 * @exception XMPPException if the request or the answer failed, it raises an exception.

+	 */ 

+	public void setDefaultListName(String listName) throws XMPPException {

+		

+		// The request of the list is an privacy message with an empty list

+		Privacy request = new Privacy();

+		request.setDefaultName(listName);

+		

+		// Send the package to the server

+		setRequest(request);

+	}

+	

+	/**

+	 * Client declines the use of default lists.

+     *

+     * @throws XMPPException if an error occurs.

+	 */ 

+	public void declineDefaultList() throws XMPPException {

+		

+		// The request of the list is an privacy message with an empty list

+		Privacy request = new Privacy();

+		request.setDeclineDefaultList(true);

+		

+		// Send the package to the server

+		setRequest(request);

+	}

+	

+	/**

+	 * The client has created a new list. It send the new one to the server.

+	 * 

+     * @param listName the list that has changed its content.

+     * @param privacyItems a List with every privacy item in the list.

+     * @throws XMPPException if an error occurs.

+	 */ 

+	public void createPrivacyList(String listName, List<PrivacyItem> privacyItems) throws XMPPException {

+

+		this.updatePrivacyList(listName, privacyItems);

+	}

+

+    /**

+     * The client has edited an existing list. It updates the server content with the resulting 

+     * list of privacy items. The {@link PrivacyItem} list MUST contain all elements in the 

+     * list (not the "delta").

+     * 

+     * @param listName the list that has changed its content.

+     * @param privacyItems a List with every privacy item in the list.

+     * @throws XMPPException if an error occurs.

+     */ 

+    public void updatePrivacyList(String listName, List<PrivacyItem> privacyItems) throws XMPPException {

+

+        // Build the privacy package to add or update the new list

+        Privacy request = new Privacy();

+        request.setPrivacyList(listName, privacyItems);

+

+        // Send the package to the server

+        setRequest(request);

+    }

+    

+	/**

+	 * Remove a privacy list.

+	 * 

+     * @param listName the list that has changed its content.

+     * @throws XMPPException if an error occurs.

+	 */ 

+	public void deletePrivacyList(String listName) throws XMPPException {

+		

+		// The request of the list is an privacy message with an empty list

+		Privacy request = new Privacy();

+		request.setPrivacyList(listName, new ArrayList<PrivacyItem>());

+

+		// Send the package to the server

+		setRequest(request);

+	}

+	

+    /**

+     * Adds a packet listener that will be notified of any new update in the user

+     * privacy communication.

+     *

+     * @param listener a packet listener.

+     */

+    public void addListener(PrivacyListListener listener) {

+        // Keep track of the listener so that we can manually deliver extra

+        // messages to it later if needed.

+        synchronized (listeners) {

+            listeners.add(listener);

+        }

+    }    

+}

diff --git a/src/org/jivesoftware/smack/ReconnectionManager.java b/src/org/jivesoftware/smack/ReconnectionManager.java
new file mode 100644
index 0000000..cc3e3af
--- /dev/null
+++ b/src/org/jivesoftware/smack/ReconnectionManager.java
@@ -0,0 +1,227 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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;

+

+import org.jivesoftware.smack.packet.StreamError;

+import java.util.Random;

+/**

+ * Handles the automatic reconnection process. Every time a connection is dropped without

+ * the application explictly closing it, the manager automatically tries to reconnect to

+ * the server.<p>

+ *

+ * The reconnection mechanism will try to reconnect periodically:

+ * <ol>

+ *  <li>For the first minute it will attempt to connect once every ten seconds.

+ *  <li>For the next five minutes it will attempt to connect once a minute.

+ *  <li>If that fails it will indefinitely try to connect once every five minutes.

+ * </ol>

+ *

+ * @author Francisco Vives

+ */

+public class ReconnectionManager implements ConnectionListener {

+

+    // Holds the connection to the server

+    private Connection connection;

+    private Thread reconnectionThread;

+    private int randomBase = new Random().nextInt(11) + 5; // between 5 and 15 seconds

+    

+    // Holds the state of the reconnection

+    boolean done = false;

+

+    static {

+        // Create a new PrivacyListManager on every established connection. In the init()

+        // method of PrivacyListManager, we'll add a listener that will delete the

+        // instance when the connection is closed.

+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {

+            public void connectionCreated(Connection connection) {

+                connection.addConnectionListener(new ReconnectionManager(connection));

+            }

+        });

+    }

+

+    private ReconnectionManager(Connection connection) {

+        this.connection = connection;

+    }

+

+

+    /**

+     * Returns true if the reconnection mechanism is enabled.

+     *

+     * @return true if automatic reconnections are allowed.

+     */

+    private boolean isReconnectionAllowed() {

+        return !done && !connection.isConnected()
+                && connection.isReconnectionAllowed();
+    }

+

+    /**

+     * Starts a reconnection mechanism if it was configured to do that.

+     * The algorithm is been executed when the first connection error is detected.

+     * <p/>

+     * The reconnection mechanism will try to reconnect periodically in this way:

+     * <ol>

+     * <li>First it will try 6 times every 10 seconds.

+     * <li>Then it will try 10 times every 1 minute.

+     * <li>Finally it will try indefinitely every 5 minutes.

+     * </ol>

+     */

+    synchronized protected void reconnect() {

+        if (this.isReconnectionAllowed()) {

+            // Since there is no thread running, creates a new one to attempt

+            // the reconnection.

+            // avoid to run duplicated reconnectionThread -- fd: 16/09/2010

+            if (reconnectionThread!=null && reconnectionThread.isAlive()) return;

+            

+            reconnectionThread = new Thread() {

+             			

+                /**

+                 * Holds the current number of reconnection attempts

+                 */

+                private int attempts = 0;

+

+                /**

+                 * Returns the number of seconds until the next reconnection attempt.

+                 *

+                 * @return the number of seconds until the next reconnection attempt.

+                 */

+                private int timeDelay() {

+                    attempts++;

+                    if (attempts > 13) {

+                	return randomBase*6*5;      // between 2.5 and 7.5 minutes (~5 minutes)

+                    }

+                    if (attempts > 7) {

+                	return randomBase*6;       // between 30 and 90 seconds (~1 minutes)

+                    }

+                    return randomBase;       // 10 seconds

+                }

+

+                /**

+                 * The process will try the reconnection until the connection succeed or the user

+                 * cancell it

+                 */

+                public void run() {

+                    // The process will try to reconnect until the connection is established or

+                    // the user cancel the reconnection process {@link Connection#disconnect()}

+                    while (ReconnectionManager.this.isReconnectionAllowed()) {

+                        // Find how much time we should wait until the next reconnection

+                        int remainingSeconds = timeDelay();

+                        // Sleep until we're ready for the next reconnection attempt. Notify

+                        // listeners once per second about how much time remains before the next

+                        // reconnection attempt.

+                        while (ReconnectionManager.this.isReconnectionAllowed() &&

+                                remainingSeconds > 0)

+                        {

+                            try {

+                                Thread.sleep(1000);

+                                remainingSeconds--;

+                                ReconnectionManager.this

+                                        .notifyAttemptToReconnectIn(remainingSeconds);

+                            }

+                            catch (InterruptedException e1) {

+                                e1.printStackTrace();

+                                // Notify the reconnection has failed

+                                ReconnectionManager.this.notifyReconnectionFailed(e1);

+                            }

+                        }

+

+                        // Makes a reconnection attempt

+                        try {

+                            if (ReconnectionManager.this.isReconnectionAllowed()) {

+                                connection.connect();

+                            }

+                        }

+                        catch (XMPPException e) {

+                            // Fires the failed reconnection notification

+                            ReconnectionManager.this.notifyReconnectionFailed(e);

+                        }

+                    }

+                }

+            };

+            reconnectionThread.setName("Smack Reconnection Manager");

+            reconnectionThread.setDaemon(true);

+            reconnectionThread.start();

+        }

+    }

+

+    /**

+     * Fires listeners when a reconnection attempt has failed.

+     *

+     * @param exception the exception that occured.

+     */

+    protected void notifyReconnectionFailed(Exception exception) {

+        if (isReconnectionAllowed()) {
+            for (ConnectionListener listener : connection.connectionListeners) {
+                listener.reconnectionFailed(exception);

+            }

+        }

+    }

+

+    /**

+     * Fires listeners when The Connection will retry a reconnection. Expressed in seconds.

+     *

+     * @param seconds the number of seconds that a reconnection will be attempted in.

+     */

+    protected void notifyAttemptToReconnectIn(int seconds) {

+        if (isReconnectionAllowed()) {
+            for (ConnectionListener listener : connection.connectionListeners) {
+                listener.reconnectingIn(seconds);

+            }

+        }

+    }

+

+    public void connectionClosed() {

+        done = true;

+    }

+

+    public void connectionClosedOnError(Exception e) {

+        done = false;

+        if (e instanceof XMPPException) {

+            XMPPException xmppEx = (XMPPException) e;

+            StreamError error = xmppEx.getStreamError();

+

+            // Make sure the error is not null

+            if (error != null) {

+                String reason = error.getCode();

+

+                if ("conflict".equals(reason)) {

+                    return;

+                }

+            }

+        }

+

+        if (this.isReconnectionAllowed()) {

+            this.reconnect();

+        }

+    }

+

+    public void reconnectingIn(int seconds) {

+        // ignore

+    }

+

+    public void reconnectionFailed(Exception e) {

+        // ignore

+    }

+

+    /**

+     * The connection has successfull gotten connected.

+     */

+    public void reconnectionSuccessful() {

+        // ignore

+    }

+

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/Roster.java b/src/org/jivesoftware/smack/Roster.java
new file mode 100644
index 0000000..66a78b2
--- /dev/null
+++ b/src/org/jivesoftware/smack/Roster.java
@@ -0,0 +1,1038 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * Represents a user's roster, which is the collection of users a person receives
+ * presence updates for. Roster items are categorized into groups for easier management.<p>
+ * <p/>
+ * Others users may attempt to subscribe to this user using a subscription request. Three
+ * modes are supported for handling these requests: <ul>
+ * <li>{@link SubscriptionMode#accept_all accept_all} -- accept all subscription requests.</li>
+ * <li>{@link SubscriptionMode#reject_all reject_all} -- reject all subscription requests.</li>
+ * <li>{@link SubscriptionMode#manual manual} -- manually process all subscription requests.</li>
+ * </ul>
+ *
+ * @author Matt Tucker
+ * @see Connection#getRoster()
+ */
+public class Roster {
+
+    /**
+     * The default subscription processing mode to use when a Roster is created. By default
+     * all subscription requests are automatically accepted.
+     */
+    private static SubscriptionMode defaultSubscriptionMode = SubscriptionMode.accept_all;
+    private RosterStorage persistentStorage;
+
+    private Connection connection;
+    private final Map<String, RosterGroup> groups;
+    private final Map<String,RosterEntry> entries;
+    private final List<RosterEntry> unfiledEntries;
+    private final List<RosterListener> rosterListeners;
+    private Map<String, Map<String, Presence>> presenceMap;
+    // The roster is marked as initialized when at least a single roster packet
+    // has been received and processed.
+    boolean rosterInitialized = false;
+    private PresencePacketListener presencePacketListener;
+
+    private SubscriptionMode subscriptionMode = getDefaultSubscriptionMode();
+    
+    private String requestPacketId;
+
+    /**
+     * Returns the default subscription processing mode to use when a new Roster is created. The
+     * subscription processing mode dictates what action Smack will take when subscription
+     * requests from other users are made. The default subscription mode
+     * is {@link SubscriptionMode#accept_all}.
+     *
+     * @return the default subscription mode to use for new Rosters
+     */
+    public static SubscriptionMode getDefaultSubscriptionMode() {
+        return defaultSubscriptionMode;
+    }
+
+    /**
+     * Sets the default subscription processing mode to use when a new Roster is created. The
+     * subscription processing mode dictates what action Smack will take when subscription
+     * requests from other users are made. The default subscription mode
+     * is {@link SubscriptionMode#accept_all}.
+     *
+     * @param subscriptionMode the default subscription mode to use for new Rosters.
+     */
+    public static void setDefaultSubscriptionMode(SubscriptionMode subscriptionMode) {
+        defaultSubscriptionMode = subscriptionMode;
+    }
+    
+    Roster(final Connection connection, RosterStorage persistentStorage){
+    	this(connection);
+    	this.persistentStorage = persistentStorage;
+    }
+
+    /**
+     * Creates a new roster.
+     *
+     * @param connection an XMPP connection.
+     */
+    Roster(final Connection connection) {
+        this.connection = connection;
+        //Disable roster versioning if server doesn't offer support for it
+        if(!connection.getConfiguration().isRosterVersioningAvailable()){
+        	persistentStorage=null;
+        }
+        groups = new ConcurrentHashMap<String, RosterGroup>();
+        unfiledEntries = new CopyOnWriteArrayList<RosterEntry>();
+        entries = new ConcurrentHashMap<String,RosterEntry>();
+        rosterListeners = new CopyOnWriteArrayList<RosterListener>();
+        presenceMap = new ConcurrentHashMap<String, Map<String, Presence>>();
+        // Listen for any roster packets.
+        PacketFilter rosterFilter = new PacketTypeFilter(RosterPacket.class);
+        connection.addPacketListener(new RosterPacketListener(), rosterFilter);
+        // Listen for any presence packets.
+        PacketFilter presenceFilter = new PacketTypeFilter(Presence.class);
+        presencePacketListener = new PresencePacketListener();
+        connection.addPacketListener(presencePacketListener, presenceFilter);
+        
+        // Listen for connection events
+        final ConnectionListener connectionListener = new AbstractConnectionListener() {
+            
+            public void connectionClosed() {
+                // Changes the presence available contacts to unavailable
+                setOfflinePresences();
+            }
+
+            public void connectionClosedOnError(Exception e) {
+                // Changes the presence available contacts to unavailable
+                setOfflinePresences();
+            }
+
+        };
+        
+        // if not connected add listener after successful login
+        if(!this.connection.isConnected()) {
+            Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+                
+                public void connectionCreated(Connection connection) {
+                    if(connection.equals(Roster.this.connection)) {
+                        Roster.this.connection.addConnectionListener(connectionListener);
+                    }
+                    
+                }
+            });
+        } else {
+            connection.addConnectionListener(connectionListener);
+        }
+    }
+
+    /**
+     * Returns the subscription processing mode, which dictates what action
+     * Smack will take when subscription requests from other users are made.
+     * The default subscription mode is {@link SubscriptionMode#accept_all}.<p>
+     * <p/>
+     * If using the manual mode, a PacketListener should be registered that
+     * listens for Presence packets that have a type of
+     * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}.
+     *
+     * @return the subscription mode.
+     */
+    public SubscriptionMode getSubscriptionMode() {
+        return subscriptionMode;
+    }
+
+    /**
+     * Sets the subscription processing mode, which dictates what action
+     * Smack will take when subscription requests from other users are made.
+     * The default subscription mode is {@link SubscriptionMode#accept_all}.<p>
+     * <p/>
+     * If using the manual mode, a PacketListener should be registered that
+     * listens for Presence packets that have a type of
+     * {@link org.jivesoftware.smack.packet.Presence.Type#subscribe}.
+     *
+     * @param subscriptionMode the subscription mode.
+     */
+    public void setSubscriptionMode(SubscriptionMode subscriptionMode) {
+        this.subscriptionMode = subscriptionMode;
+    }
+
+    /**
+     * Reloads the entire roster from the server. This is an asynchronous operation,
+     * which means the method will return immediately, and the roster will be
+     * reloaded at a later point when the server responds to the reload request.
+     * 
+     * @throws IllegalStateException if connection is not logged in or logged in anonymously
+     */
+    public void reload() {
+        if (!connection.isAuthenticated()) {
+            throw new IllegalStateException("Not logged in to server.");
+        }
+        if (connection.isAnonymous()) {
+            throw new IllegalStateException("Anonymous users can't have a roster.");
+        }
+
+    	RosterPacket packet = new RosterPacket();
+    	if(persistentStorage!=null){
+    		packet.setVersion(persistentStorage.getRosterVersion());
+    	}
+    	requestPacketId = packet.getPacketID();
+    	PacketFilter idFilter = new PacketIDFilter(requestPacketId);
+    	connection.addPacketListener(new RosterResultListener(), idFilter);
+        connection.sendPacket(packet);
+    }
+
+    /**
+     * Adds a listener to this roster. The listener will be fired anytime one or more
+     * changes to the roster are pushed from the server.
+     *
+     * @param rosterListener a roster listener.
+     */
+    public void addRosterListener(RosterListener rosterListener) {
+        if (!rosterListeners.contains(rosterListener)) {
+            rosterListeners.add(rosterListener);
+        }
+    }
+
+    /**
+     * Removes a listener from this roster. The listener will be fired anytime one or more
+     * changes to the roster are pushed from the server.
+     *
+     * @param rosterListener a roster listener.
+     */
+    public void removeRosterListener(RosterListener rosterListener) {
+        rosterListeners.remove(rosterListener);
+    }
+
+    /**
+     * Creates a new group.<p>
+     * <p/>
+     * Note: you must add at least one entry to the group for the group to be kept
+     * after a logout/login. This is due to the way that XMPP stores group information.
+     *
+     * @param name the name of the group.
+     * @return a new group.
+     * @throws IllegalStateException if connection is not logged in or logged in anonymously
+     */
+    public RosterGroup createGroup(String name) {
+        if (!connection.isAuthenticated()) {
+            throw new IllegalStateException("Not logged in to server.");
+        }
+        if (connection.isAnonymous()) {
+            throw new IllegalStateException("Anonymous users can't have a roster.");
+        }
+        if (groups.containsKey(name)) {
+            throw new IllegalArgumentException("Group with name " + name + " alread exists.");
+        }
+        
+        RosterGroup group = new RosterGroup(name, connection);
+        groups.put(name, group);
+        return group;
+    }
+
+    /**
+     * Creates a new roster entry and presence subscription. The server will asynchronously
+     * update the roster with the subscription status.
+     *
+     * @param user   the user. (e.g. johndoe@jabber.org)
+     * @param name   the nickname of the user.
+     * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the
+     *               the roster entry won't belong to a group.
+     * @throws XMPPException if an XMPP exception occurs.
+     * @throws IllegalStateException if connection is not logged in or logged in anonymously
+     */
+    public void createEntry(String user, String name, String[] groups) throws XMPPException {
+        if (!connection.isAuthenticated()) {
+            throw new IllegalStateException("Not logged in to server.");
+        }
+        if (connection.isAnonymous()) {
+            throw new IllegalStateException("Anonymous users can't have a roster.");
+        }
+
+        // Create and send roster entry creation packet.
+        RosterPacket rosterPacket = new RosterPacket();
+        rosterPacket.setType(IQ.Type.SET);
+        RosterPacket.Item item = new RosterPacket.Item(user, name);
+        if (groups != null) {
+            for (String group : groups) {
+                if (group != null && group.trim().length() > 0) {
+                    item.addGroupName(group);
+                }
+            }
+        }
+        rosterPacket.addRosterItem(item);
+        // Wait up to a certain number of seconds for a reply from the server.
+        PacketCollector collector = connection.createPacketCollector(
+                new PacketIDFilter(rosterPacket.getPacketID()));
+        connection.sendPacket(rosterPacket);
+        IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        collector.cancel();
+        if (response == null) {
+            throw new XMPPException("No response from the server.");
+        }
+        // If the server replied with an error, throw an exception.
+        else if (response.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(response.getError());
+        }
+
+        // Create a presence subscription packet and send.
+        Presence presencePacket = new Presence(Presence.Type.subscribe);
+        presencePacket.setTo(user);
+        connection.sendPacket(presencePacket);
+    }
+    
+    private void insertRosterItems(List<RosterPacket.Item> items){
+    	 Collection<String> addedEntries = new ArrayList<String>();
+         Collection<String> updatedEntries = new ArrayList<String>();
+         Collection<String> deletedEntries = new ArrayList<String>();
+         Iterator<RosterPacket.Item> iter = items.iterator();
+         while(iter.hasNext()){
+        	 insertRosterItem(iter.next(), addedEntries,updatedEntries,deletedEntries);
+         }
+         fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
+    }
+    
+    private void insertRosterItem(RosterPacket.Item item, Collection<String> addedEntries,
+    		Collection<String> updatedEntries, Collection<String> deletedEntries){
+    	RosterEntry entry = new RosterEntry(item.getUser(), item.getName(),
+                item.getItemType(), item.getItemStatus(), this, connection);
+
+        // If the packet is of the type REMOVE then remove the entry
+        if (RosterPacket.ItemType.remove.equals(item.getItemType())) {
+            // Remove the entry from the entry list.
+            if (entries.containsKey(item.getUser())) {
+                entries.remove(item.getUser());
+            }
+            // Remove the entry from the unfiled entry list.
+            if (unfiledEntries.contains(entry)) {
+                unfiledEntries.remove(entry);
+            }
+            // Removing the user from the roster, so remove any presence information
+            // about them.
+            String key = StringUtils.parseName(item.getUser()) + "@" +
+                    StringUtils.parseServer(item.getUser());
+            presenceMap.remove(key);
+            // Keep note that an entry has been removed
+            if(deletedEntries!=null){
+            	deletedEntries.add(item.getUser());
+            }
+        }
+        else {
+            // Make sure the entry is in the entry list.
+            if (!entries.containsKey(item.getUser())) {
+                entries.put(item.getUser(), entry);
+                // Keep note that an entry has been added
+                if(addedEntries!=null){
+                	addedEntries.add(item.getUser());
+                }
+            }
+            else {
+                // If the entry was in then list then update its state with the new values
+                entries.put(item.getUser(), entry);
+                
+                // Keep note that an entry has been updated
+                if(updatedEntries!=null){
+                	updatedEntries.add(item.getUser());
+                }
+            }
+            // If the roster entry belongs to any groups, remove it from the
+            // list of unfiled entries.
+            if (!item.getGroupNames().isEmpty()) {
+                unfiledEntries.remove(entry);
+            }
+            // Otherwise add it to the list of unfiled entries.
+            else {
+                if (!unfiledEntries.contains(entry)) {
+                    unfiledEntries.add(entry);
+                }
+            }
+        }
+
+        // Find the list of groups that the user currently belongs to.
+        List<String> currentGroupNames = new ArrayList<String>();
+        for (RosterGroup group: getGroups()) {
+            if (group.contains(entry)) {
+                currentGroupNames.add(group.getName());
+            }
+        }
+
+        // If the packet is not of the type REMOVE then add the entry to the groups
+        if (!RosterPacket.ItemType.remove.equals(item.getItemType())) {
+            // Create the new list of groups the user belongs to.
+            List<String> newGroupNames = new ArrayList<String>();
+            for (String groupName : item.getGroupNames()) {
+                // Add the group name to the list.
+                newGroupNames.add(groupName);
+
+                // Add the entry to the group.
+                RosterGroup group = getGroup(groupName);
+                if (group == null) {
+                    group = createGroup(groupName);
+                    groups.put(groupName, group);
+                }
+                // Add the entry.
+                group.addEntryLocal(entry);
+            }
+
+            // We have the list of old and new group names. We now need to
+            // remove the entry from the all the groups it may no longer belong
+            // to. We do this by subracting the new group set from the old.
+            for (String newGroupName : newGroupNames) {
+                currentGroupNames.remove(newGroupName);
+            }
+        }
+
+        // Loop through any groups that remain and remove the entries.
+        // This is neccessary for the case of remote entry removals.
+        for (String groupName : currentGroupNames) {
+            RosterGroup group = getGroup(groupName);
+            group.removeEntryLocal(entry);
+            if (group.getEntryCount() == 0) {
+                groups.remove(groupName);
+            }
+        }
+        // Remove all the groups with no entries. We have to do this because
+        // RosterGroup.removeEntry removes the entry immediately (locally) and the
+        // group could remain empty.
+        // TODO Check the performance/logic for rosters with large number of groups
+        for (RosterGroup group : getGroups()) {
+            if (group.getEntryCount() == 0) {
+                groups.remove(group.getName());
+            }
+        }
+    }
+
+    /**
+     * Removes a roster entry from the roster. The roster entry will also be removed from the
+     * unfiled entries or from any roster group where it could belong and will no longer be part
+     * of the roster. Note that this is an asynchronous call -- Smack must wait for the server
+     * to send an updated subscription status.
+     *
+     * @param entry a roster entry.
+     * @throws XMPPException if an XMPP error occurs.
+     * @throws IllegalStateException if connection is not logged in or logged in anonymously
+     */
+    public void removeEntry(RosterEntry entry) throws XMPPException {
+        if (!connection.isAuthenticated()) {
+            throw new IllegalStateException("Not logged in to server.");
+        }
+        if (connection.isAnonymous()) {
+            throw new IllegalStateException("Anonymous users can't have a roster.");
+        }
+
+        // Only remove the entry if it's in the entry list.
+        // The actual removal logic takes place in RosterPacketListenerprocess>>Packet(Packet)
+        if (!entries.containsKey(entry.getUser())) {
+            return;
+        }
+        RosterPacket packet = new RosterPacket();
+        packet.setType(IQ.Type.SET);
+        RosterPacket.Item item = RosterEntry.toRosterItem(entry);
+        // Set the item type as REMOVE so that the server will delete the entry
+        item.setItemType(RosterPacket.ItemType.remove);
+        packet.addRosterItem(item);
+        PacketCollector collector = connection.createPacketCollector(
+                new PacketIDFilter(packet.getPacketID()));
+        connection.sendPacket(packet);
+        IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        collector.cancel();
+        if (response == null) {
+            throw new XMPPException("No response from the server.");
+        }
+        // If the server replied with an error, throw an exception.
+        else if (response.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(response.getError());
+        }
+    }
+
+    /**
+     * Returns a count of the entries in the roster.
+     *
+     * @return the number of entries in the roster.
+     */
+    public int getEntryCount() {
+        return getEntries().size();
+    }
+
+    /**
+     * Returns an unmodifiable collection of all entries in the roster, including entries
+     * that don't belong to any groups.
+     *
+     * @return all entries in the roster.
+     */
+    public Collection<RosterEntry> getEntries() {
+        Set<RosterEntry> allEntries = new HashSet<RosterEntry>();
+        // Loop through all roster groups and add their entries to the answer
+        for (RosterGroup rosterGroup : getGroups()) {
+            allEntries.addAll(rosterGroup.getEntries());
+        }
+        // Add the roster unfiled entries to the answer
+        allEntries.addAll(unfiledEntries);
+
+        return Collections.unmodifiableCollection(allEntries);
+    }
+
+    /**
+     * Returns a count of the unfiled entries in the roster. An unfiled entry is
+     * an entry that doesn't belong to any groups.
+     *
+     * @return the number of unfiled entries in the roster.
+     */
+    public int getUnfiledEntryCount() {
+        return unfiledEntries.size();
+    }
+
+    /**
+     * Returns an unmodifiable collection for the unfiled roster entries. An unfiled entry is
+     * an entry that doesn't belong to any groups.
+     *
+     * @return the unfiled roster entries.
+     */
+    public Collection<RosterEntry> getUnfiledEntries() {
+        return Collections.unmodifiableList(unfiledEntries);
+    }
+
+    /**
+     * Returns the roster entry associated with the given XMPP address or
+     * <tt>null</tt> if the user is not an entry in the roster.
+     *
+     * @param user the XMPP address of the user (eg "jsmith@example.com"). The address could be
+     *             in any valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource").
+     * @return the roster entry or <tt>null</tt> if it does not exist.
+     */
+    public RosterEntry getEntry(String user) {
+        if (user == null) {
+            return null;
+        }
+        return entries.get(user.toLowerCase());
+    }
+
+    /**
+     * Returns true if the specified XMPP address is an entry in the roster.
+     *
+     * @param user the XMPP address of the user (eg "jsmith@example.com"). The
+     *             address could be in any valid format (e.g. "domain/resource",
+     *             "user@domain" or "user@domain/resource").
+     * @return true if the XMPP address is an entry in the roster.
+     */
+    public boolean contains(String user) {
+        return getEntry(user) != null;
+    }
+
+    /**
+     * Returns the roster group with the specified name, or <tt>null</tt> if the
+     * group doesn't exist.
+     *
+     * @param name the name of the group.
+     * @return the roster group with the specified name.
+     */
+    public RosterGroup getGroup(String name) {
+        return groups.get(name);
+    }
+
+    /**
+     * Returns the number of the groups in the roster.
+     *
+     * @return the number of groups in the roster.
+     */
+    public int getGroupCount() {
+        return groups.size();
+    }
+
+    /**
+     * Returns an unmodifiable collections of all the roster groups.
+     *
+     * @return an iterator for all roster groups.
+     */
+    public Collection<RosterGroup> getGroups() {
+        return Collections.unmodifiableCollection(groups.values());
+    }
+
+    /**
+     * Returns the presence info for a particular user. If the user is offline, or
+     * if no presence data is available (such as when you are not subscribed to the
+     * user's presence updates), unavailable presence will be returned.<p>
+     * <p/>
+     * If the user has several presences (one for each resource), then the presence with
+     * highest priority will be returned. If multiple presences have the same priority,
+     * the one with the "most available" presence mode will be returned. In order,
+     * that's {@link org.jivesoftware.smack.packet.Presence.Mode#chat free to chat},
+     * {@link org.jivesoftware.smack.packet.Presence.Mode#available available},
+     * {@link org.jivesoftware.smack.packet.Presence.Mode#away away},
+     * {@link org.jivesoftware.smack.packet.Presence.Mode#xa extended away}, and
+     * {@link org.jivesoftware.smack.packet.Presence.Mode#dnd do not disturb}.<p>
+     * <p/>
+     * Note that presence information is received asynchronously. So, just after logging
+     * in to the server, presence values for users in the roster may be unavailable
+     * even if they are actually online. In other words, the value returned by this
+     * method should only be treated as a snapshot in time, and may not accurately reflect
+     * other user's presence instant by instant. If you need to track presence over time,
+     * such as when showing a visual representation of the roster, consider using a
+     * {@link RosterListener}.
+     *
+     * @param user an XMPP ID. The address could be in any valid format (e.g.
+     *             "domain/resource", "user@domain" or "user@domain/resource"). Any resource
+     *             information that's part of the ID will be discarded.
+     * @return the user's current presence, or unavailable presence if the user is offline
+     *         or if no presence information is available..
+     */
+    public Presence getPresence(String user) {
+        String key = getPresenceMapKey(StringUtils.parseBareAddress(user));
+        Map<String, Presence> userPresences = presenceMap.get(key);
+        if (userPresences == null) {
+            Presence presence = new Presence(Presence.Type.unavailable);
+            presence.setFrom(user);
+            return presence;
+        }
+        else {
+            // Find the resource with the highest priority
+            // Might be changed to use the resource with the highest availability instead.
+            Presence presence = null;
+
+            for (String resource : userPresences.keySet()) {
+                Presence p = userPresences.get(resource);
+                if (!p.isAvailable()) {
+                    continue;
+                }
+                // Chose presence with highest priority first.
+                if (presence == null || p.getPriority() > presence.getPriority()) {
+                    presence = p;
+                }
+                // If equal priority, choose "most available" by the mode value.
+                else if (p.getPriority() == presence.getPriority()) {
+                    Presence.Mode pMode = p.getMode();
+                    // Default to presence mode of available.
+                    if (pMode == null) {
+                        pMode = Presence.Mode.available;
+                    }
+                    Presence.Mode presenceMode = presence.getMode();
+                    // Default to presence mode of available.
+                    if (presenceMode == null) {
+                        presenceMode = Presence.Mode.available;
+                    }
+                    if (pMode.compareTo(presenceMode) < 0) {
+                        presence = p;
+                    }
+                }
+            }
+            if (presence == null) {
+                presence = new Presence(Presence.Type.unavailable);
+                presence.setFrom(user);
+                return presence;
+            }
+            else {
+                return presence;
+            }
+        }
+    }
+
+    /**
+     * Returns the presence info for a particular user's resource, or unavailable presence
+     * if the user is offline or if no presence information is available, such as
+     * when you are not subscribed to the user's presence updates.
+     *
+     * @param userWithResource a fully qualified XMPP ID including a resource (user@domain/resource).
+     * @return the user's current presence, or unavailable presence if the user is offline
+     *         or if no presence information is available.
+     */
+    public Presence getPresenceResource(String userWithResource) {
+        String key = getPresenceMapKey(userWithResource);
+        String resource = StringUtils.parseResource(userWithResource);
+        Map<String, Presence> userPresences = presenceMap.get(key);
+        if (userPresences == null) {
+            Presence presence = new Presence(Presence.Type.unavailable);
+            presence.setFrom(userWithResource);
+            return presence;
+        }
+        else {
+            Presence presence = userPresences.get(resource);
+            if (presence == null) {
+                presence = new Presence(Presence.Type.unavailable);
+                presence.setFrom(userWithResource);
+                return presence;
+            }
+            else {
+                return presence;
+            }
+        }
+    }
+
+    /**
+     * Returns an iterator (of Presence objects) for all of a user's current presences
+     * or an unavailable presence if the user is unavailable (offline) or if no presence
+     * information is available, such as when you are not subscribed to the user's presence
+     * updates.
+     *
+     * @param user a XMPP ID, e.g. jdoe@example.com.
+     * @return an iterator (of Presence objects) for all the user's current presences,
+     *         or an unavailable presence if the user is offline or if no presence information
+     *         is available.
+     */
+    public Iterator<Presence> getPresences(String user) {
+        String key = getPresenceMapKey(user);
+        Map<String, Presence> userPresences = presenceMap.get(key);
+        if (userPresences == null) {
+            Presence presence = new Presence(Presence.Type.unavailable);
+            presence.setFrom(user);
+            return Arrays.asList(presence).iterator();
+        }
+        else {
+            Collection<Presence> answer = new ArrayList<Presence>();
+            for (Presence presence : userPresences.values()) {
+                if (presence.isAvailable()) {
+                    answer.add(presence);
+                }
+            }
+            if (!answer.isEmpty()) {
+                return answer.iterator();
+            }
+            else {
+                Presence presence = new Presence(Presence.Type.unavailable);
+                presence.setFrom(user);
+                return Arrays.asList(presence).iterator();    
+            }
+        }
+    }
+
+    /**
+     * Cleans up all resources used by the roster.
+     */
+    void cleanup() {
+        rosterListeners.clear();
+    }
+
+    /**
+     * Returns the key to use in the presenceMap for a fully qualified XMPP ID.
+     * The roster can contain any valid address format such us "domain/resource",
+     * "user@domain" or "user@domain/resource". If the roster contains an entry
+     * associated with the fully qualified XMPP ID then use the fully qualified XMPP
+     * ID as the key in presenceMap, otherwise use the bare address. Note: When the
+     * key in presenceMap is a fully qualified XMPP ID, the userPresences is useless
+     * since it will always contain one entry for the user.
+     *
+     * @param user the bare or fully qualified XMPP ID, e.g. jdoe@example.com or
+     *             jdoe@example.com/Work.
+     * @return the key to use in the presenceMap for the fully qualified XMPP ID.
+     */
+    private String getPresenceMapKey(String user) {
+        if (user == null) {
+            return null;
+        }
+        String key = user;
+        if (!contains(user)) {
+            key = StringUtils.parseBareAddress(user);
+        }
+        return key.toLowerCase();
+    }
+
+    /**
+     * Changes the presence of available contacts offline by simulating an unavailable
+     * presence sent from the server. After a disconnection, every Presence is set
+     * to offline.
+     */
+    private void setOfflinePresences() {
+        Presence packetUnavailable;
+        for (String user : presenceMap.keySet()) {
+            Map<String, Presence> resources = presenceMap.get(user);
+            if (resources != null) {
+                for (String resource : resources.keySet()) {
+                    packetUnavailable = new Presence(Presence.Type.unavailable);
+                    packetUnavailable.setFrom(user + "/" + resource);
+                    presencePacketListener.processPacket(packetUnavailable);
+                }
+            }
+        }
+    }
+
+    /**
+     * Fires roster changed event to roster listeners indicating that the
+     * specified collections of contacts have been added, updated or deleted
+     * from the roster.
+     *
+     * @param addedEntries   the collection of address of the added contacts.
+     * @param updatedEntries the collection of address of the updated contacts.
+     * @param deletedEntries the collection of address of the deleted contacts.
+     */
+    private void fireRosterChangedEvent(Collection<String> addedEntries, Collection<String> updatedEntries,
+            Collection<String> deletedEntries) {
+        for (RosterListener listener : rosterListeners) {
+            if (!addedEntries.isEmpty()) {
+                listener.entriesAdded(addedEntries);
+            }
+            if (!updatedEntries.isEmpty()) {
+                listener.entriesUpdated(updatedEntries);
+            }
+            if (!deletedEntries.isEmpty()) {
+                listener.entriesDeleted(deletedEntries);
+            }
+        }
+    }
+
+    /**
+     * Fires roster presence changed event to roster listeners.
+     *
+     * @param presence the presence change.
+     */
+    private void fireRosterPresenceEvent(Presence presence) {
+        for (RosterListener listener : rosterListeners) {
+            listener.presenceChanged(presence);
+        }
+    }
+
+    /**
+     * An enumeration for the subscription mode options.
+     */
+    public enum SubscriptionMode {
+
+        /**
+         * Automatically accept all subscription and unsubscription requests. This is
+         * the default mode and is suitable for simple client. More complex client will
+         * likely wish to handle subscription requests manually.
+         */
+        accept_all,
+
+        /**
+         * Automatically reject all subscription requests.
+         */
+        reject_all,
+
+        /**
+         * Subscription requests are ignored, which means they must be manually
+         * processed by registering a listener for presence packets and then looking
+         * for any presence requests that have the type Presence.Type.SUBSCRIBE or
+         * Presence.Type.UNSUBSCRIBE.
+         */
+        manual
+    }
+
+    /**
+     * Listens for all presence packets and processes them.
+     */
+    private class PresencePacketListener implements PacketListener {
+
+        public void processPacket(Packet packet) {
+            Presence presence = (Presence) packet;
+            String from = presence.getFrom();
+            String key = getPresenceMapKey(from);
+
+            // If an "available" presence, add it to the presence map. Each presence
+            // map will hold for a particular user a map with the presence
+            // packets saved for each resource.
+            if (presence.getType() == Presence.Type.available) {
+                Map<String, Presence> userPresences;
+                // Get the user presence map
+                if (presenceMap.get(key) == null) {
+                    userPresences = new ConcurrentHashMap<String, Presence>();
+                    presenceMap.put(key, userPresences);
+                }
+                else {
+                    userPresences = presenceMap.get(key);
+                }
+                // See if an offline presence was being stored in the map. If so, remove
+                // it since we now have an online presence.
+                userPresences.remove("");
+                // Add the new presence, using the resources as a key.
+                userPresences.put(StringUtils.parseResource(from), presence);
+                // If the user is in the roster, fire an event.
+                RosterEntry entry = entries.get(key);
+                if (entry != null) {
+                    fireRosterPresenceEvent(presence);
+                }
+            }
+            // If an "unavailable" packet.
+            else if (presence.getType() == Presence.Type.unavailable) {
+                // If no resource, this is likely an offline presence as part of
+                // a roster presence flood. In that case, we store it.
+                if ("".equals(StringUtils.parseResource(from))) {
+                    Map<String, Presence> userPresences;
+                    // Get the user presence map
+                    if (presenceMap.get(key) == null) {
+                        userPresences = new ConcurrentHashMap<String, Presence>();
+                        presenceMap.put(key, userPresences);
+                    }
+                    else {
+                        userPresences = presenceMap.get(key);
+                    }
+                    userPresences.put("", presence);
+                }
+                // Otherwise, this is a normal offline presence.
+                else if (presenceMap.get(key) != null) {
+                    Map<String, Presence> userPresences = presenceMap.get(key);
+                    // Store the offline presence, as it may include extra information
+                    // such as the user being on vacation.
+                    userPresences.put(StringUtils.parseResource(from), presence);
+                }
+                // If the user is in the roster, fire an event.
+                RosterEntry entry = entries.get(key);
+                if (entry != null) {
+                    fireRosterPresenceEvent(presence);
+                }
+            }
+            else if (presence.getType() == Presence.Type.subscribe) {
+                if (subscriptionMode == SubscriptionMode.accept_all) {
+                    // Accept all subscription requests.
+                    Presence response = new Presence(Presence.Type.subscribed);
+                    response.setTo(presence.getFrom());
+                    connection.sendPacket(response);
+                }
+                else if (subscriptionMode == SubscriptionMode.reject_all) {
+                    // Reject all subscription requests.
+                    Presence response = new Presence(Presence.Type.unsubscribed);
+                    response.setTo(presence.getFrom());
+                    connection.sendPacket(response);
+                }
+                // Otherwise, in manual mode so ignore.
+            }
+            else if (presence.getType() == Presence.Type.unsubscribe) {
+                if (subscriptionMode != SubscriptionMode.manual) {
+                    // Acknowledge and accept unsubscription notification so that the
+                    // server will stop sending notifications saying that the contact
+                    // has unsubscribed to our presence.
+                    Presence response = new Presence(Presence.Type.unsubscribed);
+                    response.setTo(presence.getFrom());
+                    connection.sendPacket(response);
+                }
+                // Otherwise, in manual mode so ignore.
+            }
+            // Error presence packets from a bare JID mean we invalidate all existing
+            // presence info for the user.
+            else if (presence.getType() == Presence.Type.error &&
+                    "".equals(StringUtils.parseResource(from)))
+            {
+                Map<String, Presence> userPresences;
+                if (!presenceMap.containsKey(key)) {
+                    userPresences = new ConcurrentHashMap<String, Presence>();
+                    presenceMap.put(key, userPresences);
+                }
+                else {
+                    userPresences = presenceMap.get(key);
+                    // Any other presence data is invalidated by the error packet.
+                    userPresences.clear();
+                }
+                // Set the new presence using the empty resource as a key.
+                userPresences.put("", presence);
+                // If the user is in the roster, fire an event.
+                RosterEntry entry = entries.get(key);
+                if (entry != null) {
+                    fireRosterPresenceEvent(presence);
+                }
+            }
+        }
+    }
+    
+    /**
+     * Listen for empty IQ results which indicate that the client has already a current
+     * roster version
+     * @author Till Klocke
+     *
+     */
+    
+    private class RosterResultListener implements PacketListener{
+
+		public void processPacket(Packet packet) {
+			if(packet instanceof IQ){
+				IQ result = (IQ)packet;
+				if(result.getType().equals(IQ.Type.RESULT) && result.getExtensions().isEmpty()){
+					Collection<String> addedEntries = new ArrayList<String>();
+		            Collection<String> updatedEntries = new ArrayList<String>();
+		            Collection<String> deletedEntries = new ArrayList<String>();
+		            if(persistentStorage!=null){
+		            	for(RosterPacket.Item item : persistentStorage.getEntries()){
+		            		insertRosterItem(item,addedEntries,updatedEntries,deletedEntries);
+		            	}
+                            }
+		            synchronized (Roster.this) {
+		                rosterInitialized = true;
+		                Roster.this.notifyAll();
+		            }
+		            fireRosterChangedEvent(addedEntries,updatedEntries,deletedEntries);
+			    }
+			}
+			connection.removePacketListener(this);
+		}
+    }
+
+    /**
+     * Listens for all roster packets and processes them.
+     */
+    private class RosterPacketListener implements PacketListener {
+
+        public void processPacket(Packet packet) {
+            // Keep a registry of the entries that were added, deleted or updated. An event
+            // will be fired for each affected entry
+            Collection<String> addedEntries = new ArrayList<String>();
+            Collection<String> updatedEntries = new ArrayList<String>();
+            Collection<String> deletedEntries = new ArrayList<String>();
+           
+            String version=null;
+            RosterPacket rosterPacket = (RosterPacket) packet;
+            List<RosterPacket.Item> rosterItems = new ArrayList<RosterPacket.Item>();
+            for(RosterPacket.Item item : rosterPacket.getRosterItems()){
+            	rosterItems.add(item);
+            }
+            //Here we check if the server send a versioned roster, if not we do not use
+            //the roster storage to store entries and work like in the old times 
+            if(rosterPacket.getVersion()==null){
+            	persistentStorage=null;
+            } else{
+            	version = rosterPacket.getVersion();
+            }
+            
+            if(persistentStorage!=null && !rosterInitialized){
+            	for(RosterPacket.Item item : persistentStorage.getEntries()){
+            		rosterItems.add(item);
+            	}
+            }
+            
+            for (RosterPacket.Item item : rosterItems) {
+            	insertRosterItem(item,addedEntries,updatedEntries,deletedEntries);
+            }
+            if(persistentStorage!=null){
+            	for (RosterPacket.Item i : rosterPacket.getRosterItems()){
+            		if(i.getItemType().equals(RosterPacket.ItemType.remove)){
+            			persistentStorage.removeEntry(i.getUser());
+            		}
+            		else{
+            			persistentStorage.addEntry(i, version);
+            		}
+            	}
+            }
+            // Mark the roster as initialized.
+            synchronized (Roster.this) {
+                rosterInitialized = true;
+                Roster.this.notifyAll();
+            }
+
+            // Fire event for roster listeners.
+            fireRosterChangedEvent(addedEntries, updatedEntries, deletedEntries);
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/RosterEntry.java b/src/org/jivesoftware/smack/RosterEntry.java
new file mode 100644
index 0000000..55b394e
--- /dev/null
+++ b/src/org/jivesoftware/smack/RosterEntry.java
@@ -0,0 +1,244 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.RosterPacket;
+
+import java.util.*;
+
+/**
+ * Each user in your roster is represented by a roster entry, which contains the user's
+ * JID and a name or nickname you assign.
+ *
+ * @author Matt Tucker
+ */
+public class RosterEntry {
+
+    private String user;
+    private String name;
+    private RosterPacket.ItemType type;
+    private RosterPacket.ItemStatus status;
+    final private Roster roster;
+    final private Connection connection;
+
+    /**
+     * Creates a new roster entry.
+     *
+     * @param user the user.
+     * @param name the nickname for the entry.
+     * @param type the subscription type.
+     * @param status the subscription status (related to subscriptions pending to be approbed).
+     * @param connection a connection to the XMPP server.
+     */
+    RosterEntry(String user, String name, RosterPacket.ItemType type,
+                RosterPacket.ItemStatus status, Roster roster, Connection connection) {
+        this.user = user;
+        this.name = name;
+        this.type = type;
+        this.status = status;
+        this.roster = roster;
+        this.connection = connection;
+    }
+
+    /**
+     * Returns the JID of the user associated with this entry.
+     *
+     * @return the user associated with this entry.
+     */
+    public String getUser() {
+        return user;
+    }
+
+    /**
+     * Returns the name associated with this entry.
+     *
+     * @return the name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the name associated with this entry.
+     *
+     * @param name the name.
+     */
+    public void setName(String name) {
+        // Do nothing if the name hasn't changed.
+        if (name != null && name.equals(this.name)) {
+            return;
+        }
+        this.name = name;
+        RosterPacket packet = new RosterPacket();
+        packet.setType(IQ.Type.SET);
+        packet.addRosterItem(toRosterItem(this));
+        connection.sendPacket(packet);
+    }
+
+    /**
+     * Updates the state of the entry with the new values.
+     *
+     * @param name the nickname for the entry.
+     * @param type the subscription type.
+     * @param status the subscription status (related to subscriptions pending to be approbed).
+     */
+    void updateState(String name, RosterPacket.ItemType type, RosterPacket.ItemStatus status) {
+        this.name = name;
+        this.type = type;
+        this.status = status;
+    }
+
+    /**
+     * Returns an unmodifiable collection of the roster groups that this entry belongs to.
+     *
+     * @return an iterator for the groups this entry belongs to.
+     */
+    public Collection<RosterGroup> getGroups() {
+        List<RosterGroup> results = new ArrayList<RosterGroup>();
+        // Loop through all roster groups and find the ones that contain this
+        // entry. This algorithm should be fine
+        for (RosterGroup group: roster.getGroups()) {
+            if (group.contains(this)) {
+                results.add(group);
+            }
+        }
+        return Collections.unmodifiableCollection(results);
+    }
+
+    /**
+     * Returns the roster subscription type of the entry. When the type is
+     * RosterPacket.ItemType.none or RosterPacket.ItemType.from,
+     * refer to {@link RosterEntry getStatus()} to see if a subscription request
+     * is pending.
+     *
+     * @return the type.
+     */
+    public RosterPacket.ItemType getType() {
+        return type;
+    }
+
+    /**
+     * Returns the roster subscription status of the entry. When the status is
+     * RosterPacket.ItemStatus.SUBSCRIPTION_PENDING, the contact has to answer the
+     * subscription request.
+     *
+     * @return the status.
+     */
+    public RosterPacket.ItemStatus getStatus() {
+        return status;
+    }
+
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        if (name != null) {
+            buf.append(name).append(": ");
+        }
+        buf.append(user);
+        Collection<RosterGroup> groups = getGroups();
+        if (!groups.isEmpty()) {
+            buf.append(" [");
+            Iterator<RosterGroup> iter = groups.iterator();
+            RosterGroup group = iter.next();
+            buf.append(group.getName());
+            while (iter.hasNext()) {
+            buf.append(", ");
+                group = iter.next();
+                buf.append(group.getName());
+            }
+            buf.append("]");
+        }
+        return buf.toString();
+    }
+
+    public boolean equals(Object object) {
+        if (this == object) {
+            return true;
+        }
+        if (object != null && object instanceof RosterEntry) {
+            return user.equals(((RosterEntry)object).getUser());
+        }
+        else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return this.user.hashCode();
+    }
+
+    /**
+     * Indicates whether some other object is "equal to" this by comparing all members.
+     * <p>
+     * The {@link #equals(Object)} method returns <code>true</code> if the user JIDs are equal.
+     * 
+     * @param obj the reference object with which to compare.
+     * @return <code>true</code> if this object is the same as the obj argument; <code>false</code>
+     *         otherwise.
+     */
+    public boolean equalsDeep(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        RosterEntry other = (RosterEntry) obj;
+        if (name == null) {
+            if (other.name != null)
+                return false;
+        }
+        else if (!name.equals(other.name))
+            return false;
+        if (status == null) {
+            if (other.status != null)
+                return false;
+        }
+        else if (!status.equals(other.status))
+            return false;
+        if (type == null) {
+            if (other.type != null)
+                return false;
+        }
+        else if (!type.equals(other.type))
+            return false;
+        if (user == null) {
+            if (other.user != null)
+                return false;
+        }
+        else if (!user.equals(other.user))
+            return false;
+        return true;
+    }
+    
+    static RosterPacket.Item toRosterItem(RosterEntry entry) {
+        RosterPacket.Item item = new RosterPacket.Item(entry.getUser(), entry.getName());
+        item.setItemType(entry.getType());
+        item.setItemStatus(entry.getStatus());
+        // Set the correct group names for the item.
+        for (RosterGroup group : entry.getGroups()) {
+            item.addGroupName(group.getName());
+        }
+        return item;
+    }
+
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/RosterGroup.java b/src/org/jivesoftware/smack/RosterGroup.java
new file mode 100644
index 0000000..e768f6d
--- /dev/null
+++ b/src/org/jivesoftware/smack/RosterGroup.java
@@ -0,0 +1,253 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.RosterPacket;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * A group of roster entries.
+ *
+ * @see Roster#getGroup(String)
+ * @author Matt Tucker
+ */
+public class RosterGroup {
+
+    private String name;
+    private Connection connection;
+    private final List<RosterEntry> entries;
+
+    /**
+     * Creates a new roster group instance.
+     *
+     * @param name the name of the group.
+     * @param connection the connection the group belongs to.
+     */
+    RosterGroup(String name, Connection connection) {
+        this.name = name;
+        this.connection = connection;
+        entries = new ArrayList<RosterEntry>();
+    }
+
+    /**
+     * Returns the name of the group.
+     *
+     * @return the name of the group.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the name of the group. Changing the group's name is like moving all the group entries
+     * of the group to a new group specified by the new name. Since this group won't have entries 
+     * it will be removed from the roster. This means that all the references to this object will 
+     * be invalid and will need to be updated to the new group specified by the new name.
+     *
+     * @param name the name of the group.
+     */
+    public void setName(String name) {
+        synchronized (entries) {
+            for (RosterEntry entry : entries) {
+                RosterPacket packet = new RosterPacket();
+                packet.setType(IQ.Type.SET);
+                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
+                item.removeGroupName(this.name);
+                item.addGroupName(name);
+                packet.addRosterItem(item);
+                connection.sendPacket(packet);
+            }
+        }
+    }
+
+    /**
+     * Returns the number of entries in the group.
+     *
+     * @return the number of entries in the group.
+     */
+    public int getEntryCount() {
+        synchronized (entries) {
+            return entries.size();
+        }
+    }
+
+    /**
+     * Returns an unmodifiable collection of all entries in the group.
+     *
+     * @return all entries in the group.
+     */
+    public Collection<RosterEntry> getEntries() {
+        synchronized (entries) {
+            return Collections.unmodifiableList(new ArrayList<RosterEntry>(entries));
+        }
+    }
+
+    /**
+     * Returns the roster entry associated with the given XMPP address or
+     * <tt>null</tt> if the user is not an entry in the group.
+     *
+     * @param user the XMPP address of the user (eg "jsmith@example.com").
+     * @return the roster entry or <tt>null</tt> if it does not exist in the group.
+     */
+    public RosterEntry getEntry(String user) {
+        if (user == null) {
+            return null;
+        }
+        // Roster entries never include a resource so remove the resource
+        // if it's a part of the XMPP address.
+        user = StringUtils.parseBareAddress(user);
+        String userLowerCase = user.toLowerCase();
+        synchronized (entries) {
+            for (RosterEntry entry : entries) {
+                if (entry.getUser().equals(userLowerCase)) {
+                    return entry;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns true if the specified entry is part of this group.
+     *
+     * @param entry a roster entry.
+     * @return true if the entry is part of this group.
+     */
+    public boolean contains(RosterEntry entry) {
+        synchronized (entries) {
+            return entries.contains(entry);
+        }
+    }
+
+    /**
+     * Returns true if the specified XMPP address is an entry in this group.
+     *
+     * @param user the XMPP address of the user.
+     * @return true if the XMPP address is an entry in this group.
+     */
+    public boolean contains(String user) {
+        return getEntry(user) != null;
+    }
+
+    /**
+     * Adds a roster entry to this group. If the entry was unfiled then it will be removed from 
+     * the unfiled list and will be added to this group.
+     * Note that this is an asynchronous call -- Smack must wait for the server
+     * to receive the updated roster.
+     *
+     * @param entry a roster entry.
+     * @throws XMPPException if an error occured while trying to add the entry to the group.
+     */
+    public void addEntry(RosterEntry entry) throws XMPPException {
+        PacketCollector collector = null;
+        // Only add the entry if it isn't already in the list.
+        synchronized (entries) {
+            if (!entries.contains(entry)) {
+                RosterPacket packet = new RosterPacket();
+                packet.setType(IQ.Type.SET);
+                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
+                item.addGroupName(getName());
+                packet.addRosterItem(item);
+                // Wait up to a certain number of seconds for a reply from the server.
+                collector = connection
+                        .createPacketCollector(new PacketIDFilter(packet.getPacketID()));
+                connection.sendPacket(packet);
+            }
+        }
+        if (collector != null) {
+            IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+            collector.cancel();
+            if (response == null) {
+                throw new XMPPException("No response from the server.");
+            }
+            // If the server replied with an error, throw an exception.
+            else if (response.getType() == IQ.Type.ERROR) {
+                throw new XMPPException(response.getError());
+            }
+        }
+    }
+
+    /**
+     * Removes a roster entry from this group. If the entry does not belong to any other group 
+     * then it will be considered as unfiled, therefore it will be added to the list of unfiled 
+     * entries.
+     * Note that this is an asynchronous call -- Smack must wait for the server
+     * to receive the updated roster.
+     *
+     * @param entry a roster entry.
+     * @throws XMPPException if an error occured while trying to remove the entry from the group. 
+     */
+    public void removeEntry(RosterEntry entry) throws XMPPException {
+        PacketCollector collector = null;
+        // Only remove the entry if it's in the entry list.
+        // Remove the entry locally, if we wait for RosterPacketListenerprocess>>Packet(Packet)
+        // to take place the entry will exist in the group until a packet is received from the 
+        // server.
+        synchronized (entries) {
+            if (entries.contains(entry)) {
+                RosterPacket packet = new RosterPacket();
+                packet.setType(IQ.Type.SET);
+                RosterPacket.Item item = RosterEntry.toRosterItem(entry);
+                item.removeGroupName(this.getName());
+                packet.addRosterItem(item);
+                // Wait up to a certain number of seconds for a reply from the server.
+                collector = connection
+                        .createPacketCollector(new PacketIDFilter(packet.getPacketID()));
+                connection.sendPacket(packet);
+            }
+        }
+        if (collector != null) {
+            IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+            collector.cancel();
+            if (response == null) {
+                throw new XMPPException("No response from the server.");
+            }
+            // If the server replied with an error, throw an exception.
+            else if (response.getType() == IQ.Type.ERROR) {
+                throw new XMPPException(response.getError());
+            }
+        }
+    }
+
+    public void addEntryLocal(RosterEntry entry) {
+        // Only add the entry if it isn't already in the list.
+        synchronized (entries) {
+            entries.remove(entry);
+            entries.add(entry);
+        }
+    }
+
+    void removeEntryLocal(RosterEntry entry) {
+         // Only remove the entry if it's in the entry list.
+        synchronized (entries) {
+            if (entries.contains(entry)) {
+                entries.remove(entry);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/RosterListener.java b/src/org/jivesoftware/smack/RosterListener.java
new file mode 100644
index 0000000..8be9ddc
--- /dev/null
+++ b/src/org/jivesoftware/smack/RosterListener.java
@@ -0,0 +1,83 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.packet.Presence;
+
+import java.util.Collection;
+
+/**
+ * A listener that is fired any time a roster is changed or the presence of
+ * a user in the roster is changed.
+ * 
+ * @see Roster#addRosterListener(RosterListener)
+ * @author Matt Tucker
+ */
+public interface RosterListener {
+
+    /**
+     * Called when roster entries are added.
+     *
+     * @param addresses the XMPP addresses of the contacts that have been added to the roster.
+     */
+    public void entriesAdded(Collection<String> addresses);
+
+    /**
+     * Called when a roster entries are updated.
+     *
+     * @param addresses the XMPP addresses of the contacts whose entries have been updated.
+     */
+    public void entriesUpdated(Collection<String> addresses);
+
+    /**
+     * Called when a roster entries are removed.
+     *
+     * @param addresses the XMPP addresses of the contacts that have been removed from the roster.
+     */
+    public void entriesDeleted(Collection<String> addresses);
+
+    /**
+     * Called when the presence of a roster entry is changed. Care should be taken
+     * when using the presence data delivered as part of this event. Specifically,
+     * when a user account is online with multiple resources, the UI should account
+     * for that. For example, say a user is online with their desktop computer and
+     * mobile phone. If the user logs out of the IM client on their mobile phone, the
+     * user should not be shown in the roster (contact list) as offline since they're
+     * still available as another resource.<p>
+     *
+     * To get the current "best presence" for a user after the presence update, query the roster:
+     * <pre>
+     *    String user = presence.getFrom();
+     *    Presence bestPresence = roster.getPresence(user);
+     * </pre>
+     *
+     * That will return the presence value for the user with the highest priority and
+     * availability.
+     *
+     * Note that this listener is triggered for presence (mode) changes only
+     * (e.g presence of types available and unavailable. Subscription-related
+     * presence packets will not cause this method to be called.
+     *
+     * @param presence the presence that changed.
+     * @see Roster#getPresence(String)
+     */
+    public void presenceChanged(Presence presence);
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/RosterStorage.java b/src/org/jivesoftware/smack/RosterStorage.java
new file mode 100644
index 0000000..8c5f386
--- /dev/null
+++ b/src/org/jivesoftware/smack/RosterStorage.java
@@ -0,0 +1,54 @@
+package org.jivesoftware.smack;
+
+import java.util.List;
+
+import org.jivesoftware.smack.packet.RosterPacket;
+
+/**
+ * This is an interface for persistent roster storage needed to implement XEP-0237
+ * @author Till Klocke
+ *
+ */
+
+public interface RosterStorage {
+	
+	/**
+	 * This method returns a List object with all RosterEntries contained in this store.
+	 * @return List object with all entries in local roster storage
+	 */
+	public List<RosterPacket.Item> getEntries();
+	/**
+	 * This method returns the RosterEntry which belongs to a specific user.
+	 * @param bareJid The bare JID of the RosterEntry
+	 * @return The RosterEntry which belongs to that user
+	 */
+	public RosterPacket.Item getEntry(String bareJid);
+	/**
+	 * Returns the number of entries in this roster store
+	 * @return the number of entries
+	 */
+	public int getEntryCount();
+	/**
+	 * This methos returns the version number as specified by the "ver" attribute
+	 * of the local store. Should return an emtpy string if store is empty.
+	 * @return local roster version
+	 */
+	public String getRosterVersion();
+	/**
+	 * This method stores a new RosterEntry in this store or overrides an existing one.
+	 * If ver is null an IllegalArgumentException should be thrown.
+	 * @param entry the entry to save
+	 * @param ver the version this roster push contained
+	 */
+	public void addEntry(RosterPacket.Item item, String ver);
+	/**
+	 * Removes an entry from the persistent storage
+	 * @param bareJid The bare JID of the entry to be removed
+	 */
+	public void removeEntry(String bareJid);
+	/**
+	 * Update an entry which has been modified locally
+	 * @param entry the entry to be updated
+	 */
+	public void updateLocalEntry(RosterPacket.Item item);
+}
diff --git a/src/org/jivesoftware/smack/SASLAuthentication.java b/src/org/jivesoftware/smack/SASLAuthentication.java
new file mode 100644
index 0000000..d7a7449
--- /dev/null
+++ b/src/org/jivesoftware/smack/SASLAuthentication.java
@@ -0,0 +1,586 @@
+/**

+ * $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;

+

+import org.jivesoftware.smack.filter.PacketIDFilter;

+import org.jivesoftware.smack.packet.Bind;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.Session;

+import org.jivesoftware.smack.sasl.*;

+

+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;

+import java.io.IOException;

+import java.lang.reflect.Constructor;

+import java.util.*;

+

+/**

+ * <p>This class is responsible authenticating the user using SASL, binding the resource

+ * to the connection and establishing a session with the server.</p>

+ *

+ * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to

+ * register with the server, authenticate using Non-SASL or authenticate using SASL. If the

+ * server supports SASL then Smack will first try to authenticate using SASL. But if that

+ * fails then Non-SASL will be tried.</p>

+ *

+ * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box

+ * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use

+ * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered

+ * mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default,

+ * the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}. </p>

+ *

+ * <p>Once the user has been authenticated with SASL, it is necessary to bind a resource for

+ * the connection. If no resource is passed in {@link #authenticate(String, String, String)}

+ * then the server will assign a resource for the connection. In case a resource is passed

+ * then the server will receive the desired resource but may assign a modified resource for

+ * the connection.</p>

+ *

+ * <p>Once a resource has been binded and if the server supports sessions then Smack will establish

+ * a session so that instant messaging and presence functionalities may be used.</p>

+ *

+ * @see org.jivesoftware.smack.sasl.SASLMechanism

+ *

+ * @author Gaston Dombiak

+ * @author Jay Kline

+ */

+public class SASLAuthentication implements UserAuthentication {

+

+    private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>();

+    private static List<String> mechanismsPreferences = new ArrayList<String>();

+

+    private Connection connection;

+    private Collection<String> serverMechanisms = new ArrayList<String>();

+    private SASLMechanism currentMechanism = null;

+    /**

+     * Boolean indicating if SASL negotiation has finished and was successful.

+     */

+    private boolean saslNegotiated;

+    /**

+     * Boolean indication if SASL authentication has failed. When failed the server may end

+     * the connection.

+     */

+    private boolean saslFailed;

+    private boolean resourceBinded;

+    private boolean sessionSupported;

+    /**

+     * The SASL related error condition if there was one provided by the server.

+     */

+    private String errorCondition;

+

+    static {

+

+        // Register SASL mechanisms supported by Smack

+        registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class);

+        registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class);

+        registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class);

+        registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class);

+        registerSASLMechanism("PLAIN", SASLPlainMechanism.class);

+        registerSASLMechanism("ANONYMOUS", SASLAnonymous.class);

+

+//        supportSASLMechanism("GSSAPI",0);

+        supportSASLMechanism("DIGEST-MD5",0);

+//        supportSASLMechanism("CRAM-MD5",2);

+        supportSASLMechanism("PLAIN",1);

+        supportSASLMechanism("ANONYMOUS",2);

+

+    }

+

+    /**

+     * Registers a new SASL mechanism

+     *

+     * @param name   common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.

+     * @param mClass a SASLMechanism subclass.

+     */

+    public static void registerSASLMechanism(String name, Class<? extends SASLMechanism> mClass) {

+        implementedMechanisms.put(name, mClass);

+    }

+

+    /**

+     * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't

+     * be possible to authenticate users using the removed SASL mechanism. It also removes the

+     * mechanism from the supported list.

+     *

+     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.

+     */

+    public static void unregisterSASLMechanism(String name) {

+        implementedMechanisms.remove(name);

+        mechanismsPreferences.remove(name);

+    }

+

+

+    /**

+     * Registers a new SASL mechanism in the specified preference position. The client will try

+     * to authenticate using the most prefered SASL mechanism that is also supported by the server.

+     * The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)}

+     *

+     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.

+     */

+    public static void supportSASLMechanism(String name) {

+        mechanismsPreferences.add(0, name);

+    }

+

+    /**

+     * Registers a new SASL mechanism in the specified preference position. The client will try

+     * to authenticate using the most prefered SASL mechanism that is also supported by the server.

+     * Use the <tt>index</tt> parameter to set the level of preference of the new SASL mechanism.

+     * A value of 0 means that the mechanism is the most prefered one. The SASL mechanism must be

+     * registered via {@link #registerSASLMechanism(String, Class)}

+     *

+     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.

+     * @param index preference position amongst all the implemented SASL mechanism. Starts with 0.

+     */

+    public static void supportSASLMechanism(String name, int index) {

+        mechanismsPreferences.add(index, name);

+    }

+

+    /**

+     * Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't

+     * be possible to authenticate users using the removed SASL mechanism. Note that the mechanism

+     * is still registered, but will just not be used.

+     *

+     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.

+     */

+    public static void unsupportSASLMechanism(String name) {

+        mechanismsPreferences.remove(name);

+    }

+

+    /**

+     * Returns the registerd SASLMechanism classes sorted by the level of preference.

+     *

+     * @return the registerd SASLMechanism classes sorted by the level of preference.

+     */

+    public static List<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() {

+        List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>();

+        for (String mechanismsPreference : mechanismsPreferences) {

+            answer.add(implementedMechanisms.get(mechanismsPreference));

+        }

+        return answer;

+    }

+

+    SASLAuthentication(Connection connection) {

+        super();

+        this.connection = connection;

+        this.init();

+    }

+

+    /**

+     * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users.

+     *

+     * @return true if the server offered ANONYMOUS SASL as a way to authenticate users.

+     */

+    public boolean hasAnonymousAuthentication() {

+        return serverMechanisms.contains("ANONYMOUS");

+    }

+

+    /**

+     * Returns true if the server offered SASL authentication besides ANONYMOUS SASL.

+     *

+     * @return true if the server offered SASL authentication besides ANONYMOUS SASL.

+     */

+    public boolean hasNonAnonymousAuthentication() {

+        return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication());

+    }

+

+    /**

+     * Performs SASL authentication of the specified user. If SASL authentication was successful

+     * then resource binding and session establishment will be performed. This method will return

+     * the full JID provided by the server while binding a resource to the connection.<p>

+     *

+     * The server may assign a full JID with a username or resource different than the requested

+     * by this method.

+     *

+     * @param username the username that is authenticating with the server.

+     * @param resource the desired resource.

+     * @param cbh the CallbackHandler used to get information from the user

+     * @return the full JID provided by the server while binding a resource to the connection.

+     * @throws XMPPException if an error occures while authenticating.

+     */

+    public String authenticate(String username, String resource, CallbackHandler cbh) 

+            throws XMPPException {

+        // Locate the SASLMechanism to use

+        String selectedMechanism = null;

+        for (String mechanism : mechanismsPreferences) {

+            if (implementedMechanisms.containsKey(mechanism) &&

+                    serverMechanisms.contains(mechanism)) {

+                selectedMechanism = mechanism;

+                break;

+            }

+        }

+        if (selectedMechanism != null) {

+            // A SASL mechanism was found. Authenticate using the selected mechanism and then

+            // proceed to bind a resource

+            try {

+                Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);

+                Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);

+                currentMechanism = constructor.newInstance(this);

+                // Trigger SASL authentication with the selected mechanism. We use

+                // connection.getHost() since GSAPI requires the FQDN of the server, which

+                // may not match the XMPP domain.

+                currentMechanism.authenticate(username, connection.getHost(), cbh);

+

+                // Wait until SASL negotiation finishes

+                synchronized (this) {

+                    if (!saslNegotiated && !saslFailed) {

+                        try {

+                            wait(30000);

+                        }

+                        catch (InterruptedException e) {

+                            // Ignore

+                        }

+                    }

+                }

+

+                if (saslFailed) {

+                    // SASL authentication failed and the server may have closed the connection

+                    // so throw an exception

+                    if (errorCondition != null) {

+                        throw new XMPPException("SASL authentication " +

+                                selectedMechanism + " failed: " + errorCondition);

+                    }

+                    else {

+                        throw new XMPPException("SASL authentication failed using mechanism " +

+                                selectedMechanism);

+                    }

+                }

+

+                if (saslNegotiated) {

+                    // Bind a resource for this connection and

+                    return bindResourceAndEstablishSession(resource);

+                } else {

+                    // SASL authentication failed

+                }

+            }

+            catch (XMPPException e) {

+                throw e;

+            }

+            catch (Exception e) {

+                e.printStackTrace();

+            }

+        }

+        else {

+            throw new XMPPException("SASL Authentication failed. No known authentication mechanisims.");

+        }

+        throw new XMPPException("SASL authentication failed");

+    }

+

+    /**

+     * Performs SASL authentication of the specified user. If SASL authentication was successful

+     * then resource binding and session establishment will be performed. This method will return

+     * the full JID provided by the server while binding a resource to the connection.<p>

+     *

+     * The server may assign a full JID with a username or resource different than the requested

+     * by this method.

+     *

+     * @param username the username that is authenticating with the server.

+     * @param password the password to send to the server.

+     * @param resource the desired resource.

+     * @return the full JID provided by the server while binding a resource to the connection.

+     * @throws XMPPException if an error occures while authenticating.

+     */

+    public String authenticate(String username, String password, String resource)

+            throws XMPPException {

+        // Locate the SASLMechanism to use

+        String selectedMechanism = null;

+        for (String mechanism : mechanismsPreferences) {

+            if (implementedMechanisms.containsKey(mechanism) &&

+                    serverMechanisms.contains(mechanism)) {

+                selectedMechanism = mechanism;

+                break;

+            }

+        }

+        if (selectedMechanism != null) {

+            // A SASL mechanism was found. Authenticate using the selected mechanism and then

+            // proceed to bind a resource

+            try {

+                Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);

+                Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);

+                currentMechanism = constructor.newInstance(this);

+                // Trigger SASL authentication with the selected mechanism. We use

+                // connection.getHost() since GSAPI requires the FQDN of the server, which

+                // may not match the XMPP domain.

+                currentMechanism.authenticate(username, connection.getServiceName(), password);

+

+                // Wait until SASL negotiation finishes

+                synchronized (this) {

+                    if (!saslNegotiated && !saslFailed) {

+                        try {

+                            wait(30000);

+                        }

+                        catch (InterruptedException e) {

+                            // Ignore

+                        }

+                    }

+                }

+

+                if (saslFailed) {

+                    // SASL authentication failed and the server may have closed the connection

+                    // so throw an exception

+                    if (errorCondition != null) {

+                        throw new XMPPException("SASL authentication " +

+                                selectedMechanism + " failed: " + errorCondition);

+                    }

+                    else {

+                        throw new XMPPException("SASL authentication failed using mechanism " +

+                                selectedMechanism);

+                    }

+                }

+

+                if (saslNegotiated) {

+                    // Bind a resource for this connection and

+                    return bindResourceAndEstablishSession(resource);

+                }

+                else {

+                    // SASL authentication failed so try a Non-SASL authentication

+                    return new NonSASLAuthentication(connection)

+                            .authenticate(username, password, resource);

+                }

+            }

+            catch (XMPPException e) {

+                throw e;

+            }

+            catch (Exception e) {

+                e.printStackTrace();

+                // SASL authentication failed so try a Non-SASL authentication

+                return new NonSASLAuthentication(connection)

+                        .authenticate(username, password, resource);

+            }

+        }

+        else {

+            // No SASL method was found so try a Non-SASL authentication

+            return new NonSASLAuthentication(connection).authenticate(username, password, resource);

+        }

+    }

+

+    /**

+     * Performs ANONYMOUS SASL authentication. If SASL authentication was successful

+     * then resource binding and session establishment will be performed. This method will return

+     * the full JID provided by the server while binding a resource to the connection.<p>

+     *

+     * The server will assign a full JID with a randomly generated resource and possibly with

+     * no username.

+     *

+     * @return the full JID provided by the server while binding a resource to the connection.

+     * @throws XMPPException if an error occures while authenticating.

+     */

+    public String authenticateAnonymously() throws XMPPException {

+        try {

+            currentMechanism = new SASLAnonymous(this);

+            currentMechanism.authenticate(null,null,"");

+

+            // Wait until SASL negotiation finishes

+            synchronized (this) {

+                if (!saslNegotiated && !saslFailed) {

+                    try {

+                        wait(5000);

+                    }

+                    catch (InterruptedException e) {

+                        // Ignore

+                    }

+                }

+            }

+

+            if (saslFailed) {

+                // SASL authentication failed and the server may have closed the connection

+                // so throw an exception

+                if (errorCondition != null) {

+                    throw new XMPPException("SASL authentication failed: " + errorCondition);

+                }

+                else {

+                    throw new XMPPException("SASL authentication failed");

+                }

+            }

+

+            if (saslNegotiated) {

+                // Bind a resource for this connection and

+                return bindResourceAndEstablishSession(null);

+            }

+            else {

+                return new NonSASLAuthentication(connection).authenticateAnonymously();

+            }

+        } catch (IOException e) {

+            return new NonSASLAuthentication(connection).authenticateAnonymously();

+        }

+    }

+

+    private String bindResourceAndEstablishSession(String resource) throws XMPPException {

+        // Wait until server sends response containing the <bind> element

+        synchronized (this) {

+            if (!resourceBinded) {

+                try {

+                    wait(30000);

+                }

+                catch (InterruptedException e) {

+                    // Ignore

+                }

+            }

+        }

+

+        if (!resourceBinded) {

+            // Server never offered resource binding

+            throw new XMPPException("Resource binding not offered by server");

+        }

+

+        Bind bindResource = new Bind();

+        bindResource.setResource(resource);

+

+        PacketCollector collector = connection

+                .createPacketCollector(new PacketIDFilter(bindResource.getPacketID()));

+        // Send the packet

+        connection.sendPacket(bindResource);

+        // Wait up to a certain number of seconds for a response from the server.

+        Bind response = (Bind) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from the server.");

+        }

+        // If the server replied with an error, throw an exception.

+        else if (response.getType() == IQ.Type.ERROR) {

+            throw new XMPPException(response.getError());

+        }

+        String userJID = response.getJid();

+

+        if (sessionSupported) {

+            Session session = new Session();

+            collector = connection.createPacketCollector(new PacketIDFilter(session.getPacketID()));

+            // Send the packet

+            connection.sendPacket(session);

+            // Wait up to a certain number of seconds for a response from the server.

+            IQ ack = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+            collector.cancel();

+            if (ack == null) {

+                throw new XMPPException("No response from the server.");

+            }

+            // If the server replied with an error, throw an exception.

+            else if (ack.getType() == IQ.Type.ERROR) {

+                throw new XMPPException(ack.getError());

+            }

+        }

+        return userJID;

+    }

+

+    /**

+     * Sets the available SASL mechanism reported by the server. The server will report the

+     * available SASL mechanism once the TLS negotiation was successful. This information is

+     * stored and will be used when doing the authentication for logging in the user.

+     *

+     * @param mechanisms collection of strings with the available SASL mechanism reported

+     *                   by the server.

+     */

+    void setAvailableSASLMethods(Collection<String> mechanisms) {

+        this.serverMechanisms = mechanisms;

+    }

+

+    /**

+     * Returns true if the user was able to authenticate with the server usins SASL.

+     *

+     * @return true if the user was able to authenticate with the server usins SASL.

+     */

+    public boolean isAuthenticated() {

+        return saslNegotiated;

+    }

+

+    /**

+     * The server is challenging the SASL authentication we just sent. Forward the challenge

+     * to the current SASLMechanism we are using. The SASLMechanism will send a response to

+     * the server. The length of the challenge-response sequence varies according to the

+     * SASLMechanism in use.

+     *

+     * @param challenge a base64 encoded string representing the challenge.

+     * @throws IOException If a network error occures while authenticating.

+     */

+    void challengeReceived(String challenge) throws IOException {

+        currentMechanism.challengeReceived(challenge);

+    }

+

+    /**

+     * Notification message saying that SASL authentication was successful. The next step

+     * would be to bind the resource.

+     */

+    void authenticated() {

+        synchronized (this) {

+            saslNegotiated = true;

+            // Wake up the thread that is waiting in the #authenticate method

+            notify();

+        }

+    }

+

+    /**

+     * Notification message saying that SASL authentication has failed. The server may have

+     * closed the connection depending on the number of possible retries.

+     * 

+     * @deprecated replaced by {@see #authenticationFailed(String)}.

+     */

+    void authenticationFailed() {

+        authenticationFailed(null);

+    }

+

+    /**

+     * Notification message saying that SASL authentication has failed. The server may have

+     * closed the connection depending on the number of possible retries.

+     * 

+     * @param condition the error condition provided by the server.

+     */

+    void authenticationFailed(String condition) {

+        synchronized (this) {

+            saslFailed = true;

+            errorCondition = condition;

+            // Wake up the thread that is waiting in the #authenticate method

+            notify();

+        }

+    }

+

+    /**

+     * Notification message saying that the server requires the client to bind a

+     * resource to the stream.

+     */

+    void bindingRequired() {

+        synchronized (this) {

+            resourceBinded = true;

+            // Wake up the thread that is waiting in the #authenticate method

+            notify();

+        }

+    }

+

+    public void send(Packet stanza) {

+        connection.sendPacket(stanza);

+    }

+

+    /**

+     * Notification message saying that the server supports sessions. When a server supports

+     * sessions the client needs to send a Session packet after successfully binding a resource

+     * for the session.

+     */

+    void sessionsSupported() {

+        sessionSupported = true;

+    }

+    

+    /**

+     * Initializes the internal state in order to be able to be reused. The authentication

+     * is used by the connection at the first login and then reused after the connection

+     * is disconnected and then reconnected.

+     */

+    protected void init() {

+        saslNegotiated = false;

+        saslFailed = false;

+        resourceBinded = false;

+        sessionSupported = false;

+    }

+}

diff --git a/src/org/jivesoftware/smack/SASLAuthentication.java.orig b/src/org/jivesoftware/smack/SASLAuthentication.java.orig
new file mode 100644
index 0000000..66ff693
--- /dev/null
+++ b/src/org/jivesoftware/smack/SASLAuthentication.java.orig
@@ -0,0 +1,586 @@
+/**

+ * $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;

+

+import org.jivesoftware.smack.filter.PacketIDFilter;

+import org.jivesoftware.smack.packet.Bind;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.Session;

+import org.jivesoftware.smack.sasl.*;

+

+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;

+import java.io.IOException;

+import java.lang.reflect.Constructor;

+import java.util.*;

+

+/**

+ * <p>This class is responsible authenticating the user using SASL, binding the resource

+ * to the connection and establishing a session with the server.</p>

+ *

+ * <p>Once TLS has been negotiated (i.e. the connection has been secured) it is possible to

+ * register with the server, authenticate using Non-SASL or authenticate using SASL. If the

+ * server supports SASL then Smack will first try to authenticate using SASL. But if that

+ * fails then Non-SASL will be tried.</p>

+ *

+ * <p>The server may support many SASL mechanisms to use for authenticating. Out of the box

+ * Smack provides several SASL mechanisms, but it is possible to register new SASL Mechanisms. Use

+ * {@link #registerSASLMechanism(String, Class)} to register a new mechanisms. A registered

+ * mechanism wont be used until {@link #supportSASLMechanism(String, int)} is called. By default,

+ * the list of supported SASL mechanisms is determined from the {@link SmackConfiguration}. </p>

+ *

+ * <p>Once the user has been authenticated with SASL, it is necessary to bind a resource for

+ * the connection. If no resource is passed in {@link #authenticate(String, String, String)}

+ * then the server will assign a resource for the connection. In case a resource is passed

+ * then the server will receive the desired resource but may assign a modified resource for

+ * the connection.</p>

+ *

+ * <p>Once a resource has been binded and if the server supports sessions then Smack will establish

+ * a session so that instant messaging and presence functionalities may be used.</p>

+ *

+ * @see org.jivesoftware.smack.sasl.SASLMechanism

+ *

+ * @author Gaston Dombiak

+ * @author Jay Kline

+ */

+public class SASLAuthentication implements UserAuthentication {

+

+    private static Map<String, Class<? extends SASLMechanism>> implementedMechanisms = new HashMap<String, Class<? extends SASLMechanism>>();

+    private static List<String> mechanismsPreferences = new ArrayList<String>();

+

+    private Connection connection;

+    private Collection<String> serverMechanisms = new ArrayList<String>();

+    private SASLMechanism currentMechanism = null;

+    /**

+     * Boolean indicating if SASL negotiation has finished and was successful.

+     */

+    private boolean saslNegotiated;

+    /**

+     * Boolean indication if SASL authentication has failed. When failed the server may end

+     * the connection.

+     */

+    private boolean saslFailed;

+    private boolean resourceBinded;

+    private boolean sessionSupported;

+    /**

+     * The SASL related error condition if there was one provided by the server.

+     */

+    private String errorCondition;

+

+    static {

+

+        // Register SASL mechanisms supported by Smack

+        registerSASLMechanism("EXTERNAL", SASLExternalMechanism.class);

+        registerSASLMechanism("GSSAPI", SASLGSSAPIMechanism.class);

+        registerSASLMechanism("DIGEST-MD5", SASLDigestMD5Mechanism.class);

+        registerSASLMechanism("CRAM-MD5", SASLCramMD5Mechanism.class);

+        registerSASLMechanism("PLAIN", SASLPlainMechanism.class);

+        registerSASLMechanism("ANONYMOUS", SASLAnonymous.class);

+

+        supportSASLMechanism("GSSAPI",0);

+        supportSASLMechanism("DIGEST-MD5",1);

+        supportSASLMechanism("CRAM-MD5",2);

+        supportSASLMechanism("PLAIN",3);

+        supportSASLMechanism("ANONYMOUS",4);

+

+    }

+

+    /**

+     * Registers a new SASL mechanism

+     *

+     * @param name   common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.

+     * @param mClass a SASLMechanism subclass.

+     */

+    public static void registerSASLMechanism(String name, Class<? extends SASLMechanism> mClass) {

+        implementedMechanisms.put(name, mClass);

+    }

+

+    /**

+     * Unregisters an existing SASL mechanism. Once the mechanism has been unregistered it won't

+     * be possible to authenticate users using the removed SASL mechanism. It also removes the

+     * mechanism from the supported list.

+     *

+     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.

+     */

+    public static void unregisterSASLMechanism(String name) {

+        implementedMechanisms.remove(name);

+        mechanismsPreferences.remove(name);

+    }

+

+

+    /**

+     * Registers a new SASL mechanism in the specified preference position. The client will try

+     * to authenticate using the most prefered SASL mechanism that is also supported by the server.

+     * The SASL mechanism must be registered via {@link #registerSASLMechanism(String, Class)}

+     *

+     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.

+     */

+    public static void supportSASLMechanism(String name) {

+        mechanismsPreferences.add(0, name);

+    }

+

+    /**

+     * Registers a new SASL mechanism in the specified preference position. The client will try

+     * to authenticate using the most prefered SASL mechanism that is also supported by the server.

+     * Use the <tt>index</tt> parameter to set the level of preference of the new SASL mechanism.

+     * A value of 0 means that the mechanism is the most prefered one. The SASL mechanism must be

+     * registered via {@link #registerSASLMechanism(String, Class)}

+     *

+     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.

+     * @param index preference position amongst all the implemented SASL mechanism. Starts with 0.

+     */

+    public static void supportSASLMechanism(String name, int index) {

+        mechanismsPreferences.add(index, name);

+    }

+

+    /**

+     * Un-supports an existing SASL mechanism. Once the mechanism has been unregistered it won't

+     * be possible to authenticate users using the removed SASL mechanism. Note that the mechanism

+     * is still registered, but will just not be used.

+     *

+     * @param name common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or KERBEROS_V4.

+     */

+    public static void unsupportSASLMechanism(String name) {

+        mechanismsPreferences.remove(name);

+    }

+

+    /**

+     * Returns the registerd SASLMechanism classes sorted by the level of preference.

+     *

+     * @return the registerd SASLMechanism classes sorted by the level of preference.

+     */

+    public static List<Class<? extends SASLMechanism>> getRegisterSASLMechanisms() {

+        List<Class<? extends SASLMechanism>> answer = new ArrayList<Class<? extends SASLMechanism>>();

+        for (String mechanismsPreference : mechanismsPreferences) {

+            answer.add(implementedMechanisms.get(mechanismsPreference));

+        }

+        return answer;

+    }

+

+    SASLAuthentication(Connection connection) {

+        super();

+        this.connection = connection;

+        this.init();

+    }

+

+    /**

+     * Returns true if the server offered ANONYMOUS SASL as a way to authenticate users.

+     *

+     * @return true if the server offered ANONYMOUS SASL as a way to authenticate users.

+     */

+    public boolean hasAnonymousAuthentication() {

+        return serverMechanisms.contains("ANONYMOUS");

+    }

+

+    /**

+     * Returns true if the server offered SASL authentication besides ANONYMOUS SASL.

+     *

+     * @return true if the server offered SASL authentication besides ANONYMOUS SASL.

+     */

+    public boolean hasNonAnonymousAuthentication() {

+        return !serverMechanisms.isEmpty() && (serverMechanisms.size() != 1 || !hasAnonymousAuthentication());

+    }

+

+    /**

+     * Performs SASL authentication of the specified user. If SASL authentication was successful

+     * then resource binding and session establishment will be performed. This method will return

+     * the full JID provided by the server while binding a resource to the connection.<p>

+     *

+     * The server may assign a full JID with a username or resource different than the requested

+     * by this method.

+     *

+     * @param username the username that is authenticating with the server.

+     * @param resource the desired resource.

+     * @param cbh the CallbackHandler used to get information from the user

+     * @return the full JID provided by the server while binding a resource to the connection.

+     * @throws XMPPException if an error occures while authenticating.

+     */

+    public String authenticate(String username, String resource, CallbackHandler cbh) 

+            throws XMPPException {

+        // Locate the SASLMechanism to use

+        String selectedMechanism = null;

+        for (String mechanism : mechanismsPreferences) {

+            if (implementedMechanisms.containsKey(mechanism) &&

+                    serverMechanisms.contains(mechanism)) {

+                selectedMechanism = mechanism;

+                break;

+            }

+        }

+        if (selectedMechanism != null) {

+            // A SASL mechanism was found. Authenticate using the selected mechanism and then

+            // proceed to bind a resource

+            try {

+                Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);

+                Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);

+                currentMechanism = constructor.newInstance(this);

+                // Trigger SASL authentication with the selected mechanism. We use

+                // connection.getHost() since GSAPI requires the FQDN of the server, which

+                // may not match the XMPP domain.

+                currentMechanism.authenticate(username, connection.getHost(), cbh);

+

+                // Wait until SASL negotiation finishes

+                synchronized (this) {

+                    if (!saslNegotiated && !saslFailed) {

+                        try {

+                            wait(30000);

+                        }

+                        catch (InterruptedException e) {

+                            // Ignore

+                        }

+                    }

+                }

+

+                if (saslFailed) {

+                    // SASL authentication failed and the server may have closed the connection

+                    // so throw an exception

+                    if (errorCondition != null) {

+                        throw new XMPPException("SASL authentication " +

+                                selectedMechanism + " failed: " + errorCondition);

+                    }

+                    else {

+                        throw new XMPPException("SASL authentication failed using mechanism " +

+                                selectedMechanism);

+                    }

+                }

+

+                if (saslNegotiated) {

+                    // Bind a resource for this connection and

+                    return bindResourceAndEstablishSession(resource);

+                } else {

+                    // SASL authentication failed

+                }

+            }

+            catch (XMPPException e) {

+                throw e;

+            }

+            catch (Exception e) {

+                e.printStackTrace();

+            }

+        }

+        else {

+            throw new XMPPException("SASL Authentication failed. No known authentication mechanisims.");

+        }

+        throw new XMPPException("SASL authentication failed");

+    }

+

+    /**

+     * Performs SASL authentication of the specified user. If SASL authentication was successful

+     * then resource binding and session establishment will be performed. This method will return

+     * the full JID provided by the server while binding a resource to the connection.<p>

+     *

+     * The server may assign a full JID with a username or resource different than the requested

+     * by this method.

+     *

+     * @param username the username that is authenticating with the server.

+     * @param password the password to send to the server.

+     * @param resource the desired resource.

+     * @return the full JID provided by the server while binding a resource to the connection.

+     * @throws XMPPException if an error occures while authenticating.

+     */

+    public String authenticate(String username, String password, String resource)

+            throws XMPPException {

+        // Locate the SASLMechanism to use

+        String selectedMechanism = null;

+        for (String mechanism : mechanismsPreferences) {

+            if (implementedMechanisms.containsKey(mechanism) &&

+                    serverMechanisms.contains(mechanism)) {

+                selectedMechanism = mechanism;

+                break;

+            }

+        }

+        if (selectedMechanism != null) {

+            // A SASL mechanism was found. Authenticate using the selected mechanism and then

+            // proceed to bind a resource

+            try {

+                Class<? extends SASLMechanism> mechanismClass = implementedMechanisms.get(selectedMechanism);

+                Constructor<? extends SASLMechanism> constructor = mechanismClass.getConstructor(SASLAuthentication.class);

+                currentMechanism = constructor.newInstance(this);

+                // Trigger SASL authentication with the selected mechanism. We use

+                // connection.getHost() since GSAPI requires the FQDN of the server, which

+                // may not match the XMPP domain.

+                currentMechanism.authenticate(username, connection.getServiceName(), password);

+

+                // Wait until SASL negotiation finishes

+                synchronized (this) {

+                    if (!saslNegotiated && !saslFailed) {

+                        try {

+                            wait(30000);

+                        }

+                        catch (InterruptedException e) {

+                            // Ignore

+                        }

+                    }

+                }

+

+                if (saslFailed) {

+                    // SASL authentication failed and the server may have closed the connection

+                    // so throw an exception

+                    if (errorCondition != null) {

+                        throw new XMPPException("SASL authentication " +

+                                selectedMechanism + " failed: " + errorCondition);

+                    }

+                    else {

+                        throw new XMPPException("SASL authentication failed using mechanism " +

+                                selectedMechanism);

+                    }

+                }

+

+                if (saslNegotiated) {

+                    // Bind a resource for this connection and

+                    return bindResourceAndEstablishSession(resource);

+                }

+                else {

+                    // SASL authentication failed so try a Non-SASL authentication

+                    return new NonSASLAuthentication(connection)

+                            .authenticate(username, password, resource);

+                }

+            }

+            catch (XMPPException e) {

+                throw e;

+            }

+            catch (Exception e) {

+                e.printStackTrace();

+                // SASL authentication failed so try a Non-SASL authentication

+                return new NonSASLAuthentication(connection)

+                        .authenticate(username, password, resource);

+            }

+        }

+        else {

+            // No SASL method was found so try a Non-SASL authentication

+            return new NonSASLAuthentication(connection).authenticate(username, password, resource);

+        }

+    }

+

+    /**

+     * Performs ANONYMOUS SASL authentication. If SASL authentication was successful

+     * then resource binding and session establishment will be performed. This method will return

+     * the full JID provided by the server while binding a resource to the connection.<p>

+     *

+     * The server will assign a full JID with a randomly generated resource and possibly with

+     * no username.

+     *

+     * @return the full JID provided by the server while binding a resource to the connection.

+     * @throws XMPPException if an error occures while authenticating.

+     */

+    public String authenticateAnonymously() throws XMPPException {

+        try {

+            currentMechanism = new SASLAnonymous(this);

+            currentMechanism.authenticate(null,null,"");

+

+            // Wait until SASL negotiation finishes

+            synchronized (this) {

+                if (!saslNegotiated && !saslFailed) {

+                    try {

+                        wait(5000);

+                    }

+                    catch (InterruptedException e) {

+                        // Ignore

+                    }

+                }

+            }

+

+            if (saslFailed) {

+                // SASL authentication failed and the server may have closed the connection

+                // so throw an exception

+                if (errorCondition != null) {

+                    throw new XMPPException("SASL authentication failed: " + errorCondition);

+                }

+                else {

+                    throw new XMPPException("SASL authentication failed");

+                }

+            }

+

+            if (saslNegotiated) {

+                // Bind a resource for this connection and

+                return bindResourceAndEstablishSession(null);

+            }

+            else {

+                return new NonSASLAuthentication(connection).authenticateAnonymously();

+            }

+        } catch (IOException e) {

+            return new NonSASLAuthentication(connection).authenticateAnonymously();

+        }

+    }

+

+    private String bindResourceAndEstablishSession(String resource) throws XMPPException {

+        // Wait until server sends response containing the <bind> element

+        synchronized (this) {

+            if (!resourceBinded) {

+                try {

+                    wait(30000);

+                }

+                catch (InterruptedException e) {

+                    // Ignore

+                }

+            }

+        }

+

+        if (!resourceBinded) {

+            // Server never offered resource binding

+            throw new XMPPException("Resource binding not offered by server");

+        }

+

+        Bind bindResource = new Bind();

+        bindResource.setResource(resource);

+

+        PacketCollector collector = connection

+                .createPacketCollector(new PacketIDFilter(bindResource.getPacketID()));

+        // Send the packet

+        connection.sendPacket(bindResource);

+        // Wait up to a certain number of seconds for a response from the server.

+        Bind response = (Bind) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from the server.");

+        }

+        // If the server replied with an error, throw an exception.

+        else if (response.getType() == IQ.Type.ERROR) {

+            throw new XMPPException(response.getError());

+        }

+        String userJID = response.getJid();

+

+        if (sessionSupported) {

+            Session session = new Session();

+            collector = connection.createPacketCollector(new PacketIDFilter(session.getPacketID()));

+            // Send the packet

+            connection.sendPacket(session);

+            // Wait up to a certain number of seconds for a response from the server.

+            IQ ack = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+            collector.cancel();

+            if (ack == null) {

+                throw new XMPPException("No response from the server.");

+            }

+            // If the server replied with an error, throw an exception.

+            else if (ack.getType() == IQ.Type.ERROR) {

+                throw new XMPPException(ack.getError());

+            }

+        }

+        return userJID;

+    }

+

+    /**

+     * Sets the available SASL mechanism reported by the server. The server will report the

+     * available SASL mechanism once the TLS negotiation was successful. This information is

+     * stored and will be used when doing the authentication for logging in the user.

+     *

+     * @param mechanisms collection of strings with the available SASL mechanism reported

+     *                   by the server.

+     */

+    void setAvailableSASLMethods(Collection<String> mechanisms) {

+        this.serverMechanisms = mechanisms;

+    }

+

+    /**

+     * Returns true if the user was able to authenticate with the server usins SASL.

+     *

+     * @return true if the user was able to authenticate with the server usins SASL.

+     */

+    public boolean isAuthenticated() {

+        return saslNegotiated;

+    }

+

+    /**

+     * The server is challenging the SASL authentication we just sent. Forward the challenge

+     * to the current SASLMechanism we are using. The SASLMechanism will send a response to

+     * the server. The length of the challenge-response sequence varies according to the

+     * SASLMechanism in use.

+     *

+     * @param challenge a base64 encoded string representing the challenge.

+     * @throws IOException If a network error occures while authenticating.

+     */

+    void challengeReceived(String challenge) throws IOException {

+        currentMechanism.challengeReceived(challenge);

+    }

+

+    /**

+     * Notification message saying that SASL authentication was successful. The next step

+     * would be to bind the resource.

+     */

+    void authenticated() {

+        synchronized (this) {

+            saslNegotiated = true;

+            // Wake up the thread that is waiting in the #authenticate method

+            notify();

+        }

+    }

+

+    /**

+     * Notification message saying that SASL authentication has failed. The server may have

+     * closed the connection depending on the number of possible retries.

+     * 

+     * @deprecated replaced by {@see #authenticationFailed(String)}.

+     */

+    void authenticationFailed() {

+        authenticationFailed(null);

+    }

+

+    /**

+     * Notification message saying that SASL authentication has failed. The server may have

+     * closed the connection depending on the number of possible retries.

+     * 

+     * @param condition the error condition provided by the server.

+     */

+    void authenticationFailed(String condition) {

+        synchronized (this) {

+            saslFailed = true;

+            errorCondition = condition;

+            // Wake up the thread that is waiting in the #authenticate method

+            notify();

+        }

+    }

+

+    /**

+     * Notification message saying that the server requires the client to bind a

+     * resource to the stream.

+     */

+    void bindingRequired() {

+        synchronized (this) {

+            resourceBinded = true;

+            // Wake up the thread that is waiting in the #authenticate method

+            notify();

+        }

+    }

+

+    public void send(Packet stanza) {

+        connection.sendPacket(stanza);

+    }

+

+    /**

+     * Notification message saying that the server supports sessions. When a server supports

+     * sessions the client needs to send a Session packet after successfully binding a resource

+     * for the session.

+     */

+    void sessionsSupported() {

+        sessionSupported = true;

+    }

+    

+    /**

+     * Initializes the internal state in order to be able to be reused. The authentication

+     * is used by the connection at the first login and then reused after the connection

+     * is disconnected and then reconnected.

+     */

+    protected void init() {

+        saslNegotiated = false;

+        saslFailed = false;

+        resourceBinded = false;

+        sessionSupported = false;

+    }

+}

diff --git a/src/org/jivesoftware/smack/ServerTrustManager.java b/src/org/jivesoftware/smack/ServerTrustManager.java
new file mode 100644
index 0000000..63da3e7
--- /dev/null
+++ b/src/org/jivesoftware/smack/ServerTrustManager.java
@@ -0,0 +1,331 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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;
+
+import javax.net.ssl.X509TrustManager;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.security.*;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateParsingException;
+import java.security.cert.X509Certificate;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Trust manager that checks all certificates presented by the server. This class
+ * is used during TLS negotiation. It is possible to disable/enable some or all checkings
+ * by configuring the {@link ConnectionConfiguration}. The truststore file that contains
+ * knows and trusted CA root certificates can also be configure in {@link ConnectionConfiguration}.
+ *
+ * @author Gaston Dombiak
+ */
+class ServerTrustManager implements X509TrustManager {
+
+    private static Pattern cnPattern = Pattern.compile("(?i)(cn=)([^,]*)");
+
+    private ConnectionConfiguration configuration;
+
+    /**
+     * Holds the domain of the remote server we are trying to connect
+     */
+    private String server;
+    private KeyStore trustStore;
+    
+    private static Map<KeyStoreOptions, KeyStore> stores = new HashMap<KeyStoreOptions, KeyStore>();
+
+    public ServerTrustManager(String server, ConnectionConfiguration configuration) {
+        this.configuration = configuration;
+        this.server = server;
+
+        InputStream in = null;
+        synchronized (stores) {
+            KeyStoreOptions options = new KeyStoreOptions(configuration.getTruststoreType(),
+                    configuration.getTruststorePath(), configuration.getTruststorePassword());
+            if (stores.containsKey(options)) {
+                trustStore = stores.get(options);
+            } else {
+                try {
+                    trustStore = KeyStore.getInstance(options.getType());
+                    in = new FileInputStream(options.getPath());
+                    trustStore.load(in, options.getPassword().toCharArray());
+                } catch (Exception e) {
+                    trustStore = null;
+                    e.printStackTrace();
+                } finally {
+                    if (in != null) {
+                        try {
+                            in.close();
+                        } catch (IOException ioe) {
+                            // Ignore.
+                        }
+                    }
+                }
+                stores.put(options, trustStore);
+            }
+            if (trustStore == null)
+                // Disable root CA checking
+                configuration.setVerifyRootCAEnabled(false);
+        }
+    }
+
+    public X509Certificate[] getAcceptedIssuers() {
+        return new X509Certificate[0];
+    }
+
+    public void checkClientTrusted(X509Certificate[] arg0, String arg1)
+            throws CertificateException {
+    }
+
+    public void checkServerTrusted(X509Certificate[] x509Certificates, String arg1)
+            throws CertificateException {
+
+        int nSize = x509Certificates.length;
+
+        List<String> peerIdentities = getPeerIdentity(x509Certificates[0]);
+
+        if (configuration.isVerifyChainEnabled()) {
+            // Working down the chain, for every certificate in the chain,
+            // verify that the subject of the certificate is the issuer of the
+            // next certificate in the chain.
+            Principal principalLast = null;
+            for (int i = nSize -1; i >= 0 ; i--) {
+                X509Certificate x509certificate = x509Certificates[i];
+                Principal principalIssuer = x509certificate.getIssuerDN();
+                Principal principalSubject = x509certificate.getSubjectDN();
+                if (principalLast != null) {
+                    if (principalIssuer.equals(principalLast)) {
+                        try {
+                            PublicKey publickey =
+                                    x509Certificates[i + 1].getPublicKey();
+                            x509Certificates[i].verify(publickey);
+                        }
+                        catch (GeneralSecurityException generalsecurityexception) {
+                            throw new CertificateException(
+                                    "signature verification failed of " + peerIdentities);
+                        }
+                    }
+                    else {
+                        throw new CertificateException(
+                                "subject/issuer verification failed of " + peerIdentities);
+                    }
+                }
+                principalLast = principalSubject;
+            }
+        }
+
+        if (configuration.isVerifyRootCAEnabled()) {
+            // Verify that the the last certificate in the chain was issued
+            // by a third-party that the client trusts.
+            boolean trusted = false;
+            try {
+                trusted = trustStore.getCertificateAlias(x509Certificates[nSize - 1]) != null;
+                if (!trusted && nSize == 1 && configuration.isSelfSignedCertificateEnabled())
+                {
+                    System.out.println("Accepting self-signed certificate of remote server: " +
+                            peerIdentities);
+                    trusted = true;
+                }
+            }
+            catch (KeyStoreException e) {
+                e.printStackTrace();
+            }
+            if (!trusted) {
+                throw new CertificateException("root certificate not trusted of " + peerIdentities);
+            }
+        }
+
+        if (configuration.isNotMatchingDomainCheckEnabled()) {
+            // Verify that the first certificate in the chain corresponds to
+            // the server we desire to authenticate.
+            // Check if the certificate uses a wildcard indicating that subdomains are valid
+            if (peerIdentities.size() == 1 && peerIdentities.get(0).startsWith("*.")) {
+                // Remove the wildcard
+                String peerIdentity = peerIdentities.get(0).replace("*.", "");
+                // Check if the requested subdomain matches the certified domain
+                if (!server.endsWith(peerIdentity)) {
+                    throw new CertificateException("target verification failed of " + peerIdentities);
+                }
+            }
+            else if (!peerIdentities.contains(server)) {
+                throw new CertificateException("target verification failed of " + peerIdentities);
+            }
+        }
+
+        if (configuration.isExpiredCertificatesCheckEnabled()) {
+            // For every certificate in the chain, verify that the certificate
+            // is valid at the current time.
+            Date date = new Date();
+            for (int i = 0; i < nSize; i++) {
+                try {
+                    x509Certificates[i].checkValidity(date);
+                }
+                catch (GeneralSecurityException generalsecurityexception) {
+                    throw new CertificateException("invalid date of " + server);
+                }
+            }
+        }
+
+    }
+
+    /**
+     * Returns the identity of the remote server as defined in the specified certificate. The
+     * identity is defined in the subjectDN of the certificate and it can also be defined in
+     * the subjectAltName extension of type "xmpp". When the extension is being used then the
+     * identity defined in the extension in going to be returned. Otherwise, the value stored in
+     * the subjectDN is returned.
+     *
+     * @param x509Certificate the certificate the holds the identity of the remote server.
+     * @return the identity of the remote server as defined in the specified certificate.
+     */
+    public static List<String> getPeerIdentity(X509Certificate x509Certificate) {
+        // Look the identity in the subjectAltName extension if available
+        List<String> names = getSubjectAlternativeNames(x509Certificate);
+        if (names.isEmpty()) {
+            String name = x509Certificate.getSubjectDN().getName();
+            Matcher matcher = cnPattern.matcher(name);
+            if (matcher.find()) {
+                name = matcher.group(2);
+            }
+            // Create an array with the unique identity
+            names = new ArrayList<String>();
+            names.add(name);
+        }
+        return names;
+    }
+
+    /**
+     * Returns the JID representation of an XMPP entity contained as a SubjectAltName extension
+     * in the certificate. If none was found then return <tt>null</tt>.
+     *
+     * @param certificate the certificate presented by the remote entity.
+     * @return the JID representation of an XMPP entity contained as a SubjectAltName extension
+     *         in the certificate. If none was found then return <tt>null</tt>.
+     */
+    private static List<String> getSubjectAlternativeNames(X509Certificate certificate) {
+        List<String> identities = new ArrayList<String>();
+        try {
+            Collection<List<?>> altNames = certificate.getSubjectAlternativeNames();
+            // Check that the certificate includes the SubjectAltName extension
+            if (altNames == null) {
+                return Collections.emptyList();
+            }
+            // Use the type OtherName to search for the certified server name
+            /*for (List item : altNames) {
+                Integer type = (Integer) item.get(0);
+                if (type == 0) {
+                    // Type OtherName found so return the associated value
+                    try {
+                        // Value is encoded using ASN.1 so decode it to get the server's identity
+                        ASN1InputStream decoder = new ASN1InputStream((byte[]) item.toArray()[1]);
+                        DEREncodable encoded = decoder.readObject();
+                        encoded = ((DERSequence) encoded).getObjectAt(1);
+                        encoded = ((DERTaggedObject) encoded).getObject();
+                        encoded = ((DERTaggedObject) encoded).getObject();
+                        String identity = ((DERUTF8String) encoded).getString();
+                        // Add the decoded server name to the list of identities
+                        identities.add(identity);
+                    }
+                    catch (UnsupportedEncodingException e) {
+                        // Ignore
+                    }
+                    catch (IOException e) {
+                        // Ignore
+                    }
+                    catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                }
+                // Other types are not good for XMPP so ignore them
+                System.out.println("SubjectAltName of invalid type found: " + certificate);
+            }*/
+        }
+        catch (CertificateParsingException e) {
+            e.printStackTrace();
+        }
+        return identities;
+    }
+
+    private static class KeyStoreOptions {
+        private final String type;
+        private final String path;
+        private final String password;
+
+        public KeyStoreOptions(String type, String path, String password) {
+            super();
+            this.type = type;
+            this.path = path;
+            this.password = password;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public String getPath() {
+            return path;
+        }
+
+        public String getPassword() {
+            return password;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((password == null) ? 0 : password.hashCode());
+            result = prime * result + ((path == null) ? 0 : path.hashCode());
+            result = prime * result + ((type == null) ? 0 : type.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj)
+                return true;
+            if (obj == null)
+                return false;
+            if (getClass() != obj.getClass())
+                return false;
+            KeyStoreOptions other = (KeyStoreOptions) obj;
+            if (password == null) {
+                if (other.password != null)
+                    return false;
+            } else if (!password.equals(other.password))
+                return false;
+            if (path == null) {
+                if (other.path != null)
+                    return false;
+            } else if (!path.equals(other.path))
+                return false;
+            if (type == null) {
+                if (other.type != null)
+                    return false;
+            } else if (!type.equals(other.type))
+                return false;
+            return true;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/SmackAndroid.java b/src/org/jivesoftware/smack/SmackAndroid.java
new file mode 100644
index 0000000..a18d675
--- /dev/null
+++ b/src/org/jivesoftware/smack/SmackAndroid.java
@@ -0,0 +1,59 @@
+package org.jivesoftware.smack;
+
+import org.jivesoftware.smack.util.DNSUtil;
+import org.jivesoftware.smack.util.dns.DNSJavaResolver;
+import org.jivesoftware.smackx.ConfigureProviderManager;
+import org.jivesoftware.smackx.InitStaticCode;
+import org.xbill.DNS.ResolverConfig;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+
+public class SmackAndroid {
+    private static SmackAndroid sSmackAndroid = null;
+
+    private BroadcastReceiver mConnectivityChangedReceiver;
+    private Context mCtx;
+
+    private SmackAndroid(Context ctx) {
+        mCtx = ctx;
+        DNSUtil.setDNSResolver(DNSJavaResolver.getInstance());
+        InitStaticCode.initStaticCode(ctx);
+        ConfigureProviderManager.configureProviderManager();
+        maybeRegisterReceiver();
+    }
+
+    public static SmackAndroid init(Context ctx) {
+        if (sSmackAndroid == null) {
+            sSmackAndroid = new SmackAndroid(ctx);
+        } else {
+            sSmackAndroid.maybeRegisterReceiver();
+        }
+        return sSmackAndroid;
+    }
+
+    public void onDestroy() {
+        if (mConnectivityChangedReceiver != null) {
+            mCtx.unregisterReceiver(mConnectivityChangedReceiver);
+            mConnectivityChangedReceiver = null;
+        }
+    }
+
+    private void maybeRegisterReceiver() {
+        if (mConnectivityChangedReceiver == null) {
+            mConnectivityChangedReceiver = new ConnectivtyChangedReceiver();
+            mCtx.registerReceiver(mConnectivityChangedReceiver, new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"));
+        }
+    }
+
+    class ConnectivtyChangedReceiver extends BroadcastReceiver {
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            ResolverConfig.refresh();
+        }
+
+    }
+}
diff --git a/src/org/jivesoftware/smack/SmackConfiguration.java b/src/org/jivesoftware/smack/SmackConfiguration.java
new file mode 100644
index 0000000..2696d87
--- /dev/null
+++ b/src/org/jivesoftware/smack/SmackConfiguration.java
@@ -0,0 +1,371 @@
+/**
+ * $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;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Vector;
+
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Represents the configuration of Smack. The configuration is used for:
+ * <ul>
+ *      <li> Initializing classes by loading them at start-up.
+ *      <li> Getting the current Smack version.
+ *      <li> Getting and setting global library behavior, such as the period of time
+ *          to wait for replies to packets from the server. Note: setting these values
+ *          via the API will override settings in the configuration file.
+ * </ul>
+ *
+ * Configuration settings are stored in META-INF/smack-config.xml (typically inside the
+ * smack.jar file).
+ * 
+ * @author Gaston Dombiak
+ */
+public final class SmackConfiguration {
+
+    private static final String SMACK_VERSION = "3.2.2";
+
+    private static int packetReplyTimeout = 5000;
+    private static Vector<String> defaultMechs = new Vector<String>();
+
+    private static boolean localSocks5ProxyEnabled = true;
+    private static int localSocks5ProxyPort = 7777;
+    private static int packetCollectorSize = 5000;
+
+    /**
+     * defaultPingInterval (in seconds)
+     */
+    private static int defaultPingInterval = 1800; // 30 min (30*60)
+
+    /**
+     * This automatically enables EntityCaps for new connections if it is set to true
+     */
+    private static boolean autoEnableEntityCaps = false;
+
+    private SmackConfiguration() {
+    }
+
+    /**
+     * Loads the configuration from the smack-config.xml file.<p>
+     * 
+     * So far this means that:
+     * 1) a set of classes will be loaded in order to execute their static init block
+     * 2) retrieve and set the current Smack release
+     */
+    static {
+        try {
+            // Get an array of class loaders to try loading the providers files from.
+            ClassLoader[] classLoaders = getClassLoaders();
+            for (ClassLoader classLoader : classLoaders) {
+                Enumeration<URL> configEnum = classLoader.getResources("META-INF/smack-config.xml");
+                while (configEnum.hasMoreElements()) {
+                    URL url = configEnum.nextElement();
+                    InputStream systemStream = null;
+                    try {
+                        systemStream = url.openStream();
+                        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+                        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+                        parser.setInput(systemStream, "UTF-8");
+                        int eventType = parser.getEventType();
+                        do {
+                            if (eventType == XmlPullParser.START_TAG) {
+                                if (parser.getName().equals("className")) {
+                                    // Attempt to load the class so that the class can get initialized
+                                    parseClassToLoad(parser);
+                                }
+                                else if (parser.getName().equals("packetReplyTimeout")) {
+                                    packetReplyTimeout = parseIntProperty(parser, packetReplyTimeout);
+                                }
+                                else if (parser.getName().equals("mechName")) {
+                                    defaultMechs.add(parser.nextText());
+                                } 
+                                else if (parser.getName().equals("localSocks5ProxyEnabled")) {
+                                    localSocks5ProxyEnabled = Boolean.parseBoolean(parser.nextText());
+                                } 
+                                else if (parser.getName().equals("localSocks5ProxyPort")) {
+                                    localSocks5ProxyPort = parseIntProperty(parser, localSocks5ProxyPort);
+                                }
+                                else if (parser.getName().equals("packetCollectorSize")) {
+                                    packetCollectorSize = parseIntProperty(parser, packetCollectorSize);
+                                }
+                                else if (parser.getName().equals("defaultPingInterval")) {
+                                    defaultPingInterval = parseIntProperty(parser, defaultPingInterval);
+                                }
+                                else if (parser.getName().equals("autoEnableEntityCaps")) {
+                                    autoEnableEntityCaps = Boolean.parseBoolean(parser.nextText());
+                                }
+                            }
+                            eventType = parser.next();
+                        }
+                        while (eventType != XmlPullParser.END_DOCUMENT);
+                    }
+                    catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                    finally {
+                        try {
+                            systemStream.close();
+                        }
+                        catch (Exception e) {
+                            // Ignore.
+                        }
+                    }
+                }
+            }
+        }
+        catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Returns the Smack version information, eg "1.3.0".
+     * 
+     * @return the Smack version information.
+     */
+    public static String getVersion() {
+        return SMACK_VERSION;
+    }
+
+    /**
+     * Returns the number of milliseconds to wait for a response from
+     * the server. The default value is 5000 ms.
+     * 
+     * @return the milliseconds to wait for a response from the server
+     */
+    public static int getPacketReplyTimeout() {
+        // The timeout value must be greater than 0 otherwise we will answer the default value
+        if (packetReplyTimeout <= 0) {
+            packetReplyTimeout = 5000;
+        }
+        return packetReplyTimeout;
+    }
+
+    /**
+     * Sets the number of milliseconds to wait for a response from
+     * the server.
+     * 
+     * @param timeout the milliseconds to wait for a response from the server
+     */
+    public static void setPacketReplyTimeout(int timeout) {
+        if (timeout <= 0) {
+            throw new IllegalArgumentException();
+        }
+        packetReplyTimeout = timeout;
+    }
+
+    /**
+     * Gets the default max size of a packet collector before it will delete 
+     * the older packets.
+     * 
+     * @return The number of packets to queue before deleting older packets.
+     */
+    public static int getPacketCollectorSize() {
+    	return packetCollectorSize;
+    }
+
+    /**
+     * Sets the default max size of a packet collector before it will delete 
+     * the older packets.
+     * 
+     * @param The number of packets to queue before deleting older packets.
+     */
+    public static void setPacketCollectorSize(int collectorSize) {
+    	packetCollectorSize = collectorSize;
+    }
+    
+    /**
+     * Add a SASL mechanism to the list to be used.
+     *
+     * @param mech the SASL mechanism to be added
+     */
+    public static void addSaslMech(String mech) {
+        if(! defaultMechs.contains(mech) ) {
+            defaultMechs.add(mech);
+        }
+    }
+
+   /**
+     * Add a Collection of SASL mechanisms to the list to be used.
+     *
+     * @param mechs the Collection of SASL mechanisms to be added
+     */
+    public static void addSaslMechs(Collection<String> mechs) {
+        for(String mech : mechs) {
+            addSaslMech(mech);
+        }
+    }
+
+    /**
+     * Remove a SASL mechanism from the list to be used.
+     *
+     * @param mech the SASL mechanism to be removed
+     */
+    public static void removeSaslMech(String mech) {
+        if( defaultMechs.contains(mech) ) {
+            defaultMechs.remove(mech);
+        }
+    }
+
+   /**
+     * Remove a Collection of SASL mechanisms to the list to be used.
+     *
+     * @param mechs the Collection of SASL mechanisms to be removed
+     */
+    public static void removeSaslMechs(Collection<String> mechs) {
+        for(String mech : mechs) {
+            removeSaslMech(mech);
+        }
+    }
+
+    /**
+     * Returns the list of SASL mechanisms to be used. If a SASL mechanism is
+     * listed here it does not guarantee it will be used. The server may not
+     * support it, or it may not be implemented.
+     *
+     * @return the list of SASL mechanisms to be used.
+     */
+    public static List<String> getSaslMechs() {
+        return defaultMechs;
+    }
+
+    /**
+     * Returns true if the local Socks5 proxy should be started. Default is true.
+     * 
+     * @return if the local Socks5 proxy should be started
+     */
+    public static boolean isLocalSocks5ProxyEnabled() {
+        return localSocks5ProxyEnabled;
+    }
+
+    /**
+     * Sets if the local Socks5 proxy should be started. Default is true.
+     * 
+     * @param localSocks5ProxyEnabled if the local Socks5 proxy should be started
+     */
+    public static void setLocalSocks5ProxyEnabled(boolean localSocks5ProxyEnabled) {
+        SmackConfiguration.localSocks5ProxyEnabled = localSocks5ProxyEnabled;
+    }
+
+    /**
+     * Return the port of the local Socks5 proxy. Default is 7777.
+     * 
+     * @return the port of the local Socks5 proxy
+     */
+    public static int getLocalSocks5ProxyPort() {
+        return localSocks5ProxyPort;
+    }
+
+    /**
+     * Sets the port of the local Socks5 proxy. Default is 7777. If you set the port to a negative
+     * value Smack tries the absolute value and all following until it finds an open port.
+     * 
+     * @param localSocks5ProxyPort the port of the local Socks5 proxy to set
+     */
+    public static void setLocalSocks5ProxyPort(int localSocks5ProxyPort) {
+        SmackConfiguration.localSocks5ProxyPort = localSocks5ProxyPort;
+    }
+
+    /**
+     * Returns the default ping interval (seconds)
+     * 
+     * @return
+     */
+    public static int getDefaultPingInterval() {
+        return defaultPingInterval;
+    }
+
+    /**
+     * Sets the default ping interval (seconds). Set it to '-1' to disable the periodic ping
+     *
+     * @param defaultPingInterval
+     */
+    public static void setDefaultPingInterval(int defaultPingInterval) {
+        SmackConfiguration.defaultPingInterval = defaultPingInterval;
+    }
+
+    /**
+     * Check if Entity Caps are enabled as default for every new connection
+     * @return
+     */
+    public static boolean autoEnableEntityCaps() {
+        return autoEnableEntityCaps;
+    }
+
+    /**
+     * Set if Entity Caps are enabled or disabled for every new connection
+     * 
+     * @param true if Entity Caps should be auto enabled, false if not
+     */
+    public static void setAutoEnableEntityCaps(boolean b) {
+        autoEnableEntityCaps = b;
+    }
+
+    private static void parseClassToLoad(XmlPullParser parser) throws Exception {
+        String className = parser.nextText();
+        // Attempt to load the class so that the class can get initialized
+        try {
+            Class.forName(className);
+        }
+        catch (ClassNotFoundException cnfe) {
+            System.err.println("Error! A startup class specified in smack-config.xml could " +
+                    "not be loaded: " + className);
+        }
+    }
+
+    private static int parseIntProperty(XmlPullParser parser, int defaultValue)
+            throws Exception
+    {
+        try {
+            return Integer.parseInt(parser.nextText());
+        }
+        catch (NumberFormatException nfe) {
+            nfe.printStackTrace();
+            return defaultValue;
+        }
+    }
+
+    /**
+     * Returns an array of class loaders to load resources from.
+     *
+     * @return an array of ClassLoader instances.
+     */
+    private static ClassLoader[] getClassLoaders() {
+        ClassLoader[] classLoaders = new ClassLoader[2];
+        classLoaders[0] = SmackConfiguration.class.getClassLoader();
+        classLoaders[1] = Thread.currentThread().getContextClassLoader();
+        // Clean up possible null values. Note that #getClassLoader may return a null value.
+        List<ClassLoader> loaders = new ArrayList<ClassLoader>();
+        for (ClassLoader classLoader : classLoaders) {
+            if (classLoader != null) {
+                loaders.add(classLoader);
+            }
+        }
+        return loaders.toArray(new ClassLoader[loaders.size()]);
+    }
+}
diff --git a/src/org/jivesoftware/smack/UserAuthentication.java b/src/org/jivesoftware/smack/UserAuthentication.java
new file mode 100644
index 0000000..38b30ca
--- /dev/null
+++ b/src/org/jivesoftware/smack/UserAuthentication.java
@@ -0,0 +1,79 @@
+/**

+ * $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;

+

+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;

+

+/**

+ * There are two ways to authenticate a user with a server. Using SASL or Non-SASL

+ * authentication. This interface makes {@link SASLAuthentication} and

+ * {@link NonSASLAuthentication} polyphormic.

+ *

+ * @author Gaston Dombiak

+ * @author Jay Kline

+ */

+interface UserAuthentication {

+

+    /**

+     * Authenticates the user with the server.  This method will return the full JID provided by

+     * the server.  The server may assign a full JID with a username and resource different than

+     * requested by this method.

+     *

+     * Note that using callbacks is the prefered method of authenticating users since it allows

+     * more flexability in the mechanisms used.

+     *

+     * @param username the requested username (authorization ID) for authenticating to the server

+     * @param resource the requested resource.

+     * @param cbh the CallbackHandler used to obtain authentication ID, password, or other

+     * information

+     * @return the full JID provided by the server while binding a resource for the connection.

+     * @throws XMPPException if an error occurs while authenticating.

+     */

+    String authenticate(String username, String resource, CallbackHandler cbh) throws

+            XMPPException;

+

+    /**

+     * Authenticates the user with the server. This method will return the full JID provided by

+     * the server. The server may assign a full JID with a username and resource different than

+     * the requested by this method.

+     *

+     * It is recommended that @{link #authenticate(String, String, CallbackHandler)} be used instead

+     * since it provides greater flexability in authenticaiton and authorization.

+     *

+     * @param username the username that is authenticating with the server.

+     * @param password the password to send to the server.

+     * @param resource the desired resource.

+     * @return the full JID provided by the server while binding a resource for the connection.

+     * @throws XMPPException if an error occures while authenticating.

+     */

+    String authenticate(String username, String password, String resource) throws

+            XMPPException;

+

+    /**

+     * Performs an anonymous authentication with the server. The server will created a new full JID

+     * for this connection. An exception will be thrown if the server does not support anonymous

+     * authentication.

+     *

+     * @return the full JID provided by the server while binding a resource for the connection.

+     * @throws XMPPException if an error occures while authenticating.

+     */

+    String authenticateAnonymously() throws XMPPException;

+}

diff --git a/src/org/jivesoftware/smack/XMPPConnection.java b/src/org/jivesoftware/smack/XMPPConnection.java
new file mode 100644
index 0000000..badf29c
--- /dev/null
+++ b/src/org/jivesoftware/smack/XMPPConnection.java
@@ -0,0 +1,1116 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.compression.XMPPInputOutputStream;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smack.util.dns.HostAddress;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.ByteArrayInputStream;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.lang.reflect.Constructor;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.KeyStore;
+import java.security.Provider;
+import java.security.Security;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Creates a socket connection to a XMPP server. This is the default connection
+ * to a Jabber server and is specified in the XMPP Core (RFC 3920).
+ * 
+ * @see Connection
+ * @author Matt Tucker
+ */
+public class XMPPConnection extends Connection {
+
+    /**
+     * The socket which is used for this connection.
+     */
+    Socket socket;
+
+    String connectionID = null;
+    private String user = null;
+    private boolean connected = false;
+    // socketClosed is used concurrent
+    // by XMPPConnection, PacketReader, PacketWriter
+    private volatile boolean socketClosed = false;
+
+    /**
+     * Flag that indicates if the user is currently authenticated with the server.
+     */
+    private boolean authenticated = false;
+    /**
+     * Flag that indicates if the user was authenticated with the server when the connection
+     * to the server was closed (abruptly or not).
+     */
+    private boolean wasAuthenticated = false;
+    private boolean anonymous = false;
+    private boolean usingTLS = false;
+
+    PacketWriter packetWriter;
+    PacketReader packetReader;
+
+    Roster roster = null;
+
+    /**
+     * Collection of available stream compression methods offered by the server.
+     */
+    private Collection<String> compressionMethods;
+
+    /**
+     * Set to true by packet writer if the server acknowledged the compression
+     */
+    private boolean serverAckdCompression = false;
+
+    /**
+     * Creates a new connection to the specified XMPP server. A DNS SRV lookup will be
+     * performed to determine the IP address and port corresponding to the
+     * service name; if that lookup fails, it's assumed that server resides at
+     * <tt>serviceName</tt> with the default port of 5222. Encrypted connections (TLS)
+     * will be used if available, stream compression is disabled, and standard SASL
+     * mechanisms will be used for authentication.<p>
+     * <p/>
+     * This is the simplest constructor for connecting to an XMPP server. Alternatively,
+     * you can get fine-grained control over connection settings using the
+     * {@link #XMPPConnection(ConnectionConfiguration)} constructor.<p>
+     * <p/>
+     * Note that XMPPConnection constructors do not establish a connection to the server
+     * and you must call {@link #connect()}.<p>
+     * <p/>
+     * The CallbackHandler will only be used if the connection requires the client provide
+     * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback
+     * to prompt for a password to unlock the keystore containing the SSL certificate.
+     *
+     * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>.
+     * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore.
+     */
+    public XMPPConnection(String serviceName, CallbackHandler callbackHandler) {
+        // Create the configuration for this new connection
+        super(new ConnectionConfiguration(serviceName));
+        config.setCompressionEnabled(false);
+        config.setSASLAuthenticationEnabled(true);
+        config.setDebuggerEnabled(DEBUG_ENABLED);
+        config.setCallbackHandler(callbackHandler);
+    }
+
+    /**
+     * Creates a new XMPP connection in the same way {@link #XMPPConnection(String,CallbackHandler)} does, but
+     * with no callback handler for password prompting of the keystore.  This will work
+     * in most cases, provided the client is not required to provide a certificate to 
+     * the server.
+     *
+     * @param serviceName the name of the XMPP server to connect to; e.g. <tt>example.com</tt>.
+     */
+    public XMPPConnection(String serviceName) {
+        // Create the configuration for this new connection
+        super(new ConnectionConfiguration(serviceName));
+        config.setCompressionEnabled(false);
+        config.setSASLAuthenticationEnabled(true);
+        config.setDebuggerEnabled(DEBUG_ENABLED);
+    }
+
+    /**
+     * Creates a new XMPP connection in the same way {@link #XMPPConnection(ConnectionConfiguration,CallbackHandler)} does, but
+     * with no callback handler for password prompting of the keystore.  This will work
+     * in most cases, provided the client is not required to provide a certificate to 
+     * the server.
+     *
+     *
+     * @param config the connection configuration.
+     */
+    public XMPPConnection(ConnectionConfiguration config) {
+        super(config);
+    }
+
+    /**
+     * Creates a new XMPP connection using the specified connection configuration.<p>
+     * <p/>
+     * Manually specifying connection configuration information is suitable for
+     * advanced users of the API. In many cases, using the
+     * {@link #XMPPConnection(String)} constructor is a better approach.<p>
+     * <p/>
+     * Note that XMPPConnection constructors do not establish a connection to the server
+     * and you must call {@link #connect()}.<p>
+     * <p/>
+     *
+     * The CallbackHandler will only be used if the connection requires the client provide
+     * an SSL certificate to the server. The CallbackHandler must handle the PasswordCallback
+     * to prompt for a password to unlock the keystore containing the SSL certificate.
+     *
+     * @param config the connection configuration.
+     * @param callbackHandler the CallbackHandler used to prompt for the password to the keystore.
+     */
+    public XMPPConnection(ConnectionConfiguration config, CallbackHandler callbackHandler) {
+        super(config);
+        config.setCallbackHandler(callbackHandler);
+    }
+
+    public String getConnectionID() {
+        if (!isConnected()) {
+            return null;
+        }
+        return connectionID;
+    }
+
+    public String getUser() {
+        if (!isAuthenticated()) {
+            return null;
+        }
+        return user;
+    }
+
+    @Override
+    public synchronized void login(String username, String password, String resource) throws XMPPException {
+        if (!isConnected()) {
+            throw new IllegalStateException("Not connected to server.");
+        }
+        if (authenticated) {
+            throw new IllegalStateException("Already logged in to server.");
+        }
+        // Do partial version of nameprep on the username.
+        username = username.toLowerCase().trim();
+
+        String response;
+        if (config.isSASLAuthenticationEnabled() &&
+                saslAuthentication.hasNonAnonymousAuthentication()) {
+            // Authenticate using SASL
+            if (password != null) {
+                response = saslAuthentication.authenticate(username, password, resource);
+            }
+            else {
+                response = saslAuthentication
+                        .authenticate(username, resource, config.getCallbackHandler());
+            }
+        }
+        else {
+            // Authenticate using Non-SASL
+            response = new NonSASLAuthentication(this).authenticate(username, password, resource);
+        }
+
+        // Set the user.
+        if (response != null) {
+            this.user = response;
+            // Update the serviceName with the one returned by the server
+            config.setServiceName(StringUtils.parseServer(response));
+        }
+        else {
+            this.user = username + "@" + getServiceName();
+            if (resource != null) {
+                this.user += "/" + resource;
+            }
+        }
+
+        // If compression is enabled then request the server to use stream compression
+        if (config.isCompressionEnabled()) {
+            useCompression();
+        }
+
+        // Indicate that we're now authenticated.
+        authenticated = true;
+        anonymous = false;
+
+        // Create the roster if it is not a reconnection or roster already created by getRoster()
+        if (this.roster == null) {
+        	if(rosterStorage==null){
+        		this.roster = new Roster(this);
+        	}
+        	else{
+        		this.roster = new Roster(this,rosterStorage);
+        	}
+        }
+        if (config.isRosterLoadedAtLogin()) {
+            this.roster.reload();
+        }
+
+        // Set presence to online.
+        if (config.isSendPresence()) {
+            packetWriter.sendPacket(new Presence(Presence.Type.available));
+        }
+
+        // Stores the authentication for future reconnection
+        config.setLoginInfo(username, password, resource);
+
+        // If debugging is enabled, change the the debug window title to include the
+        // name we are now logged-in as.
+        // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
+        // will be null
+        if (config.isDebuggerEnabled() && debugger != null) {
+            debugger.userHasLogged(user);
+        }
+    }
+
+    @Override
+    public synchronized void loginAnonymously() throws XMPPException {
+        if (!isConnected()) {
+            throw new IllegalStateException("Not connected to server.");
+        }
+        if (authenticated) {
+            throw new IllegalStateException("Already logged in to server.");
+        }
+
+        String response;
+        if (config.isSASLAuthenticationEnabled() &&
+                saslAuthentication.hasAnonymousAuthentication()) {
+            response = saslAuthentication.authenticateAnonymously();
+        }
+        else {
+            // Authenticate using Non-SASL
+            response = new NonSASLAuthentication(this).authenticateAnonymously();
+        }
+
+        // Set the user value.
+        this.user = response;
+        // Update the serviceName with the one returned by the server
+        config.setServiceName(StringUtils.parseServer(response));
+
+        // If compression is enabled then request the server to use stream compression
+        if (config.isCompressionEnabled()) {
+            useCompression();
+        }
+
+        // Set presence to online.
+        packetWriter.sendPacket(new Presence(Presence.Type.available));
+
+        // Indicate that we're now authenticated.
+        authenticated = true;
+        anonymous = true;
+
+        // If debugging is enabled, change the the debug window title to include the
+        // name we are now logged-in as.
+        // If DEBUG_ENABLED was set to true AFTER the connection was created the debugger
+        // will be null
+        if (config.isDebuggerEnabled() && debugger != null) {
+            debugger.userHasLogged(user);
+        }
+    }
+
+    public Roster getRoster() {
+        // synchronize against login()
+        synchronized(this) {
+            // if connection is authenticated the roster is already set by login() 
+            // or a previous call to getRoster()
+            if (!isAuthenticated() || isAnonymous()) {
+                if (roster == null) {
+                    roster = new Roster(this);
+                }
+                return roster;
+            }
+        }
+
+        if (!config.isRosterLoadedAtLogin()) {
+            roster.reload();
+        }
+        // If this is the first time the user has asked for the roster after calling
+        // login, we want to wait for the server to send back the user's roster. This
+        // behavior shields API users from having to worry about the fact that roster
+        // operations are asynchronous, although they'll still have to listen for
+        // changes to the roster. Note: because of this waiting logic, internal
+        // Smack code should be wary about calling the getRoster method, and may need to
+        // access the roster object directly.
+        if (!roster.rosterInitialized) {
+            try {
+                synchronized (roster) {
+                    long waitTime = SmackConfiguration.getPacketReplyTimeout();
+                    long start = System.currentTimeMillis();
+                    while (!roster.rosterInitialized) {
+                        if (waitTime <= 0) {
+                            break;
+                        }
+                        roster.wait(waitTime);
+                        long now = System.currentTimeMillis();
+                        waitTime -= now - start;
+                        start = now;
+                    }
+                }
+            }
+            catch (InterruptedException ie) {
+                // Ignore.
+            }
+        }
+        return roster;
+    }
+
+    public boolean isConnected() {
+        return connected;
+    }
+
+    public boolean isSecureConnection() {
+        return isUsingTLS();
+    }
+
+    public boolean isSocketClosed() {
+        return socketClosed;
+    }
+
+    public boolean isAuthenticated() {
+        return authenticated;
+    }
+
+    public boolean isAnonymous() {
+        return anonymous;
+    }
+
+    /**
+     * Closes the connection by setting presence to unavailable then closing the stream to
+     * the XMPP server. The shutdown logic will be used during a planned disconnection or when
+     * dealing with an unexpected disconnection. Unlike {@link #disconnect()} the connection's
+     * packet reader, packet writer, and {@link Roster} will not be removed; thus
+     * connection's state is kept.
+     *
+     * @param unavailablePresence the presence packet to send during shutdown.
+     */
+    protected void shutdown(Presence unavailablePresence) {
+        // Set presence to offline.
+        if (packetWriter != null) {
+                packetWriter.sendPacket(unavailablePresence);
+        }
+
+        this.setWasAuthenticated(authenticated);
+        authenticated = false;
+
+        if (packetReader != null) {
+                packetReader.shutdown();
+        }
+        if (packetWriter != null) {
+                packetWriter.shutdown();
+        }
+
+        // Wait 150 ms for processes to clean-up, then shutdown.
+        try {
+            Thread.sleep(150);
+        }
+        catch (Exception e) {
+            // Ignore.
+        }
+
+	// Set socketClosed to true. This will cause the PacketReader
+	// and PacketWriter to ingore any Exceptions that are thrown
+	// because of a read/write from/to a closed stream.
+	// It is *important* that this is done before socket.close()!
+        socketClosed = true;
+        try {
+                socket.close();
+        } catch (Exception e) {
+                e.printStackTrace();
+        }
+	// In most cases the close() should be successful, so set
+	// connected to false here.
+        connected = false;
+
+        // Close down the readers and writers.
+        if (reader != null) {
+            try {
+		// Should already be closed by the previous
+		// socket.close(). But just in case do it explicitly.
+                reader.close();
+            }
+            catch (Throwable ignore) { /* ignore */ }
+            reader = null;
+        }
+        if (writer != null) {
+            try {
+		// Should already be closed by the previous
+		// socket.close(). But just in case do it explicitly.
+                writer.close();
+            }
+            catch (Throwable ignore) { /* ignore */ }
+            writer = null;
+        }
+
+        // Make sure that the socket is really closed
+        try {
+	    // Does nothing if the socket is already closed
+            socket.close();
+        }
+        catch (Exception e) {
+            // Ignore.
+        }
+
+        saslAuthentication.init();
+    }
+
+    public synchronized void disconnect(Presence unavailablePresence) {
+        // If not connected, ignore this request.
+        PacketReader packetReader = this.packetReader;
+        PacketWriter packetWriter = this.packetWriter;
+        if (packetReader == null || packetWriter == null) {
+            return;
+        }
+
+        if (!isConnected()) {
+            return;
+        }
+
+        shutdown(unavailablePresence);
+
+        if (roster != null) {
+            roster.cleanup();
+            roster = null;
+        }
+        chatManager = null;
+
+        wasAuthenticated = false;
+
+        packetWriter.cleanup();
+        packetReader.cleanup();
+    }
+
+    public void sendPacket(Packet packet) {
+        if (!isConnected()) {
+            throw new IllegalStateException("Not connected to server.");
+        }
+        if (packet == null) {
+            throw new NullPointerException("Packet is null.");
+        }
+        packetWriter.sendPacket(packet);
+    }
+
+    /**
+     * Registers a packet interceptor with this connection. The interceptor will be
+     * invoked every time a packet is about to be sent by this connection. Interceptors
+     * may modify the packet to be sent. A packet filter determines which packets
+     * will be delivered to the interceptor.
+     *
+     * @param packetInterceptor the packet interceptor to notify of packets about to be sent.
+     * @param packetFilter      the packet filter to use.
+     * @deprecated replaced by {@link Connection#addPacketInterceptor(PacketInterceptor, PacketFilter)}.
+     */
+    public void addPacketWriterInterceptor(PacketInterceptor packetInterceptor,
+            PacketFilter packetFilter) {
+        addPacketInterceptor(packetInterceptor, packetFilter);
+    }
+
+    /**
+     * Removes a packet interceptor.
+     *
+     * @param packetInterceptor the packet interceptor to remove.
+     * @deprecated replaced by {@link Connection#removePacketInterceptor(PacketInterceptor)}.
+     */
+    public void removePacketWriterInterceptor(PacketInterceptor packetInterceptor) {
+        removePacketInterceptor(packetInterceptor);
+    }
+
+    /**
+     * Registers a packet listener with this connection. The listener will be
+     * notified of every packet that this connection sends. A packet filter determines
+     * which packets will be delivered to the listener. Note that the thread
+     * that writes packets will be used to invoke the listeners. Therefore, each
+     * packet listener should complete all operations quickly or use a different
+     * thread for processing.
+     *
+     * @param packetListener the packet listener to notify of sent packets.
+     * @param packetFilter   the packet filter to use.
+     * @deprecated replaced by {@link #addPacketSendingListener(PacketListener, PacketFilter)}.
+     */
+    public void addPacketWriterListener(PacketListener packetListener, PacketFilter packetFilter) {
+        addPacketSendingListener(packetListener, packetFilter);
+    }
+
+    /**
+     * Removes a packet listener for sending packets from this connection.
+     *
+     * @param packetListener the packet listener to remove.
+     * @deprecated replaced by {@link #removePacketSendingListener(PacketListener)}.
+     */
+    public void removePacketWriterListener(PacketListener packetListener) {
+        removePacketSendingListener(packetListener);
+    }
+
+    private void connectUsingConfiguration(ConnectionConfiguration config) throws XMPPException {
+        XMPPException exception = null;
+        Iterator<HostAddress> it = config.getHostAddresses().iterator();
+        List<HostAddress> failedAddresses = new LinkedList<HostAddress>();
+        boolean xmppIOError = false;
+        while (it.hasNext()) {
+            exception = null;
+            HostAddress hostAddress = it.next();
+            String host = hostAddress.getFQDN();
+            int port = hostAddress.getPort();
+            try {
+                if (config.getSocketFactory() == null) {
+                    this.socket = new Socket(host, port);
+                }
+                else {
+                    this.socket = config.getSocketFactory().createSocket(host, port);
+                }
+            } catch (UnknownHostException uhe) {
+                String errorMessage = "Could not connect to " + host + ":" + port + ".";
+                exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_timeout,
+                        errorMessage), uhe);
+            } catch (IOException ioe) {
+                String errorMessage = "XMPPError connecting to " + host + ":" + port + ".";
+                exception = new XMPPException(errorMessage, new XMPPError(XMPPError.Condition.remote_server_error,
+                        errorMessage), ioe);
+                xmppIOError = true;
+            }
+            if (exception == null) {
+                // We found a host to connect to, break here
+                config.setUsedHostAddress(hostAddress);
+                break;
+            }
+            hostAddress.setException(exception);
+            failedAddresses.add(hostAddress);
+            if (!it.hasNext()) {
+                // There are no more host addresses to try
+                // throw an exception and report all tried
+                // HostAddresses in the exception
+                StringBuilder sb = new StringBuilder();
+                for (HostAddress fha : failedAddresses) {
+                    sb.append(fha.getErrorMessage());
+                    sb.append("; ");
+                }
+                XMPPError xmppError;
+                if (xmppIOError) {
+                    xmppError = new XMPPError(XMPPError.Condition.remote_server_error);
+                }
+                else {
+                    xmppError = new XMPPError(XMPPError.Condition.remote_server_timeout);
+                }
+                throw new XMPPException(sb.toString(), xmppError);
+            }
+        }
+        socketClosed = false;
+        initConnection();
+    }
+
+    /**
+     * Initializes the connection by creating a packet reader and writer and opening a
+     * XMPP stream to the server.
+     *
+     * @throws XMPPException if establishing a connection to the server fails.
+     */
+    private void initConnection() throws XMPPException {
+        boolean isFirstInitialization = packetReader == null || packetWriter == null;
+        compressionHandler = null;
+        serverAckdCompression = false;
+
+        // Set the reader and writer instance variables
+        initReaderAndWriter();
+
+        try {
+            if (isFirstInitialization) {
+                packetWriter = new PacketWriter(this);
+                packetReader = new PacketReader(this);
+
+                // If debugging is enabled, we should start the thread that will listen for
+                // all packets and then log them.
+                if (config.isDebuggerEnabled()) {
+                    addPacketListener(debugger.getReaderListener(), null);
+                    if (debugger.getWriterListener() != null) {
+                        addPacketSendingListener(debugger.getWriterListener(), null);
+                    }
+                }
+            }
+            else {
+                packetWriter.init();
+                packetReader.init();
+            }
+
+            // Start the packet writer. This will open a XMPP stream to the server
+            packetWriter.startup();
+            // Start the packet reader. The startup() method will block until we
+            // get an opening stream packet back from server.
+            packetReader.startup();
+
+            // Make note of the fact that we're now connected.
+            connected = true;
+
+            if (isFirstInitialization) {
+                // Notify listeners that a new connection has been established
+                for (ConnectionCreationListener listener : getConnectionCreationListeners()) {
+                    listener.connectionCreated(this);
+                }
+            }
+            else if (!wasAuthenticated) {
+                notifyReconnection();
+            }
+
+        }
+        catch (XMPPException ex) {
+            // An exception occurred in setting up the connection. Make sure we shut down the
+            // readers and writers and close the socket.
+
+            if (packetWriter != null) {
+                try {
+                    packetWriter.shutdown();
+                }
+                catch (Throwable ignore) { /* ignore */ }
+                packetWriter = null;
+            }
+            if (packetReader != null) {
+                try {
+                    packetReader.shutdown();
+                }
+                catch (Throwable ignore) { /* ignore */ }
+                packetReader = null;
+            }
+            if (reader != null) {
+                try {
+                    reader.close();
+                }
+                catch (Throwable ignore) { /* ignore */ }
+                reader = null;
+            }
+            if (writer != null) {
+                try {
+                    writer.close();
+                }
+                catch (Throwable ignore) {  /* ignore */}
+                writer = null;
+            }
+            if (socket != null) {
+                try {
+                    socket.close();
+                }
+                catch (Exception e) { /* ignore */ }
+                socket = null;
+            }
+            this.setWasAuthenticated(authenticated);
+            chatManager = null;
+            authenticated = false;
+            connected = false;
+
+            throw ex;        // Everything stoppped. Now throw the exception.
+        }
+    }
+
+    private void initReaderAndWriter() throws XMPPException {
+        try {
+            if (compressionHandler == null) {
+                reader =
+                        new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
+                writer = new BufferedWriter(
+                        new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
+            }
+            else {
+                try {
+                    OutputStream os = compressionHandler.getOutputStream(socket.getOutputStream());
+                    writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
+
+                    InputStream is = compressionHandler.getInputStream(socket.getInputStream());
+                    reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+                }
+                catch (Exception e) {
+                    e.printStackTrace();
+                    compressionHandler = null;
+                    reader = new BufferedReader(
+                            new InputStreamReader(socket.getInputStream(), "UTF-8"));
+                    writer = new BufferedWriter(
+                            new OutputStreamWriter(socket.getOutputStream(), "UTF-8"));
+                }
+            }
+        }
+        catch (IOException ioe) {
+            throw new XMPPException(
+                    "XMPPError establishing connection with server.",
+                    new XMPPError(XMPPError.Condition.remote_server_error,
+                            "XMPPError establishing connection with server."),
+                    ioe);
+        }
+
+        // If debugging is enabled, we open a window and write out all network traffic.
+        initDebugger();
+    }
+
+    /***********************************************
+     * TLS code below
+     **********************************************/
+
+    /**
+     * Returns true if the connection to the server has successfully negotiated TLS. Once TLS
+     * has been negotiatied the connection has been secured.
+     *
+     * @return true if the connection to the server has successfully negotiated TLS.
+     */
+    public boolean isUsingTLS() {
+        return usingTLS;
+    }
+
+    /**
+     * Notification message saying that the server supports TLS so confirm the server that we
+     * want to secure the connection.
+     *
+     * @param required true when the server indicates that TLS is required.
+     */
+    void startTLSReceived(boolean required) {
+        if (required && config.getSecurityMode() ==
+                ConnectionConfiguration.SecurityMode.disabled) {
+            notifyConnectionError(new IllegalStateException(
+                    "TLS required by server but not allowed by connection configuration"));
+            return;
+        }
+
+        if (config.getSecurityMode() == ConnectionConfiguration.SecurityMode.disabled) {
+            // Do not secure the connection using TLS since TLS was disabled
+            return;
+        }
+        try {
+            writer.write("<starttls xmlns=\"urn:ietf:params:xml:ns:xmpp-tls\"/>");
+            writer.flush();
+        }
+        catch (IOException e) {
+            notifyConnectionError(e);
+        }
+    }
+
+    /**
+     * The server has indicated that TLS negotiation can start. We now need to secure the
+     * existing plain connection and perform a handshake. This method won't return until the
+     * connection has finished the handshake or an error occured while securing the connection.
+     *
+     * @throws Exception if an exception occurs.
+     */
+    void proceedTLSReceived() throws Exception {
+        SSLContext context = this.config.getCustomSSLContext();
+        KeyStore ks = null;
+        KeyManager[] kms = null;
+        PasswordCallback pcb = null;
+
+        if(config.getCallbackHandler() == null) {
+           ks = null;
+        } else if (context == null) {
+            //System.out.println("Keystore type: "+configuration.getKeystoreType());
+            if(config.getKeystoreType().equals("NONE")) {
+                ks = null;
+                pcb = null;
+            }
+            else if(config.getKeystoreType().equals("PKCS11")) {
+                try {
+                    Constructor<?> c = Class.forName("sun.security.pkcs11.SunPKCS11").getConstructor(InputStream.class);
+                    String pkcs11Config = "name = SmartCard\nlibrary = "+config.getPKCS11Library();
+                    ByteArrayInputStream config = new ByteArrayInputStream(pkcs11Config.getBytes());
+                    Provider p = (Provider)c.newInstance(config);
+                    Security.addProvider(p);
+                    ks = KeyStore.getInstance("PKCS11",p);
+                    pcb = new PasswordCallback("PKCS11 Password: ",false);
+                    this.config.getCallbackHandler().handle(new Callback[]{pcb});
+                    ks.load(null,pcb.getPassword());
+                }
+                catch (Exception e) {
+                    ks = null;
+                    pcb = null;
+                }
+            }
+            else if(config.getKeystoreType().equals("Apple")) {
+                ks = KeyStore.getInstance("KeychainStore","Apple");
+                ks.load(null,null);
+                //pcb = new PasswordCallback("Apple Keychain",false);
+                //pcb.setPassword(null);
+            }
+            else {
+                ks = KeyStore.getInstance(config.getKeystoreType());
+                try {
+                    pcb = new PasswordCallback("Keystore Password: ",false);
+                    config.getCallbackHandler().handle(new Callback[]{pcb});
+                    ks.load(new FileInputStream(config.getKeystorePath()), pcb.getPassword());
+                }
+                catch(Exception e) {
+                    ks = null;
+                    pcb = null;
+                }
+            }
+            KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
+            try {
+                if(pcb == null) {
+                    kmf.init(ks,null);
+                } else {
+                    kmf.init(ks,pcb.getPassword());
+                    pcb.clearPassword();
+                }
+                kms = kmf.getKeyManagers();
+            } catch (NullPointerException npe) {
+                kms = null;
+            }
+        }
+
+        // Verify certificate presented by the server
+        if (context == null) {
+            context = SSLContext.getInstance("TLS");
+            context.init(kms, new javax.net.ssl.TrustManager[] { new ServerTrustManager(getServiceName(), config) },
+                    new java.security.SecureRandom());
+        }
+        Socket plain = socket;
+        // Secure the plain connection
+        socket = context.getSocketFactory().createSocket(plain,
+                plain.getInetAddress().getHostAddress(), plain.getPort(), true);
+        socket.setSoTimeout(0);
+        socket.setKeepAlive(true);
+        // Initialize the reader and writer with the new secured version
+        initReaderAndWriter();
+        // Proceed to do the handshake
+        ((SSLSocket) socket).startHandshake();
+        //if (((SSLSocket) socket).getWantClientAuth()) {
+        //    System.err.println("Connection wants client auth");
+        //}
+        //else if (((SSLSocket) socket).getNeedClientAuth()) {
+        //    System.err.println("Connection needs client auth");
+        //}
+        //else {
+        //    System.err.println("Connection does not require client auth");
+       // }
+        // Set that TLS was successful
+        usingTLS = true;
+
+        // Set the new  writer to use
+        packetWriter.setWriter(writer);
+        // Send a new opening stream to the server
+        packetWriter.openStream();
+    }
+
+    /**
+     * Sets the available stream compression methods offered by the server.
+     *
+     * @param methods compression methods offered by the server.
+     */
+    void setAvailableCompressionMethods(Collection<String> methods) {
+        compressionMethods = methods;
+    }
+
+    /**
+     * Returns the compression handler that can be used for one compression methods offered by the server.
+     * 
+     * @return a instance of XMPPInputOutputStream or null if no suitable instance was found
+     * 
+     */
+    private XMPPInputOutputStream maybeGetCompressionHandler() {
+        if (compressionMethods != null) {
+            for (XMPPInputOutputStream handler : compressionHandlers) {
+                if (!handler.isSupported())
+                    continue;
+
+                String method = handler.getCompressionMethod();
+                if (compressionMethods.contains(method))
+                    return handler;
+            }
+        }
+        return null;
+    }
+
+    public boolean isUsingCompression() {
+        return compressionHandler != null && serverAckdCompression;
+    }
+
+    /**
+     * Starts using stream compression that will compress network traffic. Traffic can be
+     * reduced up to 90%. Therefore, stream compression is ideal when using a slow speed network
+     * connection. However, the server and the client will need to use more CPU time in order to
+     * un/compress network data so under high load the server performance might be affected.<p>
+     * <p/>
+     * Stream compression has to have been previously offered by the server. Currently only the
+     * zlib method is supported by the client. Stream compression negotiation has to be done
+     * before authentication took place.<p>
+     * <p/>
+     * Note: to use stream compression the smackx.jar file has to be present in the classpath.
+     *
+     * @return true if stream compression negotiation was successful.
+     */
+    private boolean useCompression() {
+        // If stream compression was offered by the server and we want to use
+        // compression then send compression request to the server
+        if (authenticated) {
+            throw new IllegalStateException("Compression should be negotiated before authentication.");
+        }
+
+        if ((compressionHandler = maybeGetCompressionHandler()) != null) {
+            requestStreamCompression(compressionHandler.getCompressionMethod());
+            // Wait until compression is being used or a timeout happened
+            synchronized (this) {
+                try {
+                    this.wait(SmackConfiguration.getPacketReplyTimeout() * 5);
+                }
+                catch (InterruptedException e) {
+                    // Ignore.
+                }
+            }
+            return isUsingCompression();
+        }
+        return false;
+    }
+
+    /**
+     * Request the server that we want to start using stream compression. When using TLS
+     * then negotiation of stream compression can only happen after TLS was negotiated. If TLS
+     * compression is being used the stream compression should not be used.
+     */
+    private void requestStreamCompression(String method) {
+        try {
+            writer.write("<compress xmlns='http://jabber.org/protocol/compress'>");
+            writer.write("<method>" + method + "</method></compress>");
+            writer.flush();
+        }
+        catch (IOException e) {
+            notifyConnectionError(e);
+        }
+    }
+
+    /**
+     * Start using stream compression since the server has acknowledged stream compression.
+     *
+     * @throws Exception if there is an exception starting stream compression.
+     */
+    void startStreamCompression() throws Exception {
+        serverAckdCompression = true;
+        // Initialize the reader and writer with the new secured version
+        initReaderAndWriter();
+
+        // Set the new  writer to use
+        packetWriter.setWriter(writer);
+        // Send a new opening stream to the server
+        packetWriter.openStream();
+        // Notify that compression is being used
+        synchronized (this) {
+            this.notify();
+        }
+    }
+
+    /**
+     * Notifies the XMPP connection that stream compression was denied so that
+     * the connection process can proceed.
+     */
+    void streamCompressionDenied() {
+        synchronized (this) {
+            this.notify();
+        }
+    }
+
+    /**
+     * Establishes a connection to the XMPP server and performs an automatic login
+     * only if the previous connection state was logged (authenticated). It basically
+     * creates and maintains a socket connection to the server.<p>
+     * <p/>
+     * Listeners will be preserved from a previous connection if the reconnection
+     * occurs after an abrupt termination.
+     *
+     * @throws XMPPException if an error occurs while trying to establish the connection.
+     *      Two possible errors can occur which will be wrapped by an XMPPException --
+     *      UnknownHostException (XMPP error code 504), and IOException (XMPP error code
+     *      502). The error codes and wrapped exceptions can be used to present more
+     *      appropriate error messages to end-users.
+     */
+    public void connect() throws XMPPException {
+        // Establishes the connection, readers and writers
+        connectUsingConfiguration(config);
+        // Automatically makes the login if the user was previously connected successfully
+        // to the server and the connection was terminated abruptly
+        if (connected && wasAuthenticated) {
+            // Make the login
+            if (isAnonymous()) {
+                // Make the anonymous login
+                loginAnonymously();
+            }
+            else {
+                login(config.getUsername(), config.getPassword(), config.getResource());
+            }
+            notifyReconnection();
+        }
+    }
+
+    /**
+     * Sets whether the connection has already logged in the server.
+     *
+     * @param wasAuthenticated true if the connection has already been authenticated.
+     */
+    private void setWasAuthenticated(boolean wasAuthenticated) {
+        if (!this.wasAuthenticated) {
+            this.wasAuthenticated = wasAuthenticated;
+        }
+    }
+
+	@Override
+	public void setRosterStorage(RosterStorage storage)
+			throws IllegalStateException {
+		if(roster!=null){
+			throw new IllegalStateException("Roster is already initialized");
+		}
+		this.rosterStorage = storage;
+	}
+
+    /**
+     * Sends out a notification that there was an error with the connection
+     * and closes the connection. Also prints the stack trace of the given exception
+     *
+     * @param e the exception that causes the connection close event.
+     */
+    synchronized void notifyConnectionError(Exception e) {
+        // Listeners were already notified of the exception, return right here.
+        if (packetReader.done && packetWriter.done) return;
+
+        packetReader.done = true;
+        packetWriter.done = true;
+        // Closes the connection temporary. A reconnection is possible
+        shutdown(new Presence(Presence.Type.unavailable));
+        // Print the stack trace to help catch the problem
+        e.printStackTrace();
+        // Notify connection listeners of the error.
+        for (ConnectionListener listener : getConnectionListeners()) {
+            try {
+                listener.connectionClosedOnError(e);
+            }
+            catch (Exception e2) {
+                // Catch and print any exception so we can recover
+                // from a faulty listener
+                e2.printStackTrace();
+            }
+        }
+    }
+    
+
+    /**
+     * Sends a notification indicating that the connection was reconnected successfully.
+     */
+    protected void notifyReconnection() {
+        // Notify connection listeners of the reconnection.
+        for (ConnectionListener listener : getConnectionListeners()) {
+            try {
+                listener.reconnectionSuccessful();
+            }
+            catch (Exception e) {
+                // Catch and print any exception so we can recover
+                // from a faulty listener
+                e.printStackTrace();
+            }
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/XMPPException.java b/src/org/jivesoftware/smack/XMPPException.java
new file mode 100644
index 0000000..6da24c2
--- /dev/null
+++ b/src/org/jivesoftware/smack/XMPPException.java
@@ -0,0 +1,219 @@
+/**
+ * $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;
+
+import org.jivesoftware.smack.packet.StreamError;
+import org.jivesoftware.smack.packet.XMPPError;
+
+import java.io.PrintStream;
+import java.io.PrintWriter;
+
+/**
+ * A generic exception that is thrown when an error occurs performing an
+ * XMPP operation. XMPP servers can respond to error conditions with an error code
+ * and textual description of the problem, which are encapsulated in the XMPPError
+ * class. When appropriate, an XMPPError instance is attached instances of this exception.<p>
+ *
+ * When a stream error occured, the server will send a stream error to the client before
+ * closing the connection. Stream errors are unrecoverable errors. When a stream error
+ * is sent to the client an XMPPException will be thrown containing the StreamError sent
+ * by the server.
+ *
+ * @see XMPPError
+ * @author Matt Tucker
+ */
+public class XMPPException extends Exception {
+
+    private StreamError streamError = null;
+    private XMPPError error = null;
+    private Throwable wrappedThrowable = null;
+
+    /**
+     * Creates a new XMPPException.
+     */
+    public XMPPException() {
+        super();
+    }
+
+    /**
+     * Creates a new XMPPException with a description of the exception.
+     *
+     * @param message description of the exception.
+     */
+    public XMPPException(String message) {
+        super(message);
+    }
+
+    /**
+     * Creates a new XMPPException with the Throwable that was the root cause of the
+     * exception.
+     *
+     * @param wrappedThrowable the root cause of the exception.
+     */
+    public XMPPException(Throwable wrappedThrowable) {
+        super();
+        this.wrappedThrowable = wrappedThrowable;
+    }
+
+    /**
+     * Cretaes a new XMPPException with the stream error that was the root case of the
+     * exception. When a stream error is received from the server then the underlying
+     * TCP connection will be closed by the server.
+     *
+     * @param streamError the root cause of the exception.
+     */
+    public XMPPException(StreamError streamError) {
+        super();
+        this.streamError = streamError;
+    }
+
+    /**
+     * Cretaes a new XMPPException with the XMPPError that was the root case of the
+     * exception.
+     *
+     * @param error the root cause of the exception.
+     */
+    public XMPPException(XMPPError error) {
+        super();
+        this.error = error;
+    }
+
+    /**
+     * Creates a new XMPPException with a description of the exception and the
+     * Throwable that was the root cause of the exception.
+     *
+     * @param message a description of the exception.
+     * @param wrappedThrowable the root cause of the exception.
+     */
+    public XMPPException(String message, Throwable wrappedThrowable) {
+        super(message);
+        this.wrappedThrowable = wrappedThrowable;
+    }
+
+    /**
+     * Creates a new XMPPException with a description of the exception, an XMPPError,
+     * and the Throwable that was the root cause of the exception.
+     *
+     * @param message a description of the exception.
+     * @param error the root cause of the exception.
+     * @param wrappedThrowable the root cause of the exception.
+     */
+    public XMPPException(String message, XMPPError error, Throwable wrappedThrowable) {
+        super(message);
+        this.error = error;
+        this.wrappedThrowable = wrappedThrowable;
+    }
+
+    /**
+     * Creates a new XMPPException with a description of the exception and the
+     * XMPPException that was the root cause of the exception.
+     *
+     * @param message a description of the exception.
+     * @param error the root cause of the exception.
+     */
+    public XMPPException(String message, XMPPError error) {
+        super(message);
+        this.error = error;
+    }
+
+    /**
+     * Returns the XMPPError asscociated with this exception, or <tt>null</tt> if there
+     * isn't one.
+     *
+     * @return the XMPPError asscociated with this exception.
+     */
+    public XMPPError getXMPPError() {
+        return error;
+    }
+
+    /**
+     * Returns the StreamError asscociated with this exception, or <tt>null</tt> if there
+     * isn't one. The underlying TCP connection is closed by the server after sending the
+     * stream error to the client.
+     *
+     * @return the StreamError asscociated with this exception.
+     */
+    public StreamError getStreamError() {
+        return streamError;
+    }
+
+    /**
+     * Returns the Throwable asscociated with this exception, or <tt>null</tt> if there
+     * isn't one.
+     *
+     * @return the Throwable asscociated with this exception.
+     */
+    public Throwable getWrappedThrowable() {
+        return wrappedThrowable;
+    }
+
+    public void printStackTrace() {
+        printStackTrace(System.err);
+    }
+
+    public void printStackTrace(PrintStream out) {
+        super.printStackTrace(out);
+        if (wrappedThrowable != null) {
+            out.println("Nested Exception: ");
+            wrappedThrowable.printStackTrace(out);
+        }
+    }
+
+    public void printStackTrace(PrintWriter out) {
+        super.printStackTrace(out);
+        if (wrappedThrowable != null) {
+            out.println("Nested Exception: ");
+            wrappedThrowable.printStackTrace(out);
+        }
+    }
+
+    public String getMessage() {
+        String msg = super.getMessage();
+        // If the message was not set, but there is an XMPPError, return the
+        // XMPPError as the message.
+        if (msg == null && error != null) {
+            return error.toString();
+        }
+        else if (msg == null && streamError != null) {
+            return streamError.toString();
+        }
+        return msg;
+    }
+
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        String message = super.getMessage();
+        if (message != null) {
+            buf.append(message).append(": ");
+        }
+        if (error != null) {
+            buf.append(error);
+        }
+        if (streamError != null) {
+            buf.append(streamError);
+        }
+        if (wrappedThrowable != null) {
+            buf.append("\n  -- caused by: ").append(wrappedThrowable);
+        }
+
+        return buf.toString();
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java b/src/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java
new file mode 100644
index 0000000..dc50451
--- /dev/null
+++ b/src/org/jivesoftware/smack/compression/Java7ZlibInputOutputStream.java
@@ -0,0 +1,126 @@
+/**
+ * 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.compression;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterOutputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * This class provides XMPP "zlib" compression with the help of the Deflater class of the Java API. Note that the method
+ * needed is available since Java7, so it will only work with Java7 or higher (hence it's name).
+ * 
+ * @author Florian Schmaus
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/zip/Deflater.html#deflate(byte[], int, int, int)">The
+ * required deflate() method</a>
+ * 
+ */
+public class Java7ZlibInputOutputStream extends XMPPInputOutputStream {
+    private final static Method method;
+    private final static boolean supported;
+    private final static int compressionLevel = Deflater.DEFAULT_STRATEGY;
+
+    static {
+        Method m = null;
+        try {
+            m = Deflater.class.getMethod("deflate", byte[].class, int.class, int.class, int.class);
+        } catch (SecurityException e) {
+        } catch (NoSuchMethodException e) {
+        }
+        method = m;
+        supported = (method != null);
+    }
+
+    public Java7ZlibInputOutputStream() {
+        compressionMethod = "zlib";
+    }
+
+    @Override
+    public boolean isSupported() {
+        return supported;
+    }
+
+    @Override
+    public InputStream getInputStream(InputStream inputStream) {
+        return new InflaterInputStream(inputStream, new Inflater(), 512) {
+            /**
+             * Provide a more InputStream compatible version. A return value of 1 means that it is likely to read one
+             * byte without blocking, 0 means that the system is known to block for more input.
+             * 
+             * @return 0 if no data is available, 1 otherwise
+             * @throws IOException
+             */
+            @Override
+            public int available() throws IOException {
+                /*
+                 * aSmack related remark (where KXmlParser is used):
+                 * This is one of the funny code blocks. InflaterInputStream.available violates the contract of
+                 * InputStream.available, which breaks kXML2.
+                 * 
+                 * I'm not sure who's to blame, oracle/sun for a broken api or the google guys for mixing a sun bug with
+                 * a xml reader that can't handle it....
+                 * 
+                 * Anyway, this simple if breaks suns distorted reality, but helps to use the api as intended.
+                 */
+                if (inf.needsInput()) {
+                    return 0;
+                }
+                return super.available();
+            }
+        };
+    }
+
+    @Override
+    public OutputStream getOutputStream(OutputStream outputStream) {
+        return new DeflaterOutputStream(outputStream, new Deflater(compressionLevel)) {
+            public void flush() throws IOException {
+                if (!supported) {
+                    super.flush();
+                    return;
+                }
+                int count = 0;
+                if (!def.needsInput()) {
+                    do {
+                        count = def.deflate(buf, 0, buf.length);
+                        out.write(buf, 0, count);
+                    } while (count > 0);
+                    out.flush();
+                }
+                try {
+                    do {
+                        count = (Integer) method.invoke(def, buf, 0, buf.length, 2);
+                        out.write(buf, 0, count);
+                    } while (count > 0);
+                } catch (IllegalArgumentException e) {
+                    throw new IOException("Can't flush");
+                } catch (IllegalAccessException e) {
+                    throw new IOException("Can't flush");
+                } catch (InvocationTargetException e) {
+                    throw new IOException("Can't flush");
+                }
+                super.flush();
+            }
+        };
+    }
+
+}
diff --git a/src/org/jivesoftware/smack/compression/JzlibInputOutputStream.java b/src/org/jivesoftware/smack/compression/JzlibInputOutputStream.java
new file mode 100644
index 0000000..7db0773
--- /dev/null
+++ b/src/org/jivesoftware/smack/compression/JzlibInputOutputStream.java
@@ -0,0 +1,75 @@
+/**
+ * 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.compression;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+/**
+ * This class provides XMPP "zlib" compression with the help of JZLib. Note that jzlib-1.0.7 must be used (i.e. in the
+ * classpath), newer versions won't work!
+ * 
+ * @author Florian Schmaus
+ * @see <a href="http://www.jcraft.com/jzlib/">JZLib</a>
+ * 
+ */
+public class JzlibInputOutputStream extends XMPPInputOutputStream {
+
+    private static Class<?> zoClass = null;
+    private static Class<?> ziClass = null;
+
+    static {
+        try {
+            zoClass = Class.forName("com.jcraft.jzlib.ZOutputStream");
+            ziClass = Class.forName("com.jcraft.jzlib.ZInputStream");
+        } catch (ClassNotFoundException e) {
+        }
+    }
+
+    public JzlibInputOutputStream() {
+        compressionMethod = "zlib";
+    }
+
+    @Override
+    public boolean isSupported() {
+        return (zoClass != null && ziClass != null);
+    }
+
+    @Override
+    public InputStream getInputStream(InputStream inputStream) throws SecurityException, NoSuchMethodException,
+            IllegalArgumentException, IllegalAccessException, InvocationTargetException, InstantiationException {
+        Constructor<?> constructor = ziClass.getConstructor(InputStream.class);
+        Object in = constructor.newInstance(inputStream);
+
+        Method method = ziClass.getMethod("setFlushMode", Integer.TYPE);
+        method.invoke(in, 2);
+        return (InputStream) in;
+    }
+
+    @Override
+    public OutputStream getOutputStream(OutputStream outputStream) throws SecurityException, NoSuchMethodException,
+            IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
+        Constructor<?> constructor = zoClass.getConstructor(OutputStream.class, Integer.TYPE);
+        Object out = constructor.newInstance(outputStream, 9);
+
+        Method method = zoClass.getMethod("setFlushMode", Integer.TYPE);
+        method.invoke(out, 2);
+        return (OutputStream) out;
+    }
+}
diff --git a/src/org/jivesoftware/smack/compression/XMPPInputOutputStream.java b/src/org/jivesoftware/smack/compression/XMPPInputOutputStream.java
new file mode 100644
index 0000000..d44416a
--- /dev/null
+++ b/src/org/jivesoftware/smack/compression/XMPPInputOutputStream.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.compression;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+public abstract class XMPPInputOutputStream {
+    protected String compressionMethod;
+
+    public String getCompressionMethod() {
+        return compressionMethod;
+    }
+
+    public abstract boolean isSupported();
+
+    public abstract InputStream getInputStream(InputStream inputStream) throws Exception;
+
+    public abstract OutputStream getOutputStream(OutputStream outputStream) throws Exception;
+}
diff --git a/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java b/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java
new file mode 100644
index 0000000..7e078b4
--- /dev/null
+++ b/src/org/jivesoftware/smack/debugger/ConsoleDebugger.java
@@ -0,0 +1,198 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.debugger;
+
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.*;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Very simple debugger that prints to the console (stdout) the sent and received stanzas. Use
+ * this debugger with caution since printing to the console is an expensive operation that may
+ * even block the thread since only one thread may print at a time.<p>
+ * <p/>
+ * It is possible to not only print the raw sent and received stanzas but also the interpreted
+ * packets by Smack. By default interpreted packets won't be printed. To enable this feature
+ * just change the <tt>printInterpreted</tt> static variable to <tt>true</tt>.
+ *
+ * @author Gaston Dombiak
+ */
+public class ConsoleDebugger implements SmackDebugger {
+
+    public static boolean printInterpreted = false;
+    private SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa");
+
+    private Connection connection = null;
+
+    private PacketListener listener = null;
+    private ConnectionListener connListener = null;
+
+    private Writer writer;
+    private Reader reader;
+    private ReaderListener readerListener;
+    private WriterListener writerListener;
+
+    public ConsoleDebugger(Connection connection, Writer writer, Reader reader) {
+        this.connection = connection;
+        this.writer = writer;
+        this.reader = reader;
+        createDebug();
+    }
+
+    /**
+     * Creates the listeners that will print in the console when new activity is detected.
+     */
+    private void createDebug() {
+        // Create a special Reader that wraps the main Reader and logs data to the GUI.
+        ObservableReader debugReader = new ObservableReader(reader);
+        readerListener = new ReaderListener() {
+            public void read(String str) {
+                System.out.println(
+                        dateFormatter.format(new Date()) + " RCV  (" + connection.hashCode() +
+                        "): " +
+                        str);
+            }
+        };
+        debugReader.addReaderListener(readerListener);
+
+        // Create a special Writer that wraps the main Writer and logs data to the GUI.
+        ObservableWriter debugWriter = new ObservableWriter(writer);
+        writerListener = new WriterListener() {
+            public void write(String str) {
+                System.out.println(
+                        dateFormatter.format(new Date()) + " SENT (" + connection.hashCode() +
+                        "): " +
+                        str);
+            }
+        };
+        debugWriter.addWriterListener(writerListener);
+
+        // Assign the reader/writer objects to use the debug versions. The packet reader
+        // and writer will use the debug versions when they are created.
+        reader = debugReader;
+        writer = debugWriter;
+
+        // Create a thread that will listen for all incoming packets and write them to
+        // the GUI. This is what we call "interpreted" packet data, since it's the packet
+        // data as Smack sees it and not as it's coming in as raw XML.
+        listener = new PacketListener() {
+            public void processPacket(Packet packet) {
+                if (printInterpreted) {
+                    System.out.println(
+                            dateFormatter.format(new Date()) + " RCV PKT (" +
+                            connection.hashCode() +
+                            "): " +
+                            packet.toXML());
+                }
+            }
+        };
+
+        connListener = new ConnectionListener() {
+            public void connectionClosed() {
+                System.out.println(
+                        dateFormatter.format(new Date()) + " Connection closed (" +
+                        connection.hashCode() +
+                        ")");
+            }
+
+            public void connectionClosedOnError(Exception e) {
+                System.out.println(
+                        dateFormatter.format(new Date()) +
+                        " Connection closed due to an exception (" +
+                        connection.hashCode() +
+                        ")");
+                e.printStackTrace();
+            }
+            public void reconnectionFailed(Exception e) {
+                System.out.println(
+                        dateFormatter.format(new Date()) +
+                        " Reconnection failed due to an exception (" +
+                        connection.hashCode() +
+                        ")");
+                e.printStackTrace();
+            }
+            public void reconnectionSuccessful() {
+                System.out.println(
+                        dateFormatter.format(new Date()) + " Connection reconnected (" +
+                        connection.hashCode() +
+                        ")");
+            }
+            public void reconnectingIn(int seconds) {
+                System.out.println(
+                        dateFormatter.format(new Date()) + " Connection (" +
+                        connection.hashCode() +
+                        ") will reconnect in " + seconds);
+            }
+        };
+    }
+
+    public Reader newConnectionReader(Reader newReader) {
+        ((ObservableReader)reader).removeReaderListener(readerListener);
+        ObservableReader debugReader = new ObservableReader(newReader);
+        debugReader.addReaderListener(readerListener);
+        reader = debugReader;
+        return reader;
+    }
+
+    public Writer newConnectionWriter(Writer newWriter) {
+        ((ObservableWriter)writer).removeWriterListener(writerListener);
+        ObservableWriter debugWriter = new ObservableWriter(newWriter);
+        debugWriter.addWriterListener(writerListener);
+        writer = debugWriter;
+        return writer;
+    }
+
+    public void userHasLogged(String user) {
+        boolean isAnonymous = "".equals(StringUtils.parseName(user));
+        String title =
+                "User logged (" + connection.hashCode() + "): "
+                + (isAnonymous ? "" : StringUtils.parseBareAddress(user))
+                + "@"
+                + connection.getServiceName()
+                + ":"
+                + connection.getPort();
+        title += "/" + StringUtils.parseResource(user);
+        System.out.println(title);
+        // Add the connection listener to the connection so that the debugger can be notified
+        // whenever the connection is closed.
+        connection.addConnectionListener(connListener);
+    }
+
+    public Reader getReader() {
+        return reader;
+    }
+
+    public Writer getWriter() {
+        return writer;
+    }
+
+    public PacketListener getReaderListener() {
+        return listener;
+    }
+
+    public PacketListener getWriterListener() {
+        return null;
+    }
+}
diff --git a/src/org/jivesoftware/smack/debugger/SmackDebugger.java b/src/org/jivesoftware/smack/debugger/SmackDebugger.java
new file mode 100644
index 0000000..562720b
--- /dev/null
+++ b/src/org/jivesoftware/smack/debugger/SmackDebugger.java
@@ -0,0 +1,98 @@
+/**
+ * $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.debugger;
+
+import java.io.*;
+
+import org.jivesoftware.smack.*;
+
+/**
+ * Interface that allows for implementing classes to debug XML traffic. That is a GUI window that 
+ * displays XML traffic.<p>
+ * 
+ * Every implementation of this interface <b>must</b> have a public constructor with the following 
+ * arguments: Connection, Writer, Reader.
+ * 
+ * @author Gaston Dombiak
+ */
+public interface SmackDebugger {
+
+    /**
+     * Called when a user has logged in to the server. The user could be an anonymous user, this 
+     * means that the user would be of the form host/resource instead of the form 
+     * user@host/resource.
+     * 
+     * @param user the user@host/resource that has just logged in
+     */
+    public abstract void userHasLogged(String user);
+
+    /**
+     * Returns the special Reader that wraps the main Reader and logs data to the GUI.
+     * 
+     * @return the special Reader that wraps the main Reader and logs data to the GUI.
+     */
+    public abstract Reader getReader();
+
+    /**
+     * Returns the special Writer that wraps the main Writer and logs data to the GUI.
+     * 
+     * @return the special Writer that wraps the main Writer and logs data to the GUI.
+     */
+    public abstract Writer getWriter();
+
+    /**
+     * Returns a new special Reader that wraps the new connection Reader. The connection
+     * has been secured so the connection is using a new reader and writer. The debugger
+     * needs to wrap the new reader and writer to keep being notified of the connection
+     * traffic.
+     *
+     * @return a new special Reader that wraps the new connection Reader.
+     */
+    public abstract Reader newConnectionReader(Reader reader);
+
+    /**
+     * Returns a new special Writer that wraps the new connection Writer. The connection
+     * has been secured so the connection is using a new reader and writer. The debugger
+     * needs to wrap the new reader and writer to keep being notified of the connection
+     * traffic.
+     *
+     * @return a new special Writer that wraps the new connection Writer.
+     */
+    public abstract Writer newConnectionWriter(Writer writer);
+
+    /**
+     * Returns the thread that will listen for all incoming packets and write them to the GUI. 
+     * This is what we call "interpreted" packet data, since it's the packet data as Smack sees 
+     * it and not as it's coming in as raw XML.
+     * 
+     * @return the PacketListener that will listen for all incoming packets and write them to 
+     * the GUI
+     */
+    public abstract PacketListener getReaderListener();
+
+    /**
+     * Returns the thread that will listen for all outgoing packets and write them to the GUI. 
+     * 
+     * @return the PacketListener that will listen for all sent packets and write them to 
+     * the GUI
+     */
+    public abstract PacketListener getWriterListener();
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/debugger/package.html b/src/org/jivesoftware/smack/debugger/package.html
new file mode 100644
index 0000000..afb861f
--- /dev/null
+++ b/src/org/jivesoftware/smack/debugger/package.html
@@ -0,0 +1 @@
+<body>Core debugger functionality.</body>
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/filter/AndFilter.java b/src/org/jivesoftware/smack/filter/AndFilter.java
new file mode 100644
index 0000000..847b618
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/AndFilter.java
@@ -0,0 +1,91 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Implements the logical AND operation over two or more packet filters.
+ * In other words, packets pass this filter if they pass <b>all</b> of the filters.
+ *
+ * @author Matt Tucker
+ */
+public class AndFilter implements PacketFilter {
+
+    /**
+     * The list of filters.
+     */
+    private List<PacketFilter> filters = new ArrayList<PacketFilter>();
+
+    /**
+     * Creates an empty AND filter. Filters should be added using the
+     * {@link #addFilter(PacketFilter)} method.
+     */
+    public AndFilter() {
+
+    }
+
+    /**
+     * Creates an AND filter using the specified filters.
+     *
+     * @param filters the filters to add.
+     */
+    public AndFilter(PacketFilter... filters) {
+        if (filters == null) {
+            throw new IllegalArgumentException("Parameter cannot be null.");
+        }
+        for(PacketFilter filter : filters) {
+            if(filter == null) {
+                throw new IllegalArgumentException("Parameter cannot be null.");
+            }
+            this.filters.add(filter);
+        }
+    }
+
+    /**
+     * Adds a filter to the filter list for the AND operation. A packet
+     * will pass the filter if all of the filters in the list accept it.
+     *
+     * @param filter a filter to add to the filter list.
+     */
+    public void addFilter(PacketFilter filter) {
+        if (filter == null) {
+            throw new IllegalArgumentException("Parameter cannot be null.");
+        }
+        filters.add(filter);
+    }
+
+    public boolean accept(Packet packet) {
+        for (PacketFilter filter : filters) {
+            if (!filter.accept(packet)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public String toString() {
+        return filters.toString();
+    }
+}
diff --git a/src/org/jivesoftware/smack/filter/FromContainsFilter.java b/src/org/jivesoftware/smack/filter/FromContainsFilter.java
new file mode 100644
index 0000000..f8e9e97
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/FromContainsFilter.java
@@ -0,0 +1,54 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003 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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets where the "from" field contains a specified value.
+ *
+ * @author Matt Tucker
+ */
+public class FromContainsFilter implements PacketFilter {
+
+    private String from;
+
+    /**
+     * Creates a "from" contains filter using the "from" field part.
+     *
+     * @param from the from field value the packet must contain.
+     */
+    public FromContainsFilter(String from) {
+        if (from == null) {
+            throw new IllegalArgumentException("Parameter cannot be null.");
+        }
+        this.from = from.toLowerCase();
+    }
+
+    public boolean accept(Packet packet) {
+        if (packet.getFrom() == null) {
+            return false;
+        }
+        else {
+            return packet.getFrom().toLowerCase().indexOf(from) != -1;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/filter/FromMatchesFilter.java b/src/org/jivesoftware/smack/filter/FromMatchesFilter.java
new file mode 100644
index 0000000..e1dfa6c
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/FromMatchesFilter.java
@@ -0,0 +1,75 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * Filter for packets where the "from" field exactly matches a specified JID. If the specified
+ * address is a bare JID then the filter will match any address whose bare JID matches the
+ * specified JID. But if the specified address is a full JID then the filter will only match
+ * if the sender of the packet matches the specified resource.
+ *
+ * @author Gaston Dombiak
+ */
+public class FromMatchesFilter implements PacketFilter {
+
+    private String address;
+    /**
+     * Flag that indicates if the checking will be done against bare JID addresses or full JIDs.
+     */
+    private boolean matchBareJID = false;
+
+    /**
+     * Creates a "from" filter using the "from" field part. If the specified address is a bare JID
+     * then the filter will match any address whose bare JID matches the specified JID. But if the
+     * specified address is a full JID then the filter will only match if the sender of the packet
+     * matches the specified resource.
+     *
+     * @param address the from field value the packet must match. Could be a full or bare JID.
+     */
+    public FromMatchesFilter(String address) {
+        if (address == null) {
+            throw new IllegalArgumentException("Parameter cannot be null.");
+        }
+        this.address = address.toLowerCase();
+        matchBareJID = "".equals(StringUtils.parseResource(address));
+    }
+
+    public boolean accept(Packet packet) {
+        if (packet.getFrom() == null) {
+            return false;
+        }
+        else if (matchBareJID) {
+            // Check if the bare JID of the sender of the packet matches the specified JID
+            return packet.getFrom().toLowerCase().startsWith(address);
+        }
+        else {
+            // Check if the full JID of the sender of the packet matches the specified JID
+            return address.equals(packet.getFrom().toLowerCase());
+        }
+    }
+
+    public String toString() {
+        return "FromMatchesFilter: " + address;
+    }
+}
diff --git a/src/org/jivesoftware/smack/filter/IQTypeFilter.java b/src/org/jivesoftware/smack/filter/IQTypeFilter.java
new file mode 100644
index 0000000..dbab1c3
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/IQTypeFilter.java
@@ -0,0 +1,48 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.filter;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+

+/**

+ * A filter for IQ packet types. Returns true only if the packet is an IQ packet

+ * and it matches the type provided in the constructor.

+ * 

+ * @author Alexander Wenckus

+ * 

+ */

+public class IQTypeFilter implements PacketFilter {

+

+	private IQ.Type type;

+

+	public IQTypeFilter(IQ.Type type) {

+		this.type = type;

+	}

+

+	/*

+	 * (non-Javadoc)

+	 * 

+	 * @see org.jivesoftware.smack.filter.PacketFilter#accept(org.jivesoftware.smack.packet.Packet)

+	 */

+	public boolean accept(Packet packet) {

+		return (packet instanceof IQ && ((IQ) packet).getType().equals(type));

+	}

+}

diff --git a/src/org/jivesoftware/smack/filter/MessageTypeFilter.java b/src/org/jivesoftware/smack/filter/MessageTypeFilter.java
new file mode 100644
index 0000000..a3430ec
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/MessageTypeFilter.java
@@ -0,0 +1,54 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets of a specific type of Message (e.g. CHAT).
+ * 
+ * @see org.jivesoftware.smack.packet.Message.Type
+ * @author Ward Harold
+ */
+public class MessageTypeFilter implements PacketFilter {
+
+    private final Message.Type type;
+
+    /**
+     * Creates a new message type filter using the specified message type.
+     * 
+     * @param type the message type.
+     */
+    public MessageTypeFilter(Message.Type type) {
+        this.type = type;
+    }
+
+    public boolean accept(Packet packet) {
+        if (!(packet instanceof Message)) {
+            return false;
+        }
+        else {
+            return ((Message) packet).getType().equals(this.type);
+        }
+    }
+
+}
diff --git a/src/org/jivesoftware/smack/filter/NotFilter.java b/src/org/jivesoftware/smack/filter/NotFilter.java
new file mode 100644
index 0000000..59537d0
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/NotFilter.java
@@ -0,0 +1,50 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Implements the logical NOT operation on a packet filter. In other words, packets
+ * pass this filter if they do not pass the supplied filter.
+ *
+ * @author Matt Tucker
+ */
+public class NotFilter implements PacketFilter {
+
+    private PacketFilter filter;
+
+    /**
+     * Creates a NOT filter using the specified filter.
+     *
+     * @param filter the filter.
+     */
+    public NotFilter(PacketFilter filter) {
+        if (filter == null) {
+            throw new IllegalArgumentException("Parameter cannot be null.");
+        }
+        this.filter = filter;
+    }
+
+    public boolean accept(Packet packet) {
+        return !filter.accept(packet);
+    }
+}
diff --git a/src/org/jivesoftware/smack/filter/OrFilter.java b/src/org/jivesoftware/smack/filter/OrFilter.java
new file mode 100644
index 0000000..4c34fd0
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/OrFilter.java
@@ -0,0 +1,103 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Implements the logical OR operation over two or more packet filters. In
+ * other words, packets pass this filter if they pass <b>any</b> of the filters.
+ *
+ * @author Matt Tucker
+ */
+public class OrFilter implements PacketFilter {
+
+    /**
+     * The current number of elements in the filter.
+     */
+    private int size;
+
+    /**
+     * The list of filters.
+     */
+    private PacketFilter [] filters;
+
+    /**
+     * Creates an empty OR filter. Filters should be added using the
+     * {@link #addFilter(PacketFilter)} method.
+     */
+    public OrFilter() {
+        size = 0;
+        filters = new PacketFilter[3];
+    }
+
+    /**
+     * Creates an OR filter using the two specified filters.
+     *
+     * @param filter1 the first packet filter.
+     * @param filter2 the second packet filter.
+     */
+    public OrFilter(PacketFilter filter1, PacketFilter filter2) {
+        if (filter1 == null || filter2 == null) {
+            throw new IllegalArgumentException("Parameters cannot be null.");
+        }
+        size = 2;
+        filters = new PacketFilter[2];
+        filters[0] = filter1;
+        filters[1] = filter2;
+    }
+
+    /**
+     * Adds a filter to the filter list for the OR operation. A packet
+     * will pass the filter if any filter in the list accepts it.
+     *
+     * @param filter a filter to add to the filter list.
+     */
+    public void addFilter(PacketFilter filter) {
+        if (filter == null) {
+            throw new IllegalArgumentException("Parameter cannot be null.");
+        }
+        // If there is no more room left in the filters array, expand it.
+        if (size == filters.length) {
+            PacketFilter [] newFilters = new PacketFilter[filters.length+2];
+            for (int i=0; i<filters.length; i++) {
+                newFilters[i] = filters[i];
+            }
+            filters = newFilters;
+        }
+        // Add the new filter to the array.
+        filters[size] = filter;
+        size++;
+    }
+
+    public boolean accept(Packet packet) {
+        for (int i=0; i<size; i++) {
+            if (filters[i].accept(packet)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public String toString() {
+        return filters.toString();
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java b/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java
new file mode 100644
index 0000000..3cdc09c
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/PacketExtensionFilter.java
@@ -0,0 +1,61 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets with a particular type of packet extension.
+ *
+ * @author Matt Tucker
+ */
+public class PacketExtensionFilter implements PacketFilter {
+
+    private String elementName;
+    private String namespace;
+
+    /**
+     * Creates a new packet extension filter. Packets will pass the filter if
+     * they have a packet extension that matches the specified element name
+     * and namespace.
+     *
+     * @param elementName the XML element name of the packet extension.
+     * @param namespace the XML namespace of the packet extension.
+     */
+    public PacketExtensionFilter(String elementName, String namespace) {
+        this.elementName = elementName;
+        this.namespace = namespace;
+    }
+
+    /**
+     * Creates a new packet extension filter. Packets will pass the filter if they have a packet
+     * extension that matches the specified namespace.
+     *
+     * @param namespace the XML namespace of the packet extension.
+     */
+    public PacketExtensionFilter(String namespace) {
+        this(null, namespace);
+    }
+
+    public boolean accept(Packet packet) {
+        return packet.getExtension(elementName, namespace) != null;
+    }
+}
diff --git a/src/org/jivesoftware/smack/filter/PacketFilter.java b/src/org/jivesoftware/smack/filter/PacketFilter.java
new file mode 100644
index 0000000..634e68e
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/PacketFilter.java
@@ -0,0 +1,63 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Defines a way to filter packets for particular attributes. Packet filters are
+ * used when constructing packet listeners or collectors -- the filter defines
+ * what packets match the criteria of the collector or listener for further
+ * packet processing.<p>
+ *
+ * Several pre-defined filters are defined. These filters can be logically combined
+ * for more complex packet filtering by using the
+ * {@link org.jivesoftware.smack.filter.AndFilter AndFilter} and
+ * {@link org.jivesoftware.smack.filter.OrFilter OrFilter} filters. It's also possible
+ * to define your own filters by implementing this interface. The code example below
+ * creates a trivial filter for packets with a specific ID.
+ *
+ * <pre>
+ * // Use an anonymous inner class to define a packet filter that returns
+ * // all packets that have a packet ID of "RS145".
+ * PacketFilter myFilter = new PacketFilter() {
+ *     public boolean accept(Packet packet) {
+ *         return "RS145".equals(packet.getPacketID());
+ *     }
+ * };
+ * // Create a new packet collector using the filter we created.
+ * PacketCollector myCollector = packetReader.createPacketCollector(myFilter);
+ * </pre>
+ *
+ * @see org.jivesoftware.smack.PacketCollector
+ * @see org.jivesoftware.smack.PacketListener
+ * @author Matt Tucker
+ */
+public interface PacketFilter {
+
+    /**
+     * Tests whether or not the specified packet should pass the filter.
+     *
+     * @param packet the packet to test.
+     * @return true if and only if <tt>packet</tt> passes the filter.
+     */
+    public boolean accept(Packet packet);
+}
diff --git a/src/org/jivesoftware/smack/filter/PacketIDFilter.java b/src/org/jivesoftware/smack/filter/PacketIDFilter.java
new file mode 100644
index 0000000..8d68201
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/PacketIDFilter.java
@@ -0,0 +1,53 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets with a particular packet ID.
+ *
+ * @author Matt Tucker
+ */
+public class PacketIDFilter implements PacketFilter {
+
+    private String packetID;
+
+    /**
+     * Creates a new packet ID filter using the specified packet ID.
+     *
+     * @param packetID the packet ID to filter for.
+     */
+    public PacketIDFilter(String packetID) {
+        if (packetID == null) {
+            throw new IllegalArgumentException("Packet ID cannot be null.");
+        }
+        this.packetID = packetID;
+    }
+
+    public boolean accept(Packet packet) {
+        return packetID.equals(packet.getPacketID());
+    }
+
+    public String toString() {
+        return "PacketIDFilter by id: " + packetID;
+    }
+}
diff --git a/src/org/jivesoftware/smack/filter/PacketTypeFilter.java b/src/org/jivesoftware/smack/filter/PacketTypeFilter.java
new file mode 100644
index 0000000..19c573f
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/PacketTypeFilter.java
@@ -0,0 +1,61 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets of a particular type. The type is given as a Class object, so
+ * example types would:
+ * <ul>
+ *      <li><tt>Message.class</tt>
+ *      <li><tt>IQ.class</tt>
+ *      <li><tt>Presence.class</tt>
+ * </ul>
+ *
+ * @author Matt Tucker
+ */
+public class PacketTypeFilter implements PacketFilter {
+
+    Class<? extends Packet> packetType;
+
+    /**
+     * Creates a new packet type filter that will filter for packets that are the
+     * same type as <tt>packetType</tt>.
+     *
+     * @param packetType the Class type.
+     */
+    public PacketTypeFilter(Class<? extends Packet> packetType) {
+        // Ensure the packet type is a sub-class of Packet.
+        if (!Packet.class.isAssignableFrom(packetType)) {
+            throw new IllegalArgumentException("Packet type must be a sub-class of Packet.");
+        }
+        this.packetType = packetType;
+    }
+
+    public boolean accept(Packet packet) {
+        return packetType.isInstance(packet);
+    }
+
+    public String toString() {
+        return "PacketTypeFilter: " + packetType.getName();
+    }
+}
diff --git a/src/org/jivesoftware/smack/filter/ThreadFilter.java b/src/org/jivesoftware/smack/filter/ThreadFilter.java
new file mode 100644
index 0000000..8ba8b2e
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/ThreadFilter.java
@@ -0,0 +1,50 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Message;
+
+/**
+ * Filters for message packets with a particular thread value.
+ *
+ * @author Matt Tucker
+ */
+public class ThreadFilter implements PacketFilter {
+
+    private String thread;
+
+    /**
+     * Creates a new thread filter using the specified thread value.
+     *
+     * @param thread the thread value to filter for.
+     */
+    public ThreadFilter(String thread) {
+        if (thread == null) {
+            throw new IllegalArgumentException("Thread cannot be null.");
+        }
+        this.thread = thread;
+    }
+
+    public boolean accept(Packet packet) {
+        return packet instanceof Message && thread.equals(((Message) packet).getThread());
+    }
+}
diff --git a/src/org/jivesoftware/smack/filter/ToContainsFilter.java b/src/org/jivesoftware/smack/filter/ToContainsFilter.java
new file mode 100644
index 0000000..8069fcc
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/ToContainsFilter.java
@@ -0,0 +1,55 @@
+/**
+ * $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.filter;
+
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * Filters for packets where the "to" field contains a specified value. For example,
+ * the filter could be used to listen for all packets sent to a group chat nickname.
+ *
+ * @author Matt Tucker
+ */
+public class ToContainsFilter implements PacketFilter {
+
+    private String to;
+
+    /**
+     * Creates a "to" contains filter using the "to" field part.
+     *
+     * @param to the to field value the packet must contain.
+     */
+    public ToContainsFilter(String to) {
+        if (to == null) {
+            throw new IllegalArgumentException("Parameter cannot be null.");
+        }
+        this.to = to.toLowerCase();
+    }
+
+    public boolean accept(Packet packet) {
+        if (packet.getTo() == null) {
+            return false;
+        }
+        else {
+            return packet.getTo().toLowerCase().indexOf(to) != -1;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/filter/package.html b/src/org/jivesoftware/smack/filter/package.html
new file mode 100644
index 0000000..8b3fe80
--- /dev/null
+++ b/src/org/jivesoftware/smack/filter/package.html
@@ -0,0 +1 @@
+<body>Allows {@link org.jivesoftware.smack.PacketCollector} and {@link org.jivesoftware.smack.PacketListener} instances to filter for packets with particular attributes.</body>
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/package.html b/src/org/jivesoftware/smack/package.html
new file mode 100644
index 0000000..2758d78
--- /dev/null
+++ b/src/org/jivesoftware/smack/package.html
@@ -0,0 +1 @@
+<body>Core classes of the Smack API.</body>
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/packet/Authentication.java b/src/org/jivesoftware/smack/packet/Authentication.java
new file mode 100644
index 0000000..a47c079
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Authentication.java
@@ -0,0 +1,186 @@
+/**
+ * $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.packet;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * Authentication packet, which can be used to login to a XMPP server as well
+ * as discover login information from the server.
+ */
+public class Authentication extends IQ {
+
+    private String username = null;
+    private String password = null;
+    private String digest = null;
+    private String resource = null;
+
+    /**
+     * Create a new authentication packet. By default, the packet will be in
+     * "set" mode in order to perform an actual authentication with the server.
+     * In order to send a "get" request to get the available authentication
+     * modes back from the server, change the type of the IQ packet to "get":
+     * <p/>
+     * <p><tt>setType(IQ.Type.GET);</tt>
+     */
+    public Authentication() {
+        setType(IQ.Type.SET);
+    }
+
+    /**
+     * Returns the username, or <tt>null</tt> if the username hasn't been sent.
+     *
+     * @return the username.
+     */
+    public String getUsername() {
+        return username;
+    }
+
+    /**
+     * Sets the username.
+     *
+     * @param username the username.
+     */
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    /**
+     * Returns the plain text password or <tt>null</tt> if the password hasn't
+     * been set.
+     *
+     * @return the password.
+     */
+    public String getPassword() {
+        return password;
+    }
+
+    /**
+     * Sets the plain text password.
+     *
+     * @param password the password.
+     */
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    /**
+     * Returns the password digest or <tt>null</tt> if the digest hasn't
+     * been set. Password digests offer a more secure alternative for
+     * authentication compared to plain text. The digest is the hex-encoded
+     * SHA-1 hash of the connection ID plus the user's password. If the
+     * digest and password are set, digest authentication will be used. If
+     * only one value is set, the respective authentication mode will be used.
+     *
+     * @return the digest of the user's password.
+     */
+    public String getDigest() {
+        return digest;
+    }
+
+    /**
+     * Sets the digest value using a connection ID and password. Password
+     * digests offer a more secure alternative for authentication compared to
+     * plain text. The digest is the hex-encoded SHA-1 hash of the connection ID
+     * plus the user's password. If the digest and password are set, digest
+     * authentication will be used. If only one value is set, the respective
+     * authentication mode will be used.
+     *
+     * @param connectionID the connection ID.
+     * @param password     the password.
+     * @see org.jivesoftware.smack.Connection#getConnectionID()
+     */
+    public void setDigest(String connectionID, String password) {
+        this.digest = StringUtils.hash(connectionID + password);
+    }
+
+    /**
+     * Sets the digest value directly. Password digests offer a more secure
+     * alternative for authentication compared to plain text. The digest is
+     * the hex-encoded SHA-1 hash of the connection ID plus the user's password.
+     * If the digest and password are set, digest authentication will be used.
+     * If only one value is set, the respective authentication mode will be used.
+     *
+     * @param digest the digest, which is the SHA-1 hash of the connection ID
+     *               the user's password, encoded as hex.
+     * @see org.jivesoftware.smack.Connection#getConnectionID()
+     */
+    public void setDigest(String digest) {
+        this.digest = digest;
+    }
+
+    /**
+     * Returns the resource or <tt>null</tt> if the resource hasn't been set.
+     *
+     * @return the resource.
+     */
+    public String getResource() {
+        return resource;
+    }
+
+    /**
+     * Sets the resource.
+     *
+     * @param resource the resource.
+     */
+    public void setResource(String resource) {
+        this.resource = resource;
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"jabber:iq:auth\">");
+        if (username != null) {
+            if (username.equals("")) {
+                buf.append("<username/>");
+            }
+            else {
+                buf.append("<username>").append(username).append("</username>");
+            }
+        }
+        if (digest != null) {
+            if (digest.equals("")) {
+                buf.append("<digest/>");
+            }
+            else {
+                buf.append("<digest>").append(digest).append("</digest>");
+            }
+        }
+        if (password != null && digest == null) {
+            if (password.equals("")) {
+                buf.append("<password/>");
+            }
+            else {
+                buf.append("<password>").append(StringUtils.escapeForXML(password)).append("</password>");
+            }
+        }
+        if (resource != null) {
+            if (resource.equals("")) {
+                buf.append("<resource/>");
+            }
+            else {
+                buf.append("<resource>").append(resource).append("</resource>");
+            }
+        }
+        buf.append("</query>");
+        return buf.toString();
+    }
+}
diff --git a/src/org/jivesoftware/smack/packet/Bind.java b/src/org/jivesoftware/smack/packet/Bind.java
new file mode 100644
index 0000000..07cd193
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Bind.java
@@ -0,0 +1,71 @@
+/**

+ * $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.packet;

+

+/**

+ * IQ packet used by Smack to bind a resource and to obtain the jid assigned by the server.

+ * There are two ways to bind a resource. One is simply sending an empty Bind packet where the

+ * server will assign a new resource for this connection. The other option is to set a desired

+ * resource but the server may return a modified version of the sent resource.<p>

+ *

+ * For more information refer to the following

+ * <a href=http://www.xmpp.org/specs/rfc3920.html#bind>link</a>. 

+ *

+ * @author Gaston Dombiak

+ */

+public class Bind extends IQ {

+

+    private String resource = null;

+    private String jid = null;

+

+    public Bind() {

+        setType(IQ.Type.SET);

+    }

+

+    public String getResource() {

+        return resource;

+    }

+

+    public void setResource(String resource) {

+        this.resource = resource;

+    }

+

+    public String getJid() {

+        return jid;

+    }

+

+    public void setJid(String jid) {

+        this.jid = jid;

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<bind xmlns=\"urn:ietf:params:xml:ns:xmpp-bind\">");

+        if (resource != null) {

+            buf.append("<resource>").append(resource).append("</resource>");

+        }

+        if (jid != null) {

+            buf.append("<jid>").append(jid).append("</jid>");

+        }

+        buf.append("</bind>");

+        return buf.toString();

+    }

+}

diff --git a/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java b/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java
new file mode 100644
index 0000000..6cc7934
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/DefaultPacketExtension.java
@@ -0,0 +1,133 @@
+/**
+ * $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.packet;
+
+import java.util.*;
+
+/**
+ * Default implementation of the PacketExtension interface. Unless a PacketExtensionProvider
+ * is registered with {@link org.jivesoftware.smack.provider.ProviderManager ProviderManager},
+ * instances of this class will be returned when getting packet extensions.<p>
+ *
+ * This class provides a very simple representation of an XML sub-document. Each element
+ * is a key in a Map with its CDATA being the value. For example, given the following
+ * XML sub-document:
+ *
+ * <pre>
+ * &lt;foo xmlns="http://bar.com"&gt;
+ *     &lt;color&gt;blue&lt;/color&gt;
+ *     &lt;food&gt;pizza&lt;/food&gt;
+ * &lt;/foo&gt;</pre>
+ *
+ * In this case, getValue("color") would return "blue", and getValue("food") would
+ * return "pizza". This parsing mechanism mechanism is very simplistic and will not work
+ * as desired in all cases (for example, if some of the elements have attributes. In those
+ * cases, a custom PacketExtensionProvider should be used.
+ *
+ * @author Matt Tucker
+ */
+public class DefaultPacketExtension implements PacketExtension {
+
+    private String elementName;
+    private String namespace;
+    private Map<String,String> map;
+
+    /**
+     * Creates a new generic packet extension.
+     *
+     * @param elementName the name of the element of the XML sub-document.
+     * @param namespace the namespace of the element.
+     */
+    public DefaultPacketExtension(String elementName, String namespace) {
+        this.elementName = elementName;
+        this.namespace = namespace;
+    }
+
+     /**
+     * Returns the XML element name of the extension sub-packet root element.
+     *
+     * @return the XML element name of the packet extension.
+     */
+    public String getElementName() {
+        return elementName;
+    }
+
+    /**
+     * Returns the XML namespace of the extension sub-packet root element.
+     *
+     * @return the XML namespace of the packet extension.
+     */
+    public String getNamespace() {
+        return namespace;
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\">");
+        for (String name : getNames()) {
+            String value = getValue(name);
+            buf.append("<").append(name).append(">");
+            buf.append(value);
+            buf.append("</").append(name).append(">");
+        }
+        buf.append("</").append(elementName).append(">");
+        return buf.toString();
+    }
+
+    /**
+     * Returns an unmodifiable collection of the names that can be used to get
+     * values of the packet extension.
+     *
+     * @return the names.
+     */
+    public synchronized Collection<String> getNames() {
+        if (map == null) {
+            return Collections.emptySet();
+        }
+        return Collections.unmodifiableSet(new HashMap<String,String>(map).keySet());
+    }
+
+    /**
+     * Returns a packet extension value given a name.
+     *
+     * @param name the name.
+     * @return the value.
+     */
+    public synchronized String getValue(String name) {
+        if (map == null) {
+            return null;
+        }
+        return map.get(name);
+    }
+
+    /**
+     * Sets a packet extension value using the given name.
+     *
+     * @param name the name.
+     * @param value the value.
+     */
+    public synchronized void setValue(String name, String value) {
+        if (map == null) {
+            map = new HashMap<String,String>();
+        }
+        map.put(name, value);
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/packet/IQ.java b/src/org/jivesoftware/smack/packet/IQ.java
new file mode 100644
index 0000000..8e1f7d4
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/IQ.java
@@ -0,0 +1,244 @@
+/**
+ * $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.packet;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * The base IQ (Info/Query) packet. IQ packets are used to get and set information
+ * on the server, including authentication, roster operations, and creating
+ * accounts. Each IQ packet has a specific type that indicates what type of action
+ * is being taken: "get", "set", "result", or "error".<p>
+ *
+ * IQ packets can contain a single child element that exists in a specific XML
+ * namespace. The combination of the element name and namespace determines what
+ * type of IQ packet it is. Some example IQ subpacket snippets:<ul>
+ *
+ *  <li>&lt;query xmlns="jabber:iq:auth"&gt; -- an authentication IQ.
+ *  <li>&lt;query xmlns="jabber:iq:private"&gt; -- a private storage IQ.
+ *  <li>&lt;pubsub xmlns="http://jabber.org/protocol/pubsub"&gt; -- a pubsub IQ.
+ * </ul>
+ *
+ * @author Matt Tucker
+ */
+public abstract class IQ extends Packet {
+
+    private Type type = Type.GET;
+
+    public IQ() {
+        super();
+    }
+
+    public IQ(IQ iq) {
+        super(iq);
+        type = iq.getType();
+    }
+    /**
+     * Returns the type of the IQ packet.
+     *
+     * @return the type of the IQ packet.
+     */
+    public Type getType() {
+        return type;
+    }
+
+    /**
+     * Sets the type of the IQ packet.
+     *
+     * @param type the type of the IQ packet.
+     */
+    public void setType(Type type) {
+        if (type == null) {
+            this.type = Type.GET;
+        }
+        else {
+            this.type = type;
+        }
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<iq ");
+        if (getPacketID() != null) {
+            buf.append("id=\"" + getPacketID() + "\" ");
+        }
+        if (getTo() != null) {
+            buf.append("to=\"").append(StringUtils.escapeForXML(getTo())).append("\" ");
+        }
+        if (getFrom() != null) {
+            buf.append("from=\"").append(StringUtils.escapeForXML(getFrom())).append("\" ");
+        }
+        if (type == null) {
+            buf.append("type=\"get\">");
+        }
+        else {
+            buf.append("type=\"").append(getType()).append("\">");
+        }
+        // Add the query section if there is one.
+        String queryXML = getChildElementXML();
+        if (queryXML != null) {
+            buf.append(queryXML);
+        }
+        // Add the error sub-packet, if there is one.
+        XMPPError error = getError();
+        if (error != null) {
+            buf.append(error.toXML());
+        }
+        buf.append("</iq>");
+        return buf.toString();
+    }
+
+    /**
+     * Returns the sub-element XML section of the IQ packet, or <tt>null</tt> if there
+     * isn't one. Packet extensions <b>must</b> be included, if any are defined.<p>
+     *
+     * Extensions of this class must override this method.
+     *
+     * @return the child element section of the IQ XML.
+     */
+    public abstract String getChildElementXML();
+
+    /**
+     * Convenience method to create a new empty {@link Type#RESULT IQ.Type.RESULT}
+     * IQ based on a {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET}
+     * IQ. The new packet will be initialized with:<ul>
+     *      <li>The sender set to the recipient of the originating IQ.
+     *      <li>The recipient set to the sender of the originating IQ.
+     *      <li>The type set to {@link Type#RESULT IQ.Type.RESULT}.
+     *      <li>The id set to the id of the originating IQ.
+     *      <li>No child element of the IQ element.
+     * </ul>
+     *
+     * @param iq the {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET} IQ packet.
+     * @throws IllegalArgumentException if the IQ packet does not have a type of
+     *      {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET}.
+     * @return a new {@link Type#RESULT IQ.Type.RESULT} IQ based on the originating IQ.
+     */
+    public static IQ createResultIQ(final IQ request) {
+        if (!(request.getType() == Type.GET || request.getType() == Type.SET)) {
+            throw new IllegalArgumentException(
+                    "IQ must be of type 'set' or 'get'. Original IQ: " + request.toXML());
+        }
+        final IQ result = new IQ() {
+            public String getChildElementXML() {
+                return null;
+            }
+        };
+        result.setType(Type.RESULT);
+        result.setPacketID(request.getPacketID());
+        result.setFrom(request.getTo());
+        result.setTo(request.getFrom());
+        return result;
+    }
+
+    /**
+     * Convenience method to create a new {@link Type#ERROR IQ.Type.ERROR} IQ
+     * based on a {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET}
+     * IQ. The new packet will be initialized with:<ul>
+     *      <li>The sender set to the recipient of the originating IQ.
+     *      <li>The recipient set to the sender of the originating IQ.
+     *      <li>The type set to {@link Type#ERROR IQ.Type.ERROR}.
+     *      <li>The id set to the id of the originating IQ.
+     *      <li>The child element contained in the associated originating IQ.
+     *      <li>The provided {@link XMPPError XMPPError}.
+     * </ul>
+     *
+     * @param iq the {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET} IQ packet.
+     * @param error the error to associate with the created IQ packet.
+     * @throws IllegalArgumentException if the IQ packet does not have a type of
+     *      {@link Type#GET IQ.Type.GET} or {@link Type#SET IQ.Type.SET}.
+     * @return a new {@link Type#ERROR IQ.Type.ERROR} IQ based on the originating IQ.
+     */
+    public static IQ createErrorResponse(final IQ request, final XMPPError error) {
+        if (!(request.getType() == Type.GET || request.getType() == Type.SET)) {
+            throw new IllegalArgumentException(
+                    "IQ must be of type 'set' or 'get'. Original IQ: " + request.toXML());
+        }
+        final IQ result = new IQ() {
+            public String getChildElementXML() {
+                return request.getChildElementXML();
+            }
+        };
+        result.setType(Type.ERROR);
+        result.setPacketID(request.getPacketID());
+        result.setFrom(request.getTo());
+        result.setTo(request.getFrom());
+        result.setError(error);
+        return result;
+    }
+
+    /**
+     * A class to represent the type of the IQ packet. The types are:
+     *
+     * <ul>
+     *      <li>IQ.Type.GET
+     *      <li>IQ.Type.SET
+     *      <li>IQ.Type.RESULT
+     *      <li>IQ.Type.ERROR
+     * </ul>
+     */
+    public static class Type {
+
+        public static final Type GET = new Type("get");
+        public static final Type SET = new Type("set");
+        public static final Type RESULT = new Type("result");
+        public static final Type ERROR = new Type("error");
+
+        /**
+         * Converts a String into the corresponding types. Valid String values
+         * that can be converted to types are: "get", "set", "result", and "error".
+         *
+         * @param type the String value to covert.
+         * @return the corresponding Type.
+         */
+        public static Type fromString(String type) {
+            if (type == null) {
+                return null;
+            }
+            type = type.toLowerCase();
+            if (GET.toString().equals(type)) {
+                return GET;
+            }
+            else if (SET.toString().equals(type)) {
+                return SET;
+            }
+            else if (ERROR.toString().equals(type)) {
+                return ERROR;
+            }
+            else if (RESULT.toString().equals(type)) {
+                return RESULT;
+            }
+            else {
+                return null;
+            }
+        }
+
+        private String value;
+
+        private Type(String value) {
+            this.value = value;
+        }
+
+        public String toString() {
+            return value;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/packet/Message.java b/src/org/jivesoftware/smack/packet/Message.java
new file mode 100644
index 0000000..d28a9f4
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Message.java
@@ -0,0 +1,672 @@
+/**
+ * $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.packet;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.*;
+
+/**
+ * Represents XMPP message packets. A message can be one of several types:
+ *
+ * <ul>
+ *      <li>Message.Type.NORMAL -- (Default) a normal text message used in email like interface.
+ *      <li>Message.Type.CHAT -- a typically short text message used in line-by-line chat interfaces.
+ *      <li>Message.Type.GROUP_CHAT -- a chat message sent to a groupchat server for group chats.
+ *      <li>Message.Type.HEADLINE -- a text message to be displayed in scrolling marquee displays.
+ *      <li>Message.Type.ERROR -- indicates a messaging error.
+ * </ul>
+ *
+ * For each message type, different message fields are typically used as follows:
+ * <p>
+ * <table border="1">
+ * <tr><td>&nbsp;</td><td colspan="5"><b>Message type</b></td></tr>
+ * <tr><td><i>Field</i></td><td><b>Normal</b></td><td><b>Chat</b></td><td><b>Group Chat</b></td><td><b>Headline</b></td><td><b>XMPPError</b></td></tr>
+ * <tr><td><i>subject</i></td> <td>SHOULD</td><td>SHOULD NOT</td><td>SHOULD NOT</td><td>SHOULD NOT</td><td>SHOULD NOT</td></tr>
+ * <tr><td><i>thread</i></td>  <td>OPTIONAL</td><td>SHOULD</td><td>OPTIONAL</td><td>OPTIONAL</td><td>SHOULD NOT</td></tr>
+ * <tr><td><i>body</i></td>    <td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD</td><td>SHOULD NOT</td></tr>
+ * <tr><td><i>error</i></td>   <td>MUST NOT</td><td>MUST NOT</td><td>MUST NOT</td><td>MUST NOT</td><td>MUST</td></tr>
+ * </table>
+ *
+ * @author Matt Tucker
+ */
+public class Message extends Packet {
+
+    private Type type = Type.normal;
+    private String thread = null;
+    private String language;
+
+    private final Set<Subject> subjects = new HashSet<Subject>();
+    private final Set<Body> bodies = new HashSet<Body>();
+
+    /**
+     * Creates a new, "normal" message.
+     */
+    public Message() {
+    }
+
+    /**
+     * Creates a new "normal" message to the specified recipient.
+     *
+     * @param to the recipient of the message.
+     */
+    public Message(String to) {
+        setTo(to);
+    }
+
+    /**
+     * Creates a new message of the specified type to a recipient.
+     *
+     * @param to the user to send the message to.
+     * @param type the message type.
+     */
+    public Message(String to, Type type) {
+        setTo(to);
+        this.type = type;
+    }
+
+    /**
+     * Returns the type of the message. If no type has been set this method will return {@link
+     * org.jivesoftware.smack.packet.Message.Type#normal}.
+     *
+     * @return the type of the message.
+     */
+    public Type getType() {
+        return type;
+    }
+
+    /**
+     * Sets the type of the message.
+     *
+     * @param type the type of the message.
+     * @throws IllegalArgumentException if null is passed in as the type
+     */
+    public void setType(Type type) {
+        if (type == null) {
+            throw new IllegalArgumentException("Type cannot be null.");
+        }
+        this.type = type;
+    }
+
+    /**
+     * Returns the default subject of the message, or null if the subject has not been set.
+     * The subject is a short description of message contents.
+     * <p>
+     * The default subject of a message is the subject that corresponds to the message's language.
+     * (see {@link #getLanguage()}) or if no language is set to the applications default
+     * language (see {@link Packet#getDefaultLanguage()}).
+     *
+     * @return the subject of the message.
+     */
+    public String getSubject() {
+        return getSubject(null);
+    }
+    
+    /**
+     * Returns the subject corresponding to the language. If the language is null, the method result
+     * will be the same as {@link #getSubject()}. Null will be returned if the language does not have
+     * a corresponding subject.
+     *
+     * @param language the language of the subject to return.
+     * @return the subject related to the passed in language.
+     */
+    public String getSubject(String language) {
+        Subject subject = getMessageSubject(language);
+        return subject == null ? null : subject.subject;
+    }
+    
+    private Subject getMessageSubject(String language) {
+        language = determineLanguage(language);
+        for (Subject subject : subjects) {
+            if (language.equals(subject.language)) {
+                return subject;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns a set of all subjects in this Message, including the default message subject accessible
+     * from {@link #getSubject()}.
+     *
+     * @return a collection of all subjects in this message.
+     */
+    public Collection<Subject> getSubjects() {
+        return Collections.unmodifiableCollection(subjects);
+    }
+
+    /**
+     * Sets the subject of the message. The subject is a short description of
+     * message contents.
+     *
+     * @param subject the subject of the message.
+     */
+    public void setSubject(String subject) {
+        if (subject == null) {
+            removeSubject(""); // use empty string because #removeSubject(null) is ambiguous 
+            return;
+        }
+        addSubject(null, subject);
+    }
+
+    /**
+     * Adds a subject with a corresponding language.
+     *
+     * @param language the language of the subject being added.
+     * @param subject the subject being added to the message.
+     * @return the new {@link org.jivesoftware.smack.packet.Message.Subject}
+     * @throws NullPointerException if the subject is null, a null pointer exception is thrown
+     */
+    public Subject addSubject(String language, String subject) {
+        language = determineLanguage(language);
+        Subject messageSubject = new Subject(language, subject);
+        subjects.add(messageSubject);
+        return messageSubject;
+    }
+
+    /**
+     * Removes the subject with the given language from the message.
+     *
+     * @param language the language of the subject which is to be removed
+     * @return true if a subject was removed and false if it was not.
+     */
+    public boolean removeSubject(String language) {
+        language = determineLanguage(language);
+        for (Subject subject : subjects) {
+            if (language.equals(subject.language)) {
+                return subjects.remove(subject);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Removes the subject from the message and returns true if the subject was removed.
+     *
+     * @param subject the subject being removed from the message.
+     * @return true if the subject was successfully removed and false if it was not.
+     */
+    public boolean removeSubject(Subject subject) {
+        return subjects.remove(subject);
+    }
+
+    /**
+     * Returns all the languages being used for the subjects, not including the default subject.
+     *
+     * @return the languages being used for the subjects.
+     */
+    public Collection<String> getSubjectLanguages() {
+        Subject defaultSubject = getMessageSubject(null);
+        List<String> languages = new ArrayList<String>();
+        for (Subject subject : subjects) {
+            if (!subject.equals(defaultSubject)) {
+                languages.add(subject.language);
+            }
+        }
+        return Collections.unmodifiableCollection(languages);
+    }
+
+    /**
+     * Returns the default body of the message, or null if the body has not been set. The body
+     * is the main message contents.
+     * <p>
+     * The default body of a message is the body that corresponds to the message's language.
+     * (see {@link #getLanguage()}) or if no language is set to the applications default
+     * language (see {@link Packet#getDefaultLanguage()}).
+     *
+     * @return the body of the message.
+     */
+    public String getBody() {
+        return getBody(null);
+    }
+
+    /**
+     * Returns the body corresponding to the language. If the language is null, the method result
+     * will be the same as {@link #getBody()}. Null will be returned if the language does not have
+     * a corresponding body.
+     *
+     * @param language the language of the body to return.
+     * @return the body related to the passed in language.
+     * @since 3.0.2
+     */
+    public String getBody(String language) {
+        Body body = getMessageBody(language);
+        return body == null ? null : body.message;
+    }
+    
+    private Body getMessageBody(String language) {
+        language = determineLanguage(language);
+        for (Body body : bodies) {
+            if (language.equals(body.language)) {
+                return body;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns a set of all bodies in this Message, including the default message body accessible
+     * from {@link #getBody()}.
+     *
+     * @return a collection of all bodies in this Message.
+     * @since 3.0.2
+     */
+    public Collection<Body> getBodies() {
+        return Collections.unmodifiableCollection(bodies);
+    }
+
+    /**
+     * Sets the body of the message. The body is the main message contents.
+     *
+     * @param body the body of the message.
+     */
+    public void setBody(String body) {
+        if (body == null) {
+            removeBody(""); // use empty string because #removeBody(null) is ambiguous
+            return;
+        }
+        addBody(null, body);
+    }
+
+    /**
+     * Adds a body with a corresponding language.
+     *
+     * @param language the language of the body being added.
+     * @param body the body being added to the message.
+     * @return the new {@link org.jivesoftware.smack.packet.Message.Body}
+     * @throws NullPointerException if the body is null, a null pointer exception is thrown
+     * @since 3.0.2
+     */
+    public Body addBody(String language, String body) {
+        language = determineLanguage(language);
+        Body messageBody = new Body(language, body);
+        bodies.add(messageBody);
+        return messageBody;
+    }
+
+    /**
+     * Removes the body with the given language from the message.
+     *
+     * @param language the language of the body which is to be removed
+     * @return true if a body was removed and false if it was not.
+     */
+    public boolean removeBody(String language) {
+        language = determineLanguage(language);
+        for (Body body : bodies) {
+            if (language.equals(body.language)) {
+                return bodies.remove(body);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Removes the body from the message and returns true if the body was removed.
+     *
+     * @param body the body being removed from the message.
+     * @return true if the body was successfully removed and false if it was not.
+     * @since 3.0.2
+     */
+    public boolean removeBody(Body body) {
+        return bodies.remove(body);
+    }
+
+    /**
+     * Returns all the languages being used for the bodies, not including the default body.
+     *
+     * @return the languages being used for the bodies.
+     * @since 3.0.2
+     */
+    public Collection<String> getBodyLanguages() {
+        Body defaultBody = getMessageBody(null);
+        List<String> languages = new ArrayList<String>();
+        for (Body body : bodies) {
+            if (!body.equals(defaultBody)) {
+                languages.add(body.language);
+            }
+        }
+        return Collections.unmodifiableCollection(languages);
+    }
+
+    /**
+     * Returns the thread id of the message, which is a unique identifier for a sequence
+     * of "chat" messages. If no thread id is set, <tt>null</tt> will be returned.
+     *
+     * @return the thread id of the message, or <tt>null</tt> if it doesn't exist.
+     */
+    public String getThread() {
+        return thread;
+    }
+
+    /**
+     * Sets the thread id of the message, which is a unique identifier for a sequence
+     * of "chat" messages.
+     *
+     * @param thread the thread id of the message.
+     */
+    public void setThread(String thread) {
+        this.thread = thread;
+    }
+
+    /**
+     * Returns the xml:lang of this Message.
+     *
+     * @return the xml:lang of this Message.
+     * @since 3.0.2
+     */
+    public String getLanguage() {
+        return language;
+    }
+
+    /**
+     * Sets the xml:lang of this Message.
+     *
+     * @param language the xml:lang of this Message.
+     * @since 3.0.2
+     */
+    public void setLanguage(String language) {
+        this.language = language;
+    }
+
+    private String determineLanguage(String language) {
+        
+        // empty string is passed by #setSubject() and #setBody() and is the same as null
+        language = "".equals(language) ? null : language;
+
+        // if given language is null check if message language is set
+        if (language == null && this.language != null) {
+            return this.language;
+        }
+        else if (language == null) {
+            return getDefaultLanguage();
+        }
+        else {
+            return language;
+        }
+        
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<message");
+        if (getXmlns() != null) {
+            buf.append(" xmlns=\"").append(getXmlns()).append("\"");
+        }
+        if (language != null) {
+            buf.append(" xml:lang=\"").append(getLanguage()).append("\"");
+        }
+        if (getPacketID() != null) {
+            buf.append(" id=\"").append(getPacketID()).append("\"");
+        }
+        if (getTo() != null) {
+            buf.append(" to=\"").append(StringUtils.escapeForXML(getTo())).append("\"");
+        }
+        if (getFrom() != null) {
+            buf.append(" from=\"").append(StringUtils.escapeForXML(getFrom())).append("\"");
+        }
+        if (type != Type.normal) {
+            buf.append(" type=\"").append(type).append("\"");
+        }
+        buf.append(">");
+        // Add the subject in the default language
+        Subject defaultSubject = getMessageSubject(null);
+        if (defaultSubject != null) {
+            buf.append("<subject>").append(StringUtils.escapeForXML(defaultSubject.subject)).append("</subject>");
+        }
+        // Add the subject in other languages
+        for (Subject subject : getSubjects()) {
+            // Skip the default language
+            if(subject.equals(defaultSubject))
+                continue;
+            buf.append("<subject xml:lang=\"").append(subject.language).append("\">");
+            buf.append(StringUtils.escapeForXML(subject.subject));
+            buf.append("</subject>");
+        }
+        // Add the body in the default language
+        Body defaultBody = getMessageBody(null);
+        if (defaultBody != null) {
+            buf.append("<body>").append(StringUtils.escapeForXML(defaultBody.message)).append("</body>");
+        }
+        // Add the bodies in other languages
+        for (Body body : getBodies()) {
+            // Skip the default language
+            if(body.equals(defaultBody))
+                continue;
+            buf.append("<body xml:lang=\"").append(body.getLanguage()).append("\">");
+            buf.append(StringUtils.escapeForXML(body.getMessage()));
+            buf.append("</body>");
+        }
+        if (thread != null) {
+            buf.append("<thread>").append(thread).append("</thread>");
+        }
+        // Append the error subpacket if the message type is an error.
+        if (type == Type.error) {
+            XMPPError error = getError();
+            if (error != null) {
+                buf.append(error.toXML());
+            }
+        }
+        // Add packet extensions, if any are defined.
+        buf.append(getExtensionsXML());
+        buf.append("</message>");
+        return buf.toString();
+    }
+
+
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Message message = (Message) o;
+
+        if(!super.equals(message)) { return false; }
+        if (bodies.size() != message.bodies.size() || !bodies.containsAll(message.bodies)) {
+            return false;
+        }
+        if (language != null ? !language.equals(message.language) : message.language != null) {
+            return false;
+        }
+        if (subjects.size() != message.subjects.size() || !subjects.containsAll(message.subjects)) {
+            return false;
+        }
+        if (thread != null ? !thread.equals(message.thread) : message.thread != null) {
+            return false;
+        }
+        return type == message.type;
+
+    }
+
+    public int hashCode() {
+        int result;
+        result = (type != null ? type.hashCode() : 0);
+        result = 31 * result + subjects.hashCode();
+        result = 31 * result + (thread != null ? thread.hashCode() : 0);
+        result = 31 * result + (language != null ? language.hashCode() : 0);
+        result = 31 * result + bodies.hashCode();
+        return result;
+    }
+
+    /**
+     * Represents a message subject, its language and the content of the subject.
+     */
+    public static class Subject {
+
+        private String subject;
+        private String language;
+
+        private Subject(String language, String subject) {
+            if (language == null) {
+                throw new NullPointerException("Language cannot be null.");
+            }
+            if (subject == null) {
+                throw new NullPointerException("Subject cannot be null.");
+            }
+            this.language = language;
+            this.subject = subject;
+        }
+
+        /**
+         * Returns the language of this message subject.
+         *
+         * @return the language of this message subject.
+         */
+        public String getLanguage() {
+            return language;
+        }
+
+        /**
+         * Returns the subject content.
+         *
+         * @return the content of the subject.
+         */
+        public String getSubject() {
+            return subject;
+        }
+
+
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + this.language.hashCode();
+            result = prime * result + this.subject.hashCode();
+            return result;
+        }
+
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            Subject other = (Subject) obj;
+            // simplified comparison because language and subject are always set
+            return this.language.equals(other.language) && this.subject.equals(other.subject);
+        }
+        
+    }
+
+    /**
+     * Represents a message body, its language and the content of the message.
+     */
+    public static class Body {
+
+        private String message;
+        private String language;
+
+        private Body(String language, String message) {
+            if (language == null) {
+                throw new NullPointerException("Language cannot be null.");
+            }
+            if (message == null) {
+                throw new NullPointerException("Message cannot be null.");
+            }
+            this.language = language;
+            this.message = message;
+        }
+
+        /**
+         * Returns the language of this message body.
+         *
+         * @return the language of this message body.
+         */
+        public String getLanguage() {
+            return language;
+        }
+
+        /**
+         * Returns the message content.
+         *
+         * @return the content of the message.
+         */
+        public String getMessage() {
+            return message;
+        }
+
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + this.language.hashCode();
+            result = prime * result + this.message.hashCode();
+            return result;
+        }
+
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            Body other = (Body) obj;
+            // simplified comparison because language and message are always set
+            return this.language.equals(other.language) && this.message.equals(other.message);
+        }
+        
+    }
+
+    /**
+     * Represents the type of a message.
+     */
+    public enum Type {
+
+        /**
+         * (Default) a normal text message used in email like interface.
+         */
+        normal,
+
+        /**
+         * Typically short text message used in line-by-line chat interfaces.
+         */
+        chat,
+
+        /**
+         * Chat message sent to a groupchat server for group chats.
+         */
+        groupchat,
+
+        /**
+         * Text message to be displayed in scrolling marquee displays.
+         */
+        headline,
+
+        /**
+         * indicates a messaging error.
+         */
+        error;
+
+        public static Type fromString(String name) {
+            try {
+                return Type.valueOf(name);
+            }
+            catch (Exception e) {
+                return normal;
+            }
+        }
+
+    }
+}
diff --git a/src/org/jivesoftware/smack/packet/Packet.java b/src/org/jivesoftware/smack/packet/Packet.java
new file mode 100644
index 0000000..3f1185e
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Packet.java
@@ -0,0 +1,509 @@
+/**
+ * $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.packet;
+
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * Base class for XMPP packets. Every packet has a unique ID (which is automatically
+ * generated, but can be overriden). Optionally, the "to" and "from" fields can be set,
+ * as well as an arbitrary number of properties.
+ *
+ * Properties provide an easy mechanism for clients to share data. Each property has a
+ * String name, and a value that is a Java primitive (int, long, float, double, boolean)
+ * or any Serializable object (a Java object is Serializable when it implements the
+ * Serializable interface).
+ *
+ * @author Matt Tucker
+ */
+public abstract class Packet {
+
+    protected static final String DEFAULT_LANGUAGE =
+            java.util.Locale.getDefault().getLanguage().toLowerCase();
+
+    private static String DEFAULT_XML_NS = null;
+
+    /**
+     * Constant used as packetID to indicate that a packet has no id. To indicate that a packet
+     * has no id set this constant as the packet's id. When the packet is asked for its id the
+     * answer will be <tt>null</tt>.
+     */
+    public static final String ID_NOT_AVAILABLE = "ID_NOT_AVAILABLE";
+    
+    /**
+     * 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. 
+     */
+    public static final DateFormat XEP_0082_UTC_FORMAT = new SimpleDateFormat(
+                    "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+    static {
+        XEP_0082_UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
+    }
+
+
+    /**
+     * A prefix helps to make sure that ID's are unique across mutliple instances.
+     */
+    private static String prefix = StringUtils.randomString(5) + "-";
+
+    /**
+     * Keeps track of the current increment, which is appended to the prefix to
+     * forum a unique ID.
+     */
+    private static long id = 0;
+
+    private String xmlns = DEFAULT_XML_NS;
+
+    /**
+     * Returns the next unique id. Each id made up of a short alphanumeric
+     * prefix along with a unique numeric value.
+     *
+     * @return the next id.
+     */
+    public static synchronized String nextID() {
+        return prefix + Long.toString(id++);
+    }
+
+    public static void setDefaultXmlns(String defaultXmlns) {
+        DEFAULT_XML_NS = defaultXmlns;
+    }
+
+    private String packetID = null;
+    private String to = null;
+    private String from = null;
+    private final List<PacketExtension> packetExtensions
+            = new CopyOnWriteArrayList<PacketExtension>();
+
+    private final Map<String,Object> properties = new HashMap<String, Object>();
+    private XMPPError error = null;
+
+    public Packet() {
+    }
+
+    public Packet(Packet p) {
+        packetID = p.getPacketID();
+        to = p.getTo();
+        from = p.getFrom();
+        xmlns = p.xmlns;
+        error = p.error;
+
+        // Copy extensions
+        for (PacketExtension pe : p.getExtensions()) {
+            addExtension(pe);
+        }
+    }
+
+    /**
+     * Returns the unique ID of the packet. The returned value could be <tt>null</tt> when
+     * ID_NOT_AVAILABLE was set as the packet's id.
+     *
+     * @return the packet's unique ID or <tt>null</tt> if the packet's id is not available.
+     */
+    public String getPacketID() {
+        if (ID_NOT_AVAILABLE.equals(packetID)) {
+            return null;
+        }
+
+        if (packetID == null) {
+            packetID = nextID();
+        }
+        return packetID;
+    }
+
+    /**
+     * Sets the unique ID of the packet. To indicate that a packet has no id
+     * pass the constant ID_NOT_AVAILABLE as the packet's id value.
+     *
+     * @param packetID the unique ID for the packet.
+     */
+    public void setPacketID(String packetID) {
+        this.packetID = packetID;
+    }
+
+    /**
+     * Returns who the packet is being sent "to", or <tt>null</tt> if
+     * the value is not set. The XMPP protocol often makes the "to"
+     * attribute optional, so it does not always need to be set.<p>
+     *
+     * The StringUtils class provides several useful methods for dealing with
+     * XMPP addresses such as parsing the
+     * {@link StringUtils#parseBareAddress(String) bare address},
+     * {@link StringUtils#parseName(String) user name},
+     * {@link StringUtils#parseServer(String) server}, and
+     * {@link StringUtils#parseResource(String) resource}.  
+     *
+     * @return who the packet is being sent to, or <tt>null</tt> if the
+     *      value has not been set.
+     */
+    public String getTo() {
+        return to;
+    }
+
+    /**
+     * Sets who the packet is being sent "to". The XMPP protocol often makes
+     * the "to" attribute optional, so it does not always need to be set.
+     *
+     * @param to who the packet is being sent to.
+     */
+    public void setTo(String to) {
+        this.to = to;
+    }
+
+    /**
+     * Returns who the packet is being sent "from" or <tt>null</tt> if
+     * the value is not set. The XMPP protocol often makes the "from"
+     * attribute optional, so it does not always need to be set.<p>
+     *
+     * The StringUtils class provides several useful methods for dealing with
+     * XMPP addresses such as parsing the
+     * {@link StringUtils#parseBareAddress(String) bare address},
+     * {@link StringUtils#parseName(String) user name},
+     * {@link StringUtils#parseServer(String) server}, and
+     * {@link StringUtils#parseResource(String) resource}.  
+     *
+     * @return who the packet is being sent from, or <tt>null</tt> if the
+     *      value has not been set.
+     */
+    public String getFrom() {
+        return from;
+    }
+
+    /**
+     * Sets who the packet is being sent "from". The XMPP protocol often
+     * makes the "from" attribute optional, so it does not always need to
+     * be set.
+     *
+     * @param from who the packet is being sent to.
+     */
+    public void setFrom(String from) {
+        this.from = from;
+    }
+
+    /**
+     * Returns the error associated with this packet, or <tt>null</tt> if there are
+     * no errors.
+     *
+     * @return the error sub-packet or <tt>null</tt> if there isn't an error.
+     */
+    public XMPPError getError() {
+        return error;
+    }
+
+    /**
+     * Sets the error for this packet.
+     *
+     * @param error the error to associate with this packet.
+     */
+    public void setError(XMPPError error) {
+        this.error = error;
+    }
+
+    /**
+     * Returns an unmodifiable collection of the packet extensions attached to the packet.
+     *
+     * @return the packet extensions.
+     */
+    public synchronized Collection<PacketExtension> getExtensions() {
+        if (packetExtensions == null) {
+            return Collections.emptyList();
+        }
+        return Collections.unmodifiableList(new ArrayList<PacketExtension>(packetExtensions));
+    }
+
+    /**
+     * Returns the first extension of this packet that has the given namespace.
+     *
+     * @param namespace the namespace of the extension that is desired.
+     * @return the packet extension with the given namespace.
+     */
+    public PacketExtension getExtension(String namespace) {
+        return getExtension(null, namespace);
+    }
+
+    /**
+     * Returns the first packet extension that matches the specified element name and
+     * namespace, or <tt>null</tt> if it doesn't exist. If the provided elementName is null
+     * than only the provided namespace is attempted to be matched. Packet extensions are
+     * are arbitrary XML sub-documents in standard XMPP packets. By default, a 
+     * DefaultPacketExtension instance will be returned for each extension. However, 
+     * PacketExtensionProvider instances can be registered with the 
+     * {@link org.jivesoftware.smack.provider.ProviderManager ProviderManager}
+     * class to handle custom parsing. In that case, the type of the Object
+     * will be determined by the provider.
+     *
+     * @param elementName the XML element name of the packet extension. (May be null)
+     * @param namespace the XML element namespace of the packet extension.
+     * @return the extension, or <tt>null</tt> if it doesn't exist.
+     */
+    public PacketExtension getExtension(String elementName, String namespace) {
+        if (namespace == null) {
+            return null;
+        }
+        for (PacketExtension ext : packetExtensions) {
+            if ((elementName == null || elementName.equals(ext.getElementName()))
+                    && namespace.equals(ext.getNamespace()))
+            {
+                return ext;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Adds a packet extension to the packet. Does nothing if extension is null.
+     *
+     * @param extension a packet extension.
+     */
+    public void addExtension(PacketExtension extension) {
+        if (extension == null) return;
+        packetExtensions.add(extension);
+    }
+
+    /**
+     * Adds a collection of packet extensions to the packet. Does nothing if extensions is null.
+     * 
+     * @param extensions a collection of packet extensions
+     */
+    public void addExtensions(Collection<PacketExtension> extensions) {
+        if (extensions == null) return;
+        packetExtensions.addAll(extensions);
+    }
+
+    /**
+     * Removes a packet extension from the packet.
+     *
+     * @param extension the packet extension to remove.
+     */
+    public void removeExtension(PacketExtension extension)  {
+        packetExtensions.remove(extension);
+    }
+
+    /**
+     * Returns the packet property with the specified name or <tt>null</tt> if the
+     * property doesn't exist. Property values that were originally primitives will
+     * be returned as their object equivalent. For example, an int property will be
+     * returned as an Integer, a double as a Double, etc.
+     *
+     * @param name the name of the property.
+     * @return the property, or <tt>null</tt> if the property doesn't exist.
+     */
+    public synchronized Object getProperty(String name) {
+        if (properties == null) {
+            return null;
+        }
+        return properties.get(name);
+    }
+
+    /**
+     * Sets a property with an Object as the value. The value must be Serializable
+     * or an IllegalArgumentException will be thrown.
+     *
+     * @param name the name of the property.
+     * @param value the value of the property.
+     */
+    public synchronized void setProperty(String name, Object value) {
+        if (!(value instanceof Serializable)) {
+            throw new IllegalArgumentException("Value must be serialiazble");
+        }
+        properties.put(name, value);
+    }
+
+    /**
+     * Deletes a property.
+     *
+     * @param name the name of the property to delete.
+     */
+    public synchronized void deleteProperty(String name) {
+        if (properties == null) {
+            return;
+        }
+        properties.remove(name);
+    }
+
+    /**
+     * Returns an unmodifiable collection of all the property names that are set.
+     *
+     * @return all property names.
+     */
+    public synchronized Collection<String> getPropertyNames() {
+        if (properties == null) {
+            return Collections.emptySet();
+        }
+        return Collections.unmodifiableSet(new HashSet<String>(properties.keySet()));
+    }
+
+    /**
+     * Returns the packet as XML. Every concrete extension of Packet must implement
+     * this method. In addition to writing out packet-specific data, every sub-class
+     * should also write out the error and the extensions data if they are defined.
+     *
+     * @return the XML format of the packet as a String.
+     */
+    public abstract String toXML();
+
+    /**
+     * Returns the extension sub-packets (including properties data) as an XML
+     * String, or the Empty String if there are no packet extensions.
+     *
+     * @return the extension sub-packets as XML or the Empty String if there
+     * are no packet extensions.
+     */
+    protected synchronized String getExtensionsXML() {
+        StringBuilder buf = new StringBuilder();
+        // Add in all standard extension sub-packets.
+        for (PacketExtension extension : getExtensions()) {
+            buf.append(extension.toXML());
+        }
+        // Add in packet properties.
+        if (properties != null && !properties.isEmpty()) {
+            buf.append("<properties xmlns=\"http://www.jivesoftware.com/xmlns/xmpp/properties\">");
+            // Loop through all properties and write them out.
+            for (String name : getPropertyNames()) {
+                Object value = getProperty(name);
+                buf.append("<property>");
+                buf.append("<name>").append(StringUtils.escapeForXML(name)).append("</name>");
+                buf.append("<value type=\"");
+                if (value instanceof Integer) {
+                    buf.append("integer\">").append(value).append("</value>");
+                }
+                else if (value instanceof Long) {
+                    buf.append("long\">").append(value).append("</value>");
+                }
+                else if (value instanceof Float) {
+                    buf.append("float\">").append(value).append("</value>");
+                }
+                else if (value instanceof Double) {
+                    buf.append("double\">").append(value).append("</value>");
+                }
+                else if (value instanceof Boolean) {
+                    buf.append("boolean\">").append(value).append("</value>");
+                }
+                else if (value instanceof String) {
+                    buf.append("string\">");
+                    buf.append(StringUtils.escapeForXML((String)value));
+                    buf.append("</value>");
+                }
+                // Otherwise, it's a generic Serializable object. Serialized objects are in
+                // a binary format, which won't work well inside of XML. Therefore, we base-64
+                // encode the binary data before adding it.
+                else {
+                    ByteArrayOutputStream byteStream = null;
+                    ObjectOutputStream out = null;
+                    try {
+                        byteStream = new ByteArrayOutputStream();
+                        out = new ObjectOutputStream(byteStream);
+                        out.writeObject(value);
+                        buf.append("java-object\">");
+                        String encodedVal = StringUtils.encodeBase64(byteStream.toByteArray());
+                        buf.append(encodedVal).append("</value>");
+                    }
+                    catch (Exception e) {
+                        e.printStackTrace();
+                    }
+                    finally {
+                        if (out != null) {
+                            try {
+                                out.close();
+                            }
+                            catch (Exception e) {
+                                // Ignore.
+                            }
+                        }
+                        if (byteStream != null) {
+                            try {
+                                byteStream.close();
+                            }
+                            catch (Exception e) {
+                                // Ignore.
+                            }
+                        }
+                    }
+                }
+                buf.append("</property>");
+            }
+            buf.append("</properties>");
+        }
+        return buf.toString();
+    }
+
+    public String getXmlns() {
+        return this.xmlns;
+    }
+
+    /**
+     * Returns the default language used for all messages containing localized content.
+     * 
+     * @return the default language
+     */
+    public static String getDefaultLanguage() {
+        return DEFAULT_LANGUAGE;
+    }
+
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Packet packet = (Packet) o;
+
+        if (error != null ? !error.equals(packet.error) : packet.error != null) { return false; }
+        if (from != null ? !from.equals(packet.from) : packet.from != null) { return false; }
+        if (!packetExtensions.equals(packet.packetExtensions)) { return false; }
+        if (packetID != null ? !packetID.equals(packet.packetID) : packet.packetID != null) {
+            return false;
+        }
+        if (properties != null ? !properties.equals(packet.properties)
+                : packet.properties != null) {
+            return false;
+        }
+        if (to != null ? !to.equals(packet.to) : packet.to != null)  { return false; }
+        return !(xmlns != null ? !xmlns.equals(packet.xmlns) : packet.xmlns != null);
+    }
+
+    public int hashCode() {
+        int result;
+        result = (xmlns != null ? xmlns.hashCode() : 0);
+        result = 31 * result + (packetID != null ? packetID.hashCode() : 0);
+        result = 31 * result + (to != null ? to.hashCode() : 0);
+        result = 31 * result + (from != null ? from.hashCode() : 0);
+        result = 31 * result + packetExtensions.hashCode();
+        result = 31 * result + properties.hashCode();
+        result = 31 * result + (error != null ? error.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/src/org/jivesoftware/smack/packet/PacketExtension.java b/src/org/jivesoftware/smack/packet/PacketExtension.java
new file mode 100644
index 0000000..d2afbf8
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/PacketExtension.java
@@ -0,0 +1,56 @@
+/**
+ * $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.packet;
+
+/**
+ * Interface to represent packet extensions. A packet extension is an XML subdocument
+ * with a root element name and namespace. Packet extensions are used to provide
+ * extended functionality beyond what is in the base XMPP specification. Examples of
+ * packet extensions include message events, message properties, and extra presence data.
+ * IQ packets cannot contain packet extensions.
+ *
+ * @see DefaultPacketExtension
+ * @see org.jivesoftware.smack.provider.PacketExtensionProvider
+ * @author Matt Tucker
+ */
+public interface PacketExtension {
+
+    /**
+     * Returns the root element name.
+     *
+     * @return the element name.
+     */
+    public String getElementName();
+
+    /**
+     * Returns the root element XML namespace.
+     *
+     * @return the namespace.
+     */
+    public String getNamespace();
+
+    /**
+     * Returns the XML representation of the PacketExtension.
+     *
+     * @return the packet extension as XML.
+     */
+    public String toXML();
+}
diff --git a/src/org/jivesoftware/smack/packet/Presence.java b/src/org/jivesoftware/smack/packet/Presence.java
new file mode 100644
index 0000000..84fcfef
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Presence.java
@@ -0,0 +1,358 @@
+/**
+ * $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.packet;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * Represents XMPP presence packets. Every presence packet has a type, which is one of
+ * the following values:
+ * <ul>
+ *      <li>{@link Presence.Type#available available} -- (Default) indicates the user is available to
+ *          receive messages.
+ *      <li>{@link Presence.Type#unavailable unavailable} -- the user is unavailable to receive messages.
+ *      <li>{@link Presence.Type#subscribe subscribe} -- request subscription to recipient's presence.
+ *      <li>{@link Presence.Type#subscribed subscribed} -- grant subscription to sender's presence.
+ *      <li>{@link Presence.Type#unsubscribe unsubscribe} -- request removal of subscription to
+ *          sender's presence.
+ *      <li>{@link Presence.Type#unsubscribed unsubscribed} -- grant removal of subscription to
+ *          sender's presence.
+ *      <li>{@link Presence.Type#error error} -- the presence packet contains an error message.
+ * </ul><p>
+ *
+ * A number of attributes are optional:
+ * <ul>
+ *      <li>Status -- free-form text describing a user's presence (i.e., gone to lunch).
+ *      <li>Priority -- non-negative numerical priority of a sender's resource. The
+ *          highest resource priority is the default recipient of packets not addressed
+ *          to a particular resource.
+ *      <li>Mode -- one of five presence modes: {@link Mode#available available} (the default),
+ *          {@link Mode#chat chat}, {@link Mode#away away}, {@link Mode#xa xa} (extended away), and
+ *          {@link Mode#dnd dnd} (do not disturb).
+ * </ul><p>
+ *
+ * Presence packets are used for two purposes. First, to notify the server of our
+ * the clients current presence status. Second, they are used to subscribe and
+ * unsubscribe users from the roster.
+ *
+ * @see RosterPacket
+ * @author Matt Tucker
+ */
+public class Presence extends Packet {
+
+    private Type type = Type.available;
+    private String status = null;
+    private int priority = Integer.MIN_VALUE;
+    private Mode mode = null;
+    private String language;
+
+    /**
+     * Creates a new presence update. Status, priority, and mode are left un-set.
+     *
+     * @param type the type.
+     */
+    public Presence(Type type) {
+        setType(type);
+    }
+
+    /**
+     * Creates a new presence update with a specified status, priority, and mode.
+     *
+     * @param type the type.
+     * @param status a text message describing the presence update.
+     * @param priority the priority of this presence update.
+     * @param mode the mode type for this presence update.
+     */
+    public Presence(Type type, String status, int priority, Mode mode) {
+        setType(type);
+        setStatus(status);
+        setPriority(priority);
+        setMode(mode);
+    }
+
+    /**
+     * Returns true if the {@link Type presence type} is available (online) and
+     * false if the user is unavailable (offline), or if this is a presence packet
+     * involved in a subscription operation. This is a convenience method
+     * equivalent to <tt>getType() == Presence.Type.available</tt>. Note that even
+     * when the user is available, their presence mode may be {@link Mode#away away},
+     * {@link Mode#xa extended away} or {@link Mode#dnd do not disturb}. Use
+     * {@link #isAway()} to determine if the user is away.
+     *
+     * @return true if the presence type is available.
+     */
+    public boolean isAvailable() {
+        return type == Type.available;    
+    }
+
+    /**
+     * Returns true if the presence type is {@link Type#available available} and the presence
+     * mode is {@link Mode#away away}, {@link Mode#xa extended away}, or
+     * {@link Mode#dnd do not disturb}. False will be returned when the type or mode
+     * is any other value, including when the presence type is unavailable (offline).
+     * This is a convenience method equivalent to
+     * <tt>type == Type.available && (mode == Mode.away || mode == Mode.xa || mode == Mode.dnd)</tt>.
+     *
+     * @return true if the presence type is available and the presence mode is away, xa, or dnd.
+     */
+    public boolean isAway() {
+        return type == Type.available && (mode == Mode.away || mode == Mode.xa || mode == Mode.dnd); 
+    }
+
+    /**
+     * Returns the type of this presence packet.
+     *
+     * @return the type of the presence packet.
+     */
+    public Type getType() {
+        return type;
+    }
+
+    /**
+     * Sets the type of the presence packet.
+     *
+     * @param type the type of the presence packet.
+     */
+    public void setType(Type type) {
+        if(type == null) {
+            throw new NullPointerException("Type cannot be null");
+        }
+        this.type = type;
+    }
+
+    /**
+     * Returns the status message of the presence update, or <tt>null</tt> if there
+     * is not a status. The status is free-form text describing a user's presence
+     * (i.e., "gone to lunch").
+     *
+     * @return the status message.
+     */
+    public String getStatus() {
+        return status;
+    }
+
+    /**
+     * Sets the status message of the presence update. The status is free-form text
+     * describing a user's presence (i.e., "gone to lunch").
+     *
+     * @param status the status message.
+     */
+    public void setStatus(String status) {
+        this.status = status;
+    }
+
+    /**
+     * Returns the priority of the presence, or Integer.MIN_VALUE if no priority has been set.
+     *
+     * @return the priority.
+     */
+    public int getPriority() {
+        return priority;
+    }
+
+    /**
+     * Sets the priority of the presence. The valid range is -128 through 128.
+     *
+     * @param priority the priority of the presence.
+     * @throws IllegalArgumentException if the priority is outside the valid range.
+     */
+    public void setPriority(int priority) {
+        if (priority < -128 || priority > 128) {
+            throw new IllegalArgumentException("Priority value " + priority +
+                    " is not valid. Valid range is -128 through 128.");
+        }
+        this.priority = priority;
+    }
+
+    /**
+     * Returns the mode of the presence update, or <tt>null</tt> if the mode is not set.
+     * A null presence mode value is interpreted to be the same thing as
+     * {@link Presence.Mode#available}.
+     *
+     * @return the mode.
+     */
+    public Mode getMode() {
+        return mode;
+    }
+
+    /**
+     * Sets the mode of the presence update. A null presence mode value is interpreted
+     * to be the same thing as {@link Presence.Mode#available}.
+     *
+     * @param mode the mode.
+     */
+    public void setMode(Mode mode) {
+        this.mode = mode;
+    }
+
+    /**
+     * Returns the xml:lang of this Presence, or null if one has not been set.
+     *
+     * @return the xml:lang of this Presence, or null if one has not been set.
+     * @since 3.0.2
+     */
+    public String getLanguage() {
+        return language;
+    }
+
+    /**
+     * Sets the xml:lang of this Presence.
+     *
+     * @param language the xml:lang of this Presence.
+     * @since 3.0.2
+     */
+    public void setLanguage(String language) {
+        this.language = language;
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<presence");
+        if(getXmlns() != null) {
+            buf.append(" xmlns=\"").append(getXmlns()).append("\"");
+        }
+        if (language != null) {
+            buf.append(" xml:lang=\"").append(getLanguage()).append("\"");
+        }
+        if (getPacketID() != null) {
+            buf.append(" id=\"").append(getPacketID()).append("\"");
+        }
+        if (getTo() != null) {
+            buf.append(" to=\"").append(StringUtils.escapeForXML(getTo())).append("\"");
+        }
+        if (getFrom() != null) {
+            buf.append(" from=\"").append(StringUtils.escapeForXML(getFrom())).append("\"");
+        }
+        if (type != Type.available) {
+            buf.append(" type=\"").append(type).append("\"");
+        }
+        buf.append(">");
+        if (status != null) {
+            buf.append("<status>").append(StringUtils.escapeForXML(status)).append("</status>");
+        }
+        if (priority != Integer.MIN_VALUE) {
+            buf.append("<priority>").append(priority).append("</priority>");
+        }
+        if (mode != null && mode != Mode.available) {
+            buf.append("<show>").append(mode).append("</show>");
+        }
+
+        buf.append(this.getExtensionsXML());
+
+        // Add the error sub-packet, if there is one.
+        XMPPError error = getError();
+        if (error != null) {
+            buf.append(error.toXML());
+        }
+
+        buf.append("</presence>");
+        
+        return buf.toString();
+    }
+
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+        buf.append(type);
+        if (mode != null) {
+            buf.append(": ").append(mode);
+        }
+        if (getStatus() != null) {
+            buf.append(" (").append(getStatus()).append(")");
+        }
+        return buf.toString();
+    }
+
+    /**
+     * A enum to represent the presecence type. Not that presence type is often confused
+     * with presence mode. Generally, if a user is signed into a server, they have a presence
+     * type of {@link #available available}, even if the mode is {@link Mode#away away},
+     * {@link Mode#dnd dnd}, etc. The presence type is only {@link #unavailable unavailable} when
+     * the user is signing out of the server.
+     */
+    public enum Type {
+
+       /**
+        * The user is available to receive messages (default).
+        */
+        available,
+
+        /**
+         * The user is unavailable to receive messages.
+         */
+        unavailable,
+
+        /**
+         * Request subscription to recipient's presence.
+         */
+        subscribe,
+
+        /**
+         * Grant subscription to sender's presence.
+         */
+        subscribed,
+
+        /**
+         * Request removal of subscription to sender's presence.
+         */
+        unsubscribe,
+
+        /**
+         * Grant removal of subscription to sender's presence.
+         */
+        unsubscribed,
+
+        /**
+         * The presence packet contains an error message.
+         */
+        error
+    }
+
+    /**
+     * An enum to represent the presence mode.
+     */
+    public enum Mode {
+
+        /**
+         * Free to chat.
+         */
+        chat,
+
+        /**
+         * Available (the default).
+         */
+        available,
+
+        /**
+         * Away.
+         */
+        away,
+
+        /**
+         * Away for an extended period of time.
+         */
+        xa,
+
+        /**
+         * Do not disturb.
+         */
+        dnd
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/packet/Privacy.java b/src/org/jivesoftware/smack/packet/Privacy.java
new file mode 100644
index 0000000..a62d578
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Privacy.java
@@ -0,0 +1,323 @@
+/**

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2006-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.packet;

+

+import java.util.*;

+

+/**

+ * A Privacy IQ Packet, is used by the {@link org.jivesoftware.smack.PrivacyListManager}

+ * and {@link org.jivesoftware.smack.provider.PrivacyProvider} to allow and block

+ * communications from other users. It contains the appropriate structure to suit

+ * user-defined privacy lists. Different configured Privacy packages are used in the

+ * server & manager communication in order to:

+ * <ul>

+ * <li>Retrieving one's privacy lists. 

+ * <li>Adding, removing, and editing one's privacy lists. 

+ * <li>Setting, changing, or declining active lists. 

+ * <li>Setting, changing, or declining the default list (i.e., the list that is active by default). 

+ * </ul>

+ * Privacy Items can handle different kind of blocking communications based on JID, group, 

+ * subscription type or globally {@link PrivacyItem}

+ * 

+ * @author Francisco Vives

+ */

+public class Privacy extends IQ {

+	/** declineActiveList is true when the user declines the use of the active list **/

+	private boolean declineActiveList=false;

+	/** activeName is the name associated with the active list set for the session **/

+	private String activeName;

+	/** declineDefaultList is true when the user declines the use of the default list **/

+	private boolean declineDefaultList=false;

+	/** defaultName is the name of the default list that applies to the user as a whole **/

+	private String defaultName;

+	/** itemLists holds the set of privacy items classified in lists. It is a map where the 

+	 * key is the name of the list and the value a collection with privacy items. **/

+	private Map<String, List<PrivacyItem>> itemLists = new HashMap<String, List<PrivacyItem>>();

+

+    /**

+     * Set or update a privacy list with privacy items.

+     *

+     * @param listName the name of the new privacy list.

+     * @param listItem the {@link PrivacyItem} that rules the list.

+     * @return the privacy List.

+     */

+    public List<PrivacyItem> setPrivacyList(String listName, List<PrivacyItem> listItem) {

+        // Add new list to the itemLists

+        this.getItemLists().put(listName, listItem);

+        return listItem;

+    }

+

+    /**

+     * Set the active list based on the default list.

+     *

+     * @return the active List.

+     */

+    public List<PrivacyItem> setActivePrivacyList() {

+        this.setActiveName(this.getDefaultName());

+        return this.getItemLists().get(this.getActiveName());

+    }

+

+    /**

+     * Deletes an existing privacy list. If the privacy list being deleted was the default list 

+     * then the user will end up with no default list. Therefore, the user will have to set a new 

+     * default list.

+     *

+     * @param listName the name of the list being deleted.

+     */

+    public void deletePrivacyList(String listName) {

+        // Remove the list from the cache

+    	this.getItemLists().remove(listName);

+

+        // Check if deleted list was the default list

+        if (this.getDefaultName() != null && listName.equals(this.getDefaultName())) {

+        	this.setDefaultName(null);

+        }

+    }

+

+    /**

+     * Returns the active privacy list or <tt>null</tt> if none was found.

+     *

+     * @return list with {@link PrivacyItem} or <tt>null</tt> if none was found.

+     */

+    public List<PrivacyItem> getActivePrivacyList() {

+        // Check if we have the default list

+        if (this.getActiveName() == null) {

+        	return null;

+        } else {

+        	return this.getItemLists().get(this.getActiveName());

+        }

+    }

+

+    /**

+     * Returns the default privacy list or <tt>null</tt> if none was found.

+     *

+     * @return list with {@link PrivacyItem} or <tt>null</tt> if none was found.

+     */

+    public List<PrivacyItem> getDefaultPrivacyList() {

+        // Check if we have the default list

+        if (this.getDefaultName() == null) {

+        	return null;

+        } else {

+        	return this.getItemLists().get(this.getDefaultName());

+        }

+    }

+

+    /**

+     * Returns a specific privacy list.

+     *

+     * @param listName the name of the list to get.

+     * @return a List with {@link PrivacyItem}

+     */

+    public List<PrivacyItem> getPrivacyList(String listName) {

+        return this.getItemLists().get(listName);

+    }

+

+    /**

+     * Returns the privacy item in the specified order.

+     *

+     * @param listName the name of the privacy list.

+     * @param order the order of the element.

+     * @return a List with {@link PrivacyItem}

+     */

+    public PrivacyItem getItem(String listName, int order) {

+    	Iterator<PrivacyItem> values = getPrivacyList(listName).iterator();

+    	PrivacyItem itemFound = null;

+    	while (itemFound == null && values.hasNext()) {

+    		PrivacyItem element = values.next();

+			if (element.getOrder() == order) {

+				itemFound = element;

+			}

+		}

+    	return itemFound;

+    }

+

+    /**

+     * Sets a given privacy list as the new user default list.

+     *

+     * @param newDefault the new default privacy list.

+     * @return if the default list was changed.

+     */

+    public boolean changeDefaultList(String newDefault) {

+        if (this.getItemLists().containsKey(newDefault)) {

+           this.setDefaultName(newDefault);

+           return true;

+        } else {

+        	return false; 

+        }

+    }

+

+    /**

+     * Remove the list.

+     *

+     * @param listName name of the list to remove.

+     */

+     public void deleteList(String listName) {

+    	 this.getItemLists().remove(listName);

+     }

+

+    /**

+     * Returns the name associated with the active list set for the session. Communications

+     * will be verified against the active list.

+     *

+     * @return the name of the active list.

+     */

+	public String getActiveName() {

+		return activeName;

+	}

+

+    /**

+     * Sets the name associated with the active list set for the session. Communications

+     * will be verified against the active list.

+     * 

+     * @param activeName is the name of the active list.

+     */

+	public void setActiveName(String activeName) {

+		this.activeName = activeName;

+	}

+

+    /**

+     * Returns the name of the default list that applies to the user as a whole. Default list is 

+     * processed if there is no active list set for the target session/resource to which a stanza 

+     * is addressed, or if there are no current sessions for the user.

+     * 

+     * @return the name of the default list.

+     */

+	public String getDefaultName() {

+		return defaultName;

+	}

+

+    /**

+     * Sets the name of the default list that applies to the user as a whole. Default list is 

+     * processed if there is no active list set for the target session/resource to which a stanza 

+     * is addressed, or if there are no current sessions for the user.

+     * 

+     * If there is no default list set, then all Privacy Items are processed.

+     * 

+     * @param defaultName is the name of the default list.

+     */

+	public void setDefaultName(String defaultName) {

+		this.defaultName = defaultName;

+	}

+

+    /**

+     * Returns the collection of privacy list that the user holds. A Privacy List contains a set of 

+     * rules that define if communication with the list owner is allowed or denied. 

+     * Users may have zero, one or more privacy items.

+     * 

+     * @return a map where the key is the name of the list and the value the 

+     * collection of privacy items.

+     */

+	public Map<String, List<PrivacyItem>> getItemLists() {

+		return itemLists;

+	}

+

+    /** 

+     * Returns whether the receiver allows or declines the use of an active list.

+     * 

+     * @return the decline status of the list.

+     */

+	public boolean isDeclineActiveList() {

+		return declineActiveList;

+	}

+

+    /** 

+     * Sets whether the receiver allows or declines the use of an active list.

+     * 

+     * @param declineActiveList indicates if the receiver declines the use of an active list.

+     */

+	public void setDeclineActiveList(boolean declineActiveList) {

+		this.declineActiveList = declineActiveList;

+	}

+

+    /** 

+     * Returns whether the receiver allows or declines the use of a default list.

+     * 

+     * @return the decline status of the list.

+     */

+	public boolean isDeclineDefaultList() {

+		return declineDefaultList;

+	}

+

+    /** 

+     * Sets whether the receiver allows or declines the use of a default list.

+     * 

+     * @param declineDefaultList indicates if the receiver declines the use of a default list.

+     */

+	public void setDeclineDefaultList(boolean declineDefaultList) {

+		this.declineDefaultList = declineDefaultList;

+	}

+

+	/** 

+     * Returns all the list names the user has defined to group restrictions.

+     * 

+     * @return a Set with Strings containing every list names.

+     */

+	public Set<String> getPrivacyListNames() {

+		return this.itemLists.keySet();

+	}

+	

+	public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<query xmlns=\"jabber:iq:privacy\">");

+        

+        // Add the active tag

+        if (this.isDeclineActiveList()) {

+        	buf.append("<active/>");

+        } else {

+        	if (this.getActiveName() != null) {

+            	buf.append("<active name=\"").append(this.getActiveName()).append("\"/>");

+            }

+        }

+        // Add the default tag

+        if (this.isDeclineDefaultList()) {

+        	buf.append("<default/>");

+        } else {

+	        if (this.getDefaultName() != null) {

+	        	buf.append("<default name=\"").append(this.getDefaultName()).append("\"/>");

+	        }

+        }

+        

+        // Add the list with their privacy items

+        for (Map.Entry<String, List<PrivacyItem>> entry : this.getItemLists().entrySet()) {

+          String listName = entry.getKey();

+          List<PrivacyItem> items = entry.getValue();

+			// Begin the list tag

+			if (items.isEmpty()) {

+				buf.append("<list name=\"").append(listName).append("\"/>");

+			} else {

+				buf.append("<list name=\"").append(listName).append("\">");

+			}

+	        for (PrivacyItem item : items) {

+	        	// Append the item xml representation

+	        	buf.append(item.toXML());

+	        }

+	        // Close the list tag

+	        if (!items.isEmpty()) {

+				buf.append("</list>");

+			}

+		}

+

+        // Add packet extensions, if any are defined.

+        buf.append(getExtensionsXML());

+        buf.append("</query>");

+        return buf.toString();

+    }

+    

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/packet/PrivacyItem.java b/src/org/jivesoftware/smack/packet/PrivacyItem.java
new file mode 100644
index 0000000..2e144ee
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/PrivacyItem.java
@@ -0,0 +1,462 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.packet;

+

+/**

+ * A privacy item acts a rule that when matched defines if a packet should be blocked or not.

+ *

+ * Privacy Items can handle different kind of blocking communications based on JID, group,

+ * subscription type or globally by:<ul>

+ * <li>Allowing or blocking messages.

+ * <li>Allowing or blocking inbound presence notifications.

+ * <li>Allowing or blocking outbound presence notifications.

+ * <li>Allowing or blocking IQ stanzas.

+ * <li>Allowing or blocking all communications.

+ * </ul>

+ * @author Francisco Vives

+ */

+public class PrivacyItem {

+	/** allow is the action associated with the item, it can allow or deny the communication. */

+	private boolean allow;

+	/** order is a non-negative integer that is unique among all items in the list. */

+    private int order;

+    /** rule hold the kind of communication ([jid|group|subscription]) it will allow or block and

+     * identifier to apply the action.

+     * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.

+     * If the type is "group", then the 'value' attribute SHOULD contain the name of a group

+     * in the user's roster.

+     * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",

+     * "from", or "none". */

+    private PrivacyRule rule;

+

+    /** blocks incoming IQ stanzas. */

+    private boolean filterIQ = false;

+    /** filterMessage blocks incoming message stanzas. */

+    private boolean filterMessage = false;

+    /** blocks incoming presence notifications. */

+    private boolean filterPresence_in = false;

+    /** blocks outgoing presence notifications. */

+    private boolean filterPresence_out = false;

+

+    /**

+     * Creates a new privacy item.

+     *

+     * @param type the type.

+     */

+    public PrivacyItem(String type, boolean allow, int order) {

+        this.setRule(PrivacyRule.fromString(type));

+        this.setAllow(allow);

+        this.setOrder(order);

+    }

+

+    /**

+     * Returns the action associated with the item, it MUST be filled and will allow or deny

+     * the communication.

+     *

+     * @return the allow communication status.

+     */

+    public boolean isAllow() {

+		return allow;

+	}

+

+    /**

+     * Sets the action associated with the item, it can allow or deny the communication.

+     *

+     * @param allow indicates if the receiver allow or deny the communication.

+     */

+    private void setAllow(boolean allow) {

+		this.allow = allow;

+	}

+

+

+    /**

+     * Returns whether the receiver allow or deny incoming IQ stanzas or not.

+     *

+     * @return the iq filtering status.

+     */

+    public boolean isFilterIQ() {

+		return filterIQ;

+	}

+

+

+    /**

+     * Sets whether the receiver allows or denies incoming IQ stanzas or not.

+     *

+     * @param filterIQ indicates if the receiver allows or denies incoming IQ stanzas.

+     */

+    public void setFilterIQ(boolean filterIQ) {

+		this.filterIQ = filterIQ;

+	}

+

+

+    /**

+     * Returns whether the receiver allows or denies incoming messages or not.

+     *

+     * @return the message filtering status.

+     */

+    public boolean isFilterMessage() {

+		return filterMessage;

+	}

+

+

+    /**

+     * Sets wheather the receiver allows or denies incoming messages or not.

+     *

+     * @param filterMessage indicates if the receiver allows or denies incoming messages or not.

+     */

+    public void setFilterMessage(boolean filterMessage) {

+		this.filterMessage = filterMessage;

+	}

+

+

+    /**

+     * Returns whether the receiver allows or denies incoming presence or not.

+     *

+     * @return the iq filtering incoming presence status.

+     */

+    public boolean isFilterPresence_in() {

+		return filterPresence_in;

+	}

+

+

+    /**

+     * Sets whether the receiver allows or denies incoming presence or not.

+     *

+     * @param filterPresence_in indicates if the receiver allows or denies filtering incoming presence.

+     */

+    public void setFilterPresence_in(boolean filterPresence_in) {

+		this.filterPresence_in = filterPresence_in;

+	}

+

+

+    /**

+     * Returns whether the receiver allows or denies incoming presence or not.

+     *

+     * @return the iq filtering incoming presence status.

+     */

+    public boolean isFilterPresence_out() {

+		return filterPresence_out;

+	}

+

+

+    /**

+     * Sets whether the receiver allows or denies outgoing presence or not.

+     *

+     * @param filterPresence_out indicates if the receiver allows or denies filtering outgoing presence

+     */

+    public void setFilterPresence_out(boolean filterPresence_out) {

+		this.filterPresence_out = filterPresence_out;

+	}

+

+

+    /**

+     * Returns the order where the receiver is processed. List items are processed in

+     * ascending order.

+     *

+     * The order MUST be filled and its value MUST be a non-negative integer

+     * that is unique among all items in the list.

+     *

+     * @return the order number.

+     */

+    public int getOrder() {

+		return order;

+	}

+

+

+    /**

+     * Sets the order where the receiver is processed.

+     *

+     * The order MUST be filled and its value MUST be a non-negative integer

+     * that is unique among all items in the list.

+     *

+     * @param order indicates the order in the list.

+     */

+    public void setOrder(int order) {

+		this.order = order;

+	}

+

+    /**

+     * Sets the element identifier to apply the action.

+     *

+     * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.

+     * If the type is "group", then the 'value' attribute SHOULD contain the name of a group

+     * in the user's roster.

+     * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",

+     * "from", or "none".

+     *

+     * @param value is the identifier to apply the action.

+     */

+    public void setValue(String value) {

+    	if (!(this.getRule() == null && value == null)) {

+    		this.getRule().setValue(value);

+    	}

+	}

+

+    /**

+     * Returns the type hold the kind of communication it will allow or block.

+     * It MUST be filled with one of these values: jid, group or subscription.

+     *

+     * @return the type of communication it represent.

+     */

+    public Type getType() {

+    	if (this.getRule() == null) {

+    		return null;

+    	} else {

+		return this.getRule().getType();

+    	}

+	}

+

+    /**

+     * Returns the element identifier to apply the action.

+     *

+     * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.

+     * If the type is "group", then the 'value' attribute SHOULD contain the name of a group

+     * in the user's roster.

+     * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",

+     * "from", or "none".

+     *

+     * @return the identifier to apply the action.

+     */

+    public String getValue() {

+    	if (this.getRule() == null) {

+    		return null;

+    	} else {

+		return this.getRule().getValue();

+    	}

+	}

+

+

+    /**

+     * Returns whether the receiver allows or denies every kind of communication.

+     *

+     * When filterIQ, filterMessage, filterPresence_in and filterPresence_out are not set

+     * the receiver will block all communications.

+     *

+     * @return the all communications status.

+     */

+    public boolean isFilterEverything() {

+		return !(this.isFilterIQ() || this.isFilterMessage() || this.isFilterPresence_in()

+				|| this.isFilterPresence_out());

+	}

+

+

+	private PrivacyRule getRule() {

+		return rule;

+	}

+

+	private void setRule(PrivacyRule rule) {

+		this.rule = rule;

+	}

+	/**

+	 * Answer an xml representation of the receiver according to the RFC 3921.

+	 *

+	 * @return the text xml representation.

+     */

+    public String toXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<item");

+        if (this.isAllow()) {

+        	buf.append(" action=\"allow\"");

+        } else {

+        	buf.append(" action=\"deny\"");

+        }

+        buf.append(" order=\"").append(getOrder()).append("\"");

+        if (getType() != null) {

+            buf.append(" type=\"").append(getType()).append("\"");

+        }

+        if (getValue() != null) {

+            buf.append(" value=\"").append(getValue()).append("\"");

+        }

+        if (isFilterEverything()) {

+        	buf.append("/>");

+        } else {

+        	buf.append(">");

+        	if (this.isFilterIQ()) {

+            	buf.append("<iq/>");

+            }

+        	if (this.isFilterMessage()) {

+            	buf.append("<message/>");

+            }

+        	if (this.isFilterPresence_in()) {

+            	buf.append("<presence-in/>");

+            }

+        	if (this.isFilterPresence_out()) {

+            	buf.append("<presence-out/>");

+            }

+        	buf.append("</item>");

+        }

+        return buf.toString();

+    }

+

+

+    /**

+     * Privacy Rule represents the kind of action to apply.

+     * It holds the kind of communication ([jid|group|subscription]) it will allow or block and

+     * identifier to apply the action.

+     */

+

+	public static class PrivacyRule {

+    	 /**

+    	  * Type defines if the rule is based on JIDs, roster groups or presence subscription types.

+    	  * Available values are: [jid|group|subscription]

+    	  */

+         private Type type;

+         /**

+          * The value hold the element identifier to apply the action.

+          * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.

+          * If the type is "group", then the 'value' attribute SHOULD contain the name of a group

+          * in the user's roster.

+          * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",

+          * "from", or "none".

+          */

+         private String value;

+

+         /**

+     	 * If the type is "subscription", then the 'value' attribute MUST be one of "both",

+     	 * "to", "from", or "none"

+     	 */

+     	public static final String SUBSCRIPTION_BOTH = "both";

+     	public static final String SUBSCRIPTION_TO = "to";

+     	public static final String SUBSCRIPTION_FROM = "from";

+     	public static final String SUBSCRIPTION_NONE = "none";

+

+         /**

+          * Returns the type constant associated with the String value.

+          */

+         protected static PrivacyRule fromString(String value) {

+             if (value == null) {

+                 return null;

+             }

+             PrivacyRule rule = new PrivacyRule();

+             rule.setType(Type.valueOf(value.toLowerCase()));

+             return rule;

+         }

+

+         /**

+          * Returns the type hold the kind of communication it will allow or block.

+          * It MUST be filled with one of these values: jid, group or subscription.

+          *

+          * @return the type of communication it represent.

+          */

+         public Type getType() {

+     		return type;

+     	}

+

+         /**

+          * Sets the action associated with the item, it can allow or deny the communication.

+          *

+          * @param type indicates if the receiver allows or denies the communication.

+          */

+         private void setType(Type type) {

+     		this.type = type;

+     	}

+

+         /**

+          * Returns the element identifier to apply the action.

+          *

+          * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.

+          * If the type is "group", then the 'value' attribute SHOULD contain the name of a group

+          * in the user's roster.

+          * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",

+          * "from", or "none".

+          *

+          * @return the identifier to apply the action.

+          */

+         public String getValue() {

+     		return value;

+     	}

+

+         /**

+          * Sets the element identifier to apply the action.

+          *

+          * If the type is "jid", then the 'value' attribute MUST contain a valid Jabber ID.

+          * If the type is "group", then the 'value' attribute SHOULD contain the name of a group

+          * in the user's roster.

+          * If the type is "subscription", then the 'value' attribute MUST be one of "both", "to",

+          * "from", or "none".

+          *

+          * @param value is the identifier to apply the action.

+          */

+         protected void setValue(String value) {

+        	 if (this.isSuscription()) {

+        		 setSuscriptionValue(value);

+        	 } else {

+        		 this.value = value;

+        	 }

+     	}

+

+         /**

+          * Sets the element identifier to apply the action.

+          *

+          * The 'value' attribute MUST be one of "both", "to", "from", or "none".

+          *

+          * @param value is the identifier to apply the action.

+          */

+         private void setSuscriptionValue(String value) {

+        	 String setValue;

+             if (value == null) {

+            	 // Do nothing

+             }

+             if (SUBSCRIPTION_BOTH.equalsIgnoreCase(value)) {

+            	 setValue = SUBSCRIPTION_BOTH;

+             }

+             else if (SUBSCRIPTION_TO.equalsIgnoreCase(value)) {

+            	 setValue = SUBSCRIPTION_TO;

+             }

+             else if (SUBSCRIPTION_FROM.equalsIgnoreCase(value)) {

+            	 setValue = SUBSCRIPTION_FROM;

+             }

+             else if (SUBSCRIPTION_NONE.equalsIgnoreCase(value)) {

+            	 setValue = SUBSCRIPTION_NONE;

+             }

+             // Default to available.

+             else {

+            	 setValue = null;

+             }

+     		this.value = setValue;

+     	}

+

+         /**

+          * Returns if the receiver represents a subscription rule.

+          *

+          * @return if the receiver represents a subscription rule.

+          */

+         public boolean isSuscription () {

+     		return this.getType() == Type.subscription;

+     	}

+    }

+

+    /**

+     * Type defines if the rule is based on JIDs, roster groups or presence subscription types.

+     */

+    public static enum Type {

+        /**

+         * JID being analyzed should belong to a roster group of the list's owner.

+         */

+        group,

+        /**

+         * JID being analyzed should have a resource match, domain match or bare JID match.

+         */

+        jid,

+        /**

+         * JID being analyzed should belong to a contact present in the owner's roster with

+         * the specified subscription status.

+         */

+        subscription

+    }

+}

diff --git a/src/org/jivesoftware/smack/packet/Registration.java b/src/org/jivesoftware/smack/packet/Registration.java
new file mode 100644
index 0000000..df22e27
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Registration.java
@@ -0,0 +1,155 @@
+/**
+ * $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.packet;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents registration packets. An empty GET query will cause the server to return information
+ * about it's registration support. SET queries can be used to create accounts or update
+ * existing account information. XMPP servers may require a number of attributes to be set
+ * when creating a new account. The standard account attributes are as follows:
+ * <ul>
+ *      <li>name -- the user's name.
+ *      <li>first -- the user's first name.
+ *      <li>last -- the user's last name.
+ *      <li>email -- the user's email address.
+ *      <li>city -- the user's city.
+ *      <li>state -- the user's state.
+ *      <li>zip -- the user's ZIP code.
+ *      <li>phone -- the user's phone number.
+ *      <li>url -- the user's website.
+ *      <li>date -- the date the registration took place.
+ *      <li>misc -- other miscellaneous information to associate with the account.
+ *      <li>text -- textual information to associate with the account.
+ *      <li>remove -- empty flag to remove account.
+ * </ul>
+ *
+ * @author Matt Tucker
+ */
+public class Registration extends IQ {
+
+    private String instructions = null;
+    private Map<String, String> attributes = new HashMap<String,String>();
+    private List<String> requiredFields = new ArrayList<String>();
+    private boolean registered = false;
+    private boolean remove = false;
+
+    /**
+     * Returns the registration instructions, or <tt>null</tt> if no instructions
+     * have been set. If present, instructions should be displayed to the end-user
+     * that will complete the registration process.
+     *
+     * @return the registration instructions, or <tt>null</tt> if there are none.
+     */
+    public String getInstructions() {
+        return instructions;
+    }
+
+    /**
+     * Sets the registration instructions.
+     *
+     * @param instructions the registration instructions.
+     */
+    public void setInstructions(String instructions) {
+        this.instructions = instructions;
+    }
+
+    /**
+     * Returns the map of String key/value pairs of account attributes.
+     *
+     * @return the account attributes.
+     */
+    public Map<String, String> getAttributes() {
+        return attributes;
+    }
+
+    /**
+     * Sets the account attributes. The map must only contain String key/value pairs.
+     *
+     * @param attributes the account attributes.
+     */
+    public void setAttributes(Map<String, String> attributes) {
+        this.attributes = attributes;
+    }
+    
+    public List<String> getRequiredFields(){
+    	return requiredFields;
+    }
+    
+    public void addAttribute(String key, String value){
+    	attributes.put(key, value);
+    }
+    
+    public void setRegistered(boolean registered){
+    	this.registered = registered;
+    }
+    
+    public boolean isRegistered(){
+    	return this.registered;
+    }
+    
+    public String getField(String key){
+    	return attributes.get(key);
+    }
+    
+    public List<String> getFieldNames(){
+    	return new ArrayList<String>(attributes.keySet());
+    }
+    
+    public void setUsername(String username){
+    	attributes.put("username", username);
+    }
+    
+    public void setPassword(String password){
+    	attributes.put("password", password);
+    }
+    
+    public void setRemove(boolean remove){
+    	this.remove = remove;
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"jabber:iq:register\">");
+        if (instructions != null && !remove) {
+            buf.append("<instructions>").append(instructions).append("</instructions>");
+        }
+        if (attributes != null && attributes.size() > 0 && !remove) {
+            for (String name : attributes.keySet()) {
+                String value = attributes.get(name);
+                buf.append("<").append(name).append(">");
+                buf.append(value);
+                buf.append("</").append(name).append(">");
+            }
+        }
+        else if(remove){
+        	buf.append("</remove>");
+        }
+        // Add packet extensions, if any are defined.
+        buf.append(getExtensionsXML());
+        buf.append("</query>");
+        return buf.toString();
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/packet/RosterPacket.java b/src/org/jivesoftware/smack/packet/RosterPacket.java
new file mode 100644
index 0000000..98483c8
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/RosterPacket.java
@@ -0,0 +1,311 @@
+/**
+ * $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.packet;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.*;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+/**
+ * Represents XMPP roster packets.
+ *
+ * @author Matt Tucker
+ */
+public class RosterPacket extends IQ {
+
+    private final List<Item> rosterItems = new ArrayList<Item>();
+    /*
+     * The ver attribute following XEP-0237
+     */
+    private String version;
+
+    /**
+     * Adds a roster item to the packet.
+     *
+     * @param item a roster item.
+     */
+    public void addRosterItem(Item item) {
+        synchronized (rosterItems) {
+            rosterItems.add(item);
+        }
+    }
+    
+    public String getVersion(){
+    	return version;
+    }
+    
+    public void setVersion(String version){
+    	this.version = version;
+    }
+
+    /**
+     * Returns the number of roster items in this roster packet.
+     *
+     * @return the number of roster items.
+     */
+    public int getRosterItemCount() {
+        synchronized (rosterItems) {
+            return rosterItems.size();
+        }
+    }
+
+    /**
+     * Returns an unmodifiable collection for the roster items in the packet.
+     *
+     * @return an unmodifiable collection for the roster items in the packet.
+     */
+    public Collection<Item> getRosterItems() {
+        synchronized (rosterItems) {
+            return Collections.unmodifiableList(new ArrayList<Item>(rosterItems));
+        }
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"jabber:iq:roster\" ");
+        if(version!=null){
+        	buf.append(" ver=\""+version+"\" ");
+        }
+        buf.append(">");
+        synchronized (rosterItems) {
+            for (Item entry : rosterItems) {
+                buf.append(entry.toXML());
+            }
+        }
+        buf.append("</query>");
+        return buf.toString();
+    }
+
+    /**
+     * A roster item, which consists of a JID, their name, the type of subscription, and
+     * the groups the roster item belongs to.
+     */
+    public static class Item {
+
+        private String user;
+        private String name;
+        private ItemType itemType;
+        private ItemStatus itemStatus;
+        private final Set<String> groupNames;
+
+        /**
+         * Creates a new roster item.
+         *
+         * @param user the user.
+         * @param name the user's name.
+         */
+        public Item(String user, String name) {
+            this.user = user.toLowerCase();
+            this.name = name;
+            itemType = null;
+            itemStatus = null;
+            groupNames = new CopyOnWriteArraySet<String>();
+        }
+
+        /**
+         * Returns the user.
+         *
+         * @return the user.
+         */
+        public String getUser() {
+            return user;
+        }
+
+        /**
+         * Returns the user's name.
+         *
+         * @return the user's name.
+         */
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * Sets the user's name.
+         *
+         * @param name the user's name.
+         */
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        /**
+         * Returns the roster item type.
+         *
+         * @return the roster item type.
+         */
+        public ItemType getItemType() {
+            return itemType;
+        }
+
+        /**
+         * Sets the roster item type.
+         *
+         * @param itemType the roster item type.
+         */
+        public void setItemType(ItemType itemType) {
+            this.itemType = itemType;
+        }
+
+        /**
+         * Returns the roster item status.
+         *
+         * @return the roster item status.
+         */
+        public ItemStatus getItemStatus() {
+            return itemStatus;
+        }
+
+        /**
+         * Sets the roster item status.
+         *
+         * @param itemStatus the roster item status.
+         */
+        public void setItemStatus(ItemStatus itemStatus) {
+            this.itemStatus = itemStatus;
+        }
+
+        /**
+         * Returns an unmodifiable set of the group names that the roster item
+         * belongs to.
+         *
+         * @return an unmodifiable set of the group names.
+         */
+        public Set<String> getGroupNames() {
+            return Collections.unmodifiableSet(groupNames);
+        }
+
+        /**
+         * Adds a group name.
+         *
+         * @param groupName the group name.
+         */
+        public void addGroupName(String groupName) {
+            groupNames.add(groupName);
+        }
+
+        /**
+         * Removes a group name.
+         *
+         * @param groupName the group name.
+         */
+        public void removeGroupName(String groupName) {
+            groupNames.remove(groupName);
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<item jid=\"").append(user).append("\"");
+            if (name != null) {
+                buf.append(" name=\"").append(StringUtils.escapeForXML(name)).append("\"");
+            }
+            if (itemType != null) {
+                buf.append(" subscription=\"").append(itemType).append("\"");
+            }
+            if (itemStatus != null) {
+                buf.append(" ask=\"").append(itemStatus).append("\"");
+            }
+            buf.append(">");
+            for (String groupName : groupNames) {
+                buf.append("<group>").append(StringUtils.escapeForXML(groupName)).append("</group>");
+            }
+            buf.append("</item>");
+            return buf.toString();
+        }
+    }
+
+    /**
+     * The subscription status of a roster item. An optional element that indicates
+     * the subscription status if a change request is pending.
+     */
+    public static class ItemStatus {
+
+        /**
+         * Request to subcribe.
+         */
+        public static final ItemStatus SUBSCRIPTION_PENDING = new ItemStatus("subscribe");
+
+        /**
+         * Request to unsubscribe.
+         */
+        public static final ItemStatus UNSUBSCRIPTION_PENDING = new ItemStatus("unsubscribe");
+
+        public static ItemStatus fromString(String value) {
+            if (value == null) {
+                return null;
+            }
+            value = value.toLowerCase();
+            if ("unsubscribe".equals(value)) {
+                return UNSUBSCRIPTION_PENDING;
+            }
+            else if ("subscribe".equals(value)) {
+                return SUBSCRIPTION_PENDING;
+            }
+            else {
+                return null;
+            }
+        }
+
+        private String value;
+
+        /**
+         * Returns the item status associated with the specified string.
+         *
+         * @param value the item status.
+         */
+        private ItemStatus(String value) {
+            this.value = value;
+        }
+
+        public String toString() {
+            return value;
+        }
+    }
+
+    public static enum ItemType {
+
+        /**
+         * The user and subscriber have no interest in each other's presence.
+         */
+        none,
+
+        /**
+         * The user is interested in receiving presence updates from the subscriber.
+         */
+        to,
+
+        /**
+         * The subscriber is interested in receiving presence updates from the user.
+         */
+        from,
+
+        /**
+         * The user and subscriber have a mutual interest in each other's presence.
+         */
+        both,
+
+        /**
+         * The user wishes to stop receiving presence updates from the subscriber.
+         */
+        remove
+    }
+}
diff --git a/src/org/jivesoftware/smack/packet/Session.java b/src/org/jivesoftware/smack/packet/Session.java
new file mode 100644
index 0000000..fd403ae
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/Session.java
@@ -0,0 +1,45 @@
+/**

+ * $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.packet;

+

+/**

+ * IQ packet that will be sent to the server to establish a session.<p>

+ *

+ * If a server supports sessions, it MUST include a <i>session</i> element in the

+ * stream features it advertises to a client after the completion of stream authentication.

+ * Upon being informed that session establishment is required by the server the client MUST

+ * establish a session if it desires to engage in instant messaging and presence functionality.<p>

+ *

+ * For more information refer to the following

+ * <a href=http://www.xmpp.org/specs/rfc3921.html#session>link</a>.

+ *

+ * @author Gaston Dombiak

+ */

+public class Session extends IQ {

+

+    public Session() {

+        setType(IQ.Type.SET);

+    }

+

+    public String getChildElementXML() {

+        return "<session xmlns=\"urn:ietf:params:xml:ns:xmpp-session\"/>";

+    }

+}

diff --git a/src/org/jivesoftware/smack/packet/StreamError.java b/src/org/jivesoftware/smack/packet/StreamError.java
new file mode 100644
index 0000000..8bb4c75
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/StreamError.java
@@ -0,0 +1,106 @@
+/**

+ * $Revision: 2408 $

+ * $Date: 2004-11-02 20:53:30 -0300 (Tue, 02 Nov 2004) $

+ *

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

+

+/**

+ * Represents a stream error packet. Stream errors are unrecoverable errors where the server

+ * will close the unrelying TCP connection after the stream error was sent to the client.

+ * These is the list of stream errors as defined in the XMPP spec:<p>

+ *

+ * <table border=1>

+ *      <tr><td><b>Code</b></td><td><b>Description</b></td></tr>

+ *      <tr><td> bad-format </td><td> the entity has sent XML that cannot be processed </td></tr>

+ *      <tr><td> unsupported-encoding </td><td>  the entity has sent a namespace prefix that is

+ *          unsupported </td></tr>

+ *      <tr><td> bad-namespace-prefix </td><td> Remote Server Timeout </td></tr>

+ *      <tr><td> conflict </td><td> the server is closing the active stream for this entity

+ *          because a new stream has been initiated that conflicts with the existing

+ *          stream. </td></tr>

+ *      <tr><td> connection-timeout </td><td> the entity has not generated any traffic over

+ *          the stream for some period of time. </td></tr>

+ *      <tr><td> host-gone </td><td> the value of the 'to' attribute provided by the initiating

+ *          entity in the stream header corresponds to a hostname that is no longer hosted by

+ *          the server. </td></tr>

+ *      <tr><td> host-unknown </td><td> the value of the 'to' attribute provided by the

+ *          initiating entity in the stream header does not correspond to a hostname that is

+ *          hosted by the server. </td></tr>

+ *      <tr><td> improper-addressing </td><td> a stanza sent between two servers lacks a 'to'

+ *          or 'from' attribute </td></tr>

+ *      <tr><td> internal-server-error </td><td> the server has experienced a

+ *          misconfiguration. </td></tr>

+ *      <tr><td> invalid-from </td><td> the JID or hostname provided in a 'from' address does

+ *          not match an authorized JID. </td></tr>

+ *      <tr><td> invalid-id </td><td> the stream ID or dialback ID is invalid or does not match

+ *          an ID previously provided. </td></tr>

+ *      <tr><td> invalid-namespace </td><td> the streams namespace name is invalid. </td></tr>

+ *      <tr><td> invalid-xml </td><td> the entity has sent invalid XML over the stream. </td></tr>

+ *      <tr><td> not-authorized </td><td> the entity has attempted to send data before the

+ *          stream has been authenticated </td></tr>

+ *      <tr><td> policy-violation </td><td> the entity has violated some local service

+ *          policy. </td></tr>

+ *      <tr><td> remote-connection-failed </td><td> Rthe server is unable to properly connect

+ *          to a remote entity. </td></tr>

+ *      <tr><td> resource-constraint </td><td> Rthe server lacks the system resources necessary

+ *          to service the stream. </td></tr>

+ *      <tr><td> restricted-xml </td><td> the entity has attempted to send restricted XML

+ *          features. </td></tr>

+ *      <tr><td> see-other-host </td><td>  the server will not provide service to the initiating

+ *          entity but is redirecting traffic to another host. </td></tr>

+ *      <tr><td> system-shutdown </td><td> the server is being shut down and all active streams

+ *          are being closed. </td></tr>

+ *      <tr><td> undefined-condition </td><td> the error condition is not one of those defined

+ *          by the other conditions in this list. </td></tr>

+ *      <tr><td> unsupported-encoding </td><td> the initiating entity has encoded the stream in

+ *          an encoding that is not supported. </td></tr>

+ *      <tr><td> unsupported-stanza-type </td><td> the initiating entity has sent a first-level

+ *          child of the stream that is not supported. </td></tr>

+ *      <tr><td> unsupported-version </td><td> the value of the 'version' attribute provided by

+ *          the initiating entity in the stream header specifies a version of XMPP that is not

+ *          supported. </td></tr>

+ *      <tr><td> xml-not-well-formed </td><td> the initiating entity has sent XML that is

+ *          not well-formed. </td></tr>

+ * </table>

+ *

+ * @author Gaston Dombiak

+ */

+public class StreamError {

+

+    private String code;

+

+    public StreamError(String code) {

+        super();

+        this.code = code;

+    }

+

+    /**

+     * Returns the error code.

+     *

+     * @return the error code.

+     */

+    public String getCode() {

+        return code;

+    }

+

+    public String toString() {

+        StringBuilder txt = new StringBuilder();

+        txt.append("stream:error (").append(code).append(")");

+        return txt.toString();

+    }

+}

diff --git a/src/org/jivesoftware/smack/packet/XMPPError.java b/src/org/jivesoftware/smack/packet/XMPPError.java
new file mode 100644
index 0000000..770a09c
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/XMPPError.java
@@ -0,0 +1,453 @@
+/**
+ * $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.packet;
+
+import java.util.*;
+
+/**
+ * Represents a XMPP error sub-packet. Typically, a server responds to a request that has
+ * problems by sending the packet back and including an error packet. Each error has a code, type, 
+ * error condition as well as as an optional text explanation. Typical errors are:<p>
+ *
+ * <table border=1>
+ *      <hr><td><b>Code</b></td><td><b>XMPP Error</b></td><td><b>Type</b></td></hr>
+ *      <tr><td>500</td><td>interna-server-error</td><td>WAIT</td></tr>
+ *      <tr><td>403</td><td>forbidden</td><td>AUTH</td></tr>
+ *      <tr><td>400</td<td>bad-request</td><td>MODIFY</td>></tr>
+ *      <tr><td>404</td><td>item-not-found</td><td>CANCEL</td></tr>
+ *      <tr><td>409</td><td>conflict</td><td>CANCEL</td></tr>
+ *      <tr><td>501</td><td>feature-not-implemented</td><td>CANCEL</td></tr>
+ *      <tr><td>302</td><td>gone</td><td>MODIFY</td></tr>
+ *      <tr><td>400</td><td>jid-malformed</td><td>MODIFY</td></tr>
+ *      <tr><td>406</td><td>no-acceptable</td><td> MODIFY</td></tr>
+ *      <tr><td>405</td><td>not-allowed</td><td>CANCEL</td></tr>
+ *      <tr><td>401</td><td>not-authorized</td><td>AUTH</td></tr>
+ *      <tr><td>402</td><td>payment-required</td><td>AUTH</td></tr>
+ *      <tr><td>404</td><td>recipient-unavailable</td><td>WAIT</td></tr>
+ *      <tr><td>302</td><td>redirect</td><td>MODIFY</td></tr>
+ *      <tr><td>407</td><td>registration-required</td><td>AUTH</td></tr>
+ *      <tr><td>404</td><td>remote-server-not-found</td><td>CANCEL</td></tr>
+ *      <tr><td>504</td><td>remote-server-timeout</td><td>WAIT</td></tr>
+ *      <tr><td>502</td><td>remote-server-error</td><td>CANCEL</td></tr>
+ *      <tr><td>500</td><td>resource-constraint</td><td>WAIT</td></tr>
+ *      <tr><td>503</td><td>service-unavailable</td><td>CANCEL</td></tr>
+ *      <tr><td>407</td><td>subscription-required</td><td>AUTH</td></tr>
+ *      <tr><td>500</td><td>undefined-condition</td><td>WAIT</td></tr>
+ *      <tr><td>400</td><td>unexpected-condition</td><td>WAIT</td></tr>
+ *      <tr><td>408</td><td>request-timeout</td><td>CANCEL</td></tr>
+ * </table>
+ *
+ * @author Matt Tucker
+ */
+public class XMPPError {
+
+    private int code;
+    private Type type;
+    private String condition;
+    private String message;
+    private List<PacketExtension> applicationExtensions = null;
+
+
+    /**
+     * Creates a new error with the specified condition infering the type and code.
+     * If the Condition is predefined, client code should be like:
+     *     new XMPPError(XMPPError.Condition.remote_server_timeout);
+     * If the Condition is not predefined, invocations should be like 
+     *     new XMPPError(new XMPPError.Condition("my_own_error"));
+     * 
+     * @param condition the error condition.
+     */
+    public XMPPError(Condition condition) {
+        this.init(condition);
+        this.message = null;
+    }
+
+    /**
+     * Creates a new error with the specified condition and message infering the type and code.
+     * If the Condition is predefined, client code should be like:
+     *     new XMPPError(XMPPError.Condition.remote_server_timeout, "Error Explanation");
+     * If the Condition is not predefined, invocations should be like 
+     *     new XMPPError(new XMPPError.Condition("my_own_error"), "Error Explanation");
+     *
+     * @param condition the error condition.
+     * @param messageText a message describing the error.
+     */
+    public XMPPError(Condition condition, String messageText) {
+        this.init(condition);
+        this.message = messageText;
+    }
+
+    /**
+     * Creates a new  error with the specified code and no message.
+     *
+     * @param code the error code.
+     * @deprecated new errors should be created using the constructor XMPPError(condition)
+     */
+    public XMPPError(int code) {
+        this.code = code;
+        this.message = null;
+    }
+
+    /**
+     * Creates a new error with the specified code and message.
+     * deprecated
+     *
+     * @param code the error code.
+     * @param message a message describing the error.
+     * @deprecated new errors should be created using the constructor XMPPError(condition, message)
+     */
+    public XMPPError(int code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+
+    /**
+     * Creates a new error with the specified code, type, condition and message.
+     * This constructor is used when the condition is not recognized automatically by XMPPError
+     * i.e. there is not a defined instance of ErrorCondition or it does not applies the default 
+     * specification.
+     * 
+     * @param code the error code.
+     * @param type the error type.
+     * @param condition the error condition.
+     * @param message a message describing the error.
+     * @param extension list of packet extensions
+     */
+    public XMPPError(int code, Type type, String condition, String message,
+            List<PacketExtension> extension) {
+        this.code = code;
+        this.type = type;
+        this.condition = condition;
+        this.message = message;
+        this.applicationExtensions = extension;
+    }
+
+    /**
+     * Initialize the error infering the type and code for the received condition.
+     * 
+     * @param condition the error condition.
+     */
+    private void init(Condition condition) {
+        // Look for the condition and its default code and type
+        ErrorSpecification defaultErrorSpecification = ErrorSpecification.specFor(condition);
+        this.condition = condition.value;
+        if (defaultErrorSpecification != null) {
+            // If there is a default error specification for the received condition,
+            // it get configured with the infered type and code.
+            this.type = defaultErrorSpecification.getType();
+            this.code = defaultErrorSpecification.getCode();
+        }
+    }
+    /**
+     * Returns the error condition.
+     *
+     * @return the error condition.
+     */
+    public String getCondition() {
+        return condition;
+    }
+
+    /**
+     * Returns the error type.
+     *
+     * @return the error type.
+     */
+    public Type getType() {
+        return type;
+    }
+
+    /**
+     * Returns the error code.
+     *
+     * @return the error code.
+     */
+    public int getCode() {
+        return code;
+    }
+
+    /**
+     * Returns the message describing the error, or null if there is no message.
+     *
+     * @return the message describing the error, or null if there is no message.
+     */
+    public String getMessage() {
+        return message;
+    }
+
+    /**
+     * Returns the error as XML.
+     *
+     * @return the error as XML.
+     */
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<error code=\"").append(code).append("\"");
+        if (type != null) {
+            buf.append(" type=\"");
+            buf.append(type.name());
+            buf.append("\"");
+        }
+        buf.append(">");
+        if (condition != null) {
+            buf.append("<").append(condition);
+            buf.append(" xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\"/>");
+        }
+        if (message != null) {
+            buf.append("<text xml:lang=\"en\" xmlns=\"urn:ietf:params:xml:ns:xmpp-stanzas\">");
+            buf.append(message);
+            buf.append("</text>");
+        }
+        for (PacketExtension element : this.getExtensions()) {
+            buf.append(element.toXML());
+        }
+        buf.append("</error>");
+        return buf.toString();
+    }
+
+    public String toString() {
+        StringBuilder txt = new StringBuilder();
+        if (condition != null) {
+            txt.append(condition);
+        }
+        txt.append("(").append(code).append(")");
+        if (message != null) {
+            txt.append(" ").append(message);
+        }
+        return txt.toString();
+    }
+
+    /**
+     * Returns an Iterator for the error extensions attached to the xmppError.
+     * An application MAY provide application-specific error information by including a 
+     * properly-namespaced child in the error element.
+     *
+     * @return an Iterator for the error extensions.
+     */
+    public synchronized List<PacketExtension> getExtensions() {
+        if (applicationExtensions == null) {
+            return Collections.emptyList();
+        }
+        return Collections.unmodifiableList(applicationExtensions);
+    }
+
+    /**
+     * Returns the first patcket extension that matches the specified element name and
+     * namespace, or <tt>null</tt> if it doesn't exist. 
+     *
+     * @param elementName the XML element name of the packet extension.
+     * @param namespace the XML element namespace of the packet extension.
+     * @return the extension, or <tt>null</tt> if it doesn't exist.
+     */
+    public synchronized PacketExtension getExtension(String elementName, String namespace) {
+        if (applicationExtensions == null || elementName == null || namespace == null) {
+            return null;
+        }
+        for (PacketExtension ext : applicationExtensions) {
+            if (elementName.equals(ext.getElementName()) && namespace.equals(ext.getNamespace())) {
+                return ext;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Adds a packet extension to the error.
+     *
+     * @param extension a packet extension.
+     */
+    public synchronized void addExtension(PacketExtension extension) {
+        if (applicationExtensions == null) {
+            applicationExtensions = new ArrayList<PacketExtension>();
+        }
+        applicationExtensions.add(extension);
+    }
+
+    /**
+     * Set the packet extension to the error.
+     *
+     * @param extension a packet extension.
+     */
+    public synchronized void setExtension(List<PacketExtension> extension) {
+        applicationExtensions = extension;
+    }
+
+    /**
+     * A class to represent the type of the Error. The types are:
+     *
+     * <ul>
+     *      <li>XMPPError.Type.WAIT - retry after waiting (the error is temporary)
+     *      <li>XMPPError.Type.CANCEL - do not retry (the error is unrecoverable)
+     *      <li>XMPPError.Type.MODIFY - retry after changing the data sent
+     *      <li>XMPPError.Type.AUTH - retry after providing credentials
+     *      <li>XMPPError.Type.CONTINUE - proceed (the condition was only a warning)
+     * </ul>
+     */
+    public static enum Type {
+        WAIT,
+        CANCEL,
+        MODIFY,
+        AUTH,
+        CONTINUE
+    }
+
+    /**
+     * A class to represent predefined error conditions.
+     */
+    public static class Condition {
+
+        public static final Condition interna_server_error = new Condition("internal-server-error");
+        public static final Condition forbidden = new Condition("forbidden");
+        public static final Condition bad_request = new Condition("bad-request");
+        public static final Condition conflict = new Condition("conflict");
+        public static final Condition feature_not_implemented = new Condition("feature-not-implemented");
+        public static final Condition gone = new Condition("gone");
+        public static final Condition item_not_found = new Condition("item-not-found");
+        public static final Condition jid_malformed = new Condition("jid-malformed");
+        public static final Condition no_acceptable = new Condition("not-acceptable");
+        public static final Condition not_allowed = new Condition("not-allowed");
+        public static final Condition not_authorized = new Condition("not-authorized");
+        public static final Condition payment_required = new Condition("payment-required");
+        public static final Condition recipient_unavailable = new Condition("recipient-unavailable");
+        public static final Condition redirect = new Condition("redirect");
+        public static final Condition registration_required = new Condition("registration-required");
+        public static final Condition remote_server_error = new Condition("remote-server-error");
+        public static final Condition remote_server_not_found = new Condition("remote-server-not-found");
+        public static final Condition remote_server_timeout = new Condition("remote-server-timeout");
+        public static final Condition resource_constraint = new Condition("resource-constraint");
+        public static final Condition service_unavailable = new Condition("service-unavailable");
+        public static final Condition subscription_required = new Condition("subscription-required");
+        public static final Condition undefined_condition = new Condition("undefined-condition");
+        public static final Condition unexpected_request = new Condition("unexpected-request");
+        public static final Condition request_timeout = new Condition("request-timeout");
+
+        private String value;
+
+        public Condition(String value) {
+            this.value = value;
+        }
+
+        public String toString() {
+            return value;
+        }
+    }
+
+
+    /**
+     * A class to represent the error specification used to infer common usage.
+     */
+    private static class ErrorSpecification {
+        private int code;
+        private Type type;
+        private Condition condition;
+        private static Map<Condition, ErrorSpecification> instances = errorSpecifications();
+
+        private ErrorSpecification(Condition condition, Type type, int code) {
+            this.code = code;
+            this.type = type;
+            this.condition = condition;
+        }
+
+        private static Map<Condition, ErrorSpecification> errorSpecifications() {
+            Map<Condition, ErrorSpecification> instances = new HashMap<Condition, ErrorSpecification>(22);
+            instances.put(Condition.interna_server_error, new ErrorSpecification(
+                    Condition.interna_server_error, Type.WAIT, 500));
+            instances.put(Condition.forbidden, new ErrorSpecification(Condition.forbidden,
+                    Type.AUTH, 403));
+            instances.put(Condition.bad_request, new XMPPError.ErrorSpecification(
+                    Condition.bad_request, Type.MODIFY, 400));
+            instances.put(Condition.item_not_found, new XMPPError.ErrorSpecification(
+                    Condition.item_not_found, Type.CANCEL, 404));
+            instances.put(Condition.conflict, new XMPPError.ErrorSpecification(
+                    Condition.conflict, Type.CANCEL, 409));
+            instances.put(Condition.feature_not_implemented, new XMPPError.ErrorSpecification(
+                    Condition.feature_not_implemented, Type.CANCEL, 501));
+            instances.put(Condition.gone, new XMPPError.ErrorSpecification(
+                    Condition.gone, Type.MODIFY, 302));
+            instances.put(Condition.jid_malformed, new XMPPError.ErrorSpecification(
+                    Condition.jid_malformed, Type.MODIFY, 400));
+            instances.put(Condition.no_acceptable, new XMPPError.ErrorSpecification(
+                    Condition.no_acceptable, Type.MODIFY, 406));
+            instances.put(Condition.not_allowed, new XMPPError.ErrorSpecification(
+                    Condition.not_allowed, Type.CANCEL, 405));
+            instances.put(Condition.not_authorized, new XMPPError.ErrorSpecification(
+                    Condition.not_authorized, Type.AUTH, 401));
+            instances.put(Condition.payment_required, new XMPPError.ErrorSpecification(
+                    Condition.payment_required, Type.AUTH, 402));
+            instances.put(Condition.recipient_unavailable, new XMPPError.ErrorSpecification(
+                    Condition.recipient_unavailable, Type.WAIT, 404));
+            instances.put(Condition.redirect, new XMPPError.ErrorSpecification(
+                    Condition.redirect, Type.MODIFY, 302));
+            instances.put(Condition.registration_required, new XMPPError.ErrorSpecification(
+                    Condition.registration_required, Type.AUTH, 407));
+            instances.put(Condition.remote_server_not_found, new XMPPError.ErrorSpecification(
+                    Condition.remote_server_not_found, Type.CANCEL, 404));
+            instances.put(Condition.remote_server_timeout, new XMPPError.ErrorSpecification(
+                    Condition.remote_server_timeout, Type.WAIT, 504));
+            instances.put(Condition.remote_server_error, new XMPPError.ErrorSpecification(
+                    Condition.remote_server_error, Type.CANCEL, 502));
+            instances.put(Condition.resource_constraint, new XMPPError.ErrorSpecification(
+                    Condition.resource_constraint, Type.WAIT, 500));
+            instances.put(Condition.service_unavailable, new XMPPError.ErrorSpecification(
+                    Condition.service_unavailable, Type.CANCEL, 503));
+            instances.put(Condition.subscription_required, new XMPPError.ErrorSpecification(
+                    Condition.subscription_required, Type.AUTH, 407));
+            instances.put(Condition.undefined_condition, new XMPPError.ErrorSpecification(
+                    Condition.undefined_condition, Type.WAIT, 500));
+            instances.put(Condition.unexpected_request, new XMPPError.ErrorSpecification(
+                    Condition.unexpected_request, Type.WAIT, 400));
+            instances.put(Condition.request_timeout, new XMPPError.ErrorSpecification(
+                    Condition.request_timeout, Type.CANCEL, 408));
+
+            return instances;
+        }
+
+        protected static ErrorSpecification specFor(Condition condition) {
+            return instances.get(condition);
+        }
+
+        /**
+         * Returns the error condition.
+         *
+         * @return the error condition.
+         */
+        protected Condition getCondition() {
+            return condition;
+        }
+
+        /**
+         * Returns the error type.
+         *
+         * @return the error type.
+         */
+        protected Type getType() {
+            return type;
+        }
+
+        /**
+         * Returns the error code.
+         *
+         * @return the error code.
+         */
+        protected int getCode() {
+            return code;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/packet/package.html b/src/org/jivesoftware/smack/packet/package.html
new file mode 100644
index 0000000..18a6405
--- /dev/null
+++ b/src/org/jivesoftware/smack/packet/package.html
@@ -0,0 +1 @@
+<body>XML packets that are part of the XMPP protocol.</body>
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/provider/EmbeddedExtensionProvider.java b/src/org/jivesoftware/smack/provider/EmbeddedExtensionProvider.java
new file mode 100644
index 0000000..e7b4b93
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/EmbeddedExtensionProvider.java
@@ -0,0 +1,109 @@
+/**

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

+

+import java.util.ArrayList;

+import java.util.HashMap;

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.jivesoftware.smack.util.PacketParserUtils;

+import org.jivesoftware.smackx.pubsub.provider.ItemProvider;

+import org.jivesoftware.smackx.pubsub.provider.ItemsProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * 

+ * This class simplifies parsing of embedded elements by using the 

+ * <a href="http://en.wikipedia.org/wiki/Template_method_pattern">Template Method Pattern</a>.  

+ * After extracting the current element attributes and content of any child elements, the template method 

+ * ({@link #createReturnExtension(String, String, Map, List)} is called.  Subclasses

+ * then override this method to create the specific return type.

+ * 

+ * <p>To use this class, you simply register your subclasses as extension providers in the 

+ * <b>smack.properties</b> file.  Then they will be automatically picked up and used to parse

+ * any child elements.  

+ * 

+ * <pre>

+ * For example, given the following message

+ * 

+ * &lt;message from='pubsub.shakespeare.lit' to='francisco@denmark.lit' id='foo&gt;

+ *    &lt;event xmlns='http://jabber.org/protocol/pubsub#event&gt;

+ *       &lt;items node='princely_musings'&gt;

+ *          &lt;item id='asdjkwei3i34234n356'&gt;

+ *             &lt;entry xmlns='http://www.w3.org/2005/Atom'&gt;

+ *                &lt;title&gt;Soliloquy&lt;/title&gt;

+ *                &lt;link rel='alternative' type='text/html'/&gt;

+ *                &lt;id>tag:denmark.lit,2003:entry-32397&lt;/id&gt;

+ *             &lt;/entry&gt;

+ *          &lt;/item&gt;

+ *       &lt;/items&gt;

+ *    &lt;/event&gt;

+ * &lt;/message&gt;

+ * 

+ * I would have a classes

+ * {@link ItemsProvider} extends {@link EmbeddedExtensionProvider}

+ * {@link ItemProvider} extends {@link EmbeddedExtensionProvider}

+ * and

+ * AtomProvider extends {@link PacketExtensionProvider}

+ * 

+ * These classes are then registered in the meta-inf/smack.providers file

+ * as follows.

+ * 

+ *   &lt;extensionProvider&gt;

+ *      &lt;elementName&gt;items&lt;/elementName&gt;

+ *      &lt;namespace&gt;http://jabber.org/protocol/pubsub#event&lt;/namespace&gt;

+ *      &lt;className&gt;org.jivesoftware.smackx.provider.ItemsEventProvider&lt;/className&gt;

+ *   &lt;/extensionProvider&gt;

+ *   &lt;extensionProvider&gt;

+ *       &lt;elementName&gt;item&lt;/elementName&gt;

+ *       &lt;namespace&gt;http://jabber.org/protocol/pubsub#event&lt;/namespace&gt;

+ *       &lt;className&gt;org.jivesoftware.smackx.provider.ItemProvider&lt;/className&gt;

+ *   &lt;/extensionProvider&gt;

+ * 

+ * </pre>

+ * 

+ * @author Robin Collier

+ */

+abstract public class EmbeddedExtensionProvider implements PacketExtensionProvider

+{

+

+	final public PacketExtension parseExtension(XmlPullParser parser) throws Exception

+	{

+        String namespace = parser.getNamespace();

+        String name = parser.getName();

+        Map<String, String> attMap = new HashMap<String, String>();

+        

+        for(int i=0; i<parser.getAttributeCount(); i++)

+        {

+        	attMap.put(parser.getAttributeName(i), parser.getAttributeValue(i));

+        }

+        List<PacketExtension> extensions = new ArrayList<PacketExtension>();

+        

+        do

+        {

+            int tag = parser.next();

+

+            if (tag == XmlPullParser.START_TAG) 

+            	extensions.add(PacketParserUtils.parsePacketExtension(parser.getName(), parser.getNamespace(), parser));

+        } while (!name.equals(parser.getName()));

+

+		return createReturnExtension(name, namespace, attMap, extensions);

+	}

+

+	abstract protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content);

+}

diff --git a/src/org/jivesoftware/smack/provider/IQProvider.java b/src/org/jivesoftware/smack/provider/IQProvider.java
new file mode 100644
index 0000000..936c6be
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/IQProvider.java
@@ -0,0 +1,47 @@
+/**
+ * $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.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * An interface for parsing custom IQ packets. Each IQProvider must be registered with
+ * the ProviderManager class for it to be used. Every implementation of this
+ * interface <b>must</b> have a public, no-argument constructor.
+ *
+ * @author Matt Tucker
+ */
+public interface IQProvider {
+
+    /**
+     * Parse the IQ sub-document and create an IQ instance. Each IQ must have a
+     * single child element. At the beginning of the method call, the xml parser
+     * will be positioned at the opening tag of the IQ child element. At the end
+     * of the method call, the parser <b>must</b> be positioned on the closing tag
+     * of the child element.
+     *
+     * @param parser an XML parser.
+     * @return a new IQ instance.
+     * @throws Exception if an error occurs parsing the XML.
+     */
+    public IQ parseIQ(XmlPullParser parser) throws Exception;
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java b/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java
new file mode 100644
index 0000000..8fc0af3
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/PacketExtensionProvider.java
@@ -0,0 +1,46 @@
+/**
+ * $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.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * An interface for parsing custom packets extensions. Each PacketExtensionProvider must
+ * be registered with the ProviderManager class for it to be used. Every implementation
+ * of this interface <b>must</b> have a public, no-argument constructor.
+ *
+ * @author Matt Tucker
+ */
+public interface PacketExtensionProvider {
+
+    /**
+     * Parse an extension sub-packet and create a PacketExtension instance. At
+     * the beginning of the method call, the xml parser will be positioned on the
+     * opening element of the packet extension. At the end of the method call, the
+     * parser <b>must</b> be positioned on the closing element of the packet extension.
+     *
+     * @param parser an XML parser.
+     * @return a new IQ instance.
+     * @throws java.lang.Exception if an error occurs parsing the XML.
+     */
+    public PacketExtension parseExtension(XmlPullParser parser) throws Exception;
+}
diff --git a/src/org/jivesoftware/smack/provider/PrivacyProvider.java b/src/org/jivesoftware/smack/provider/PrivacyProvider.java
new file mode 100644
index 0000000..62b3120
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/PrivacyProvider.java
@@ -0,0 +1,151 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.provider;

+

+import org.jivesoftware.smack.packet.DefaultPacketExtension;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Privacy;

+import org.jivesoftware.smack.packet.PrivacyItem;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.util.ArrayList;

+

+/**

+ * The PrivacyProvider parses {@link Privacy} packets. {@link Privacy}

+ * Parses the <tt>query</tt> sub-document and creates an instance of {@link Privacy}.

+ * For each <tt>item</tt> in the <tt>list</tt> element, it creates an instance 

+ * of {@link PrivacyItem} and {@link org.jivesoftware.smack.packet.PrivacyItem.PrivacyRule}.

+ * 

+ * @author Francisco Vives

+ */

+public class PrivacyProvider implements IQProvider {

+

+	public PrivacyProvider() {

+	}

+

+	public IQ parseIQ(XmlPullParser parser) throws Exception {

+        Privacy privacy = new Privacy();

+        /* privacy.addExtension(PacketParserUtils.parsePacketExtension(parser

+                .getName(), parser.getNamespace(), parser)); */

+        privacy.addExtension(new DefaultPacketExtension(parser.getName(), parser.getNamespace()));

+        boolean done = false;

+        while (!done) {

+            int eventType = parser.next();

+            if (eventType == XmlPullParser.START_TAG) {

+                if (parser.getName().equals("active")) {

+                	String activeName = parser.getAttributeValue("", "name");

+                	if (activeName == null) {

+                		privacy.setDeclineActiveList(true);

+                	} else {

+                		privacy.setActiveName(activeName);

+                	}

+                }

+                else if (parser.getName().equals("default")) {

+                	String defaultName = parser.getAttributeValue("", "name");

+                	if (defaultName == null) {

+                		privacy.setDeclineDefaultList(true);

+                	} else {

+                		privacy.setDefaultName(defaultName);

+                	}

+                }

+                else if (parser.getName().equals("list")) {

+                    parseList(parser, privacy);

+                }

+            }

+            else if (eventType == XmlPullParser.END_TAG) {

+                if (parser.getName().equals("query")) {

+                    done = true;

+                }

+            }

+        }

+

+        return privacy;

+	}

+	

+	// Parse the list complex type

+	public void parseList(XmlPullParser parser, Privacy privacy) throws Exception {

+        boolean done = false;

+        String listName = parser.getAttributeValue("", "name");

+        ArrayList<PrivacyItem> items = new ArrayList<PrivacyItem>();

+        while (!done) {

+            int eventType = parser.next();

+            if (eventType == XmlPullParser.START_TAG) {

+                if (parser.getName().equals("item")) {

+                	items.add(parseItem(parser));

+                }

+            }

+            else if (eventType == XmlPullParser.END_TAG) {

+                if (parser.getName().equals("list")) {

+                    done = true;

+                }

+            }

+        }

+

+        privacy.setPrivacyList(listName, items);

+	}

+	

+	// Parse the list complex type

+	public PrivacyItem parseItem(XmlPullParser parser) throws Exception {

+        boolean done = false;

+        // Retrieves the required attributes

+        String actionValue = parser.getAttributeValue("", "action");

+        String orderValue = parser.getAttributeValue("", "order");

+        String type = parser.getAttributeValue("", "type");

+        

+        /* 

+         * According the action value it sets the allow status. The fall-through action is assumed 

+         * to be "allow"

+         */

+        boolean allow = true;

+        if ("allow".equalsIgnoreCase(actionValue)) {

+        	allow = true;

+        } else if ("deny".equalsIgnoreCase(actionValue)) {

+        	allow = false;

+        }

+        // Set the order number

+        int order = Integer.parseInt(orderValue);

+

+        // Create the privacy item

+        PrivacyItem item = new PrivacyItem(type, allow, order);

+        item.setValue(parser.getAttributeValue("", "value"));

+

+        while (!done) {

+            int eventType = parser.next();

+            if (eventType == XmlPullParser.START_TAG) {

+                if (parser.getName().equals("iq")) {

+                	item.setFilterIQ(true);

+                }

+                if (parser.getName().equals("message")) {

+                	item.setFilterMessage(true);

+                }

+                if (parser.getName().equals("presence-in")) {

+                	item.setFilterPresence_in(true);

+                }

+                if (parser.getName().equals("presence-out")) {

+                	item.setFilterPresence_out(true);

+                }

+            }

+            else if (eventType == XmlPullParser.END_TAG) {

+                if (parser.getName().equals("item")) {

+                    done = true;

+                }

+            }

+        }

+        return item;

+	}

+}

diff --git a/src/org/jivesoftware/smack/provider/ProviderManager.java b/src/org/jivesoftware/smack/provider/ProviderManager.java
new file mode 100644
index 0000000..4ddc8ad
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/ProviderManager.java
@@ -0,0 +1,438 @@
+/**
+ * $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.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manages providers for parsing custom XML sub-documents of XMPP packets. Two types of
+ * providers exist:<ul>
+ *      <li>IQProvider -- parses IQ requests into Java objects.
+ *      <li>PacketExtension -- parses XML sub-documents attached to packets into
+ *          PacketExtension instances.</ul>
+ *
+ * <b>IQProvider</b><p>
+ *
+ * By default, Smack only knows how to process IQ packets with sub-packets that
+ * are in a few namespaces such as:<ul>
+ *      <li>jabber:iq:auth
+ *      <li>jabber:iq:roster
+ *      <li>jabber:iq:register</ul>
+ *
+ * Because many more IQ types are part of XMPP and its extensions, a pluggable IQ parsing
+ * mechanism is provided. IQ providers are registered programatically or by creating a
+ * smack.providers file in the META-INF directory of your JAR file. The file is an XML
+ * document that contains one or more iqProvider entries, as in the following example:
+ *
+ * <pre>
+ * &lt;?xml version="1.0"?&gt;
+ * &lt;smackProviders&gt;
+ *     &lt;iqProvider&gt;
+ *         &lt;elementName&gt;query&lt;/elementName&gt;
+ *         &lt;namespace&gt;jabber:iq:time&lt;/namespace&gt;
+ *         &lt;className&gt;org.jivesoftware.smack.packet.Time&lt/className&gt;
+ *     &lt;/iqProvider&gt;
+ * &lt;/smackProviders&gt;</pre>
+ *
+ * Each IQ provider is associated with an element name and a namespace. If multiple provider
+ * entries attempt to register to handle the same namespace, the first entry loaded from the
+ * classpath will take precedence. The IQ provider class can either implement the IQProvider
+ * interface, or extend the IQ class. In the former case, each IQProvider is responsible for
+ * parsing the raw XML stream to create an IQ instance. In the latter case, bean introspection
+ * is used to try to automatically set properties of the IQ instance using the values found
+ * in the IQ packet XML. For example, an XMPP time packet resembles the following:
+ * <pre>
+ * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
+ *     &lt;query xmlns='jabber:iq:time'&gt;
+ *         &lt;utc&gt;20020910T17:58:35&lt;/utc&gt;
+ *         &lt;tz&gt;MDT&lt;/tz&gt;
+ *         &lt;display&gt;Tue Sep 10 12:58:35 2002&lt;/display&gt;
+ *     &lt;/query&gt;
+ * &lt;/iq&gt;</pre>
+ *
+ * In order for this packet to be automatically mapped to the Time object listed in the
+ * providers file above, it must have the methods setUtc(String), setTz(String), and
+ * setDisplay(String). The introspection service will automatically try to convert the String
+ * value from the XML into a boolean, int, long, float, double, or Class depending on the
+ * type the IQ instance expects.<p>
+ *
+ * A pluggable system for packet extensions, child elements in a custom namespace for
+ * message and presence packets, also exists. Each extension provider
+ * is registered with a name space in the smack.providers file as in the following example:
+ *
+ * <pre>
+ * &lt;?xml version="1.0"?&gt;
+ * &lt;smackProviders&gt;
+ *     &lt;extensionProvider&gt;
+ *         &lt;elementName&gt;x&lt;/elementName&gt;
+ *         &lt;namespace&gt;jabber:iq:event&lt;/namespace&gt;
+ *         &lt;className&gt;org.jivesoftware.smack.packet.MessageEvent&lt/className&gt;
+ *     &lt;/extensionProvider&gt;
+ * &lt;/smackProviders&gt;</pre>
+ *
+ * If multiple provider entries attempt to register to handle the same element name and namespace,
+ * the first entry loaded from the classpath will take precedence. Whenever a packet extension
+ * is found in a packet, parsing will be passed to the correct provider. Each provider
+ * can either implement the PacketExtensionProvider interface or be a standard Java Bean. In
+ * the former case, each extension provider is responsible for parsing the raw XML stream to
+ * contruct an object. In the latter case, bean introspection is used to try to automatically
+ * set the properties of the class using the values in the packet extension sub-element. When an
+ * extension provider is not registered for an element name and namespace combination, Smack will
+ * store all top-level elements of the sub-packet in DefaultPacketExtension object and then
+ * attach it to the packet.<p>
+ *
+ * It is possible to provide a custom provider manager instead of the default implementation
+ * provided by Smack. If you want to provide your own provider manager then you need to do it
+ * before creating any {@link org.jivesoftware.smack.Connection} by sending the static
+ * {@link #setInstance(ProviderManager)} message. Trying to change the provider manager after
+ * an Connection was created will result in an {@link IllegalStateException} error.
+ *
+ * @author Matt Tucker
+ */
+public class ProviderManager {
+
+    private static ProviderManager instance;
+
+    private Map<String, Object> extensionProviders = new ConcurrentHashMap<String, Object>();
+    private Map<String, Object> iqProviders = new ConcurrentHashMap<String, Object>();
+
+    /**
+     * Returns the only ProviderManager valid instance.  Use {@link #setInstance(ProviderManager)}
+     * to configure your own provider manager. If non was provided then an instance of this
+     * class will be used.
+     *
+     * @return the only ProviderManager valid instance.
+     */
+    public static synchronized ProviderManager getInstance() {
+        if (instance == null) {
+            instance = new ProviderManager();
+        }
+        return instance;
+    }
+
+    /**
+     * Sets the only ProviderManager valid instance to be used by all Connections. If you
+     * want to provide your own provider manager then you need to do it before creating
+     * any Connection. Otherwise an IllegalStateException will be thrown.
+     *
+     * @param providerManager the only ProviderManager valid instance to be used.
+     * @throws IllegalStateException if a provider manager was already configued.
+     */
+    public static synchronized void setInstance(ProviderManager providerManager) {
+        if (instance != null) {
+            throw new IllegalStateException("ProviderManager singleton already set");
+        }
+        instance = providerManager;
+    }
+
+    protected void initialize() {
+        // Load IQ processing providers.
+        try {
+            // Get an array of class loaders to try loading the providers files from.
+            ClassLoader[] classLoaders = getClassLoaders();
+            for (ClassLoader classLoader : classLoaders) {
+                Enumeration<URL> providerEnum = classLoader.getResources(
+                        "META-INF/smack.providers");
+                while (providerEnum.hasMoreElements()) {
+                    URL url = providerEnum.nextElement();
+                    InputStream providerStream = null;
+                    try {
+                        providerStream = url.openStream();
+                        XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
+                        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+                        parser.setInput(providerStream, "UTF-8");
+                        int eventType = parser.getEventType();
+                        do {
+                            if (eventType == XmlPullParser.START_TAG) {
+                                if (parser.getName().equals("iqProvider")) {
+                                    parser.next();
+                                    parser.next();
+                                    String elementName = parser.nextText();
+                                    parser.next();
+                                    parser.next();
+                                    String namespace = parser.nextText();
+                                    parser.next();
+                                    parser.next();
+                                    String className = parser.nextText();
+                                    // Only add the provider for the namespace if one isn't
+                                    // already registered.
+                                    String key = getProviderKey(elementName, namespace);
+                                    if (!iqProviders.containsKey(key)) {
+                                        // Attempt to load the provider class and then create
+                                        // a new instance if it's an IQProvider. Otherwise, if it's
+                                        // an IQ class, add the class object itself, then we'll use
+                                        // reflection later to create instances of the class.
+                                        try {
+                                            // Add the provider to the map.
+                                            Class<?> provider = Class.forName(className);
+                                            if (IQProvider.class.isAssignableFrom(provider)) {
+                                                iqProviders.put(key, provider.newInstance());
+                                            }
+                                            else if (IQ.class.isAssignableFrom(provider)) {
+                                                iqProviders.put(key, provider);
+                                            }
+                                        }
+                                        catch (ClassNotFoundException cnfe) {
+                                            cnfe.printStackTrace();
+                                        }
+                                    }
+                                }
+                                else if (parser.getName().equals("extensionProvider")) {
+                                    parser.next();
+                                    parser.next();
+                                    String elementName = parser.nextText();
+                                    parser.next();
+                                    parser.next();
+                                    String namespace = parser.nextText();
+                                    parser.next();
+                                    parser.next();
+                                    String className = parser.nextText();
+                                    // Only add the provider for the namespace if one isn't
+                                    // already registered.
+                                    String key = getProviderKey(elementName, namespace);
+                                    if (!extensionProviders.containsKey(key)) {
+                                        // Attempt to load the provider class and then create
+                                        // a new instance if it's a Provider. Otherwise, if it's
+                                        // a PacketExtension, add the class object itself and
+                                        // then we'll use reflection later to create instances
+                                        // of the class.
+                                        try {
+                                            // Add the provider to the map.
+                                            Class<?> provider = Class.forName(className);
+                                            if (PacketExtensionProvider.class.isAssignableFrom(
+                                                    provider)) {
+                                                extensionProviders.put(key, provider.newInstance());
+                                            }
+                                            else if (PacketExtension.class.isAssignableFrom(
+                                                    provider)) {
+                                                extensionProviders.put(key, provider);
+                                            }
+                                        }
+                                        catch (ClassNotFoundException cnfe) {
+                                            cnfe.printStackTrace();
+                                        }
+                                    }
+                                }
+                            }
+                            eventType = parser.next();
+                        }
+                        while (eventType != XmlPullParser.END_DOCUMENT);
+                    }
+                    finally {
+                        try {
+                            providerStream.close();
+                        }
+                        catch (Exception e) {
+                            // Ignore.
+                        }
+                    }
+                }
+            }
+        }
+        catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Returns the IQ provider registered to the specified XML element name and namespace.
+     * For example, if a provider was registered to the element name "query" and the
+     * namespace "jabber:iq:time", then the following packet would trigger the provider:
+     *
+     * <pre>
+     * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
+     *     &lt;query xmlns='jabber:iq:time'&gt;
+     *         &lt;utc&gt;20020910T17:58:35&lt;/utc&gt;
+     *         &lt;tz&gt;MDT&lt;/tz&gt;
+     *         &lt;display&gt;Tue Sep 10 12:58:35 2002&lt;/display&gt;
+     *     &lt;/query&gt;
+     * &lt;/iq&gt;</pre>
+     *
+     * <p>Note: this method is generally only called by the internal Smack classes.
+     *
+     * @param elementName the XML element name.
+     * @param namespace the XML namespace.
+     * @return the IQ provider.
+     */
+    public Object getIQProvider(String elementName, String namespace) {
+        String key = getProviderKey(elementName, namespace);
+        return iqProviders.get(key);
+    }
+
+    /**
+     * Returns an unmodifiable collection of all IQProvider instances. Each object
+     * in the collection will either be an IQProvider instance, or a Class object
+     * that implements the IQProvider interface.
+     *
+     * @return all IQProvider instances.
+     */
+    public Collection<Object> getIQProviders() {
+        return Collections.unmodifiableCollection(iqProviders.values());
+    }
+
+    /**
+     * Adds an IQ provider (must be an instance of IQProvider or Class object that is an IQ)
+     * with the specified element name and name space. The provider will override any providers
+     * loaded through the classpath.
+     *
+     * @param elementName the XML element name.
+     * @param namespace the XML namespace.
+     * @param provider the IQ provider.
+     */
+    public void addIQProvider(String elementName, String namespace,
+            Object provider)
+    {
+        if (!(provider instanceof IQProvider || (provider instanceof Class &&
+                IQ.class.isAssignableFrom((Class<?>)provider))))
+        {
+            throw new IllegalArgumentException("Provider must be an IQProvider " +
+                    "or a Class instance.");
+        }
+        String key = getProviderKey(elementName, namespace);
+        iqProviders.put(key, provider);
+    }
+
+    /**
+     * Removes an IQ provider with the specified element name and namespace. This
+     * method is typically called to cleanup providers that are programatically added
+     * using the {@link #addIQProvider(String, String, Object) addIQProvider} method.
+     *
+     * @param elementName the XML element name.
+     * @param namespace the XML namespace.
+     */
+    public void removeIQProvider(String elementName, String namespace) {
+        String key = getProviderKey(elementName, namespace);
+        iqProviders.remove(key);
+    }
+
+    /**
+     * Returns the packet extension provider registered to the specified XML element name
+     * and namespace. For example, if a provider was registered to the element name "x" and the
+     * namespace "jabber:x:event", then the following packet would trigger the provider:
+     *
+     * <pre>
+     * &lt;message to='romeo@montague.net' id='message_1'&gt;
+     *     &lt;body&gt;Art thou not Romeo, and a Montague?&lt;/body&gt;
+     *     &lt;x xmlns='jabber:x:event'&gt;
+     *         &lt;composing/&gt;
+     *     &lt;/x&gt;
+     * &lt;/message&gt;</pre>
+     *
+     * <p>Note: this method is generally only called by the internal Smack classes.
+     *
+     * @param elementName element name associated with extension provider.
+     * @param namespace namespace associated with extension provider.
+     * @return the extenion provider.
+     */
+    public Object getExtensionProvider(String elementName, String namespace) {
+        String key = getProviderKey(elementName, namespace);
+        return extensionProviders.get(key);
+    }
+
+    /**
+     * Adds an extension provider with the specified element name and name space. The provider
+     * will override any providers loaded through the classpath. The provider must be either
+     * a PacketExtensionProvider instance, or a Class object of a Javabean.
+     *
+     * @param elementName the XML element name.
+     * @param namespace the XML namespace.
+     * @param provider the extension provider.
+     */
+    public void addExtensionProvider(String elementName, String namespace,
+            Object provider)
+    {
+        if (!(provider instanceof PacketExtensionProvider || provider instanceof Class)) {
+            throw new IllegalArgumentException("Provider must be a PacketExtensionProvider " +
+                    "or a Class instance.");
+        }
+        String key = getProviderKey(elementName, namespace);
+        extensionProviders.put(key, provider);
+    }
+
+    /**
+     * Removes an extension provider with the specified element name and namespace. This
+     * method is typically called to cleanup providers that are programatically added
+     * using the {@link #addExtensionProvider(String, String, Object) addExtensionProvider} method.
+     *
+     * @param elementName the XML element name.
+     * @param namespace the XML namespace.
+     */
+    public void removeExtensionProvider(String elementName, String namespace) {
+        String key = getProviderKey(elementName, namespace);
+        extensionProviders.remove(key);
+    }
+
+    /**
+     * Returns an unmodifiable collection of all PacketExtensionProvider instances. Each object
+     * in the collection will either be a PacketExtensionProvider instance, or a Class object
+     * that implements the PacketExtensionProvider interface.
+     *
+     * @return all PacketExtensionProvider instances.
+     */
+    public Collection<Object> getExtensionProviders() {
+        return Collections.unmodifiableCollection(extensionProviders.values());
+    }
+
+    /**
+     * Returns a String key for a given element name and namespace.
+     *
+     * @param elementName the element name.
+     * @param namespace the namespace.
+     * @return a unique key for the element name and namespace pair.
+     */
+    private String getProviderKey(String elementName, String namespace) {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(elementName).append("/><").append(namespace).append("/>");
+        return buf.toString();
+    }
+
+    /**
+     * Returns an array of class loaders to load resources from.
+     *
+     * @return an array of ClassLoader instances.
+     */
+    private ClassLoader[] getClassLoaders() {
+        ClassLoader[] classLoaders = new ClassLoader[2];
+        classLoaders[0] = ProviderManager.class.getClassLoader();
+        classLoaders[1] = Thread.currentThread().getContextClassLoader();
+        // Clean up possible null values. Note that #getClassLoader may return a null value.
+        List<ClassLoader> loaders = new ArrayList<ClassLoader>();
+        for (ClassLoader classLoader : classLoaders) {
+            if (classLoader != null) {
+                loaders.add(classLoader);
+            }
+        }
+        return loaders.toArray(new ClassLoader[loaders.size()]);
+    }
+
+    private ProviderManager() {
+        super();
+        initialize();
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/provider/package.html b/src/org/jivesoftware/smack/provider/package.html
new file mode 100644
index 0000000..fccc383
--- /dev/null
+++ b/src/org/jivesoftware/smack/provider/package.html
@@ -0,0 +1 @@
+<body>Provides pluggable parsing of incoming IQ's and packet extensions.</body>
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/proxy/DirectSocketFactory.java b/src/org/jivesoftware/smack/proxy/DirectSocketFactory.java
new file mode 100644
index 0000000..6197c38
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/DirectSocketFactory.java
@@ -0,0 +1,74 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+
+/**
+ * SocketFactory for direct connection
+ * 
+ * @author Atul Aggarwal
+ */
+class DirectSocketFactory 
+    extends SocketFactory
+{
+
+    public DirectSocketFactory()
+    {
+    }
+
+    static private int roundrobin = 0;
+
+    public Socket createSocket(String host, int port) 
+        throws IOException, UnknownHostException
+    {
+        Socket newSocket = new Socket(Proxy.NO_PROXY);
+        InetAddress resolved[] = InetAddress.getAllByName(host);
+        newSocket.connect(new InetSocketAddress(resolved[(roundrobin++) % resolved.length],port));
+        return newSocket;
+    }
+
+    public Socket createSocket(String host ,int port, InetAddress localHost,
+                                int localPort)
+        throws IOException, UnknownHostException
+    {
+        return new Socket(host,port,localHost,localPort);
+    }
+
+    public Socket createSocket(InetAddress host, int port)
+        throws IOException
+    {
+        Socket newSocket = new Socket(Proxy.NO_PROXY);
+        newSocket.connect(new InetSocketAddress(host,port));
+        return newSocket;
+    }
+
+    public Socket createSocket( InetAddress address, int port, 
+                                InetAddress localAddress, int localPort) 
+        throws IOException
+    {
+        return new Socket(address,port,localAddress,localPort);
+    }
+
+}
diff --git a/src/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java b/src/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java
new file mode 100644
index 0000000..4ee5dd6
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/HTTPProxySocketFactory.java
@@ -0,0 +1,172 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Http Proxy Socket Factory which returns socket connected to Http Proxy
+ * 
+ * @author Atul Aggarwal
+ */
+class HTTPProxySocketFactory 
+    extends SocketFactory
+{
+
+    private ProxyInfo proxy;
+
+    public HTTPProxySocketFactory(ProxyInfo proxy)
+    {
+        this.proxy = proxy;
+    }
+
+    public Socket createSocket(String host, int port) 
+        throws IOException, UnknownHostException
+    {
+        return httpProxifiedSocket(host, port);
+    }
+
+    public Socket createSocket(String host ,int port, InetAddress localHost,
+                                int localPort)
+        throws IOException, UnknownHostException
+    {
+        return httpProxifiedSocket(host, port);
+    }
+
+    public Socket createSocket(InetAddress host, int port)
+        throws IOException
+    {
+        return httpProxifiedSocket(host.getHostAddress(), port);
+        
+    }
+
+    public Socket createSocket( InetAddress address, int port, 
+                                InetAddress localAddress, int localPort) 
+        throws IOException
+    {
+        return httpProxifiedSocket(address.getHostAddress(), port);
+    }
+  
+    private Socket httpProxifiedSocket(String host, int port)
+        throws IOException 
+    {
+        String proxyhost = proxy.getProxyAddress();
+        int proxyPort = proxy.getProxyPort();
+        Socket socket = new Socket(proxyhost,proxyPort);
+        String hostport = "CONNECT " + host + ":" + port;
+        String proxyLine;
+        String username = proxy.getProxyUsername();
+        if (username == null)
+        {
+            proxyLine = "";
+        }
+        else
+        {
+            String password = proxy.getProxyPassword();
+            proxyLine = "\r\nProxy-Authorization: Basic " + StringUtils.encodeBase64(username + ":" + password);
+        }
+        socket.getOutputStream().write((hostport + " HTTP/1.1\r\nHost: "
+            + hostport + proxyLine + "\r\n\r\n").getBytes("UTF-8"));
+        
+        InputStream in = socket.getInputStream();
+        StringBuilder got = new StringBuilder(100);
+        int nlchars = 0;
+        
+        while (true)
+        {
+            char c = (char) in.read();
+            got.append(c);
+            if (got.length() > 1024)
+            {
+                throw new ProxyException(ProxyInfo.ProxyType.HTTP, "Recieved " +
+                    "header of >1024 characters from "
+                    + proxyhost + ", cancelling connection");
+            }
+            if (c == -1)
+            {
+                throw new ProxyException(ProxyInfo.ProxyType.HTTP);
+            }
+            if ((nlchars == 0 || nlchars == 2) && c == '\r')
+            {
+                nlchars++;
+            }
+            else if ((nlchars == 1 || nlchars == 3) && c == '\n')
+            {
+                nlchars++;
+            }
+            else
+            {
+                nlchars = 0;
+            }
+            if (nlchars == 4)
+            {
+                break;
+            }
+        }
+
+        if (nlchars != 4)
+        {
+            throw new ProxyException(ProxyInfo.ProxyType.HTTP, "Never " +
+                "received blank line from " 
+                + proxyhost + ", cancelling connection");
+        }
+
+        String gotstr = got.toString();
+        
+        BufferedReader br = new BufferedReader(new StringReader(gotstr));
+        String response = br.readLine();
+        
+        if (response == null)
+        {
+            throw new ProxyException(ProxyInfo.ProxyType.HTTP, "Empty proxy " +
+                "response from " + proxyhost + ", cancelling");
+        }
+        
+        Matcher m = RESPONSE_PATTERN.matcher(response);
+        if (!m.matches())
+        {
+            throw new ProxyException(ProxyInfo.ProxyType.HTTP , "Unexpected " +
+                "proxy response from " + proxyhost + ": " + response);
+        }
+        
+        int code = Integer.parseInt(m.group(1));
+        
+        if (code != HttpURLConnection.HTTP_OK)
+        {
+            throw new ProxyException(ProxyInfo.ProxyType.HTTP);
+        }
+        
+        return socket;
+    }
+
+    private static final Pattern RESPONSE_PATTERN
+        = Pattern.compile("HTTP/\\S+\\s(\\d+)\\s(.*)\\s*");
+
+}
diff --git a/src/org/jivesoftware/smack/proxy/ProxyException.java b/src/org/jivesoftware/smack/proxy/ProxyException.java
new file mode 100644
index 0000000..b37910c
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/ProxyException.java
@@ -0,0 +1,44 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import java.io.IOException;
+
+/**
+ * An exception class to handle exceptions caused by proxy.
+ * 
+ * @author Atul Aggarwal
+ */
+public class ProxyException 
+    extends IOException
+{
+    public ProxyException(ProxyInfo.ProxyType type, String ex, Throwable cause)
+    {
+        super("Proxy Exception " + type.toString() + " : "+ex+", "+cause);
+    }
+    
+    public ProxyException(ProxyInfo.ProxyType type, String ex)
+    {
+        super("Proxy Exception " + type.toString() + " : "+ex);
+    }
+    
+    public ProxyException(ProxyInfo.ProxyType type)
+    {
+        super("Proxy Exception " + type.toString() + " : " + "Unknown Error");
+    }
+}
diff --git a/src/org/jivesoftware/smack/proxy/ProxyInfo.java b/src/org/jivesoftware/smack/proxy/ProxyInfo.java
new file mode 100644
index 0000000..5a7d354
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/ProxyInfo.java
@@ -0,0 +1,131 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import javax.net.SocketFactory;
+
+/**
+ * Class which stores proxy information such as proxy type, host, port, 
+ * authentication etc.
+ * 
+ * @author Atul Aggarwal
+ */
+
+public class ProxyInfo
+{
+    public static enum ProxyType
+    {
+        NONE,
+        HTTP,
+        SOCKS4,
+        SOCKS5
+    }
+    
+    private String proxyAddress;
+    private int proxyPort;
+    private String proxyUsername;
+    private String proxyPassword;
+    private ProxyType proxyType;
+    
+    public ProxyInfo(   ProxyType pType, String pHost, int pPort, String pUser, 
+                        String pPass)
+    {
+        this.proxyType = pType;
+        this.proxyAddress = pHost;
+        this.proxyPort = pPort;
+        this.proxyUsername = pUser;
+        this.proxyPassword = pPass;
+    }
+    
+    public static ProxyInfo forHttpProxy(String pHost, int pPort, String pUser, 
+                                    String pPass)
+    {
+        return new ProxyInfo(ProxyType.HTTP, pHost, pPort, pUser, pPass);
+    }
+    
+    public static ProxyInfo forSocks4Proxy(String pHost, int pPort, String pUser, 
+                                    String pPass)
+    {
+        return new ProxyInfo(ProxyType.SOCKS4, pHost, pPort, pUser, pPass);
+    }
+    
+    public static ProxyInfo forSocks5Proxy(String pHost, int pPort, String pUser, 
+                                    String pPass)
+    {
+        return new ProxyInfo(ProxyType.SOCKS5, pHost, pPort, pUser, pPass);
+    }
+    
+    public static ProxyInfo forNoProxy()
+    {
+        return new ProxyInfo(ProxyType.NONE, null, 0, null, null);
+    }
+    
+    public static ProxyInfo forDefaultProxy()
+    {
+        return new ProxyInfo(ProxyType.NONE, null, 0, null, null);
+    }
+    
+    public ProxyType getProxyType()
+    {
+        return proxyType;
+    }
+    
+    public String getProxyAddress()
+    {
+        return proxyAddress;
+    }
+    
+    public int getProxyPort()
+    {
+        return proxyPort;
+    }
+    
+    public String getProxyUsername()
+    {
+        return proxyUsername;
+    }
+    
+    public String getProxyPassword()
+    {
+        return proxyPassword;
+    }
+    
+    public SocketFactory getSocketFactory()
+    {
+        if(proxyType == ProxyType.NONE)
+        {
+            return new DirectSocketFactory();
+        }
+        else if(proxyType == ProxyType.HTTP)
+        {
+            return new HTTPProxySocketFactory(this);
+        }
+        else if(proxyType == ProxyType.SOCKS4)
+        {
+            return new Socks4ProxySocketFactory(this);
+        }
+        else if(proxyType == ProxyType.SOCKS5)
+        {
+            return new Socks5ProxySocketFactory(this);
+        }
+        else
+        {
+            return null;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java b/src/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java
new file mode 100644
index 0000000..6a32c11
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/Socks4ProxySocketFactory.java
@@ -0,0 +1,216 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+
+/**
+ * Socket factory for socks4 proxy 
+ *  
+ * @author Atul Aggarwal
+ */
+public class Socks4ProxySocketFactory 
+    extends SocketFactory
+{
+    private ProxyInfo proxy;
+    
+    public Socks4ProxySocketFactory(ProxyInfo proxy)
+    {
+        this.proxy = proxy;
+    }
+
+    public Socket createSocket(String host, int port) 
+        throws IOException, UnknownHostException
+    {
+        return socks4ProxifiedSocket(host,port);
+        
+    }
+
+    public Socket createSocket(String host ,int port, InetAddress localHost,
+                                int localPort)
+        throws IOException, UnknownHostException
+    {
+        return socks4ProxifiedSocket(host,port);
+    }
+
+    public Socket createSocket(InetAddress host, int port)
+        throws IOException
+    {
+        return socks4ProxifiedSocket(host.getHostAddress(),port);
+    }
+
+    public Socket createSocket( InetAddress address, int port, 
+                                InetAddress localAddress, int localPort) 
+        throws IOException
+    {
+        return socks4ProxifiedSocket(address.getHostAddress(),port);
+        
+    }
+    
+    private Socket socks4ProxifiedSocket(String host, int port) 
+        throws IOException
+    {
+        Socket socket = null;
+        InputStream in = null;
+        OutputStream out = null;
+        String proxy_host = proxy.getProxyAddress();
+        int proxy_port = proxy.getProxyPort();
+        String user = proxy.getProxyUsername();
+        String passwd = proxy.getProxyPassword();
+        
+        try
+        {
+            socket=new Socket(proxy_host, proxy_port);    
+            in=socket.getInputStream();
+            out=socket.getOutputStream();
+            socket.setTcpNoDelay(true);
+            
+            byte[] buf=new byte[1024];
+            int index=0;
+
+    /*
+    1) CONNECT
+
+    The client connects to the SOCKS server and sends a CONNECT request when
+    it wants to establish a connection to an application server. The client
+    includes in the request packet the IP address and the port number of the
+    destination host, and userid, in the following format.
+
+           +----+----+----+----+----+----+----+----+----+----+....+----+
+           | VN | CD | DSTPORT |      DSTIP        | USERID       |NULL|
+           +----+----+----+----+----+----+----+----+----+----+....+----+
+    # of bytes:   1    1      2              4           variable       1
+
+    VN is the SOCKS protocol version number and should be 4. CD is the
+    SOCKS command code and should be 1 for CONNECT request. NULL is a byte
+    of all zero bits.
+    */
+
+            index=0;
+            buf[index++]=4;
+            buf[index++]=1;
+
+            buf[index++]=(byte)(port>>>8);
+            buf[index++]=(byte)(port&0xff);
+
+            try
+            {
+                InetAddress addr=InetAddress.getByName(host);
+                byte[] byteAddress = addr.getAddress();
+                for (int i = 0; i < byteAddress.length; i++) 
+                {
+                    buf[index++]=byteAddress[i];
+                }
+            }
+            catch(UnknownHostException uhe)
+            {
+                throw new ProxyException(ProxyInfo.ProxyType.SOCKS4, 
+                    uhe.toString(), uhe);
+            }
+
+            if(user!=null)
+            {
+                System.arraycopy(user.getBytes(), 0, buf, index, user.length());
+                index+=user.length();
+            }
+            buf[index++]=0;
+            out.write(buf, 0, index);
+
+    /*
+    The SOCKS server checks to see whether such a request should be granted
+    based on any combination of source IP address, destination IP address,
+    destination port number, the userid, and information it may obtain by
+    consulting IDENT, cf. RFC 1413.  If the request is granted, the SOCKS
+    server makes a connection to the specified port of the destination host.
+    A reply packet is sent to the client when this connection is established,
+    or when the request is rejected or the operation fails. 
+
+           +----+----+----+----+----+----+----+----+
+           | VN | CD | DSTPORT |      DSTIP        |
+           +----+----+----+----+----+----+----+----+
+    # of bytes:   1    1      2              4
+
+    VN is the version of the reply code and should be 0. CD is the result
+    code with one of the following values:
+
+    90: request granted
+    91: request rejected or failed
+    92: request rejected becasue SOCKS server cannot connect to
+    identd on the client
+    93: request rejected because the client program and identd
+    report different user-ids
+
+    The remaining fields are ignored.
+    */
+
+            int len=6;
+            int s=0;
+            while(s<len)
+            {
+                int i=in.read(buf, s, len-s);
+                if(i<=0)
+                {
+                    throw new ProxyException(ProxyInfo.ProxyType.SOCKS4, 
+                        "stream is closed");
+                }
+                s+=i;
+            }
+            if(buf[0]!=0)
+            {
+                throw new ProxyException(ProxyInfo.ProxyType.SOCKS4, 
+                    "server returns VN "+buf[0]);
+            }
+            if(buf[1]!=90)
+            {
+                try
+                {
+                    socket.close();
+                }
+                catch(Exception eee)
+                {
+                }
+                String message="ProxySOCKS4: server returns CD "+buf[1];
+                throw new ProxyException(ProxyInfo.ProxyType.SOCKS4,message);
+            }
+            byte[] temp = new byte[2];
+            in.read(temp, 0, 2);
+            return socket;
+        }
+        catch(RuntimeException e)
+        {
+            throw e;
+        }
+        catch(Exception e)
+        {
+            try
+            {
+                if(socket!=null)socket.close(); 
+            }
+            catch(Exception eee)
+            {
+            }
+            throw new ProxyException(ProxyInfo.ProxyType.SOCKS4, e.toString());
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java b/src/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java
new file mode 100644
index 0000000..23ef623
--- /dev/null
+++ b/src/org/jivesoftware/smack/proxy/Socks5ProxySocketFactory.java
@@ -0,0 +1,375 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.proxy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+
+/**
+ * Socket factory for Socks5 proxy
+ * 
+ * @author Atul Aggarwal
+ */
+public class Socks5ProxySocketFactory 
+    extends SocketFactory
+{
+    private ProxyInfo proxy;
+    
+    public Socks5ProxySocketFactory(ProxyInfo proxy)
+    {
+        this.proxy = proxy;
+    }
+
+    public Socket createSocket(String host, int port) 
+        throws IOException, UnknownHostException
+    {
+        return socks5ProxifiedSocket(host,port);
+    }
+
+    public Socket createSocket(String host ,int port, InetAddress localHost,
+                                int localPort)
+        throws IOException, UnknownHostException
+    {
+        
+        return socks5ProxifiedSocket(host,port);
+        
+    }
+
+    public Socket createSocket(InetAddress host, int port)
+        throws IOException
+    {
+        
+        return socks5ProxifiedSocket(host.getHostAddress(),port);
+        
+    }
+
+    public Socket createSocket( InetAddress address, int port, 
+                                InetAddress localAddress, int localPort) 
+        throws IOException
+    {
+        
+        return socks5ProxifiedSocket(address.getHostAddress(),port);
+        
+    }
+    
+    private Socket socks5ProxifiedSocket(String host, int port) 
+        throws IOException
+    {
+        Socket socket = null;
+        InputStream in = null;
+        OutputStream out = null;
+        String proxy_host = proxy.getProxyAddress();
+        int proxy_port = proxy.getProxyPort();
+        String user = proxy.getProxyUsername();
+        String passwd = proxy.getProxyPassword();
+        
+        try
+        {
+            socket=new Socket(proxy_host, proxy_port);    
+            in=socket.getInputStream();
+            out=socket.getOutputStream();
+
+            socket.setTcpNoDelay(true);
+
+            byte[] buf=new byte[1024];
+            int index=0;
+
+/*
+                   +----+----------+----------+
+                   |VER | NMETHODS | METHODS  |
+                   +----+----------+----------+
+                   | 1  |    1     | 1 to 255 |
+                   +----+----------+----------+
+
+   The VER field is set to X'05' for this version of the protocol.  The
+   NMETHODS field contains the number of method identifier octets that
+   appear in the METHODS field.
+
+   The values currently defined for METHOD are:
+
+          o  X'00' NO AUTHENTICATION REQUIRED
+          o  X'01' GSSAPI
+          o  X'02' USERNAME/PASSWORD
+          o  X'03' to X'7F' IANA ASSIGNED
+          o  X'80' to X'FE' RESERVED FOR PRIVATE METHODS
+          o  X'FF' NO ACCEPTABLE METHODS
+*/
+
+            buf[index++]=5;
+
+            buf[index++]=2;
+            buf[index++]=0;           // NO AUTHENTICATION REQUIRED
+            buf[index++]=2;           // USERNAME/PASSWORD
+
+            out.write(buf, 0, index);
+
+/*
+    The server selects from one of the methods given in METHODS, and
+    sends a METHOD selection message:
+
+                         +----+--------+
+                         |VER | METHOD |
+                         +----+--------+
+                         | 1  |   1    |
+                         +----+--------+
+*/
+      //in.read(buf, 0, 2);
+            fill(in, buf, 2);
+
+            boolean check=false;
+            switch((buf[1])&0xff)
+            {
+                case 0:                // NO AUTHENTICATION REQUIRED
+                    check=true;
+                    break;
+                case 2:                // USERNAME/PASSWORD
+                    if(user==null || passwd==null)
+                    {
+                        break;
+                    }
+
+/*
+   Once the SOCKS V5 server has started, and the client has selected the
+   Username/Password Authentication protocol, the Username/Password
+   subnegotiation begins.  This begins with the client producing a
+   Username/Password request:
+
+           +----+------+----------+------+----------+
+           |VER | ULEN |  UNAME   | PLEN |  PASSWD  |
+           +----+------+----------+------+----------+
+           | 1  |  1   | 1 to 255 |  1   | 1 to 255 |
+           +----+------+----------+------+----------+
+
+   The VER field contains the current version of the subnegotiation,
+   which is X'01'. The ULEN field contains the length of the UNAME field
+   that follows. The UNAME field contains the username as known to the
+   source operating system. The PLEN field contains the length of the
+   PASSWD field that follows. The PASSWD field contains the password
+   association with the given UNAME.
+*/
+                    index=0;
+                    buf[index++]=1;
+                    buf[index++]=(byte)(user.length());
+                    System.arraycopy(user.getBytes(), 0, buf, index, 
+                        user.length());
+                    index+=user.length();
+                    buf[index++]=(byte)(passwd.length());
+                    System.arraycopy(passwd.getBytes(), 0, buf, index, 
+                        passwd.length());
+                    index+=passwd.length();
+
+                    out.write(buf, 0, index);
+
+/*
+   The server verifies the supplied UNAME and PASSWD, and sends the
+   following response:
+
+                        +----+--------+
+                        |VER | STATUS |
+                        +----+--------+
+                        | 1  |   1    |
+                        +----+--------+
+
+   A STATUS field of X'00' indicates success. If the server returns a
+   `failure' (STATUS value other than X'00') status, it MUST close the
+   connection.
+*/
+                    //in.read(buf, 0, 2);
+                    fill(in, buf, 2);
+                    if(buf[1]==0)
+                    {
+                        check=true;
+                    }
+                    break;
+                default:
+            }
+
+            if(!check)
+            {
+                try
+                {
+                    socket.close();
+                }
+                catch(Exception eee)
+                {
+                }
+                throw new ProxyException(ProxyInfo.ProxyType.SOCKS5,
+                    "fail in SOCKS5 proxy");
+            }
+
+/*
+      The SOCKS request is formed as follows:
+
+        +----+-----+-------+------+----------+----------+
+        |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
+        +----+-----+-------+------+----------+----------+
+        | 1  |  1  | X'00' |  1   | Variable |    2     |
+        +----+-----+-------+------+----------+----------+
+
+      Where:
+
+      o  VER    protocol version: X'05'
+      o  CMD
+         o  CONNECT X'01'
+         o  BIND X'02'
+         o  UDP ASSOCIATE X'03'
+      o  RSV    RESERVED
+         o  ATYP   address type of following address
+         o  IP V4 address: X'01'
+         o  DOMAINNAME: X'03'
+         o  IP V6 address: X'04'
+      o  DST.ADDR       desired destination address
+      o  DST.PORT desired destination port in network octet
+         order
+*/
+     
+            index=0;
+            buf[index++]=5;
+            buf[index++]=1;       // CONNECT
+            buf[index++]=0;
+
+            byte[] hostb=host.getBytes();
+            int len=hostb.length;
+            buf[index++]=3;      // DOMAINNAME
+            buf[index++]=(byte)(len);
+            System.arraycopy(hostb, 0, buf, index, len);
+            index+=len;
+            buf[index++]=(byte)(port>>>8);
+            buf[index++]=(byte)(port&0xff);
+
+            out.write(buf, 0, index);
+
+/*
+   The SOCKS request information is sent by the client as soon as it has
+   established a connection to the SOCKS server, and completed the
+   authentication negotiations.  The server evaluates the request, and
+   returns a reply formed as follows:
+
+        +----+-----+-------+------+----------+----------+
+        |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
+        +----+-----+-------+------+----------+----------+
+        | 1  |  1  | X'00' |  1   | Variable |    2     |
+        +----+-----+-------+------+----------+----------+
+
+   Where:
+
+   o  VER    protocol version: X'05'
+   o  REP    Reply field:
+      o  X'00' succeeded
+      o  X'01' general SOCKS server failure
+      o  X'02' connection not allowed by ruleset
+      o  X'03' Network unreachable
+      o  X'04' Host unreachable
+      o  X'05' Connection refused
+      o  X'06' TTL expired
+      o  X'07' Command not supported
+      o  X'08' Address type not supported
+      o  X'09' to X'FF' unassigned
+    o  RSV    RESERVED
+    o  ATYP   address type of following address
+      o  IP V4 address: X'01'
+      o  DOMAINNAME: X'03'
+      o  IP V6 address: X'04'
+    o  BND.ADDR       server bound address
+    o  BND.PORT       server bound port in network octet order
+*/
+
+      //in.read(buf, 0, 4);
+            fill(in, buf, 4);
+
+            if(buf[1]!=0)
+            {
+                try
+                {
+                    socket.close();
+                }
+                catch(Exception eee)
+                {
+                }
+                throw new ProxyException(ProxyInfo.ProxyType.SOCKS5, 
+                    "server returns "+buf[1]);
+            }
+
+            switch(buf[3]&0xff)
+            {
+                case 1:
+                    //in.read(buf, 0, 6);
+                    fill(in, buf, 6);
+                    break;
+                case 3:
+                    //in.read(buf, 0, 1);
+                    fill(in, buf, 1);
+                    //in.read(buf, 0, buf[0]+2);
+                    fill(in, buf, (buf[0]&0xff)+2);
+                    break;
+                case 4:
+                    //in.read(buf, 0, 18);
+                    fill(in, buf, 18);
+                    break;
+                default:
+            }
+            return socket;
+            
+        }
+        catch(RuntimeException e)
+        {
+            throw e;
+        }
+        catch(Exception e)
+        {
+            try
+            {
+                if(socket!=null)
+                {
+                    socket.close(); 
+                }
+            }
+            catch(Exception eee)
+            {
+            }
+            String message="ProxySOCKS5: "+e.toString();
+            if(e instanceof Throwable)
+            {
+                throw new ProxyException(ProxyInfo.ProxyType.SOCKS5,message, 
+                    (Throwable)e);
+            }
+            throw new IOException(message);
+        }
+    }
+    
+    private void fill(InputStream in, byte[] buf, int len) 
+      throws IOException
+    {
+        int s=0;
+        while(s<len)
+        {
+            int i=in.read(buf, s, len-s);
+            if(i<=0)
+            {
+                throw new ProxyException(ProxyInfo.ProxyType.SOCKS5, "stream " +
+                    "is closed");
+            }
+            s+=i;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smack/sasl/SASLAnonymous.java b/src/org/jivesoftware/smack/sasl/SASLAnonymous.java
new file mode 100644
index 0000000..a1b2c88
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLAnonymous.java
@@ -0,0 +1,62 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ *

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

+

+import org.jivesoftware.smack.SASLAuthentication;

+

+import java.io.IOException;

+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;

+

+/**

+ * Implementation of the SASL ANONYMOUS mechanism

+ *

+ * @author Jay Kline

+ */

+public class SASLAnonymous extends SASLMechanism {

+

+    public SASLAnonymous(SASLAuthentication saslAuthentication) {

+        super(saslAuthentication);

+    }

+

+    protected String getName() {

+        return "ANONYMOUS";

+    }

+

+    public void authenticate(String username, String host, CallbackHandler cbh) throws IOException {

+        authenticate();

+    }

+

+    public void authenticate(String username, String host, String password) throws IOException {

+        authenticate();

+    }

+

+    protected void authenticate() throws IOException {

+        // Send the authentication to the server

+        getSASLAuthentication().send(new AuthMechanism(getName(), null));

+    }

+

+    public void challengeReceived(String challenge) throws IOException {

+        // Build the challenge response stanza encoding the response text

+        // and send the authentication to the server

+        getSASLAuthentication().send(new Response());

+    }

+

+

+}

diff --git a/src/org/jivesoftware/smack/sasl/SASLCramMD5Mechanism.java b/src/org/jivesoftware/smack/sasl/SASLCramMD5Mechanism.java
new file mode 100644
index 0000000..82d218f
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLCramMD5Mechanism.java
@@ -0,0 +1,38 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ *

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

+

+import org.jivesoftware.smack.SASLAuthentication;

+

+/**

+ * Implementation of the SASL CRAM-MD5 mechanism

+ *

+ * @author Jay Kline

+ */

+public class SASLCramMD5Mechanism extends SASLMechanism {

+

+    public SASLCramMD5Mechanism(SASLAuthentication saslAuthentication) {

+        super(saslAuthentication);

+    }

+

+    protected String getName() {

+        return "CRAM-MD5";

+    }

+}

diff --git a/src/org/jivesoftware/smack/sasl/SASLDigestMD5Mechanism.java b/src/org/jivesoftware/smack/sasl/SASLDigestMD5Mechanism.java
new file mode 100644
index 0000000..7af65fb
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLDigestMD5Mechanism.java
@@ -0,0 +1,38 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ *

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

+

+import org.jivesoftware.smack.SASLAuthentication;

+

+/**

+ * Implementation of the SASL DIGEST-MD5 mechanism

+ *

+ * @author Jay Kline

+ */

+public class SASLDigestMD5Mechanism extends SASLMechanism {

+

+    public SASLDigestMD5Mechanism(SASLAuthentication saslAuthentication) {

+        super(saslAuthentication);

+    }

+

+    protected String getName() {

+        return "DIGEST-MD5";

+    }

+}

diff --git a/src/org/jivesoftware/smack/sasl/SASLExternalMechanism.java b/src/org/jivesoftware/smack/sasl/SASLExternalMechanism.java
new file mode 100644
index 0000000..dff18fb
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLExternalMechanism.java
@@ -0,0 +1,59 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ *

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

+

+import org.jivesoftware.smack.SASLAuthentication;

+

+/**

+ * Implementation of the SASL EXTERNAL mechanism.

+ *

+ * To effectively use this mechanism, Java must be configured to properly 

+ * supply a client SSL certificate (of some sort) to the server. It is up

+ * to the implementer to determine how to do this.  Here is one method:

+ *

+ * Create a java keystore with your SSL certificate in it:

+ * keytool -genkey -alias username -dname "cn=username,ou=organizationalUnit,o=organizationaName,l=locality,s=state,c=country"

+ *

+ * Next, set the System Properties:

+ *  <ul>

+ *  <li>javax.net.ssl.keyStore to the location of the keyStore

+ *  <li>javax.net.ssl.keyStorePassword to the password of the keyStore

+ *  <li>javax.net.ssl.trustStore to the location of the trustStore

+ *  <li>javax.net.ssl.trustStorePassword to the the password of the trustStore

+ *  </ul>

+ *

+ * Then, when the server requests or requires the client certificate, java will

+ * simply provide the one in the keyStore.

+ *

+ * Also worth noting is the EXTERNAL mechanism in Smack is not enabled by default.

+ * To enable it, the implementer will need to call SASLAuthentication.supportSASLMechamism("EXTERNAL");

+ *

+ * @author Jay Kline

+ */

+public class SASLExternalMechanism extends SASLMechanism  {

+

+    public SASLExternalMechanism(SASLAuthentication saslAuthentication) {

+        super(saslAuthentication);

+    }

+

+    protected String getName() {

+        return "EXTERNAL";

+    }

+}

diff --git a/src/org/jivesoftware/smack/sasl/SASLFacebookConnect.java b/src/org/jivesoftware/smack/sasl/SASLFacebookConnect.java
new file mode 100644
index 0000000..3126d83
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLFacebookConnect.java
@@ -0,0 +1,201 @@
+package org.jivesoftware.smack.sasl;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import de.measite.smack.Sasl;
+
+import org.jivesoftware.smack.SASLAuthentication;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.sasl.SASLMechanism;
+import org.jivesoftware.smack.util.Base64;
+
+/**
+ * This class is originally from http://code.google.com/p/fbgc/source/browse/trunk/daemon/src/main/java/org/albino/mechanisms/FacebookConnectSASLMechanism.java
+ * I just adapted to match the SMACK package scheme and 
+ */
+public class SASLFacebookConnect extends SASLMechanism {
+
+        private String sessionKey = "";
+        private String sessionSecret = "";
+        private String apiKey = "";
+        
+        static{
+        	SASLAuthentication.registerSASLMechanism("X-FACEBOOK-PLATFORM",
+                    SASLFacebookConnect.class);
+        	SASLAuthentication.supportSASLMechanism("X-FACEBOOK-PLATFORM", 0);
+        }
+        
+        public SASLFacebookConnect(SASLAuthentication saslAuthentication) {
+                super(saslAuthentication);
+        }
+
+        // protected void authenticate() throws IOException, XMPPException {
+        // String[] mechanisms = { getName() };
+        // Map<String, String> props = new HashMap<String, String>();
+        // sc = Sasl.createSaslClient(mechanisms, null, "xmpp", hostname, props,
+        // this);
+        //
+        // super.authenticate();
+        // }
+
+        protected void authenticate() throws IOException, XMPPException {
+                final StringBuilder stanza = new StringBuilder();
+                stanza.append("<auth mechanism=\"").append(getName());
+                stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+                stanza.append("</auth>");
+
+                // Send the authentication to the server
+                getSASLAuthentication().send(new Packet(){
+
+					@Override
+					public String toXML() {
+						return stanza.toString();
+					}    	
+                	
+                });
+        }
+
+        public void authenticate(String apiKeyAndSessionKey, String host, String sessionSecret)
+                        throws IOException, XMPPException {
+
+                if(apiKeyAndSessionKey==null || sessionSecret==null)
+                        throw new IllegalStateException("Invalid parameters!");
+                
+                String[] keyArray = apiKeyAndSessionKey.split("\\|");
+                
+                if(keyArray==null || keyArray.length != 2)
+                        throw new IllegalStateException("Api key or session key is not present!");
+                
+                this.apiKey = keyArray[0];
+                this.sessionKey = keyArray[1];
+                this.sessionSecret = sessionSecret;
+                
+                this.authenticationId = sessionKey;
+                this.password = sessionSecret;
+                this.hostname = host;
+
+                String[] mechanisms = { "DIGEST-MD5" };
+                Map<String, String> props = new HashMap<String, String>();
+                sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, this);
+                authenticate();
+        }
+
+        public void authenticate(String username, String host, CallbackHandler cbh)
+                        throws IOException, XMPPException {
+                String[] mechanisms = { "DIGEST-MD5" };
+                Map<String, String> props = new HashMap<String, String>();
+                sc = Sasl.createSaslClient(mechanisms, null, "xmpp", host, props, cbh);
+                authenticate();
+        }
+
+        protected String getName() {
+                return "X-FACEBOOK-PLATFORM";
+        }
+
+        public void challengeReceived(String challenge) throws IOException {
+                // Build the challenge response stanza encoding the response text
+                final StringBuilder stanza = new StringBuilder();
+
+                byte response[] = null;
+                if (challenge != null) {
+                        String decodedResponse = new String(Base64.decode(challenge));
+                        Map<String, String> parameters = getQueryMap(decodedResponse);
+
+                        String version = "1.0";
+                        String nonce = parameters.get("nonce");
+                        String method = parameters.get("method");
+                        
+                        Long callId = new GregorianCalendar().getTimeInMillis()/1000;
+                        
+                        String sig = "api_key="+apiKey
+                                                        +"call_id="+callId
+                                                        +"method="+method
+                                                        +"nonce="+nonce
+                                                        +"session_key="+sessionKey
+                                                        +"v="+version
+                                                        +sessionSecret;
+                        
+                        try {
+                                sig = MD5(sig);
+                        } catch (NoSuchAlgorithmException e) {
+                                throw new IllegalStateException(e);
+                        }
+                        
+                        String composedResponse = "api_key="+apiKey+"&"
+                                                                                +"call_id="+callId+"&"
+                                                                                +"method="+method+"&"
+                                                                                +"nonce="+nonce+"&"
+                                                                                +"session_key="+sessionKey+"&"
+                                                                                +"v="+version+"&"
+                                                                                +"sig="+sig;
+                        
+                        response = composedResponse.getBytes();
+                }
+
+                String authenticationText="";
+
+                if (response != null) {
+                        authenticationText = Base64.encodeBytes(response, Base64.DONT_BREAK_LINES);
+                }
+
+                stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");
+                stanza.append(authenticationText);
+                stanza.append("</response>");
+
+                // Send the authentication to the server
+                getSASLAuthentication().send(new Packet(){
+
+					@Override
+					public String toXML() {
+						return stanza.toString();
+					}
+                	
+                });
+        }
+
+        private Map<String, String> getQueryMap(String query) {
+                String[] params = query.split("&");
+                Map<String, String> map = new HashMap<String, String>();
+                for (String param : params) {
+                        String name = param.split("=")[0];
+                        String value = param.split("=")[1];
+                        map.put(name, value);
+                }
+                return map;
+        }
+        
+    private String convertToHex(byte[] data) {
+        StringBuffer buf = new StringBuffer();
+        for (int i = 0; i < data.length; i++) {
+            int halfbyte = (data[i] >>> 4) & 0x0F;
+            int two_halfs = 0;
+            do {
+                if ((0 <= halfbyte) && (halfbyte <= 9))
+                    buf.append((char) ('0' + halfbyte));
+                else
+                    buf.append((char) ('a' + (halfbyte - 10)));
+                halfbyte = data[i] & 0x0F;
+            } while(two_halfs++ < 1);
+        }
+        return buf.toString();
+    }
+ 
+    public String MD5(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException  {
+        MessageDigest md;
+        md = MessageDigest.getInstance("MD5");
+        byte[] md5hash = new byte[32];
+        md.update(text.getBytes("iso-8859-1"), 0, text.length());
+        md5hash = md.digest();
+        return convertToHex(md5hash);
+    }
+}
+
diff --git a/src/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java b/src/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java
new file mode 100644
index 0000000..e8a4967
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLGSSAPIMechanism.java
@@ -0,0 +1,89 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ *

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

+

+import org.jivesoftware.smack.SASLAuthentication;

+import org.jivesoftware.smack.XMPPException;

+

+import java.io.IOException;

+import java.util.Map;

+import java.util.HashMap;

+import de.measite.smack.Sasl;

+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;

+

+/**

+ * Implementation of the SASL GSSAPI mechanism

+ *

+ * @author Jay Kline

+ */

+public class SASLGSSAPIMechanism extends SASLMechanism {

+

+    public SASLGSSAPIMechanism(SASLAuthentication saslAuthentication) {

+        super(saslAuthentication);

+

+        System.setProperty("javax.security.auth.useSubjectCredsOnly","false");

+        System.setProperty("java.security.auth.login.config","gss.conf");

+

+    }

+

+    protected String getName() {

+        return "GSSAPI";

+    }

+

+    /**

+     * Builds and sends the <tt>auth</tt> stanza to the server.

+     * This overrides from the abstract class because the initial token

+     * needed for GSSAPI is binary, and not safe to put in a string, thus

+     * getAuthenticationText() cannot be used.

+     *

+     * @param username the username of the user being authenticated.

+     * @param host     the hostname where the user account resides.

+     * @param cbh      the CallbackHandler (not used with GSSAPI)

+     * @throws IOException If a network error occures while authenticating.

+     */

+    public void authenticate(String username, String host, CallbackHandler cbh) throws IOException, XMPPException {

+        String[] mechanisms = { getName() };

+        Map<String,String> props = new HashMap<String,String>();

+        props.put(Sasl.SERVER_AUTH,"TRUE");

+        sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, cbh);

+        authenticate();

+    }

+

+    /**

+     * Builds and sends the <tt>auth</tt> stanza to the server.

+     * This overrides from the abstract class because the initial token

+     * needed for GSSAPI is binary, and not safe to put in a string, thus

+     * getAuthenticationText() cannot be used.

+     *

+     * @param username the username of the user being authenticated.

+     * @param host     the hostname where the user account resides.

+     * @param password the password of the user (ignored for GSSAPI)

+     * @throws IOException If a network error occures while authenticating.

+     */

+    public void authenticate(String username, String host, String password) throws IOException, XMPPException {

+        String[] mechanisms = { getName() };

+        Map<String,String> props = new HashMap<String, String>();

+        props.put(Sasl.SERVER_AUTH,"TRUE");

+        sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, this);

+        authenticate();

+    }

+

+

+}

diff --git a/src/org/jivesoftware/smack/sasl/SASLMechanism.java b/src/org/jivesoftware/smack/sasl/SASLMechanism.java
new file mode 100644
index 0000000..3aeba86
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLMechanism.java
@@ -0,0 +1,323 @@
+/**

+ * $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.sasl;

+

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.SASLAuthentication;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.util.StringUtils;

+

+import java.io.IOException;

+import java.util.Map;

+import java.util.HashMap;

+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;

+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;

+import org.apache.harmony.javax.security.auth.callback.Callback;

+import org.apache.harmony.javax.security.auth.callback.NameCallback;

+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;

+import org.apache.harmony.javax.security.sasl.RealmCallback;

+import org.apache.harmony.javax.security.sasl.RealmChoiceCallback;

+import de.measite.smack.Sasl;

+import org.apache.harmony.javax.security.sasl.SaslClient;

+import org.apache.harmony.javax.security.sasl.SaslException;

+

+/**

+ * Base class for SASL mechanisms. Subclasses must implement these methods:

+ * <ul>

+ *  <li>{@link #getName()} -- returns the common name of the SASL mechanism.</li>

+ * </ul>

+ * Subclasses will likely want to implement their own versions of these mthods:

+ *  <li>{@link #authenticate(String, String, String)} -- Initiate authentication stanza using the

+ *  deprecated method.</li>

+ *  <li>{@link #authenticate(String, String, CallbackHandler)} -- Initiate authentication stanza

+ *  using the CallbackHandler method.</li>

+ *  <li>{@link #challengeReceived(String)} -- Handle a challenge from the server.</li>

+ * </ul>

+ *

+ * @author Jay Kline

+ */

+public abstract class SASLMechanism implements CallbackHandler {

+

+    private SASLAuthentication saslAuthentication;

+    protected SaslClient sc;

+    protected String authenticationId;

+    protected String password;

+    protected String hostname;

+

+

+    public SASLMechanism(SASLAuthentication saslAuthentication) {

+        this.saslAuthentication = saslAuthentication;

+    }

+

+    /**

+     * Builds and sends the <tt>auth</tt> stanza to the server. Note that this method of

+     * authentication is not recommended, since it is very inflexable.  Use

+     * {@link #authenticate(String, String, CallbackHandler)} whenever possible.

+     *

+     * @param username the username of the user being authenticated.

+     * @param host     the hostname where the user account resides.

+     * @param password the password for this account.

+     * @throws IOException If a network error occurs while authenticating.

+     * @throws XMPPException If a protocol error occurs or the user is not authenticated.

+     */

+    public void authenticate(String username, String host, String password) throws IOException, XMPPException {

+        //Since we were not provided with a CallbackHandler, we will use our own with the given

+        //information

+

+        //Set the authenticationID as the username, since they must be the same in this case.

+        this.authenticationId = username;

+        this.password = password;

+        this.hostname = host;

+

+        String[] mechanisms = { getName() };

+        Map<String,String> props = new HashMap<String,String>();

+        sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, this);

+        authenticate();

+    }

+

+    /**

+     * Builds and sends the <tt>auth</tt> stanza to the server. The callback handler will handle

+     * any additional information, such as the authentication ID or realm, if it is needed.

+     *

+     * @param username the username of the user being authenticated.

+     * @param host     the hostname where the user account resides.

+     * @param cbh      the CallbackHandler to obtain user information.

+     * @throws IOException If a network error occures while authenticating.

+     * @throws XMPPException If a protocol error occurs or the user is not authenticated.

+     */

+    public void authenticate(String username, String host, CallbackHandler cbh) throws IOException, XMPPException {

+        String[] mechanisms = { getName() };

+        Map<String,String> props = new HashMap<String,String>();

+        sc = Sasl.createSaslClient(mechanisms, username, "xmpp", host, props, cbh);

+        authenticate();

+    }

+

+    protected void authenticate() throws IOException, XMPPException {

+        String authenticationText = null;

+        try {

+            if(sc.hasInitialResponse()) {

+                byte[] response = sc.evaluateChallenge(new byte[0]);

+                authenticationText = StringUtils.encodeBase64(response, false);

+            }

+        } catch (SaslException e) {

+            throw new XMPPException("SASL authentication failed", e);

+        }

+

+        // Send the authentication to the server

+        getSASLAuthentication().send(new AuthMechanism(getName(), authenticationText));

+    }

+

+

+    /**

+     * The server is challenging the SASL mechanism for the stanza he just sent. Send a

+     * response to the server's challenge.

+     *

+     * @param challenge a base64 encoded string representing the challenge.

+     * @throws IOException if an exception sending the response occurs.

+     */

+    public void challengeReceived(String challenge) throws IOException {

+        byte response[];

+        if(challenge != null) {

+            response = sc.evaluateChallenge(StringUtils.decodeBase64(challenge));

+        } else {

+            response = sc.evaluateChallenge(new byte[0]);

+        }

+

+        Packet responseStanza;

+        if (response == null) {

+            responseStanza = new Response();

+        }

+        else {

+            responseStanza = new Response(StringUtils.encodeBase64(response, false));

+        }

+

+        // Send the authentication to the server

+        getSASLAuthentication().send(responseStanza);

+    }

+

+    /**

+     * Returns the common name of the SASL mechanism. E.g.: PLAIN, DIGEST-MD5 or GSSAPI.

+     *

+     * @return the common name of the SASL mechanism.

+     */

+    protected abstract String getName();

+

+

+    protected SASLAuthentication getSASLAuthentication() {

+        return saslAuthentication;

+    }

+

+    /**

+     * 

+     */

+    public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {

+        for (int i = 0; i < callbacks.length; i++) {

+            if (callbacks[i] instanceof NameCallback) {

+                NameCallback ncb = (NameCallback)callbacks[i];

+                ncb.setName(authenticationId);

+            } else if(callbacks[i] instanceof PasswordCallback) {

+                PasswordCallback pcb = (PasswordCallback)callbacks[i];

+                pcb.setPassword(password.toCharArray());

+            } else if(callbacks[i] instanceof RealmCallback) {

+                RealmCallback rcb = (RealmCallback)callbacks[i];

+                rcb.setText(hostname);

+            } else if(callbacks[i] instanceof RealmChoiceCallback){

+                //unused

+                //RealmChoiceCallback rccb = (RealmChoiceCallback)callbacks[i];

+            } else {

+               throw new UnsupportedCallbackException(callbacks[i]);

+            }

+         }

+    }

+

+    /**

+     * Initiating SASL authentication by select a mechanism.

+     */

+    public class AuthMechanism extends Packet {

+        final private String name;

+        final private String authenticationText;

+

+        public AuthMechanism(String name, String authenticationText) {

+            if (name == null) {

+                throw new NullPointerException("SASL mechanism name shouldn't be null.");

+            }

+            this.name = name;

+            this.authenticationText = authenticationText;

+        }

+

+        public String toXML() {

+            StringBuilder stanza = new StringBuilder();

+            stanza.append("<auth mechanism=\"").append(name);

+            stanza.append("\" xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");

+            if (authenticationText != null &&

+                    authenticationText.trim().length() > 0) {

+                stanza.append(authenticationText);

+            }

+            stanza.append("</auth>");

+            return stanza.toString();

+        }

+    }

+

+    /**

+     * A SASL challenge stanza.

+     */

+    public static class Challenge extends Packet {

+        final private String data;

+

+        public Challenge(String data) {

+            this.data = data;

+        }

+

+        public String toXML() {

+            StringBuilder stanza = new StringBuilder();

+            stanza.append("<challenge xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");

+            if (data != null &&

+                    data.trim().length() > 0) {

+                stanza.append(data);

+            }

+            stanza.append("</challenge>");

+            return stanza.toString();

+        }

+    }

+

+    /**

+     * A SASL response stanza.

+     */

+    public class Response extends Packet {

+        final private String authenticationText;

+

+        public Response() {

+            authenticationText = null;

+        }

+

+        public Response(String authenticationText) {

+            if (authenticationText == null || authenticationText.trim().length() == 0) {

+                this.authenticationText = null;

+            }

+            else {

+                this.authenticationText = authenticationText;

+            }

+        }

+

+        public String toXML() {

+            StringBuilder stanza = new StringBuilder();

+            stanza.append("<response xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");

+            if (authenticationText != null) {

+                stanza.append(authenticationText);

+            }

+            stanza.append("</response>");

+            return stanza.toString();

+        }

+    }

+

+    /**

+     * A SASL success stanza.

+     */

+    public static class Success extends Packet {

+        final private String data;

+

+        public Success(String data) {

+            this.data = data;

+        }

+

+        public String toXML() {

+            StringBuilder stanza = new StringBuilder();

+            stanza.append("<success xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");

+            if (data != null &&

+                    data.trim().length() > 0) {

+                stanza.append(data);

+            }

+            stanza.append("</success>");

+            return stanza.toString();

+        }

+    }

+

+    /**

+     * A SASL failure stanza.

+     */

+    public static class Failure extends Packet {

+        final private String condition;

+

+        public Failure(String condition) {

+            this.condition = condition;

+        }

+

+        /**

+         * Get the SASL related error condition.

+         * 

+         * @return the SASL related error condition.

+         */

+        public String getCondition() {

+            return condition;

+        }

+

+        public String toXML() {

+            StringBuilder stanza = new StringBuilder();

+            stanza.append("<failure xmlns=\"urn:ietf:params:xml:ns:xmpp-sasl\">");

+            if (condition != null &&

+                    condition.trim().length() > 0) {

+                stanza.append("<").append(condition).append("/>");

+            }

+            stanza.append("</failure>");

+            return stanza.toString();

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java b/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java
new file mode 100644
index 0000000..cd973eb
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/SASLPlainMechanism.java
@@ -0,0 +1,34 @@
+/**

+ *

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

+

+import org.jivesoftware.smack.SASLAuthentication;

+

+/**

+ * Implementation of the SASL PLAIN mechanism

+ *

+ * @author Jay Kline

+ */

+public class SASLPlainMechanism extends SASLMechanism {

+

+    public SASLPlainMechanism(SASLAuthentication saslAuthentication) {

+        super(saslAuthentication);

+    }

+

+    protected String getName() {

+        return "PLAIN";

+    }

+}

diff --git a/src/org/jivesoftware/smack/sasl/package.html b/src/org/jivesoftware/smack/sasl/package.html
new file mode 100644
index 0000000..1e8cfb7
--- /dev/null
+++ b/src/org/jivesoftware/smack/sasl/package.html
@@ -0,0 +1 @@
+<body>SASL Mechanisms.</body>
\ No newline at end of file
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
diff --git a/src/org/jivesoftware/smackx/ChatState.java b/src/org/jivesoftware/smackx/ChatState.java
new file mode 100644
index 0000000..4acaa49
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ChatState.java
@@ -0,0 +1,50 @@
+/**
+ * $RCSfile$
+ * $Revision: 2407 $
+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
+ *
+ * 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.smackx;
+
+/**
+ * Represents the current state of a users interaction with another user. Implemented according to
+ * <a href="http://www.xmpp.org/extensions/xep-0085.html">XEP-0085</a>.
+ *
+ * @author Alexander Wenckus
+ */
+public enum ChatState {
+    /**
+     * User is actively participating in the chat session.
+     */
+    active,
+    /**
+     * User is composing a message.
+     */
+    composing,
+    /**
+     * User had been composing but now has stopped.
+     */
+    paused,
+    /**
+     * User has not been actively participating in the chat session.
+     */
+    inactive,
+    /**
+     * User has effectively ended their participation in the chat session.
+     */
+    gone
+}
diff --git a/src/org/jivesoftware/smackx/ChatStateListener.java b/src/org/jivesoftware/smackx/ChatStateListener.java
new file mode 100644
index 0000000..9a1bf79
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ChatStateListener.java
@@ -0,0 +1,40 @@
+/**
+ * $RCSfile$
+ * $Revision: 2407 $
+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
+ *
+ * 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.smackx;
+
+import org.jivesoftware.smack.Chat;
+import org.jivesoftware.smack.MessageListener;
+
+/**
+ * Events for when the state of a user in a chat changes.
+ *
+ * @author Alexander Wenckus
+ */
+public interface ChatStateListener extends MessageListener {
+
+    /**
+     * Fired when the state of a chat with another user changes.
+     *
+     * @param chat the chat in which the state has changed.
+     * @param state the new state of the participant.
+     */
+    void stateChanged(Chat chat, ChatState state);
+}
diff --git a/src/org/jivesoftware/smackx/ChatStateManager.java b/src/org/jivesoftware/smackx/ChatStateManager.java
new file mode 100644
index 0000000..d452a9f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ChatStateManager.java
@@ -0,0 +1,200 @@
+/**
+ * $RCSfile$
+ * $Revision: 2407 $
+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
+ *
+ * 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.smackx;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.util.collections.ReferenceMap;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.NotFilter;
+import org.jivesoftware.smack.filter.PacketExtensionFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.packet.ChatStateExtension;
+
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Handles chat state for all chats on a particular Connection. This class manages both the
+ * packet extensions and the disco response neccesary for compliance with
+ * <a href="http://www.xmpp.org/extensions/xep-0085.html">XEP-0085</a>.
+ *
+ * NOTE: {@link org.jivesoftware.smackx.ChatStateManager#getInstance(org.jivesoftware.smack.Connection)}
+ * needs to be called in order for the listeners to be registered appropriately with the connection.
+ * If this does not occur you will not receive the update notifications.
+ *
+ * @author Alexander Wenckus
+ * @see org.jivesoftware.smackx.ChatState
+ * @see org.jivesoftware.smackx.packet.ChatStateExtension
+ */
+public class ChatStateManager {
+
+    private static final Map<Connection, ChatStateManager> managers =
+            new WeakHashMap<Connection, ChatStateManager>();
+
+    private static final PacketFilter filter = new NotFilter(
+                new PacketExtensionFilter("http://jabber.org/protocol/chatstates"));
+
+    /**
+     * Returns the ChatStateManager related to the Connection and it will create one if it does
+     * not yet exist.
+     *
+     * @param connection the connection to return the ChatStateManager
+     * @return the ChatStateManager related the the connection.
+     */
+    public static ChatStateManager getInstance(final Connection connection) {
+        if(connection == null) {
+            return null;
+        }
+        synchronized (managers) {
+            ChatStateManager manager = managers.get(connection);
+            if (manager == null) {
+                manager = new ChatStateManager(connection);
+                manager.init();
+                managers.put(connection, manager);
+            }
+
+            return manager;
+        }
+    }
+
+    private final Connection connection;
+
+    private final OutgoingMessageInterceptor outgoingInterceptor = new OutgoingMessageInterceptor();
+
+    private final IncomingMessageInterceptor incomingInterceptor = new IncomingMessageInterceptor();
+
+    /**
+     * Maps chat to last chat state.
+     */
+    private final Map<Chat, ChatState> chatStates =
+            new ReferenceMap<Chat, ChatState>(ReferenceMap.WEAK, ReferenceMap.HARD);
+
+    private ChatStateManager(Connection connection) {
+        this.connection = connection;
+    }
+
+    private void init() {
+        connection.getChatManager().addOutgoingMessageInterceptor(outgoingInterceptor,
+                filter);
+        connection.getChatManager().addChatListener(incomingInterceptor);
+
+        ServiceDiscoveryManager.getInstanceFor(connection)
+                .addFeature("http://jabber.org/protocol/chatstates");
+    }
+
+    /**
+     * Sets the current state of the provided chat. This method will send an empty bodied Message
+     * packet with the state attached as a {@link org.jivesoftware.smack.packet.PacketExtension}, if
+     * and only if the new chat state is different than the last state.
+     *
+     * @param newState the new state of the chat
+     * @param chat the chat.
+     * @throws org.jivesoftware.smack.XMPPException
+     *          when there is an error sending the message
+     *          packet.
+     */
+    public void setCurrentState(ChatState newState, Chat chat) throws XMPPException {
+        if(chat == null || newState == null) {
+            throw new IllegalArgumentException("Arguments cannot be null.");
+        }
+        if(!updateChatState(chat, newState)) {
+            return;
+        }
+        Message message = new Message();
+        ChatStateExtension extension = new ChatStateExtension(newState);
+        message.addExtension(extension);
+
+        chat.sendMessage(message);
+    }
+
+
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        ChatStateManager that = (ChatStateManager) o;
+
+        return connection.equals(that.connection);
+
+    }
+
+    public int hashCode() {
+        return connection.hashCode();
+    }
+
+    private boolean updateChatState(Chat chat, ChatState newState) {
+        ChatState lastChatState = chatStates.get(chat);
+        if (lastChatState != newState) {
+            chatStates.put(chat, newState);
+            return true;
+        }
+        return false;
+    }
+
+    private void fireNewChatState(Chat chat, ChatState state) {
+        for (MessageListener listener : chat.getListeners()) {
+            if (listener instanceof ChatStateListener) {
+                ((ChatStateListener) listener).stateChanged(chat, state);
+            }
+        }
+    }
+
+    private class OutgoingMessageInterceptor implements PacketInterceptor {
+
+        public void interceptPacket(Packet packet) {
+            Message message = (Message) packet;
+            Chat chat = connection.getChatManager().getThreadChat(message.getThread());
+            if (chat == null) {
+                return;
+            }
+            if (updateChatState(chat, ChatState.active)) {
+                message.addExtension(new ChatStateExtension(ChatState.active));
+            }
+        }
+    }
+
+    private class IncomingMessageInterceptor implements ChatManagerListener, MessageListener {
+
+        public void chatCreated(final Chat chat, boolean createdLocally) {
+            chat.addMessageListener(this);
+        }
+
+        public void processMessage(Chat chat, Message message) {
+            PacketExtension extension
+                    = message.getExtension("http://jabber.org/protocol/chatstates");
+            if (extension == null) {
+                return;
+            }
+
+            ChatState state;
+            try {
+                state = ChatState.valueOf(extension.getElementName());
+            }
+            catch (Exception ex) {
+                return;
+            }
+
+            fireNewChatState(chat, state);
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/ConfigureProviderManager.java b/src/org/jivesoftware/smackx/ConfigureProviderManager.java
new file mode 100644
index 0000000..7c0cdf2
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ConfigureProviderManager.java
@@ -0,0 +1,207 @@
+/**
+ * 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.smackx;
+
+import org.jivesoftware.smack.provider.PrivacyProvider;
+import org.jivesoftware.smack.provider.ProviderManager;
+import org.jivesoftware.smackx.GroupChatInvitation;
+import org.jivesoftware.smackx.PrivateDataManager;
+import org.jivesoftware.smackx.bytestreams.ibb.provider.CloseIQProvider;
+import org.jivesoftware.smackx.bytestreams.ibb.provider.DataPacketProvider;
+import org.jivesoftware.smackx.bytestreams.ibb.provider.OpenIQProvider;
+import org.jivesoftware.smackx.bytestreams.socks5.provider.BytestreamsProvider;
+import org.jivesoftware.smackx.carbons.Carbon;
+import org.jivesoftware.smackx.entitycaps.provider.CapsExtensionProvider;
+import org.jivesoftware.smackx.forward.Forwarded;
+import org.jivesoftware.smackx.packet.AttentionExtension;
+import org.jivesoftware.smackx.packet.ChatStateExtension;
+import org.jivesoftware.smackx.packet.LastActivity;
+import org.jivesoftware.smackx.packet.Nick;
+import org.jivesoftware.smackx.packet.OfflineMessageInfo;
+import org.jivesoftware.smackx.packet.OfflineMessageRequest;
+import org.jivesoftware.smackx.packet.SharedGroupsInfo;
+import org.jivesoftware.smackx.ping.provider.PingProvider;
+import org.jivesoftware.smackx.provider.DataFormProvider;
+import org.jivesoftware.smackx.provider.DelayInformationProvider;
+import org.jivesoftware.smackx.provider.DiscoverInfoProvider;
+import org.jivesoftware.smackx.provider.DiscoverItemsProvider;
+import org.jivesoftware.smackx.provider.HeadersProvider;
+import org.jivesoftware.smackx.provider.HeaderProvider;
+import org.jivesoftware.smackx.provider.MUCAdminProvider;
+import org.jivesoftware.smackx.provider.MUCOwnerProvider;
+import org.jivesoftware.smackx.provider.MUCUserProvider;
+import org.jivesoftware.smackx.provider.MessageEventProvider;
+import org.jivesoftware.smackx.provider.MultipleAddressesProvider;
+import org.jivesoftware.smackx.provider.RosterExchangeProvider;
+import org.jivesoftware.smackx.provider.StreamInitiationProvider;
+import org.jivesoftware.smackx.provider.VCardProvider;
+import org.jivesoftware.smackx.provider.XHTMLExtensionProvider;
+import org.jivesoftware.smackx.pubsub.provider.AffiliationProvider;
+import org.jivesoftware.smackx.pubsub.provider.AffiliationsProvider;
+import org.jivesoftware.smackx.pubsub.provider.ConfigEventProvider;
+import org.jivesoftware.smackx.pubsub.provider.EventProvider;
+import org.jivesoftware.smackx.pubsub.provider.FormNodeProvider;
+import org.jivesoftware.smackx.pubsub.provider.ItemProvider;
+import org.jivesoftware.smackx.pubsub.provider.ItemsProvider;
+import org.jivesoftware.smackx.pubsub.provider.PubSubProvider;
+import org.jivesoftware.smackx.pubsub.provider.RetractEventProvider;
+import org.jivesoftware.smackx.pubsub.provider.SimpleNodeProvider;
+import org.jivesoftware.smackx.pubsub.provider.SubscriptionProvider;
+import org.jivesoftware.smackx.pubsub.provider.SubscriptionsProvider;
+import org.jivesoftware.smackx.receipts.DeliveryReceipt;
+import org.jivesoftware.smackx.search.UserSearch;
+
+/**
+ * Since dalvik on Android does not allow the loading of META-INF files from the
+ * filesystem, you have to register every provider manually.
+ *
+ * The full list of providers is at:
+ * http://fisheye.igniterealtime.org/browse/smack/trunk/build/resources/META-INF/smack.providers?hb=true
+ *
+ * @author Florian Schmaus fschmaus@gmail.com
+ *
+ */
+public class ConfigureProviderManager {
+
+    public static void configureProviderManager() {
+        ProviderManager pm = ProviderManager.getInstance();
+
+        // The order is the same as in the smack.providers file
+
+        //  Private Data Storage
+        pm.addIQProvider("query","jabber:iq:private", new PrivateDataManager.PrivateDataIQProvider());
+        //  Time
+        try {
+            pm.addIQProvider("query","jabber:iq:time", Class.forName("org.jivesoftware.smackx.packet.Time"));
+        } catch (ClassNotFoundException e) {
+            System.err.println("Can't load class for org.jivesoftware.smackx.packet.Time");
+        }
+
+        //  Roster Exchange
+        pm.addExtensionProvider("x","jabber:x:roster", new RosterExchangeProvider());
+        //  Message Events
+        pm.addExtensionProvider("x","jabber:x:event", new MessageEventProvider());
+        //  Chat State
+        pm.addExtensionProvider("active","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
+        pm.addExtensionProvider("composing","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
+        pm.addExtensionProvider("paused","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
+        pm.addExtensionProvider("inactive","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
+        pm.addExtensionProvider("gone","http://jabber.org/protocol/chatstates", new ChatStateExtension.Provider());
+
+        //  XHTML
+        pm.addExtensionProvider("html","http://jabber.org/protocol/xhtml-im", new XHTMLExtensionProvider());
+
+        //  Group Chat Invitations
+        pm.addExtensionProvider("x","jabber:x:conference", new GroupChatInvitation.Provider());
+        //  Service Discovery # Items
+        pm.addIQProvider("query","http://jabber.org/protocol/disco#items", new DiscoverItemsProvider());
+        //  Service Discovery # Info
+        pm.addIQProvider("query","http://jabber.org/protocol/disco#info", new DiscoverInfoProvider());
+        //  Data Forms
+        pm.addExtensionProvider("x","jabber:x:data", new DataFormProvider());
+        //  MUC User
+        pm.addExtensionProvider("x","http://jabber.org/protocol/muc#user", new MUCUserProvider());
+        //  MUC Admin
+        pm.addIQProvider("query","http://jabber.org/protocol/muc#admin", new MUCAdminProvider());
+        //  MUC Owner
+        pm.addIQProvider("query","http://jabber.org/protocol/muc#owner", new MUCOwnerProvider());
+        //  Delayed Delivery
+        pm.addExtensionProvider("x","jabber:x:delay", new DelayInformationProvider());
+        pm.addExtensionProvider("delay", "urn:xmpp:delay", new DelayInformationProvider());
+        //  Version
+        try {
+            pm.addIQProvider("query","jabber:iq:version", Class.forName("org.jivesoftware.smackx.packet.Version"));
+        } catch (ClassNotFoundException e) {
+            System.err.println("Can't load class for org.jivesoftware.smackx.packet.Version");
+        }
+        //  VCard
+        pm.addIQProvider("vCard","vcard-temp", new VCardProvider());
+        //  Offline Message Requests
+        pm.addIQProvider("offline","http://jabber.org/protocol/offline", new OfflineMessageRequest.Provider());
+        //  Offline Message Indicator
+        pm.addExtensionProvider("offline","http://jabber.org/protocol/offline", new OfflineMessageInfo.Provider());
+        //  Last Activity
+        pm.addIQProvider("query","jabber:iq:last", new LastActivity.Provider());
+        //  User Search
+        pm.addIQProvider("query","jabber:iq:search", new UserSearch.Provider());
+        //  SharedGroupsInfo
+        pm.addIQProvider("sharedgroup","http://www.jivesoftware.org/protocol/sharedgroup", new SharedGroupsInfo.Provider());
+
+        //  JEP-33: Extended Stanza Addressing
+        pm.addExtensionProvider("addresses","http://jabber.org/protocol/address", new MultipleAddressesProvider());
+
+        //   FileTransfer
+        pm.addIQProvider("si","http://jabber.org/protocol/si", new StreamInitiationProvider());
+        pm.addIQProvider("query","http://jabber.org/protocol/bytestreams", new BytestreamsProvider());
+        pm.addIQProvider("open","http://jabber.org/protocol/ibb", new OpenIQProvider());
+        pm.addIQProvider("data","http://jabber.org/protocol/ibb", new DataPacketProvider());
+        pm.addIQProvider("close","http://jabber.org/protocol/ibb", new CloseIQProvider());
+        pm.addExtensionProvider("data","http://jabber.org/protocol/ibb", new DataPacketProvider());
+
+        //  Privacy
+        pm.addIQProvider("query","jabber:iq:privacy", new PrivacyProvider());
+
+        // SHIM
+        pm.addExtensionProvider("headers", "http://jabber.org/protocol/shim", new HeadersProvider());
+        pm.addExtensionProvider("header", "http://jabber.org/protocol/shim", new HeaderProvider());
+
+        // PubSub
+        pm.addIQProvider("pubsub", "http://jabber.org/protocol/pubsub", new PubSubProvider());
+        pm.addExtensionProvider("create", "http://jabber.org/protocol/pubsub", new SimpleNodeProvider());
+        pm.addExtensionProvider("items", "http://jabber.org/protocol/pubsub", new ItemsProvider());
+        pm.addExtensionProvider("item", "http://jabber.org/protocol/pubsub", new ItemProvider());
+        pm.addExtensionProvider("subscriptions", "http://jabber.org/protocol/pubsub", new SubscriptionsProvider());
+        pm.addExtensionProvider("subscription", "http://jabber.org/protocol/pubsub", new SubscriptionProvider());
+        pm.addExtensionProvider("affiliations", "http://jabber.org/protocol/pubsub", new AffiliationsProvider());
+        pm.addExtensionProvider("affiliation", "http://jabber.org/protocol/pubsub", new AffiliationProvider());
+        pm.addExtensionProvider("options", "http://jabber.org/protocol/pubsub", new FormNodeProvider());
+        // PubSub owner
+        pm.addIQProvider("pubsub", "http://jabber.org/protocol/pubsub#owner", new PubSubProvider());
+        pm.addExtensionProvider("configure", "http://jabber.org/protocol/pubsub#owner", new FormNodeProvider());
+        pm.addExtensionProvider("default", "http://jabber.org/protocol/pubsub#owner", new FormNodeProvider());
+        // PubSub event
+        pm.addExtensionProvider("event", "http://jabber.org/protocol/pubsub#event", new EventProvider());
+        pm.addExtensionProvider("configuration", "http://jabber.org/protocol/pubsub#event", new ConfigEventProvider());
+        pm.addExtensionProvider("delete", "http://jabber.org/protocol/pubsub#event", new SimpleNodeProvider());
+        pm.addExtensionProvider("options", "http://jabber.org/protocol/pubsub#event", new FormNodeProvider());
+        pm.addExtensionProvider("items", "http://jabber.org/protocol/pubsub#event", new ItemsProvider());
+        pm.addExtensionProvider("item", "http://jabber.org/protocol/pubsub#event", new ItemProvider());
+        pm.addExtensionProvider("retract", "http://jabber.org/protocol/pubsub#event", new RetractEventProvider());
+        pm.addExtensionProvider("purge", "http://jabber.org/protocol/pubsub#event", new SimpleNodeProvider());
+
+        // Nick Exchange
+        pm.addExtensionProvider("nick", "http://jabber.org/protocol/nick", new Nick.Provider());
+
+        // Attention
+        pm.addExtensionProvider("attention", "urn:xmpp:attention:0", new AttentionExtension.Provider());
+
+	// XEP-0297 Stanza Forwarding
+	pm.addExtensionProvider("forwarded", "urn:xmpp:forward:0", new Forwarded.Provider());
+
+	// XEP-0280 Message Carbons
+	pm.addExtensionProvider("sent", "urn:xmpp:carbons:2", new Carbon.Provider());
+	pm.addExtensionProvider("received", "urn:xmpp:carbons:2", new Carbon.Provider());
+
+	// XEP-0199 XMPP Ping
+	pm.addIQProvider("ping", "urn:xmpp:ping", new PingProvider());
+
+	// XEP-184 Message Delivery Receipts
+	pm.addExtensionProvider("received", "urn:xmpp:receipts", new DeliveryReceipt.Provider());
+	pm.addExtensionProvider("request", "urn:xmpp:receipts", new DeliveryReceipt.Provider());
+
+	// XEP-0115 Entity Capabilities
+	pm.addExtensionProvider("c", "http://jabber.org/protocol/caps", new CapsExtensionProvider());
+    }
+}
diff --git a/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java b/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java
new file mode 100644
index 0000000..7ae0c51
--- /dev/null
+++ b/src/org/jivesoftware/smackx/DefaultMessageEventRequestListener.java
@@ -0,0 +1,55 @@
+/**
+ * $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.smackx;
+
+/**
+ *
+ * Default implementation of the MessageEventRequestListener interface.<p>
+ *
+ * This class automatically sends a delivered notification to the sender of the message 
+ * if the sender has requested to be notified when the message is delivered. 
+ *
+ * @author Gaston Dombiak
+ */
+public class DefaultMessageEventRequestListener implements MessageEventRequestListener {
+
+    public void deliveredNotificationRequested(String from, String packetID,
+                MessageEventManager messageEventManager)
+    {
+        // Send to the message's sender that the message has been delivered
+        messageEventManager.sendDeliveredNotification(from, packetID);
+    }
+
+    public void displayedNotificationRequested(String from, String packetID,
+            MessageEventManager messageEventManager)
+    {
+    }
+
+    public void composingNotificationRequested(String from, String packetID,
+            MessageEventManager messageEventManager)
+    {
+    }
+
+    public void offlineNotificationRequested(String from, String packetID,
+            MessageEventManager messageEventManager)
+    {
+    }
+}
diff --git a/src/org/jivesoftware/smackx/Form.java b/src/org/jivesoftware/smackx/Form.java
new file mode 100644
index 0000000..992c036
--- /dev/null
+++ b/src/org/jivesoftware/smackx/Form.java
@@ -0,0 +1,551 @@
+/**
+ * $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.smackx;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.packet.DataForm;
+
+/**
+ * Represents a Form for gathering data. The form could be of the following types:
+ * <ul>
+ *  <li>form -> Indicates a form to fill out.</li>
+ *  <li>submit -> The form is filled out, and this is the data that is being returned from 
+ * the form.</li>
+ *  <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li>
+ *  <li>result -> Data results being returned from a search, or some other query.</li>
+ * </ul>
+ * 
+ * Depending of the form's type different operations are available. For example, it's only possible
+ * to set answers if the form is of type "submit".
+ * 
+ * @see <a href="http://xmpp.org/extensions/xep-0004.html">XEP-0004 Data Forms</a>
+ * 
+ * @author Gaston Dombiak
+ */
+public class Form {
+
+    public static final String TYPE_FORM = "form";
+    public static final String TYPE_SUBMIT = "submit";
+    public static final String TYPE_CANCEL = "cancel";
+    public static final String TYPE_RESULT = "result";
+
+    public static final String NAMESPACE = "jabber:x:data";
+    public static final String ELEMENT = "x";
+
+    private DataForm dataForm;
+
+    /**
+     * Returns a new ReportedData if the packet is used for gathering data and includes an 
+     * extension that matches the elementName and namespace "x","jabber:x:data".  
+     * 
+     * @param packet the packet used for gathering data.
+     * @return the data form parsed from the packet or <tt>null</tt> if there was not
+     *      a form in the packet.
+     */
+    public static Form getFormFrom(Packet packet) {
+        // Check if the packet includes the DataForm extension
+        PacketExtension packetExtension = packet.getExtension("x","jabber:x:data");
+        if (packetExtension != null) {
+            // Check if the existing DataForm is not a result of a search
+            DataForm dataForm = (DataForm) packetExtension;
+            if (dataForm.getReportedData() == null)
+                return new Form(dataForm);
+        }
+        // Otherwise return null
+        return null;
+    }
+
+    /**
+     * Creates a new Form that will wrap an existing DataForm. The wrapped DataForm must be
+     * used for gathering data. 
+     * 
+     * @param dataForm the data form used for gathering data. 
+     */
+    public Form(DataForm dataForm) {
+        this.dataForm = dataForm;
+    }
+    
+    /**
+     * Creates a new Form of a given type from scratch.<p>
+     *  
+     * Possible form types are:
+     * <ul>
+     *  <li>form -> Indicates a form to fill out.</li>
+     *  <li>submit -> The form is filled out, and this is the data that is being returned from 
+     * the form.</li>
+     *  <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li>
+     *  <li>result -> Data results being returned from a search, or some other query.</li>
+     * </ul>
+     * 
+     * @param type the form's type (e.g. form, submit,cancel,result).
+     */
+    public Form(String type) {
+        this.dataForm = new DataForm(type);
+    }
+    
+    /**
+     * Adds a new field to complete as part of the form.
+     * 
+     * @param field the field to complete.
+     */
+    public void addField(FormField field) {
+        dataForm.addField(field);
+    }
+    
+    /**
+     * Sets a new String value to a given form's field. The field whose variable matches the 
+     * requested variable will be completed with the specified value. If no field could be found 
+     * for the specified variable then an exception will be raised.<p>
+     * 
+     * If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you
+     * can use this message where the String value is the String representation of the object. 
+     * 
+     * @param variable the variable name that was completed.
+     * @param value the String value that was answered.
+     * @throws IllegalStateException if the form is not of type "submit".
+     * @throws IllegalArgumentException if the form does not include the specified variable or
+     *      if the answer type does not correspond with the field type..
+     */
+    public void setAnswer(String variable, String value) {
+        FormField field = getField(variable);
+        if (field == null) {
+            throw new IllegalArgumentException("Field not found for the specified variable name.");
+        }
+        if (!FormField.TYPE_TEXT_MULTI.equals(field.getType())
+            && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType())
+            && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())
+            && !FormField.TYPE_JID_SINGLE.equals(field.getType())
+            && !FormField.TYPE_HIDDEN.equals(field.getType())) {
+            throw new IllegalArgumentException("This field is not of type String.");
+        }
+        setAnswer(field, value);
+    }
+
+    /**
+     * Sets a new int value to a given form's field. The field whose variable matches the 
+     * requested variable will be completed with the specified value. If no field could be found 
+     * for the specified variable then an exception will be raised.
+     * 
+     * @param variable the variable name that was completed.
+     * @param value the int value that was answered.
+     * @throws IllegalStateException if the form is not of type "submit".
+     * @throws IllegalArgumentException if the form does not include the specified variable or
+     *      if the answer type does not correspond with the field type.
+     */
+    public void setAnswer(String variable, int value) {
+        FormField field = getField(variable);
+        if (field == null) {
+            throw new IllegalArgumentException("Field not found for the specified variable name.");
+        }
+        if (!FormField.TYPE_TEXT_MULTI.equals(field.getType())
+            && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType())
+            && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) {
+            throw new IllegalArgumentException("This field is not of type int.");
+        }
+        setAnswer(field, value);
+    }
+
+    /**
+     * Sets a new long value to a given form's field. The field whose variable matches the 
+     * requested variable will be completed with the specified value. If no field could be found 
+     * for the specified variable then an exception will be raised.
+     * 
+     * @param variable the variable name that was completed.
+     * @param value the long value that was answered.
+     * @throws IllegalStateException if the form is not of type "submit".
+     * @throws IllegalArgumentException if the form does not include the specified variable or
+     *      if the answer type does not correspond with the field type.
+     */
+    public void setAnswer(String variable, long value) {
+        FormField field = getField(variable);
+        if (field == null) {
+            throw new IllegalArgumentException("Field not found for the specified variable name.");
+        }
+        if (!FormField.TYPE_TEXT_MULTI.equals(field.getType())
+            && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType())
+            && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) {
+            throw new IllegalArgumentException("This field is not of type long.");
+        }
+        setAnswer(field, value);
+    }
+
+    /**
+     * Sets a new float value to a given form's field. The field whose variable matches the 
+     * requested variable will be completed with the specified value. If no field could be found 
+     * for the specified variable then an exception will be raised.
+     * 
+     * @param variable the variable name that was completed.
+     * @param value the float value that was answered.
+     * @throws IllegalStateException if the form is not of type "submit".
+     * @throws IllegalArgumentException if the form does not include the specified variable or
+     *      if the answer type does not correspond with the field type.
+     */
+    public void setAnswer(String variable, float value) {
+        FormField field = getField(variable);
+        if (field == null) {
+            throw new IllegalArgumentException("Field not found for the specified variable name.");
+        }
+        if (!FormField.TYPE_TEXT_MULTI.equals(field.getType())
+            && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType())
+            && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) {
+            throw new IllegalArgumentException("This field is not of type float.");
+        }
+        setAnswer(field, value);
+    }
+
+    /**
+     * Sets a new double value to a given form's field. The field whose variable matches the 
+     * requested variable will be completed with the specified value. If no field could be found 
+     * for the specified variable then an exception will be raised.
+     * 
+     * @param variable the variable name that was completed.
+     * @param value the double value that was answered.
+     * @throws IllegalStateException if the form is not of type "submit".
+     * @throws IllegalArgumentException if the form does not include the specified variable or
+     *      if the answer type does not correspond with the field type.
+     */
+    public void setAnswer(String variable, double value) {
+        FormField field = getField(variable);
+        if (field == null) {
+            throw new IllegalArgumentException("Field not found for the specified variable name.");
+        }
+        if (!FormField.TYPE_TEXT_MULTI.equals(field.getType())
+            && !FormField.TYPE_TEXT_PRIVATE.equals(field.getType())
+            && !FormField.TYPE_TEXT_SINGLE.equals(field.getType())) {
+            throw new IllegalArgumentException("This field is not of type double.");
+        }
+        setAnswer(field, value);
+    }
+
+    /**
+     * Sets a new boolean value to a given form's field. The field whose variable matches the 
+     * requested variable will be completed with the specified value. If no field could be found 
+     * for the specified variable then an exception will be raised.
+     * 
+     * @param variable the variable name that was completed.
+     * @param value the boolean value that was answered.
+     * @throws IllegalStateException if the form is not of type "submit".
+     * @throws IllegalArgumentException if the form does not include the specified variable or
+     *      if the answer type does not correspond with the field type.
+     */
+    public void setAnswer(String variable, boolean value) {
+        FormField field = getField(variable);
+        if (field == null) {
+            throw new IllegalArgumentException("Field not found for the specified variable name.");
+        }
+        if (!FormField.TYPE_BOOLEAN.equals(field.getType())) {
+            throw new IllegalArgumentException("This field is not of type boolean.");
+        }
+        setAnswer(field, (value ? "1" : "0"));
+    }
+
+    /**
+     * Sets a new Object value to a given form's field. In fact, the object representation 
+     * (i.e. #toString) will be the actual value of the field.<p>
+     * 
+     * If the value to set to the field is not a basic type (e.g. String, boolean, int, etc.) you
+     * will need to use {@link #setAnswer(String, String))} where the String value is the 
+     * String representation of the object.<p> 
+     * 
+     * Before setting the new value to the field we will check if the form is of type submit. If 
+     * the form isn't of type submit means that it's not possible to complete the form and an   
+     * exception will be thrown.
+     * 
+     * @param field the form field that was completed.
+     * @param value the Object value that was answered. The object representation will be the 
+     * actual value.
+     * @throws IllegalStateException if the form is not of type "submit".
+     */
+    private void setAnswer(FormField field, Object value) {
+        if (!isSubmitType()) {
+            throw new IllegalStateException("Cannot set an answer if the form is not of type " +
+            "\"submit\"");
+        }
+        field.resetValues();
+        field.addValue(value.toString());
+    }
+
+    /**
+     * Sets a new values to a given form's field. The field whose variable matches the requested 
+     * variable will be completed with the specified values. If no field could be found for 
+     * the specified variable then an exception will be raised.<p>
+     * 
+     * The Objects contained in the List could be of any type. The String representation of them
+     * (i.e. #toString) will be actually used when sending the answer to the server.
+     * 
+     * @param variable the variable that was completed.
+     * @param values the values that were answered.
+     * @throws IllegalStateException if the form is not of type "submit".
+     * @throws IllegalArgumentException if the form does not include the specified variable.
+     */
+    public void setAnswer(String variable, List<String> values) {
+        if (!isSubmitType()) {
+            throw new IllegalStateException("Cannot set an answer if the form is not of type " +
+            "\"submit\"");
+        }
+        FormField field = getField(variable);
+        if (field != null) {
+            // Check that the field can accept a collection of values
+            if (!FormField.TYPE_JID_MULTI.equals(field.getType())
+                && !FormField.TYPE_LIST_MULTI.equals(field.getType())
+                && !FormField.TYPE_LIST_SINGLE.equals(field.getType())
+                && !FormField.TYPE_TEXT_MULTI.equals(field.getType())
+                && !FormField.TYPE_HIDDEN.equals(field.getType())) {
+                throw new IllegalArgumentException("This field only accept list of values.");
+            }
+            // Clear the old values 
+            field.resetValues();
+            // Set the new values. The string representation of each value will be actually used.
+            field.addValues(values);
+        }
+        else {
+            throw new IllegalArgumentException("Couldn't find a field for the specified variable.");
+        }
+    }
+
+    /**
+     * Sets the default value as the value of a given form's field. The field whose variable matches
+     * the requested variable will be completed with its default value. If no field could be found
+     * for the specified variable then an exception will be raised.
+     *
+     * @param variable the variable to complete with its default value.
+     * @throws IllegalStateException if the form is not of type "submit".
+     * @throws IllegalArgumentException if the form does not include the specified variable.
+     */
+    public void setDefaultAnswer(String variable) {
+        if (!isSubmitType()) {
+            throw new IllegalStateException("Cannot set an answer if the form is not of type " +
+            "\"submit\"");
+        }
+        FormField field = getField(variable);
+        if (field != null) {
+            // Clear the old values
+            field.resetValues();
+            // Set the default value
+            for (Iterator<String> it = field.getValues(); it.hasNext();) {
+                field.addValue(it.next());
+            }
+        }
+        else {
+            throw new IllegalArgumentException("Couldn't find a field for the specified variable.");
+        }
+    }
+
+    /**
+     * Returns an Iterator for the fields that are part of the form.
+     *
+     * @return an Iterator for the fields that are part of the form.
+     */
+    public Iterator<FormField> getFields() {
+        return dataForm.getFields();
+    }
+
+    /**
+     * Returns the field of the form whose variable matches the specified variable.
+     * The fields of type FIXED will never be returned since they do not specify a 
+     * variable. 
+     * 
+     * @param variable the variable to look for in the form fields. 
+     * @return the field of the form whose variable matches the specified variable.
+     */
+    public FormField getField(String variable) {
+        if (variable == null || variable.equals("")) {
+            throw new IllegalArgumentException("Variable must not be null or blank.");
+        }
+        // Look for the field whose variable matches the requested variable
+        FormField field;
+        for (Iterator<FormField> it=getFields();it.hasNext();) {
+            field = it.next();
+            if (variable.equals(field.getVariable())) {
+                return field;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the instructions that explain how to fill out the form and what the form is about.
+     * 
+     * @return instructions that explain how to fill out the form.
+     */
+    public String getInstructions() {
+        StringBuilder sb = new StringBuilder();
+        // Join the list of instructions together separated by newlines
+        for (Iterator<String> it = dataForm.getInstructions(); it.hasNext();) {
+            sb.append(it.next());
+            // If this is not the last instruction then append a newline
+            if (it.hasNext()) {
+                sb.append("\n");
+            }
+        }
+        return sb.toString();
+    }
+
+
+    /**
+     * Returns the description of the data. It is similar to the title on a web page or an X 
+     * window.  You can put a <title/> on either a form to fill out, or a set of data results.
+     * 
+     * @return description of the data.
+     */
+    public String getTitle() {
+        return dataForm.getTitle();
+    }
+
+
+    /**
+     * Returns the meaning of the data within the context. The data could be part of a form
+     * to fill out, a form submission or data results.<p>
+     * 
+     * Possible form types are:
+     * <ul>
+     *  <li>form -> Indicates a form to fill out.</li>
+     *  <li>submit -> The form is filled out, and this is the data that is being returned from 
+     * the form.</li>
+     *  <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li>
+     *  <li>result -> Data results being returned from a search, or some other query.</li>
+     * </ul>
+     * 
+     * @return the form's type.
+     */
+    public String getType() {
+        return dataForm.getType(); 
+    }
+    
+
+    /**
+     * Sets instructions that explain how to fill out the form and what the form is about.
+     * 
+     * @param instructions instructions that explain how to fill out the form.
+     */
+    public void setInstructions(String instructions) {
+        // Split the instructions into multiple instructions for each existent newline
+        ArrayList<String> instructionsList = new ArrayList<String>();
+        StringTokenizer st = new StringTokenizer(instructions, "\n");
+        while (st.hasMoreTokens()) {
+            instructionsList.add(st.nextToken());
+        }
+        // Set the new list of instructions
+        dataForm.setInstructions(instructionsList);
+        
+    }
+
+
+    /**
+     * Sets the description of the data. It is similar to the title on a web page or an X window.
+     * You can put a <title/> on either a form to fill out, or a set of data results.
+     * 
+     * @param title description of the data.
+     */
+    public void setTitle(String title) {
+        dataForm.setTitle(title);
+    }
+    
+    /**
+     * Returns a DataForm that serves to send this Form to the server. If the form is of type 
+     * submit, it may contain fields with no value. These fields will be removed since they only 
+     * exist to assist the user while editing/completing the form in a UI. 
+     * 
+     * @return the wrapped DataForm.
+     */
+    public DataForm getDataFormToSend() {
+        if (isSubmitType()) {
+            // Create a new DataForm that contains only the answered fields 
+            DataForm dataFormToSend = new DataForm(getType());
+            for(Iterator<FormField> it=getFields();it.hasNext();) {
+                FormField field = it.next();
+                if (field.getValues().hasNext()) {
+                    dataFormToSend.addField(field);
+                }
+            }
+            return dataFormToSend;
+        }
+        return dataForm;
+    }
+    
+    /**
+     * Returns true if the form is a form to fill out.
+     * 
+     * @return if the form is a form to fill out.
+     */
+    private boolean isFormType() {
+        return TYPE_FORM.equals(dataForm.getType());
+    }
+    
+    /**
+     * Returns true if the form is a form to submit.
+     * 
+     * @return if the form is a form to submit.
+     */
+    private boolean isSubmitType() {
+        return TYPE_SUBMIT.equals(dataForm.getType());
+    }
+
+    /**
+     * Returns a new Form to submit the completed values. The new Form will include all the fields
+     * of the original form except for the fields of type FIXED. Only the HIDDEN fields will 
+     * include the same value of the original form. The other fields of the new form MUST be 
+     * completed. If a field remains with no answer when sending the completed form, then it won't 
+     * be included as part of the completed form.<p>
+     * 
+     * The reason why the fields with variables are included in the new form is to provide a model 
+     * for binding with any UI. This means that the UIs will use the original form (of type 
+     * "form") to learn how to render the form, but the UIs will bind the fields to the form of
+     * type submit.
+     * 
+     * @return a Form to submit the completed values.
+     */
+    public Form createAnswerForm() {
+        if (!isFormType()) {
+            throw new IllegalStateException("Only forms of type \"form\" could be answered");
+        }
+        // Create a new Form
+        Form form = new Form(TYPE_SUBMIT);
+        for (Iterator<FormField> fields=getFields(); fields.hasNext();) {
+            FormField field = fields.next();
+            // Add to the new form any type of field that includes a variable.
+            // Note: The fields of type FIXED are the only ones that don't specify a variable
+            if (field.getVariable() != null) {
+                FormField newField = new FormField(field.getVariable());
+                newField.setType(field.getType());
+                form.addField(newField);
+                // Set the answer ONLY to the hidden fields 
+                if (FormField.TYPE_HIDDEN.equals(field.getType())) {
+                    // Since a hidden field could have many values we need to collect them 
+                    // in a list
+                    List<String> values = new ArrayList<String>();
+                    for (Iterator<String> it=field.getValues();it.hasNext();) {
+                        values.add(it.next());
+                    }
+                    form.setAnswer(field.getVariable(), values);
+                }                
+            }
+        }
+        return form;
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/FormField.java b/src/org/jivesoftware/smackx/FormField.java
new file mode 100644
index 0000000..3d15e92
--- /dev/null
+++ b/src/org/jivesoftware/smackx/FormField.java
@@ -0,0 +1,409 @@
+/**
+ * $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.smackx;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Represents a field of a form. The field could be used to represent a question to complete,
+ * a completed question or a data returned from a search. The exact interpretation of the field
+ * depends on the context where the field is used.
+ *
+ * @author Gaston Dombiak
+ */
+public class FormField {
+
+    public static final String TYPE_BOOLEAN = "boolean";
+    public static final String TYPE_FIXED = "fixed";
+    public static final String TYPE_HIDDEN = "hidden";
+    public static final String TYPE_JID_MULTI = "jid-multi";
+    public static final String TYPE_JID_SINGLE = "jid-single";
+    public static final String TYPE_LIST_MULTI = "list-multi";
+    public static final String TYPE_LIST_SINGLE = "list-single";
+    public static final String TYPE_TEXT_MULTI = "text-multi";
+    public static final String TYPE_TEXT_PRIVATE = "text-private";
+    public static final String TYPE_TEXT_SINGLE = "text-single";
+
+    private String description;
+    private boolean required = false;
+    private String label;
+    private String variable;
+    private String type;
+    private final List<Option> options = new ArrayList<Option>();
+    private final List<String> values = new ArrayList<String>();
+
+    /**
+     * Creates a new FormField with the variable name that uniquely identifies the field
+     * in the context of the form.
+     *
+     * @param variable the variable name of the question.
+     */
+    public FormField(String variable) {
+        this.variable = variable;
+    }
+
+    /**
+     * Creates a new FormField of type FIXED. The fields of type FIXED do not define a variable
+     * name.
+     */
+    public FormField() {
+        this.type = FormField.TYPE_FIXED;
+    }
+
+    /**
+     * Returns a description that provides extra clarification about the question. This information
+     * could be presented to the user either in tool-tip, help button, or as a section of text
+     * before the question.<p>
+     * <p/>
+     * If the question is of type FIXED then the description should remain empty.
+     *
+     * @return description that provides extra clarification about the question.
+     */
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * Returns the label of the question which should give enough information to the user to
+     * fill out the form.
+     *
+     * @return label of the question.
+     */
+    public String getLabel() {
+        return label;
+    }
+
+    /**
+     * Returns an Iterator for the available options that the user has in order to answer
+     * the question.
+     *
+     * @return Iterator for the available options.
+     */
+    public Iterator<Option> getOptions() {
+        synchronized (options) {
+            return Collections.unmodifiableList(new ArrayList<Option>(options)).iterator();
+        }
+    }
+
+    /**
+     * Returns true if the question must be answered in order to complete the questionnaire.
+     *
+     * @return true if the question must be answered in order to complete the questionnaire.
+     */
+    public boolean isRequired() {
+        return required;
+    }
+
+    /**
+     * Returns an indicative of the format for the data to answer. Valid formats are:
+     * <p/>
+     * <ul>
+     * <li>text-single -> single line or word of text
+     * <li>text-private -> instead of showing the user what they typed, you show ***** to
+     * protect it
+     * <li>text-multi -> multiple lines of text entry
+     * <li>list-single -> given a list of choices, pick one
+     * <li>list-multi -> given a list of choices, pick one or more
+     * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0
+     * <li>fixed -> fixed for putting in text to show sections, or just advertise your web
+     * site in the middle of the form
+     * <li>hidden -> is not given to the user at all, but returned with the questionnaire
+     * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based
+     * on the rules for a JID.
+     * <li>jid-multi -> multiple entries for JIDs
+     * </ul>
+     *
+     * @return format for the data to answer.
+     */
+    public String getType() {
+        return type;
+    }
+
+    /**
+     * Returns an Iterator for the default values of the question if the question is part
+     * of a form to fill out. Otherwise, returns an Iterator for the answered values of
+     * the question.
+     *
+     * @return an Iterator for the default values or answered values of the question.
+     */
+    public Iterator<String> getValues() {
+        synchronized (values) {
+            return Collections.unmodifiableList(new ArrayList<String>(values)).iterator();
+        }
+    }
+
+    /**
+     * Returns the variable name that the question is filling out.
+     *
+     * @return the variable name of the question.
+     */
+    public String getVariable() {
+        return variable;
+    }
+
+    /**
+     * Sets a description that provides extra clarification about the question. This information
+     * could be presented to the user either in tool-tip, help button, or as a section of text
+     * before the question.<p>
+     * <p/>
+     * If the question is of type FIXED then the description should remain empty.
+     *
+     * @param description provides extra clarification about the question.
+     */
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    /**
+     * Sets the label of the question which should give enough information to the user to
+     * fill out the form.
+     *
+     * @param label the label of the question.
+     */
+    public void setLabel(String label) {
+        this.label = label;
+    }
+
+    /**
+     * Sets if the question must be answered in order to complete the questionnaire.
+     *
+     * @param required if the question must be answered in order to complete the questionnaire.
+     */
+    public void setRequired(boolean required) {
+        this.required = required;
+    }
+
+    /**
+     * Sets an indicative of the format for the data to answer. Valid formats are:
+     * <p/>
+     * <ul>
+     * <li>text-single -> single line or word of text
+     * <li>text-private -> instead of showing the user what they typed, you show ***** to
+     * protect it
+     * <li>text-multi -> multiple lines of text entry
+     * <li>list-single -> given a list of choices, pick one
+     * <li>list-multi -> given a list of choices, pick one or more
+     * <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0
+     * <li>fixed -> fixed for putting in text to show sections, or just advertise your web
+     * site in the middle of the form
+     * <li>hidden -> is not given to the user at all, but returned with the questionnaire
+     * <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based
+     * on the rules for a JID.
+     * <li>jid-multi -> multiple entries for JIDs
+     * </ul>
+     *
+     * @param type an indicative of the format for the data to answer.
+     */
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    /**
+     * Adds a default value to the question if the question is part of a form to fill out.
+     * Otherwise, adds an answered value to the question.
+     *
+     * @param value a default value or an answered value of the question.
+     */
+    public void addValue(String value) {
+        synchronized (values) {
+            values.add(value);
+        }
+    }
+
+    /**
+     * Adds a default values to the question if the question is part of a form to fill out.
+     * Otherwise, adds an answered values to the question.
+     *
+     * @param newValues default values or an answered values of the question.
+     */
+    public void addValues(List<String> newValues) {
+        synchronized (values) {
+            values.addAll(newValues);
+        }
+    }
+
+    /**
+     * Removes all the values of the field.
+     */
+    protected void resetValues() {
+        synchronized (values) {
+            values.removeAll(new ArrayList<String>(values));
+        }
+    }
+
+    /**
+     * Adss an available options to the question that the user has in order to answer
+     * the question.
+     *
+     * @param option a new available option for the question.
+     */
+    public void addOption(Option option) {
+        synchronized (options) {
+            options.add(option);
+        }
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<field");
+        // Add attributes
+        if (getLabel() != null) {
+            buf.append(" label=\"").append(getLabel()).append("\"");
+        }
+        if (getVariable() != null) {
+            buf.append(" var=\"").append(getVariable()).append("\"");
+        }
+        if (getType() != null) {
+            buf.append(" type=\"").append(getType()).append("\"");
+        }
+        buf.append(">");
+        // Add elements
+        if (getDescription() != null) {
+            buf.append("<desc>").append(getDescription()).append("</desc>");
+        }
+        if (isRequired()) {
+            buf.append("<required/>");
+        }
+        // Loop through all the values and append them to the string buffer
+        for (Iterator<String> i = getValues(); i.hasNext();) {
+            buf.append("<value>").append(i.next()).append("</value>");
+        }
+        // Loop through all the values and append them to the string buffer
+        for (Iterator<Option> i = getOptions(); i.hasNext();) {
+            buf.append((i.next()).toXML());
+        }
+        buf.append("</field>");
+        return buf.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null)
+            return false;
+        if (obj == this)
+            return true;
+        if (!(obj instanceof FormField))
+            return false;
+
+        FormField other = (FormField) obj;
+
+        return toXML().equals(other.toXML());
+    }
+
+    @Override
+    public int hashCode() {
+        return toXML().hashCode();
+    }
+
+    /**
+     * Represents the available option of a given FormField.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Option {
+
+        private String label;
+        private String value;
+
+        public Option(String value) {
+            this.value = value;
+        }
+
+        public Option(String label, String value) {
+            this.label = label;
+            this.value = value;
+        }
+
+        /**
+         * Returns the label that represents the option.
+         *
+         * @return the label that represents the option.
+         */
+        public String getLabel() {
+            return label;
+        }
+
+        /**
+         * Returns the value of the option.
+         *
+         * @return the value of the option.
+         */
+        public String getValue() {
+            return value;
+        }
+
+        @Override
+        public String toString() {
+            return getLabel();
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<option");
+            // Add attribute
+            if (getLabel() != null) {
+                buf.append(" label=\"").append(getLabel()).append("\"");
+            }
+            buf.append(">");
+            // Add element
+            buf.append("<value>").append(StringUtils.escapeForXML(getValue())).append("</value>");
+
+            buf.append("</option>");
+            return buf.toString();
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == null)
+                return false;
+            if (obj == this)
+                return true;
+            if (obj.getClass() != getClass())
+                return false;
+
+            Option other = (Option) obj;
+
+            if (!value.equals(other.value))
+                return false;
+
+            String thisLabel = label == null ? "" : label;
+            String otherLabel = other.label == null ? "" : other.label;
+
+            if (!thisLabel.equals(otherLabel))
+                return false;
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = 1;
+            result = 37 * result + value.hashCode();
+            result = 37 * result + (label == null ? 0 : label.hashCode());
+            return result;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/Gateway.java b/src/org/jivesoftware/smackx/Gateway.java
new file mode 100644
index 0000000..5b5836f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/Gateway.java
@@ -0,0 +1,333 @@
+package org.jivesoftware.smackx;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Roster;
+import org.jivesoftware.smack.RosterEntry;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.Registration;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
+
+/**
+ * This class provides an abstract view to gateways/transports. This class handles all
+ * actions regarding gateways and transports.
+ * @author Till Klocke
+ *
+ */
+public class Gateway {
+	
+	private Connection connection;
+	private ServiceDiscoveryManager sdManager;
+	private Roster roster;
+	private String entityJID;
+	private Registration registerInfo;
+	private Identity identity;
+	private DiscoverInfo info;
+	
+	Gateway(Connection connection, String entityJID){
+		this.connection = connection;
+		this.roster = connection.getRoster();
+		this.sdManager = ServiceDiscoveryManager.getInstanceFor(connection);
+		this.entityJID = entityJID;
+	}
+	
+	Gateway(Connection connection, String entityJID, DiscoverInfo info, Identity identity){
+		this(connection, entityJID);
+		this.info = info;
+		this.identity = identity;
+	}
+	
+	private void discoverInfo() throws XMPPException{
+		info = sdManager.discoverInfo(entityJID);
+		Iterator<Identity> iterator = info.getIdentities();
+		while(iterator.hasNext()){
+			Identity temp = iterator.next();
+			if(temp.getCategory().equalsIgnoreCase("gateway")){
+				this.identity = temp;
+				break;
+			}
+		}
+	}
+	
+	private Identity getIdentity() throws XMPPException{
+		if(identity==null){
+			discoverInfo();
+		}
+		return identity;
+	}
+	
+	private Registration getRegisterInfo(){
+		if(registerInfo==null){
+			refreshRegisterInfo();
+		}
+		return registerInfo;
+	}
+	
+	private void refreshRegisterInfo(){
+		Registration packet = new Registration();
+		packet.setFrom(connection.getUser());
+		packet.setType(IQ.Type.GET);
+		packet.setTo(entityJID);
+		PacketCollector collector = 
+			connection.createPacketCollector(new PacketIDFilter(packet.getPacketID()));
+		connection.sendPacket(packet);
+		Packet result = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+		collector.cancel();
+		if(result instanceof Registration && result.getError()==null){ 
+			Registration register = (Registration)result;
+			this.registerInfo = register;
+		}
+	}
+	
+	/**
+	 * Checks if this gateway supports In-Band registration
+	 * @return true if In-Band registration is supported
+	 * @throws XMPPException
+	 */
+	public boolean canRegister() throws XMPPException{
+		if(info==null){
+			discoverInfo();
+		}
+		return info.containsFeature("jabber:iq:register");
+	}
+	
+	/**
+	 * Returns all fields that are required to register to this gateway
+	 * @return a list of required fields
+	 */
+	public List<String> getRequiredFields(){
+		return getRegisterInfo().getRequiredFields();
+	}
+	
+	/**
+	 * Returns the name as proposed in this gateways identity discovered via service
+	 * discovery
+	 * @return a String of its name
+	 * @throws XMPPException
+	 */
+	public String getName() throws XMPPException{
+		if(identity==null){
+			discoverInfo();
+		}
+		return identity.getName();
+	}
+	
+	/**
+	 * Returns the type as proposed in this gateways identity discovered via service
+	 * discovery. See {@link http://xmpp.org/registrar/disco-categories.html} for 
+	 * possible types
+	 * @return a String describing the type
+	 * @throws XMPPException
+	 */
+	public String getType() throws XMPPException{
+		if(identity==null){
+			discoverInfo();
+		}
+		return identity.getType();
+	}
+	
+	/**
+	 * Returns true if the registration informations indicates that you are already 
+	 * registered with this gateway
+	 * @return true if already registered
+	 * @throws XMPPException
+	 */
+	public boolean isRegistered() throws XMPPException{
+		return getRegisterInfo().isRegistered();
+	}
+	
+	/**
+	 * Returns the value of specific field of the registration information. Can be used
+	 * to retrieve for example to retrieve username/password used on an already registered
+	 * gateway.
+	 * @param fieldName name of the field
+	 * @return a String containing the value of the field or null
+	 */
+	public String getField(String fieldName){
+		return getRegisterInfo().getField(fieldName);
+	}
+	
+	/**
+	 * Returns a List of Strings of all field names which contain values.
+	 * @return a List of field names
+	 */
+	public List<String> getFieldNames(){
+		return getRegisterInfo().getFieldNames();
+	}
+	
+	/**
+	 * A convenience method for retrieving the username of an existing account 
+	 * @return String describing the username
+	 */
+	public String getUsername(){
+		return getField("username");
+	}
+	
+	/**
+	 * A convenience method for retrieving the password of an existing accoung
+	 * @return String describing the password
+	 */
+	public String getPassword(){
+		return getField("password");
+	}
+	
+	/**
+	 * Returns instructions for registering with this gateway
+	 * @return String containing instructions
+	 */
+	public String getInstructions(){
+		return getRegisterInfo().getInstructions();	
+	}
+	
+	/**
+	 * With this method you can register with this gateway or modify an existing registration
+	 * @param username String describing the username
+	 * @param password String describing the password
+	 * @param fields additional fields like email.
+	 * @throws XMPPException 
+	 */
+	public void register(String username, String password, Map<String,String> fields)throws XMPPException{
+		if(getRegisterInfo().isRegistered()) {
+			throw new IllegalStateException("You are already registered with this gateway");
+		}
+		Registration register = new Registration();
+		register.setFrom(connection.getUser());
+		register.setTo(entityJID);
+		register.setType(IQ.Type.SET);
+		register.setUsername(username);
+		register.setPassword(password);
+		for(String s : fields.keySet()){
+			register.addAttribute(s, fields.get(s));
+		}
+		PacketCollector resultCollector = 
+			connection.createPacketCollector(new PacketIDFilter(register.getPacketID())); 
+		connection.sendPacket(register);
+		Packet result = 
+			resultCollector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+		resultCollector.cancel();
+		if(result!=null && result instanceof IQ){
+			IQ resultIQ = (IQ)result;
+			if(resultIQ.getError()!=null){
+				throw new XMPPException(resultIQ.getError());
+			}
+			if(resultIQ.getType()==IQ.Type.ERROR){
+				throw new XMPPException(resultIQ.getError());
+			}
+			connection.addPacketListener(new GatewayPresenceListener(), 
+					new PacketTypeFilter(Presence.class));
+			roster.createEntry(entityJID, getIdentity().getName(), new String[]{});
+		}
+		else{
+			throw new XMPPException("Packet reply timeout");
+		}
+	}
+	
+	/**
+	 * A convenience method for registering or modifying an account on this gateway without
+	 * additional fields
+	 * @param username String describing the username
+	 * @param password String describing the password
+	 * @throws XMPPException
+	 */
+	public void register(String username, String password) throws XMPPException{
+		register(username, password,new HashMap<String,String>());
+	}
+	
+	/**
+	 * This method removes an existing registration from this gateway
+	 * @throws XMPPException
+	 */
+	public void unregister() throws XMPPException{
+		Registration register = new Registration();
+		register.setFrom(connection.getUser());
+		register.setTo(entityJID);
+		register.setType(IQ.Type.SET);
+		register.setRemove(true);
+		PacketCollector resultCollector = 
+			connection.createPacketCollector(new PacketIDFilter(register.getPacketID()));
+		connection.sendPacket(register);
+		Packet result = resultCollector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+		resultCollector.cancel();
+		if(result!=null && result instanceof IQ){
+			IQ resultIQ = (IQ)result;
+			if(resultIQ.getError()!=null){
+				throw new XMPPException(resultIQ.getError());
+			}
+			if(resultIQ.getType()==IQ.Type.ERROR){
+				throw new XMPPException(resultIQ.getError());
+			}
+			RosterEntry gatewayEntry = roster.getEntry(entityJID);
+			roster.removeEntry(gatewayEntry);
+		}
+		else{
+			throw new XMPPException("Packet reply timeout");
+		}
+	}
+	
+	/**
+	 * Lets you login manually in this gateway. Normally a gateway logins you when it
+	 * receives the first presence broadcasted by your server. But it is possible to
+	 * manually login and logout by sending a directed presence. This method sends an
+	 * empty available presence direct to the gateway.
+	 */
+	public void login(){
+		Presence presence = new Presence(Presence.Type.available);
+		login(presence);
+	}
+	
+	/**
+	 * This method lets you send the presence direct to the gateway. Type, To and From
+	 * are modified.
+	 * @param presence the presence used to login to gateway
+	 */
+	public void login(Presence presence){
+		presence.setType(Presence.Type.available);
+		presence.setTo(entityJID);
+		presence.setFrom(connection.getUser());
+		connection.sendPacket(presence);
+	}
+	
+	/**
+	 * This method logs you out from this gateway by sending an unavailable presence
+	 * to directly to this gateway.
+	 */
+	public void logout(){
+		Presence presence = new Presence(Presence.Type.unavailable);
+		presence.setTo(entityJID);
+		presence.setFrom(connection.getUser());
+		connection.sendPacket(presence);
+	}
+	
+	private class GatewayPresenceListener implements PacketListener{
+
+		public void processPacket(Packet packet) {
+			if(packet instanceof Presence){
+				Presence presence = (Presence)packet;
+				if(entityJID.equals(presence.getFrom()) && 
+						roster.contains(presence.getFrom()) &&
+						presence.getType().equals(Presence.Type.subscribe)){
+					Presence response = new Presence(Presence.Type.subscribed);
+					response.setTo(presence.getFrom());
+					response.setFrom(StringUtils.parseBareAddress(connection.getUser()));
+					connection.sendPacket(response);
+				}
+			}
+			
+		}
+	}
+
+}
diff --git a/src/org/jivesoftware/smackx/GatewayManager.java b/src/org/jivesoftware/smackx/GatewayManager.java
new file mode 100644
index 0000000..eee9cfc
--- /dev/null
+++ b/src/org/jivesoftware/smackx/GatewayManager.java
@@ -0,0 +1,199 @@
+package org.jivesoftware.smackx;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.Roster;
+import org.jivesoftware.smack.RosterEntry;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
+import org.jivesoftware.smackx.packet.DiscoverItems.Item;
+
+/**
+ * This class is the general entry point to gateway interaction (XEP-0100). 
+ * This class discovers available gateways on the users servers, and
+ * can give you also a list of gateways the you user is registered with which
+ * are not on his server. All actual interaction with a gateway is handled in the
+ * class {@see Gateway}.
+ * @author Till Klocke
+ *
+ */
+public class GatewayManager {
+	
+	private static Map<Connection,GatewayManager> instances = 
+		new HashMap<Connection,GatewayManager>();
+	
+	private ServiceDiscoveryManager sdManager;
+	
+	private Map<String,Gateway> localGateways = new HashMap<String,Gateway>();
+	
+	private Map<String,Gateway> nonLocalGateways = new HashMap<String,Gateway>();
+	
+	private Map<String,Gateway> gateways = new HashMap<String,Gateway>();
+	
+	private Connection connection;
+	
+	private Roster roster;
+	
+	private GatewayManager(){
+		
+	}
+	
+	/**
+	 * Creates a new instance of GatewayManager
+	 * @param connection
+	 * @throws XMPPException
+	 */
+	private GatewayManager(Connection connection) throws XMPPException{
+		this.connection = connection;
+		this.roster = connection.getRoster();
+		sdManager = ServiceDiscoveryManager.getInstanceFor(connection);
+	}
+	
+	/**
+	 * Loads all gateways the users server offers
+	 * @throws XMPPException
+	 */
+	private void loadLocalGateways() throws XMPPException{
+		DiscoverItems items = sdManager.discoverItems(connection.getHost());
+		Iterator<Item> iter = items.getItems();
+		while(iter.hasNext()){
+			String itemJID = iter.next().getEntityID();
+			discoverGateway(itemJID);
+		}
+	}
+	
+	/**
+	 * Discovers {@link DiscoveryInfo} and {@link DiscoveryInfo.Identity} of a gateway
+	 * and creates a {@link Gateway} object representing this gateway.
+	 * @param itemJID
+	 * @throws XMPPException
+	 */
+	private void discoverGateway(String itemJID) throws XMPPException{
+		DiscoverInfo info = sdManager.discoverInfo(itemJID);
+		Iterator<Identity> i = info.getIdentities();
+		
+		while(i.hasNext()){
+			Identity identity = i.next();
+			String category = identity.getCategory();
+			if(category.toLowerCase().equals("gateway")){
+				gateways.put(itemJID, new Gateway(connection,itemJID));
+				if(itemJID.contains(connection.getHost())){
+					localGateways.put(itemJID, 
+							new Gateway(connection,itemJID,info,identity));
+				}
+				else{
+					nonLocalGateways.put(itemJID, 
+							new Gateway(connection,itemJID,info,identity));
+				}
+				break;
+			}
+		}
+	}
+	
+	/**
+	 * Loads all getways which are in the users roster, but are not supplied by the
+	 * users server
+	 * @throws XMPPException
+	 */
+	private void loadNonLocalGateways() throws XMPPException{
+		if(roster!=null){
+			for(RosterEntry entry : roster.getEntries()){
+				if(entry.getUser().equalsIgnoreCase(StringUtils.parseServer(entry.getUser())) &&
+						!entry.getUser().contains(connection.getHost())){
+					discoverGateway(entry.getUser());
+				}
+			}
+		}
+	}
+	
+	/**
+	 * Returns an instance of GatewayManager for the given connection. If no instance for
+	 * this connection exists a new one is created and stored in a Map.
+	 * @param connection
+	 * @return an instance of GatewayManager
+	 * @throws XMPPException
+	 */
+	public GatewayManager getInstanceFor(Connection connection) throws XMPPException{
+		synchronized(instances){
+			if(instances.containsKey(connection)){
+				return instances.get(connection);
+			}
+			GatewayManager instance = new GatewayManager(connection);
+			instances.put(connection, instance);
+			return instance;
+		}
+	}
+	
+	/**
+	 * Returns a list of gateways which are offered by the users server, wether the
+	 * user is registered to them or not.
+	 * @return a List of Gateways
+	 * @throws XMPPException
+	 */
+	public List<Gateway> getLocalGateways() throws XMPPException{
+		if(localGateways.size()==0){
+			loadLocalGateways();
+		}
+		return new ArrayList<Gateway>(localGateways.values());
+	}
+	
+	/**
+	 * Returns a list of gateways the user has in his roster, but which are offered by
+	 * remote servers. But note that this list isn't automatically refreshed. You have to
+	 * refresh is manually if needed.
+	 * @return a list of gateways
+	 * @throws XMPPException
+	 */
+	public List<Gateway> getNonLocalGateways() throws XMPPException{
+		if(nonLocalGateways.size()==0){
+			loadNonLocalGateways();
+		}
+		return new ArrayList<Gateway>(nonLocalGateways.values());
+	}
+	
+	/**
+	 * Refreshes the list of gateways offered by remote servers.
+	 * @throws XMPPException
+	 */
+	public void refreshNonLocalGateways() throws XMPPException{
+		loadNonLocalGateways();
+	}
+	
+	/**
+	 * Returns a Gateway object for a given JID. Please note that it is not checked if
+	 * the JID belongs to valid gateway. If this JID doesn't belong to valid gateway
+	 * all operations on this Gateway object should fail with a XMPPException. But there is
+	 * no guarantee for that.
+	 * @param entityJID
+	 * @return a Gateway object
+	 */
+	public Gateway getGateway(String entityJID){
+		if(localGateways.containsKey(entityJID)){
+			return localGateways.get(entityJID);
+		}
+		if(nonLocalGateways.containsKey(entityJID)){
+			return nonLocalGateways.get(entityJID);
+		}
+		if(gateways.containsKey(entityJID)){
+			return gateways.get(entityJID);
+		}
+		Gateway gateway = new Gateway(connection,entityJID);
+		if(entityJID.contains(connection.getHost())){
+			localGateways.put(entityJID, gateway);
+		}
+		else{
+			nonLocalGateways.put(entityJID, gateway);
+		}
+		gateways.put(entityJID, gateway);
+		return gateway;
+	}
+
+}
diff --git a/src/org/jivesoftware/smackx/GroupChatInvitation.java b/src/org/jivesoftware/smackx/GroupChatInvitation.java
new file mode 100644
index 0000000..a9ed35e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/GroupChatInvitation.java
@@ -0,0 +1,115 @@
+/**
+ * $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.smackx;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * A group chat invitation packet extension, which is used to invite other
+ * users to a group chat room. To invite a user to a group chat room, address
+ * a new message to the user and set the room name appropriately, as in the
+ * following code example:
+ *
+ * <pre>
+ * Message message = new Message("user@chat.example.com");
+ * message.setBody("Join me for a group chat!");
+ * message.addExtension(new GroupChatInvitation("room@chat.example.com"););
+ * con.sendPacket(message);
+ * </pre>
+ *
+ * To listen for group chat invitations, use a PacketExtensionFilter for the
+ * <tt>x</tt> element name and <tt>jabber:x:conference</tt> namespace, as in the
+ * following code example:
+ *
+ * <pre>
+ * PacketFilter filter = new PacketExtensionFilter("x", "jabber:x:conference");
+ * // Create a packet collector or packet listeners using the filter...
+ * </pre>
+ *
+ * <b>Note</b>: this protocol is outdated now that the Multi-User Chat (MUC) JEP is available
+ * (<a href="http://www.jabber.org/jeps/jep-0045.html">JEP-45</a>). However, most
+ * existing clients still use this older protocol. Once MUC support becomes more
+ * widespread, this API may be deprecated.
+ * 
+ * @author Matt Tucker
+ */
+public class GroupChatInvitation implements PacketExtension {
+
+    /**
+     * Element name of the packet extension.
+     */
+    public static final String ELEMENT_NAME = "x";
+
+    /**
+     * Namespace of the packet extension.
+     */
+    public static final String NAMESPACE = "jabber:x:conference";
+
+    private String roomAddress;
+
+    /**
+     * Creates a new group chat invitation to the specified room address.
+     * GroupChat room addresses are in the form <tt>room@service</tt>,
+     * where <tt>service</tt> is the name of groupchat server, such as
+     * <tt>chat.example.com</tt>.
+     *
+     * @param roomAddress the address of the group chat room.
+     */
+    public GroupChatInvitation(String roomAddress) {
+        this.roomAddress = roomAddress;
+    }
+
+    /**
+     * Returns the address of the group chat room. GroupChat room addresses
+     * are in the form <tt>room@service</tt>, where <tt>service</tt> is
+     * the name of groupchat server, such as <tt>chat.example.com</tt>.
+     *
+     * @return the address of the group chat room.
+     */
+    public String getRoomAddress() {
+        return roomAddress;
+    }
+
+    public String getElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public String getNamespace() {
+        return NAMESPACE;
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<x xmlns=\"jabber:x:conference\" jid=\"").append(roomAddress).append("\"/>");
+        return buf.toString();
+    }
+
+    public static class Provider implements PacketExtensionProvider {
+        public PacketExtension parseExtension (XmlPullParser parser) throws Exception {
+            String roomAddress = parser.getAttributeValue("", "jid");
+            // Advance to end of extension.
+            parser.next();
+            return new GroupChatInvitation(roomAddress);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/InitStaticCode.java b/src/org/jivesoftware/smackx/InitStaticCode.java
new file mode 100644
index 0000000..12de5af
--- /dev/null
+++ b/src/org/jivesoftware/smackx/InitStaticCode.java
@@ -0,0 +1,51 @@
+/**
+ * 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.smackx;
+
+import android.content.Context;
+
+/**
+ * Since dalvik on Android does not allow the loading of META-INF files from the
+ * filesystem, the static blocks of some classes have to be inited manually.
+ *
+ * The full list can be found here:
+ * http://fisheye.igniterealtime.org/browse/smack/trunk/build/resources/META-INF/smack-config.xml?hb=true
+ *
+ * @author Florian Schmaus fschmaus@gmail.com
+ *
+ */
+public class InitStaticCode {
+
+    public static void initStaticCode(Context ctx) {
+	    // This has the be the application class loader,
+	    // *not* the system class loader
+	    ClassLoader appClassLoader = ctx.getClassLoader();
+
+	    try {
+		    Class.forName(org.jivesoftware.smackx.ServiceDiscoveryManager.class.getName(), true, appClassLoader);
+		    Class.forName(org.jivesoftware.smack.PrivacyListManager.class.getName(), true, appClassLoader);
+		    Class.forName(org.jivesoftware.smackx.XHTMLManager.class.getName(), true, appClassLoader);
+		    Class.forName(org.jivesoftware.smackx.muc.MultiUserChat.class.getName(), true, appClassLoader);
+		    Class.forName(org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.class.getName(), true, appClassLoader);
+		    Class.forName(org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager.class.getName(), true, appClassLoader);
+		    Class.forName(org.jivesoftware.smackx.filetransfer.FileTransferManager.class.getName(), true, appClassLoader);
+		    Class.forName(org.jivesoftware.smackx.LastActivityManager.class.getName(), true, appClassLoader);
+		    Class.forName(org.jivesoftware.smack.ReconnectionManager.class.getName(), true, appClassLoader);
+		    Class.forName(org.jivesoftware.smackx.commands.AdHocCommandManager.class.getName(), true, appClassLoader);
+	    } catch (ClassNotFoundException e) {
+		    throw new IllegalStateException("Could not init static class blocks", e);
+	    }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/LastActivityManager.java b/src/org/jivesoftware/smackx/LastActivityManager.java
new file mode 100644
index 0000000..a9d1f12
--- /dev/null
+++ b/src/org/jivesoftware/smackx/LastActivityManager.java
@@ -0,0 +1,230 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx;

+

+import org.jivesoftware.smack.*;

+import org.jivesoftware.smack.filter.AndFilter;

+import org.jivesoftware.smack.filter.IQTypeFilter;

+import org.jivesoftware.smack.filter.PacketIDFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Message;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.Presence;

+import org.jivesoftware.smackx.packet.DiscoverInfo;

+import org.jivesoftware.smackx.packet.LastActivity;

+

+/**

+ * A last activity manager for handling information about the last activity

+ * associated with a Jabber ID. A manager handles incoming LastActivity requests

+ * of existing Connections. It also allows to request last activity information

+ * of other users.

+ * <p>

+ * 

+ * LastActivity (XEP-0012) based on the sending JID's type allows for retrieval

+ * of:

+ * <ol>

+ * <li>How long a particular user has been idle

+ * <li>How long a particular user has been logged-out and the message the

+ * specified when doing so.

+ * <li>How long a host has been up.

+ * </ol>

+ * <p/>

+ * 

+ * For example to get the idle time of a user logged in a resource, simple send

+ * the LastActivity packet to them, as in the following code:

+ * <p>

+ * 

+ * <pre>

+ * Connection con = new XMPPConnection(&quot;jabber.org&quot;);

+ * con.login(&quot;john&quot;, &quot;doe&quot;);

+ * LastActivity activity = LastActivity.getLastActivity(con, &quot;xray@jabber.org/Smack&quot;);

+ * </pre>

+ * 

+ * To get the lapsed time since the last user logout is the same as above but

+ * with out the resource:

+ * 

+ * <pre>

+ * LastActivity activity = LastActivity.getLastActivity(con, &quot;xray@jabber.org&quot;);

+ * </pre>

+ * 

+ * To get the uptime of a host, you simple send the LastActivity packet to it,

+ * as in the following code example:

+ * <p>

+ * 

+ * <pre>

+ * LastActivity activity = LastActivity.getLastActivity(con, &quot;jabber.org&quot;);

+ * </pre>

+ * 

+ * @author Gabriel Guardincerri

+ * @see <a href="http://xmpp.org/extensions/xep-0012.html">XEP-0012: Last

+ *      Activity</a>

+ */

+

+public class LastActivityManager {

+

+    private long lastMessageSent;

+

+    private Connection connection;

+

+    // Enable the LastActivity support on every established connection

+    static {

+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {

+            public void connectionCreated(Connection connection) {

+                new LastActivityManager(connection);

+            }

+        });

+    }

+

+    /**

+     * Creates a last activity manager to response last activity requests.

+     * 

+     * @param connection

+     *            The Connection that the last activity requests will use.

+     */

+    private LastActivityManager(Connection connection) {

+        this.connection = connection;

+

+        // Listen to all the sent messages to reset the idle time on each one

+        connection.addPacketSendingListener(new PacketListener() {

+            public void processPacket(Packet packet) {

+                Presence presence = (Presence) packet;

+                Presence.Mode mode = presence.getMode();

+                if (mode == null) return;

+                switch (mode) {

+                case available:

+                case chat:

+                    // We assume that only a switch to available and chat indicates user activity

+                    // since other mode changes could be also a result of some sort of automatism

+                    resetIdleTime();

+                }

+            }

+        }, new PacketTypeFilter(Presence.class));

+

+        connection.addPacketListener(new PacketListener() {

+            @Override

+            public void processPacket(Packet packet) {

+                Message message = (Message) packet;

+                // if it's not an error message, reset the idle time

+                if (message.getType() == Message.Type.error) return;

+                resetIdleTime();

+            }

+        }, new PacketTypeFilter(Message.class));

+

+        // Register a listener for a last activity query

+        connection.addPacketListener(new PacketListener() {

+

+            public void processPacket(Packet packet) {

+                LastActivity message = new LastActivity();

+                message.setType(IQ.Type.RESULT);

+                message.setTo(packet.getFrom());

+                message.setFrom(packet.getTo());

+                message.setPacketID(packet.getPacketID());

+                message.setLastActivity(getIdleTime());

+

+                LastActivityManager.this.connection.sendPacket(message);

+            }

+

+        }, new AndFilter(new IQTypeFilter(IQ.Type.GET), new PacketTypeFilter(LastActivity.class)));

+        ServiceDiscoveryManager.getInstanceFor(connection).addFeature(LastActivity.NAMESPACE);

+        resetIdleTime();

+    }

+

+    /**

+     * Resets the idle time to 0, this should be invoked when a new message is

+     * sent.

+     */

+    private void resetIdleTime() {

+        long now = System.currentTimeMillis();

+        synchronized (this) {

+            lastMessageSent = now;

+        }

+    }

+

+    /**

+     * The idle time is the lapsed time between the last message sent and now.

+     * 

+     * @return the lapsed time between the last message sent and now.

+     */

+    private long getIdleTime() {

+        long lms;

+        long now = System.currentTimeMillis();

+        synchronized (this) {

+            lms = lastMessageSent;

+        }

+        return ((now - lms) / 1000);

+    }

+

+    /**

+     * Returns the last activity of a particular jid. If the jid is a full JID

+     * (i.e., a JID of the form of 'user@host/resource') then the last activity

+     * is the idle time of that connected resource. On the other hand, when the

+     * jid is a bare JID (e.g. 'user@host') then the last activity is the lapsed

+     * time since the last logout or 0 if the user is currently logged in.

+     * Moreover, when the jid is a server or component (e.g., a JID of the form

+     * 'host') the last activity is the uptime.

+     * 

+     * @param con

+     *            the current Connection.

+     * @param jid

+     *            the JID of the user.

+     * @return the LastActivity packet of the jid.

+     * @throws XMPPException

+     *             thrown if a server error has occured.

+     */

+    public static LastActivity getLastActivity(Connection con, String jid) throws XMPPException {

+        LastActivity activity = new LastActivity();

+        activity.setTo(jid);

+

+        PacketCollector collector = con.createPacketCollector(new PacketIDFilter(activity.getPacketID()));

+        con.sendPacket(activity);

+

+        LastActivity response = (LastActivity) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+    /**

+     * Returns true if Last Activity (XEP-0012) is supported by a given JID

+     * 

+     * @param connection the connection to be used

+     * @param jid a JID to be tested for Last Activity support

+     * @return true if Last Activity is supported, otherwise false

+     */

+    public static boolean isLastActivitySupported(Connection connection, String jid) {

+        try {

+            DiscoverInfo result =

+                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid);

+            return result.containsFeature(LastActivity.NAMESPACE);

+        }

+        catch (XMPPException e) {

+            return false;

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/MessageEventManager.java b/src/org/jivesoftware/smackx/MessageEventManager.java
new file mode 100644
index 0000000..3502509
--- /dev/null
+++ b/src/org/jivesoftware/smackx/MessageEventManager.java
@@ -0,0 +1,310 @@
+/**
+ * $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.smackx;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.filter.PacketExtensionFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.packet.MessageEvent;
+
+/**
+ * Manages message events requests and notifications. A MessageEventManager provides a high
+ * level access to request for notifications and send event notifications. It also provides 
+ * an easy way to hook up custom logic when requests or notifications are received. 
+ *
+ * @author Gaston Dombiak
+ */
+public class MessageEventManager {
+
+    private List<MessageEventNotificationListener> messageEventNotificationListeners = new ArrayList<MessageEventNotificationListener>();
+    private List<MessageEventRequestListener> messageEventRequestListeners = new ArrayList<MessageEventRequestListener>();
+
+    private Connection con;
+
+    private PacketFilter packetFilter = new PacketExtensionFilter("x", "jabber:x:event");
+    private PacketListener packetListener;
+
+    /**
+     * Creates a new message event manager.
+     *
+     * @param con a Connection to a XMPP server.
+     */
+    public MessageEventManager(Connection con) {
+        this.con = con;
+        init();
+    }
+
+    /**
+     * Adds event notification requests to a message. For each event type that
+     * the user wishes event notifications from the message recepient for, <tt>true</tt>
+     * should be passed in to this method.
+     * 
+     * @param message the message to add the requested notifications.
+     * @param offline specifies if the offline event is requested.
+     * @param delivered specifies if the delivered event is requested.
+     * @param displayed specifies if the displayed event is requested.
+     * @param composing specifies if the composing event is requested.
+     */
+    public static void addNotificationsRequests(Message message, boolean offline,
+            boolean delivered, boolean displayed, boolean composing)
+    {
+        // Create a MessageEvent Package and add it to the message
+        MessageEvent messageEvent = new MessageEvent();
+        messageEvent.setOffline(offline);
+        messageEvent.setDelivered(delivered);
+        messageEvent.setDisplayed(displayed);
+        messageEvent.setComposing(composing);
+        message.addExtension(messageEvent);
+    }
+
+    /**
+     * Adds a message event request listener. The listener will be fired anytime a request for
+     * event notification is received.
+     *
+     * @param messageEventRequestListener a message event request listener.
+     */
+    public void addMessageEventRequestListener(MessageEventRequestListener messageEventRequestListener) {
+        synchronized (messageEventRequestListeners) {
+            if (!messageEventRequestListeners.contains(messageEventRequestListener)) {
+                messageEventRequestListeners.add(messageEventRequestListener);
+            }
+        }
+    }
+
+    /**
+     * Removes a message event request listener. The listener will be fired anytime a request for
+     * event notification is received.
+     *
+     * @param messageEventRequestListener a message event request listener.
+     */
+    public void removeMessageEventRequestListener(MessageEventRequestListener messageEventRequestListener) {
+        synchronized (messageEventRequestListeners) {
+            messageEventRequestListeners.remove(messageEventRequestListener);
+        }
+    }
+
+    /**
+     * Adds a message event notification listener. The listener will be fired anytime a notification
+     * event is received.
+     *
+     * @param messageEventNotificationListener a message event notification listener.
+     */
+    public void addMessageEventNotificationListener(MessageEventNotificationListener messageEventNotificationListener) {
+        synchronized (messageEventNotificationListeners) {
+            if (!messageEventNotificationListeners.contains(messageEventNotificationListener)) {
+                messageEventNotificationListeners.add(messageEventNotificationListener);
+            }
+        }
+    }
+
+    /**
+     * Removes a message event notification listener. The listener will be fired anytime a notification
+     * event is received.
+     *
+     * @param messageEventNotificationListener a message event notification listener.
+     */
+    public void removeMessageEventNotificationListener(MessageEventNotificationListener messageEventNotificationListener) {
+        synchronized (messageEventNotificationListeners) {
+            messageEventNotificationListeners.remove(messageEventNotificationListener);
+        }
+    }
+
+    /**
+     * Fires message event request listeners.
+     */
+    private void fireMessageEventRequestListeners(
+        String from,
+        String packetID,
+        String methodName) {
+        MessageEventRequestListener[] listeners = null;
+        Method method;
+        synchronized (messageEventRequestListeners) {
+            listeners = new MessageEventRequestListener[messageEventRequestListeners.size()];
+            messageEventRequestListeners.toArray(listeners);
+        }
+        try {
+            method =
+                MessageEventRequestListener.class.getDeclaredMethod(
+                    methodName,
+                    new Class[] { String.class, String.class, MessageEventManager.class });
+            for (int i = 0; i < listeners.length; i++) {
+                method.invoke(listeners[i], new Object[] { from, packetID, this });
+            }
+        } catch (NoSuchMethodException e) {
+            e.printStackTrace();
+        } catch (InvocationTargetException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Fires message event notification listeners.
+     */
+    private void fireMessageEventNotificationListeners(
+        String from,
+        String packetID,
+        String methodName) {
+        MessageEventNotificationListener[] listeners = null;
+        Method method;
+        synchronized (messageEventNotificationListeners) {
+            listeners =
+                new MessageEventNotificationListener[messageEventNotificationListeners.size()];
+            messageEventNotificationListeners.toArray(listeners);
+        }
+        try {
+            method =
+                MessageEventNotificationListener.class.getDeclaredMethod(
+                    methodName,
+                    new Class[] { String.class, String.class });
+            for (int i = 0; i < listeners.length; i++) {
+                method.invoke(listeners[i], new Object[] { from, packetID });
+            }
+        } catch (NoSuchMethodException e) {
+            e.printStackTrace();
+        } catch (InvocationTargetException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void init() {
+        // Listens for all message event packets and fire the proper message event listeners.
+        packetListener = new PacketListener() {
+            public void processPacket(Packet packet) {
+                Message message = (Message) packet;
+                MessageEvent messageEvent =
+                    (MessageEvent) message.getExtension("x", "jabber:x:event");
+                if (messageEvent.isMessageEventRequest()) {
+                    // Fire event for requests of message events
+                    for (Iterator<String> it = messageEvent.getEventTypes(); it.hasNext();)
+                        fireMessageEventRequestListeners(
+                            message.getFrom(),
+                            message.getPacketID(),
+                            it.next().concat("NotificationRequested"));
+                } else
+                    // Fire event for notifications of message events
+                    for (Iterator<String> it = messageEvent.getEventTypes(); it.hasNext();)
+                        fireMessageEventNotificationListeners(
+                            message.getFrom(),
+                            messageEvent.getPacketID(),
+                            it.next().concat("Notification"));
+
+            };
+
+        };
+        con.addPacketListener(packetListener, packetFilter);
+    }
+
+    /**
+     * Sends the notification that the message was delivered to the sender of the original message
+     * 
+     * @param to the recipient of the notification.
+     * @param packetID the id of the message to send.
+     */
+    public void sendDeliveredNotification(String to, String packetID) {
+        // Create the message to send
+        Message msg = new Message(to);
+        // Create a MessageEvent Package and add it to the message
+        MessageEvent messageEvent = new MessageEvent();
+        messageEvent.setDelivered(true);
+        messageEvent.setPacketID(packetID);
+        msg.addExtension(messageEvent);
+        // Send the packet
+        con.sendPacket(msg);
+    }
+
+    /**
+     * Sends the notification that the message was displayed to the sender of the original message
+     * 
+     * @param to the recipient of the notification.
+     * @param packetID the id of the message to send.
+     */
+    public void sendDisplayedNotification(String to, String packetID) {
+        // Create the message to send
+        Message msg = new Message(to);
+        // Create a MessageEvent Package and add it to the message
+        MessageEvent messageEvent = new MessageEvent();
+        messageEvent.setDisplayed(true);
+        messageEvent.setPacketID(packetID);
+        msg.addExtension(messageEvent);
+        // Send the packet
+        con.sendPacket(msg);
+    }
+
+    /**
+     * Sends the notification that the receiver of the message is composing a reply
+     * 
+     * @param to the recipient of the notification.
+     * @param packetID the id of the message to send.
+     */
+    public void sendComposingNotification(String to, String packetID) {
+        // Create the message to send
+        Message msg = new Message(to);
+        // Create a MessageEvent Package and add it to the message
+        MessageEvent messageEvent = new MessageEvent();
+        messageEvent.setComposing(true);
+        messageEvent.setPacketID(packetID);
+        msg.addExtension(messageEvent);
+        // Send the packet
+        con.sendPacket(msg);
+    }
+
+    /**
+     * Sends the notification that the receiver of the message has cancelled composing a reply.
+     * 
+     * @param to the recipient of the notification.
+     * @param packetID the id of the message to send.
+     */
+    public void sendCancelledNotification(String to, String packetID) {
+        // Create the message to send
+        Message msg = new Message(to);
+        // Create a MessageEvent Package and add it to the message
+        MessageEvent messageEvent = new MessageEvent();
+        messageEvent.setCancelled(true);
+        messageEvent.setPacketID(packetID);
+        msg.addExtension(messageEvent);
+        // Send the packet
+        con.sendPacket(msg);
+    }
+
+    public void destroy() {
+        if (con != null) {
+            con.removePacketListener(packetListener);
+        }
+    }
+
+    protected void finalize() throws Throwable {
+        destroy();
+        super.finalize();
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/MessageEventNotificationListener.java b/src/org/jivesoftware/smackx/MessageEventNotificationListener.java
new file mode 100644
index 0000000..335dae2
--- /dev/null
+++ b/src/org/jivesoftware/smackx/MessageEventNotificationListener.java
@@ -0,0 +1,74 @@
+/**
+ * $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.smackx;
+
+/**
+ *
+ * A listener that is fired anytime a message event notification is received.
+ * Message event notifications are received as a consequence of the request
+ * to receive notifications when sending a message.
+ *
+ * @author Gaston Dombiak
+ */
+public interface MessageEventNotificationListener {
+
+    /**
+     * Called when a notification of message delivered is received.
+     *  
+     * @param from the user that sent the notification.
+     * @param packetID the id of the message that was sent.
+     */
+    public void deliveredNotification(String from, String packetID);
+
+    /**
+     * Called when a notification of message displayed is received.
+     *  
+     * @param from the user that sent the notification.
+     * @param packetID the id of the message that was sent.
+     */
+    public void displayedNotification(String from, String packetID);
+
+    /**
+     * Called when a notification that the receiver of the message is composing a reply is 
+     * received.
+     *  
+     * @param from the user that sent the notification.
+     * @param packetID the id of the message that was sent.
+     */
+    public void composingNotification(String from, String packetID);
+
+    /**
+     * Called when a notification that the receiver of the message is offline is received.
+     *  
+     * @param from the user that sent the notification.
+     * @param packetID the id of the message that was sent.
+     */
+    public void offlineNotification(String from, String packetID);
+
+    /**
+     * Called when a notification that the receiver of the message cancelled the reply 
+     * is received.
+     *  
+     * @param from the user that sent the notification.
+     * @param packetID the id of the message that was sent.
+     */
+    public void cancelledNotification(String from, String packetID);
+}
diff --git a/src/org/jivesoftware/smackx/MessageEventRequestListener.java b/src/org/jivesoftware/smackx/MessageEventRequestListener.java
new file mode 100644
index 0000000..86e0808
--- /dev/null
+++ b/src/org/jivesoftware/smackx/MessageEventRequestListener.java
@@ -0,0 +1,86 @@
+/**
+ * $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.smackx;
+
+/**
+ *
+ * A listener that is fired anytime a message event request is received.
+ * Message event requests are received when the received message includes an extension 
+ * like this:
+ * 
+ * <pre>
+ * &lt;x xmlns='jabber:x:event'&gt;
+ *  &lt;offline/&gt;
+ *  &lt;delivered/&gt;
+ *  &lt;composing/&gt;
+ * &lt;/x&gt;
+ * </pre>
+ * 
+ * In this example you can see that the sender of the message requests to be notified
+ * when the user couldn't receive the message because he/she is offline, the message 
+ * was delivered or when the receiver of the message is composing a reply. 
+ *
+ * @author Gaston Dombiak
+ */
+public interface MessageEventRequestListener {
+
+    /**
+     * Called when a request for message delivered notification is received.
+     *  
+     * @param from the user that sent the notification.
+     * @param packetID the id of the message that was sent.
+     * @param messageEventManager the messageEventManager that fired the listener.
+     */
+    public void deliveredNotificationRequested(String from, String packetID,
+            MessageEventManager messageEventManager);
+
+    /**
+     * Called when a request for message displayed notification is received.
+     *  
+     * @param from the user that sent the notification.
+     * @param packetID the id of the message that was sent.
+     * @param messageEventManager the messageEventManager that fired the listener.
+     */
+    public void displayedNotificationRequested(String from, String packetID,
+            MessageEventManager messageEventManager);
+
+    /**
+     * Called when a request that the receiver of the message is composing a reply notification is 
+     * received.
+     *  
+     * @param from the user that sent the notification.
+     * @param packetID the id of the message that was sent.
+     * @param messageEventManager the messageEventManager that fired the listener.
+     */
+    public void composingNotificationRequested(String from, String packetID,
+                MessageEventManager messageEventManager);
+
+    /**
+     * Called when a request that the receiver of the message is offline is received.
+     *  
+     * @param from the user that sent the notification.
+     * @param packetID the id of the message that was sent.
+     * @param messageEventManager the messageEventManager that fired the listener.
+     */
+    public void offlineNotificationRequested(String from, String packetID,
+            MessageEventManager messageEventManager);
+
+}
diff --git a/src/org/jivesoftware/smackx/MultipleRecipientInfo.java b/src/org/jivesoftware/smackx/MultipleRecipientInfo.java
new file mode 100644
index 0000000..8738319
--- /dev/null
+++ b/src/org/jivesoftware/smackx/MultipleRecipientInfo.java
@@ -0,0 +1,98 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 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.smackx;
+
+import org.jivesoftware.smackx.packet.MultipleAddresses;
+
+import java.util.List;
+
+/**
+ * MultipleRecipientInfo keeps information about the multiple recipients extension included
+ * in a received packet. Among the information we can find the list of TO and CC addresses.
+ *
+ * @author Gaston Dombiak
+ */
+public class MultipleRecipientInfo {
+
+    MultipleAddresses extension;
+
+    MultipleRecipientInfo(MultipleAddresses extension) {
+        this.extension = extension;
+    }
+
+    /**
+     * Returns the list of {@link org.jivesoftware.smackx.packet.MultipleAddresses.Address}
+     * that were the primary recipients of the packet.
+     *
+     * @return list of primary recipients of the packet.
+     */
+    public List<MultipleAddresses.Address> getTOAddresses() {
+        return extension.getAddressesOfType(MultipleAddresses.TO);
+    }
+
+    /**
+     * Returns the list of {@link org.jivesoftware.smackx.packet.MultipleAddresses.Address}
+     * that were the secondary recipients of the packet.
+     *
+     * @return list of secondary recipients of the packet.
+     */
+    public List<MultipleAddresses.Address> getCCAddresses() {
+        return extension.getAddressesOfType(MultipleAddresses.CC);
+    }
+
+    /**
+     * Returns the JID of a MUC room to which responses should be sent or <tt>null</tt>  if
+     * no specific address was provided. When no specific address was provided then the reply
+     * can be sent to any or all recipients. Otherwise, the user should join the specified room
+     * and send the reply to the room.
+     *
+     * @return the JID of a MUC room to which responses should be sent or <tt>null</tt>  if
+     *         no specific address was provided.
+     */
+    public String getReplyRoom() {
+        List<MultipleAddresses.Address> replyRoom = extension.getAddressesOfType(MultipleAddresses.REPLY_ROOM);
+        return replyRoom.isEmpty() ? null : ((MultipleAddresses.Address) replyRoom.get(0)).getJid();
+    }
+
+    /**
+     * Returns true if the received packet should not be replied. Use
+     * {@link MultipleRecipientManager#reply(org.jivesoftware.smack.Connection, org.jivesoftware.smack.packet.Message, org.jivesoftware.smack.packet.Message)}
+     * to send replies. 
+     *
+     * @return true if the received packet should not be replied.
+     */
+    public boolean shouldNotReply() {
+        return !extension.getAddressesOfType(MultipleAddresses.NO_REPLY).isEmpty();
+    }
+
+    /**
+     * Returns the address to which all replies are requested to be sent or <tt>null</tt> if
+     * no specific address was provided. When no specific address was provided then the reply
+     * can be sent to any or all recipients.
+     *
+     * @return the address to which all replies are requested to be sent or <tt>null</tt> if
+     *         no specific address was provided.
+     */
+    public MultipleAddresses.Address getReplyAddress() {
+        List<MultipleAddresses.Address> replyTo = extension.getAddressesOfType(MultipleAddresses.REPLY_TO);
+        return replyTo.isEmpty() ? null : (MultipleAddresses.Address) replyTo.get(0);
+    }
+}
diff --git a/src/org/jivesoftware/smackx/MultipleRecipientManager.java b/src/org/jivesoftware/smackx/MultipleRecipientManager.java
new file mode 100644
index 0000000..83aface
--- /dev/null
+++ b/src/org/jivesoftware/smackx/MultipleRecipientManager.java
@@ -0,0 +1,353 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 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.smackx;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.Cache;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.packet.MultipleAddresses;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A MultipleRecipientManager allows to send packets to multiple recipients by making use of
+ * <a href="http://www.jabber.org/jeps/jep-0033.html">JEP-33: Extended Stanza Addressing</a>.
+ * It also allows to send replies to packets that were sent to multiple recipients.
+ *
+ * @author Gaston Dombiak
+ */
+public class MultipleRecipientManager {
+
+    /**
+     * Create a cache to hold the 100 most recently accessed elements for a period of
+     * 24 hours.
+     */
+    private static Cache<String, String> services = new Cache<String, String>(100, 24 * 60 * 60 * 1000);
+
+    /**
+     * Sends the specified packet to the list of specified recipients using the
+     * specified connection. If the server has support for JEP-33 then only one
+     * packet is going to be sent to the server with the multiple recipient instructions.
+     * However, if JEP-33 is not supported by the server then the client is going to send
+     * the packet to each recipient.
+     *
+     * @param connection the connection to use to send the packet.
+     * @param packet     the packet to send to the list of recipients.
+     * @param to         the list of JIDs to include in the TO list or <tt>null</tt> if no TO
+     *                   list exists.
+     * @param cc         the list of JIDs to include in the CC list or <tt>null</tt> if no CC
+     *                   list exists.
+     * @param bcc        the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC
+     *                   list exists.
+     * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and
+     *                       some JEP-33 specific features were requested.
+     */
+    public static void send(Connection connection, Packet packet, List<String> to, List<String> cc, List<String> bcc)
+            throws XMPPException {
+        send(connection, packet, to, cc, bcc, null, null, false);
+    }
+
+    /**
+     * Sends the specified packet to the list of specified recipients using the
+     * specified connection. If the server has support for JEP-33 then only one
+     * packet is going to be sent to the server with the multiple recipient instructions.
+     * However, if JEP-33 is not supported by the server then the client is going to send
+     * the packet to each recipient.
+     *
+     * @param connection the connection to use to send the packet.
+     * @param packet     the packet to send to the list of recipients.
+     * @param to         the list of JIDs to include in the TO list or <tt>null</tt> if no TO
+     *                   list exists.
+     * @param cc         the list of JIDs to include in the CC list or <tt>null</tt> if no CC
+     *                   list exists.
+     * @param bcc        the list of JIDs to include in the BCC list or <tt>null</tt> if no BCC
+     *                   list exists.
+     * @param replyTo    address to which all replies are requested to be sent or <tt>null</tt>
+     *                   indicating that they can reply to any address.
+     * @param replyRoom  JID of a MUC room to which responses should be sent or <tt>null</tt>
+     *                   indicating that they can reply to any address.
+     * @param noReply    true means that receivers should not reply to the message.
+     * @throws XMPPException if server does not support JEP-33: Extended Stanza Addressing and
+     *                       some JEP-33 specific features were requested.
+     */
+    public static void send(Connection connection, Packet packet, List<String> to, List<String> cc, List<String> bcc,
+            String replyTo, String replyRoom, boolean noReply) throws XMPPException {
+        String serviceAddress = getMultipleRecipienServiceAddress(connection);
+        if (serviceAddress != null) {
+            // Send packet to target users using multiple recipient service provided by the server
+            sendThroughService(connection, packet, to, cc, bcc, replyTo, replyRoom, noReply,
+                    serviceAddress);
+        }
+        else {
+            // Server does not support JEP-33 so try to send the packet to each recipient
+            if (noReply || (replyTo != null && replyTo.trim().length() > 0) ||
+                    (replyRoom != null && replyRoom.trim().length() > 0)) {
+                // Some specified JEP-33 features were requested so throw an exception alerting
+                // the user that this features are not available
+                throw new XMPPException("Extended Stanza Addressing not supported by server");
+            }
+            // Send the packet to each individual recipient
+            sendToIndividualRecipients(connection, packet, to, cc, bcc);
+        }
+    }
+
+    /**
+     * Sends a reply to a previously received packet that was sent to multiple recipients. Before
+     * attempting to send the reply message some checkings are performed. If any of those checkings
+     * fail then an XMPPException is going to be thrown with the specific error detail.
+     *
+     * @param connection the connection to use to send the reply.
+     * @param original   the previously received packet that was sent to multiple recipients.
+     * @param reply      the new message to send as a reply.
+     * @throws XMPPException if the original message was not sent to multiple recipients, or the
+     *                       original message cannot be replied or reply should be sent to a room.
+     */
+    public static void reply(Connection connection, Message original, Message reply)
+            throws XMPPException {
+        MultipleRecipientInfo info = getMultipleRecipientInfo(original);
+        if (info == null) {
+            throw new XMPPException("Original message does not contain multiple recipient info");
+        }
+        if (info.shouldNotReply()) {
+            throw new XMPPException("Original message should not be replied");
+        }
+        if (info.getReplyRoom() != null) {
+            throw new XMPPException("Reply should be sent through a room");
+        }
+        // Any <thread/> element from the initial message MUST be copied into the reply.
+        if (original.getThread() != null) {
+            reply.setThread(original.getThread());
+        }
+        MultipleAddresses.Address replyAddress = info.getReplyAddress();
+        if (replyAddress != null && replyAddress.getJid() != null) {
+            // Send reply to the reply_to address
+            reply.setTo(replyAddress.getJid());
+            connection.sendPacket(reply);
+        }
+        else {
+            // Send reply to multiple recipients
+            List<String> to = new ArrayList<String>();
+            List<String> cc = new ArrayList<String>();
+            for (Iterator<MultipleAddresses.Address> it = info.getTOAddresses().iterator(); it.hasNext();) {
+                String jid = it.next().getJid();
+                to.add(jid);
+            }
+            for (Iterator<MultipleAddresses.Address> it = info.getCCAddresses().iterator(); it.hasNext();) {
+                String jid = it.next().getJid();
+                cc.add(jid);
+            }
+            // Add original sender as a 'to' address (if not already present)
+            if (!to.contains(original.getFrom()) && !cc.contains(original.getFrom())) {
+                to.add(original.getFrom());
+            }
+            // Remove the sender from the TO/CC list (try with bare JID too)
+            String from = connection.getUser();
+            if (!to.remove(from) && !cc.remove(from)) {
+                String bareJID = StringUtils.parseBareAddress(from);
+                to.remove(bareJID);
+                cc.remove(bareJID);
+            }
+
+            String serviceAddress = getMultipleRecipienServiceAddress(connection);
+            if (serviceAddress != null) {
+                // Send packet to target users using multiple recipient service provided by the server
+                sendThroughService(connection, reply, to, cc, null, null, null, false,
+                        serviceAddress);
+            }
+            else {
+                // Server does not support JEP-33 so try to send the packet to each recipient
+                sendToIndividualRecipients(connection, reply, to, cc, null);
+            }
+        }
+    }
+
+    /**
+     * Returns the {@link MultipleRecipientInfo} contained in the specified packet or
+     * <tt>null</tt> if none was found. Only packets sent to multiple recipients will
+     * contain such information.
+     *
+     * @param packet the packet to check.
+     * @return the MultipleRecipientInfo contained in the specified packet or <tt>null</tt>
+     *         if none was found.
+     */
+    public static MultipleRecipientInfo getMultipleRecipientInfo(Packet packet) {
+        MultipleAddresses extension = (MultipleAddresses) packet
+                .getExtension("addresses", "http://jabber.org/protocol/address");
+        return extension == null ? null : new MultipleRecipientInfo(extension);
+    }
+
+    private static void sendToIndividualRecipients(Connection connection, Packet packet,
+            List<String> to, List<String> cc, List<String> bcc) {
+        if (to != null) {
+            for (Iterator<String> it = to.iterator(); it.hasNext();) {
+                String jid = it.next();
+                packet.setTo(jid);
+                connection.sendPacket(new PacketCopy(packet.toXML()));
+            }
+        }
+        if (cc != null) {
+            for (Iterator<String> it = cc.iterator(); it.hasNext();) {
+                String jid = it.next();
+                packet.setTo(jid);
+                connection.sendPacket(new PacketCopy(packet.toXML()));
+            }
+        }
+        if (bcc != null) {
+            for (Iterator<String> it = bcc.iterator(); it.hasNext();) {
+                String jid = it.next();
+                packet.setTo(jid);
+                connection.sendPacket(new PacketCopy(packet.toXML()));
+            }
+        }
+    }
+
+    private static void sendThroughService(Connection connection, Packet packet, List<String> to,
+            List<String> cc, List<String> bcc, String replyTo, String replyRoom, boolean noReply,
+            String serviceAddress) {
+        // Create multiple recipient extension
+        MultipleAddresses multipleAddresses = new MultipleAddresses();
+        if (to != null) {
+            for (Iterator<String> it = to.iterator(); it.hasNext();) {
+                String jid = it.next();
+                multipleAddresses.addAddress(MultipleAddresses.TO, jid, null, null, false, null);
+            }
+        }
+        if (cc != null) {
+            for (Iterator<String> it = cc.iterator(); it.hasNext();) {
+                String jid = it.next();
+                multipleAddresses.addAddress(MultipleAddresses.CC, jid, null, null, false, null);
+            }
+        }
+        if (bcc != null) {
+            for (Iterator<String> it = bcc.iterator(); it.hasNext();) {
+                String jid = it.next();
+                multipleAddresses.addAddress(MultipleAddresses.BCC, jid, null, null, false, null);
+            }
+        }
+        if (noReply) {
+            multipleAddresses.setNoReply();
+        }
+        else {
+            if (replyTo != null && replyTo.trim().length() > 0) {
+                multipleAddresses
+                        .addAddress(MultipleAddresses.REPLY_TO, replyTo, null, null, false, null);
+            }
+            if (replyRoom != null && replyRoom.trim().length() > 0) {
+                multipleAddresses.addAddress(MultipleAddresses.REPLY_ROOM, replyRoom, null, null,
+                        false, null);
+            }
+        }
+        // Set the multiple recipient service address as the target address
+        packet.setTo(serviceAddress);
+        // Add extension to packet
+        packet.addExtension(multipleAddresses);
+        // Send the packet
+        connection.sendPacket(packet);
+    }
+
+    /**
+     * Returns the address of the multiple recipients service. To obtain such address service
+     * discovery is going to be used on the connected server and if none was found then another
+     * attempt will be tried on the server items. The discovered information is going to be
+     * cached for 24 hours.
+     *
+     * @param connection the connection to use for disco. The connected server is going to be
+     *                   queried.
+     * @return the address of the multiple recipients service or <tt>null</tt> if none was found.
+     */
+    private static String getMultipleRecipienServiceAddress(Connection connection) {
+        String serviceName = connection.getServiceName();
+        String serviceAddress = (String) services.get(serviceName);
+        if (serviceAddress == null) {
+            synchronized (services) {
+                serviceAddress = (String) services.get(serviceName);
+                if (serviceAddress == null) {
+
+                    // Send the disco packet to the server itself
+                    try {
+                        DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection)
+                                .discoverInfo(serviceName);
+                        // Check if the server supports JEP-33
+                        if (info.containsFeature("http://jabber.org/protocol/address")) {
+                            serviceAddress = serviceName;
+                        }
+                        else {
+                            // Get the disco items and send the disco packet to each server item
+                            DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection)
+                                    .discoverItems(serviceName);
+                            for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
+                                DiscoverItems.Item item = it.next();
+                                info = ServiceDiscoveryManager.getInstanceFor(connection)
+                                        .discoverInfo(item.getEntityID(), item.getNode());
+                                if (info.containsFeature("http://jabber.org/protocol/address")) {
+                                    serviceAddress = serviceName;
+                                    break;
+                                }
+                            }
+
+                        }
+                        // Cache the discovered information
+                        services.put(serviceName, serviceAddress == null ? "" : serviceAddress);
+                    }
+                    catch (XMPPException e) {
+                        e.printStackTrace();
+                    }
+                }
+            }
+        }
+
+        return "".equals(serviceAddress) ? null : serviceAddress;
+    }
+
+    /**
+     * Packet that holds the XML stanza to send. This class is useful when the same packet
+     * is needed to be sent to different recipients. Since using the same packet is not possible
+     * (i.e. cannot change the TO address of a queues packet to be sent) then this class was
+     * created to keep the XML stanza to send.
+     */
+    private static class PacketCopy extends Packet {
+
+        private String text;
+
+        /**
+         * Create a copy of a packet with the text to send. The passed text must be a valid text to
+         * send to the server, no validation will be done on the passed text.
+         *
+         * @param text the whole text of the packet to send
+         */
+        public PacketCopy(String text) {
+            this.text = text;
+        }
+
+        public String toXML() {
+            return text;
+        }
+
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/NodeInformationProvider.java b/src/org/jivesoftware/smackx/NodeInformationProvider.java
new file mode 100644
index 0000000..27ee53a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/NodeInformationProvider.java
@@ -0,0 +1,75 @@
+/**
+ * $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.smackx;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+
+import java.util.List;
+
+
+/**
+ * The NodeInformationProvider is responsible for providing supported indentities, features
+ * and hosted items (i.e. DiscoverItems.Item) about a given node. This information will be
+ * requested each time this XMPPP client receives a disco info or items requests on the
+ * given node. each time this XMPPP client receives a disco info or items requests on the
+ * given node.
+ *
+ * @author Gaston Dombiak
+ */
+public interface NodeInformationProvider {
+
+    /**
+     * Returns a list of the Items {@link org.jivesoftware.smackx.packet.DiscoverItems.Item}
+     * defined in the node. For example, the MUC protocol specifies that an XMPP client should 
+     * answer an Item for each joined room when asked for the rooms where the use has joined.
+     *  
+     * @return a list of the Items defined in the node.
+     */
+    List<DiscoverItems.Item> getNodeItems();
+
+    /**
+     * Returns a list of the features defined in the node. For
+     * example, the entity caps protocol specifies that an XMPP client
+     * should answer with each feature supported by the client version
+     * or extension.
+     *
+     * @return a list of the feature strings defined in the node.
+     */
+    List<String> getNodeFeatures();
+
+    /**
+     * Returns a list of the indentites defined in the node. For
+     * example, the x-command protocol must provide an identity of
+     * category automation and type command-node for each command.
+     *
+     * @return a list of the Identities defined in the node.
+     */
+    List<DiscoverInfo.Identity> getNodeIdentities();
+
+    /**
+     * Returns a list of the packet extensions defined in the node.
+     *
+     * @return a list of the packet extensions defined in the node.
+     */
+    List<PacketExtension> getNodePacketExtensions();
+}
diff --git a/src/org/jivesoftware/smackx/OfflineMessageHeader.java b/src/org/jivesoftware/smackx/OfflineMessageHeader.java
new file mode 100644
index 0000000..55fd149
--- /dev/null
+++ b/src/org/jivesoftware/smackx/OfflineMessageHeader.java
@@ -0,0 +1,85 @@
+/**
+ * $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.smackx;
+
+import org.jivesoftware.smackx.packet.DiscoverItems;
+
+/**
+ * The OfflineMessageHeader holds header information of an offline message. The header
+ * information was retrieved using the {@link OfflineMessageManager} class.<p>
+ *
+ * Each offline message is identified by the target user of the offline message and a unique stamp.
+ * Use {@link OfflineMessageManager#getMessages(java.util.List)} to retrieve the whole message.
+ *
+ * @author Gaston Dombiak
+ */
+public class OfflineMessageHeader {
+    /**
+     * Bare JID of the user that was offline when the message was sent.
+     */
+    private String user;
+    /**
+     * Full JID of the user that sent the message.
+     */
+    private String jid;
+    /**
+     * Stamp that uniquely identifies the offline message. This stamp will be used for
+     * getting the specific message or delete it. The stamp may be of the form UTC timestamps
+     * but it is not required to have that format.
+     */
+    private String stamp;
+
+    public OfflineMessageHeader(DiscoverItems.Item item) {
+        super();
+        user = item.getEntityID();
+        jid = item.getName();
+        stamp = item.getNode();
+    }
+
+    /**
+     * Returns the bare JID of the user that was offline when the message was sent.
+     *
+     * @return the bare JID of the user that was offline when the message was sent.
+     */
+    public String getUser() {
+        return user;
+    }
+
+    /**
+     * Returns the full JID of the user that sent the message.
+     *
+     * @return the full JID of the user that sent the message.
+     */
+    public String getJid() {
+        return jid;
+    }
+
+    /**
+     * Returns the stamp that uniquely identifies the offline message. This stamp will
+     * be used for getting the specific message or delete it. The stamp may be of the
+     * form UTC timestamps but it is not required to have that format.
+     *
+     * @return the stamp that uniquely identifies the offline message.
+     */
+    public String getStamp() {
+        return stamp;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/OfflineMessageManager.java b/src/org/jivesoftware/smackx/OfflineMessageManager.java
new file mode 100644
index 0000000..dbe889d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/OfflineMessageManager.java
@@ -0,0 +1,284 @@
+/**
+ * $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.smackx;
+
+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.*;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.packet.OfflineMessageInfo;
+import org.jivesoftware.smackx.packet.OfflineMessageRequest;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * The OfflineMessageManager helps manage offline messages even before the user has sent an
+ * available presence. When a user asks for his offline messages before sending an available
+ * presence then the server will not send a flood with all the offline messages when the user
+ * becomes online. The server will not send a flood with all the offline messages to the session
+ * that made the offline messages request or to any other session used by the user that becomes
+ * online.<p>
+ *
+ * Once the session that made the offline messages request has been closed and the user becomes
+ * offline in all the resources then the server will resume storing the messages offline and will
+ * send all the offline messages to the user when he becomes online. Therefore, the server will
+ * flood the user when he becomes online unless the user uses this class to manage his offline
+ * messages.
+ *
+ * @author Gaston Dombiak
+ */
+public class OfflineMessageManager {
+
+    private final static String namespace = "http://jabber.org/protocol/offline";
+
+    private Connection connection;
+
+    private PacketFilter packetFilter;
+
+    public OfflineMessageManager(Connection connection) {
+        this.connection = connection;
+        packetFilter =
+                new AndFilter(new PacketExtensionFilter("offline", namespace),
+                        new PacketTypeFilter(Message.class));
+    }
+
+    /**
+     * Returns true if the server supports Flexible Offline Message Retrieval. When the server
+     * supports Flexible Offline Message Retrieval it is possible to get the header of the offline
+     * messages, get specific messages, delete specific messages, etc.
+     *
+     * @return a boolean indicating if the server supports Flexible Offline Message Retrieval.
+     * @throws XMPPException If the user is not allowed to make this request.
+     */
+    public boolean supportsFlexibleRetrieval() throws XMPPException {
+        DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(connection.getServiceName());
+        return info.containsFeature(namespace);
+    }
+
+    /**
+     * Returns the number of offline messages for the user of the connection.
+     *
+     * @return the number of offline messages for the user of the connection.
+     * @throws XMPPException If the user is not allowed to make this request or the server does
+     *                       not support offline message retrieval.
+     */
+    public int getMessageCount() throws XMPPException {
+        DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(null,
+                namespace);
+        Form extendedInfo = Form.getFormFrom(info);
+        if (extendedInfo != null) {
+            String value = extendedInfo.getField("number_of_messages").getValues().next();
+            return Integer.parseInt(value);
+        }
+        return 0;
+    }
+
+    /**
+     * Returns an iterator on <tt>OfflineMessageHeader</tt> that keep information about the
+     * offline message. The OfflineMessageHeader includes a stamp that could be used to retrieve
+     * the complete message or delete the specific message.
+     *
+     * @return an iterator on <tt>OfflineMessageHeader</tt> that keep information about the offline
+     *         message.
+     * @throws XMPPException If the user is not allowed to make this request or the server does
+     *                       not support offline message retrieval.
+     */
+    public Iterator<OfflineMessageHeader> getHeaders() throws XMPPException {
+        List<OfflineMessageHeader> answer = new ArrayList<OfflineMessageHeader>();
+        DiscoverItems items = ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(
+                null, namespace);
+        for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
+            DiscoverItems.Item item = it.next();
+            answer.add(new OfflineMessageHeader(item));
+        }
+        return answer.iterator();
+    }
+
+    /**
+     * Returns an Iterator with the offline <tt>Messages</tt> whose stamp matches the specified
+     * request. The request will include the list of stamps that uniquely identifies
+     * the offline messages to retrieve. The returned offline messages will not be deleted
+     * from the server. Use {@link #deleteMessages(java.util.List)} to delete the messages.
+     *
+     * @param nodes the list of stamps that uniquely identifies offline message.
+     * @return an Iterator with the offline <tt>Messages</tt> that were received as part of
+     *         this request.
+     * @throws XMPPException If the user is not allowed to make this request or the server does
+     *                       not support offline message retrieval.
+     */
+    public Iterator<Message> getMessages(final List<String> nodes) throws XMPPException {
+        List<Message> messages = new ArrayList<Message>();
+        OfflineMessageRequest request = new OfflineMessageRequest();
+        for (String node : nodes) {
+            OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node);
+            item.setAction("view");
+            request.addItem(item);
+        }
+        // Filter packets looking for an answer from the server.
+        PacketFilter responseFilter = new PacketIDFilter(request.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Filter offline messages that were requested by this request
+        PacketFilter messageFilter = new AndFilter(packetFilter, new PacketFilter() {
+            public boolean accept(Packet packet) {
+                OfflineMessageInfo info = (OfflineMessageInfo) packet.getExtension("offline",
+                        namespace);
+                return nodes.contains(info.getNode());
+            }
+        });
+        PacketCollector messageCollector = connection.createPacketCollector(messageFilter);
+        // Send the retrieval request to the server.
+        connection.sendPacket(request);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        } else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+
+        // Collect the received offline messages
+        Message message = (Message) messageCollector.nextResult(
+                SmackConfiguration.getPacketReplyTimeout());
+        while (message != null) {
+            messages.add(message);
+            message =
+                    (Message) messageCollector.nextResult(
+                            SmackConfiguration.getPacketReplyTimeout());
+        }
+        // Stop queuing offline messages
+        messageCollector.cancel();
+        return messages.iterator();
+    }
+
+    /**
+     * Returns an Iterator with all the offline <tt>Messages</tt> of the user. The returned offline
+     * messages will not be deleted from the server. Use {@link #deleteMessages(java.util.List)}
+     * to delete the messages.
+     *
+     * @return an Iterator with all the offline <tt>Messages</tt> of the user.
+     * @throws XMPPException If the user is not allowed to make this request or the server does
+     *                       not support offline message retrieval.
+     */
+    public Iterator<Message> getMessages() throws XMPPException {
+        List<Message> messages = new ArrayList<Message>();
+        OfflineMessageRequest request = new OfflineMessageRequest();
+        request.setFetch(true);
+        // Filter packets looking for an answer from the server.
+        PacketFilter responseFilter = new PacketIDFilter(request.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Filter offline messages that were requested by this request
+        PacketCollector messageCollector = connection.createPacketCollector(packetFilter);
+        // Send the retrieval request to the server.
+        connection.sendPacket(request);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        } else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+
+        // Collect the received offline messages
+        Message message = (Message) messageCollector.nextResult(
+                SmackConfiguration.getPacketReplyTimeout());
+        while (message != null) {
+            messages.add(message);
+            message =
+                    (Message) messageCollector.nextResult(
+                            SmackConfiguration.getPacketReplyTimeout());
+        }
+        // Stop queuing offline messages
+        messageCollector.cancel();
+        return messages.iterator();
+    }
+
+    /**
+     * Deletes the specified list of offline messages. The request will include the list of
+     * stamps that uniquely identifies the offline messages to delete.
+     *
+     * @param nodes the list of stamps that uniquely identifies offline message.
+     * @throws XMPPException If the user is not allowed to make this request or the server does
+     *                       not support offline message retrieval.
+     */
+    public void deleteMessages(List<String> nodes) throws XMPPException {
+        OfflineMessageRequest request = new OfflineMessageRequest();
+        for (String node : nodes) {
+            OfflineMessageRequest.Item item = new OfflineMessageRequest.Item(node);
+            item.setAction("remove");
+            request.addItem(item);
+        }
+        // Filter packets looking for an answer from the server.
+        PacketFilter responseFilter = new PacketIDFilter(request.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the deletion request to the server.
+        connection.sendPacket(request);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        } else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+    }
+
+    /**
+     * Deletes all offline messages of the user.
+     *
+     * @throws XMPPException If the user is not allowed to make this request or the server does
+     *                       not support offline message retrieval.
+     */
+    public void deleteMessages() throws XMPPException {
+        OfflineMessageRequest request = new OfflineMessageRequest();
+        request.setPurge(true);
+        // Filter packets looking for an answer from the server.
+        PacketFilter responseFilter = new PacketIDFilter(request.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the deletion request to the server.
+        connection.sendPacket(request);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        } else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/PEPListener.java b/src/org/jivesoftware/smackx/PEPListener.java
new file mode 100644
index 0000000..1d39484
--- /dev/null
+++ b/src/org/jivesoftware/smackx/PEPListener.java
@@ -0,0 +1,42 @@
+/**
+ * $RCSfile: PEPListener.java,v $
+ * $Revision: 1.1 $
+ * $Date: 2007/11/03 00:14:32 $
+ *
+ * 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.smackx;
+
+import org.jivesoftware.smackx.packet.PEPEvent;
+
+
+/**
+ *
+ * A listener that is fired anytime a PEP event message is received.
+ *
+ * @author Jeff Williams
+ */
+public interface PEPListener {
+
+    /**
+     * Called when PEP events are received as part of a presence subscribe or message filter.
+     *  
+     * @param from the user that sent the entries.
+     * @param event the event contained in the message.
+     */
+    public void eventReceived(String from, PEPEvent event);
+
+}
diff --git a/src/org/jivesoftware/smackx/PEPManager.java b/src/org/jivesoftware/smackx/PEPManager.java
new file mode 100644
index 0000000..857f1c4
--- /dev/null
+++ b/src/org/jivesoftware/smackx/PEPManager.java
@@ -0,0 +1,160 @@
+/**
+ * $RCSfile: PEPManager.java,v $
+ * $Revision: 1.4 $
+ * $Date: 2007/11/06 21:43:40 $
+ *
+ * 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.smackx;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.filter.PacketExtensionFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.IQ.Type;
+import org.jivesoftware.smackx.packet.PEPEvent;
+import org.jivesoftware.smackx.packet.PEPItem;
+import org.jivesoftware.smackx.packet.PEPPubSub;
+
+/**
+ *
+ * Manages Personal Event Publishing (XEP-163). A PEPManager provides a high level access to
+ * pubsub personal events. It also provides an easy way
+ * to hook up custom logic when events are received from another XMPP client through PEPListeners.
+ * 
+ * Use example:
+ * 
+ * <pre>
+ *   PEPManager pepManager = new PEPManager(smackConnection);
+ *   pepManager.addPEPListener(new PEPListener() {
+ *       public void eventReceived(String inFrom, PEPEvent inEvent) {
+ *           LOGGER.debug("Event received: " + inEvent);
+ *       }
+ *   });
+ *
+ *   PEPProvider pepProvider = new PEPProvider();
+ *   pepProvider.registerPEPParserExtension("http://jabber.org/protocol/tune", new TuneProvider());
+ *   ProviderManager.getInstance().addExtensionProvider("event", "http://jabber.org/protocol/pubsub#event", pepProvider);
+ *   
+ *   Tune tune = new Tune("jeff", "1", "CD", "My Title", "My Track");
+ *   pepManager.publish(tune);
+ * </pre>
+ * 
+ * @author Jeff Williams
+ */
+public class PEPManager {
+
+    private List<PEPListener> pepListeners = new ArrayList<PEPListener>();
+
+    private Connection connection;
+
+    private PacketFilter packetFilter = new PacketExtensionFilter("event", "http://jabber.org/protocol/pubsub#event");
+    private PacketListener packetListener;
+
+    /**
+     * Creates a new PEP exchange manager.
+     *
+     * @param connection a Connection which is used to send and receive messages.
+     */
+    public PEPManager(Connection connection) {
+        this.connection = connection;
+        init();
+    }
+
+    /**
+     * Adds a listener to PEPs. The listener will be fired anytime PEP events
+     * are received from remote XMPP clients.
+     *
+     * @param pepListener a roster exchange listener.
+     */
+    public void addPEPListener(PEPListener pepListener) {
+        synchronized (pepListeners) {
+            if (!pepListeners.contains(pepListener)) {
+                pepListeners.add(pepListener);
+            }
+        }
+    }
+
+    /**
+     * Removes a listener from PEP events.
+     *
+     * @param pepListener a roster exchange listener.
+     */
+    public void removePEPListener(PEPListener pepListener) {
+        synchronized (pepListeners) {
+            pepListeners.remove(pepListener);
+        }
+    }
+
+    /**
+     * Publish an event.
+     * 
+     * @param item the item to publish.
+     */
+    public void publish(PEPItem item) {
+        // Create a new message to publish the event.
+        PEPPubSub pubSub = new PEPPubSub(item);
+        pubSub.setType(Type.SET);
+        //pubSub.setFrom(connection.getUser());
+ 
+        // Send the message that contains the roster
+        connection.sendPacket(pubSub);
+    }
+
+    /**
+     * Fires roster exchange listeners.
+     */
+    private void firePEPListeners(String from, PEPEvent event) {
+        PEPListener[] listeners = null;
+        synchronized (pepListeners) {
+            listeners = new PEPListener[pepListeners.size()];
+            pepListeners.toArray(listeners);
+        }
+        for (int i = 0; i < listeners.length; i++) {
+            listeners[i].eventReceived(from, event);
+        }
+    }
+
+    private void init() {
+        // Listens for all roster exchange packets and fire the roster exchange listeners.
+        packetListener = new PacketListener() {
+            public void processPacket(Packet packet) {
+                Message message = (Message) packet;
+                PEPEvent event = (PEPEvent) message.getExtension("event", "http://jabber.org/protocol/pubsub#event");
+                // Fire event for roster exchange listeners
+                firePEPListeners(message.getFrom(), event);
+            };
+
+        };
+        connection.addPacketListener(packetListener, packetFilter);
+    }
+
+    public void destroy() {
+        if (connection != null)
+            connection.removePacketListener(packetListener);
+
+    }
+
+    protected void finalize() throws Throwable {
+        destroy();
+        super.finalize();
+    }
+}
diff --git a/src/org/jivesoftware/smackx/PrivateDataManager.java b/src/org/jivesoftware/smackx/PrivateDataManager.java
new file mode 100644
index 0000000..c6440bc
--- /dev/null
+++ b/src/org/jivesoftware/smackx/PrivateDataManager.java
@@ -0,0 +1,359 @@
+/**
+ * $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.smackx;
+
+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.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.packet.DefaultPrivateData;
+import org.jivesoftware.smackx.packet.PrivateData;
+import org.jivesoftware.smackx.provider.PrivateDataProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.Hashtable;
+import java.util.Map;
+
+/**
+ * Manages private data, which is a mechanism to allow users to store arbitrary XML
+ * data on an XMPP server. Each private data chunk is defined by a element name and
+ * XML namespace. Example private data:
+ *
+ * <pre>
+ * &lt;color xmlns="http://example.com/xmpp/color"&gt;
+ *     &lt;favorite&gt;blue&lt;/blue&gt;
+ *     &lt;leastFavorite&gt;puce&lt;/leastFavorite&gt;
+ * &lt;/color&gt;
+ * </pre>
+ *
+ * {@link PrivateDataProvider} instances are responsible for translating the XML into objects.
+ * If no PrivateDataProvider is registered for a given element name and namespace, then
+ * a {@link DefaultPrivateData} instance will be returned.<p>
+ *
+ * Warning: this is an non-standard protocol documented by
+ * <a href="http://www.jabber.org/jeps/jep-0049.html">JEP-49</a>. Because this is a
+ * non-standard protocol, it is subject to change.
+ *
+ * @author Matt Tucker
+ */
+public class PrivateDataManager {
+
+    /**
+     * Map of provider instances.
+     */
+    private static Map<String, PrivateDataProvider> privateDataProviders = new Hashtable<String, PrivateDataProvider>();
+
+    /**
+     * Returns the private data provider registered to the specified XML element name and namespace.
+     * For example, if a provider was registered to the element name "prefs" and the
+     * namespace "http://www.xmppclient.com/prefs", then the following packet would trigger
+     * the provider:
+     *
+     * <pre>
+     * &lt;iq type='result' to='joe@example.com' from='mary@example.com' id='time_1'&gt;
+     *     &lt;query xmlns='jabber:iq:private'&gt;
+     *         &lt;prefs xmlns='http://www.xmppclient.com/prefs'&gt;
+     *             &lt;value1&gt;ABC&lt;/value1&gt;
+     *             &lt;value2&gt;XYZ&lt;/value2&gt;
+     *         &lt;/prefs&gt;
+     *     &lt;/query&gt;
+     * &lt;/iq&gt;</pre>
+     *
+     * <p>Note: this method is generally only called by the internal Smack classes.
+     *
+     * @param elementName the XML element name.
+     * @param namespace the XML namespace.
+     * @return the PrivateData provider.
+     */
+    public static PrivateDataProvider getPrivateDataProvider(String elementName, String namespace) {
+        String key = getProviderKey(elementName, namespace);
+        return (PrivateDataProvider)privateDataProviders.get(key);
+    }
+
+    /**
+     * Adds a private data provider with the specified element name and name space. The provider
+     * will override any providers loaded through the classpath.
+     *
+     * @param elementName the XML element name.
+     * @param namespace the XML namespace.
+     * @param provider the private data provider.
+     */
+    public static void addPrivateDataProvider(String elementName, String namespace,
+            PrivateDataProvider provider)
+    {
+        String key = getProviderKey(elementName, namespace);
+        privateDataProviders.put(key, provider);
+    }
+
+    /**
+     * Removes a private data provider with the specified element name and namespace.
+     *
+     * @param elementName The XML element name.
+     * @param namespace The XML namespace.
+     */
+    public static void removePrivateDataProvider(String elementName, String namespace) {
+        String key = getProviderKey(elementName, namespace);
+        privateDataProviders.remove(key);
+    }
+
+
+    private Connection connection;
+
+    /**
+     * The user to get and set private data for. In most cases, this value should
+     * be <tt>null</tt>, as the typical use of private data is to get and set
+     * your own private data and not others.
+     */
+    private String user;
+
+    /**
+     * Creates a new private data manager. The connection must have
+     * undergone a successful login before being used to construct an instance of
+     * this class.
+     *
+     * @param connection an XMPP connection which must have already undergone a
+     *      successful login.
+     */
+    public PrivateDataManager(Connection connection) {
+        if (!connection.isAuthenticated()) {
+            throw new IllegalStateException("Must be logged in to XMPP server.");
+        }
+        this.connection = connection;
+    }
+
+    /**
+     * Creates a new private data manager for a specific user (special case). Most
+     * servers only support getting and setting private data for the user that
+     * authenticated via the connection. However, some servers support the ability
+     * to get and set private data for other users (for example, if you are the
+     * administrator). The connection must have undergone a successful login before
+     * being used to construct an instance of this class.
+     *
+     * @param connection an XMPP connection which must have already undergone a
+     *      successful login.
+     * @param user the XMPP address of the user to get and set private data for.
+     */
+    public PrivateDataManager(Connection connection, String user) {
+        if (!connection.isAuthenticated()) {
+            throw new IllegalStateException("Must be logged in to XMPP server.");
+        }
+        this.connection = connection;
+        this.user = user;
+    }
+
+    /**
+     * Returns the private data specified by the given element name and namespace. Each chunk
+     * of private data is uniquely identified by an element name and namespace pair.<p>
+     *
+     * If a PrivateDataProvider is registered for the specified element name/namespace pair then
+     * that provider will determine the specific object type that is returned. If no provider
+     * is registered, a {@link DefaultPrivateData} instance will be returned.
+     *
+     * @param elementName the element name.
+     * @param namespace the namespace.
+     * @return the private data.
+     * @throws XMPPException if an error occurs getting the private data.
+     */
+    public PrivateData getPrivateData(final String elementName, final String namespace)
+            throws XMPPException
+    {
+        // Create an IQ packet to get the private data.
+        IQ privateDataGet = new IQ() {
+            public String getChildElementXML() {
+                StringBuilder buf = new StringBuilder();
+                buf.append("<query xmlns=\"jabber:iq:private\">");
+                buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\"/>");
+                buf.append("</query>");
+                return buf.toString();
+            }
+        };
+        privateDataGet.setType(IQ.Type.GET);
+        // Address the packet to the other account if user has been set.
+        if (user != null) {
+            privateDataGet.setTo(user);
+        }
+
+        // Setup a listener for the reply to the set operation.
+        String packetID = privateDataGet.getPacketID();
+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID));
+
+        // Send the private data.
+        connection.sendPacket(privateDataGet);
+
+        // Wait up to five seconds for a response from the server.
+        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        collector.cancel();
+        if (response == null) {
+            throw new XMPPException("No response from the server.");
+        }
+        // If the server replied with an error, throw an exception.
+        else if (response.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(response.getError());
+        }
+        return ((PrivateDataResult)response).getPrivateData();
+    }
+
+    /**
+     * Sets a private data value. Each chunk of private data is uniquely identified by an
+     * element name and namespace pair. If private data has already been set with the
+     * element name and namespace, then the new private data will overwrite the old value.
+     *
+     * @param privateData the private data.
+     * @throws XMPPException if setting the private data fails.
+     */
+    public void setPrivateData(final PrivateData privateData) throws XMPPException {
+        // Create an IQ packet to set the private data.
+        IQ privateDataSet = new IQ() {
+            public String getChildElementXML() {
+                StringBuilder buf = new StringBuilder();
+                buf.append("<query xmlns=\"jabber:iq:private\">");
+                buf.append(privateData.toXML());
+                buf.append("</query>");
+                return buf.toString();
+            }
+        };
+        privateDataSet.setType(IQ.Type.SET);
+        // Address the packet to the other account if user has been set.
+        if (user != null) {
+            privateDataSet.setTo(user);
+        }
+
+        // Setup a listener for the reply to the set operation.
+        String packetID = privateDataSet.getPacketID();
+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(packetID));
+
+        // Send the private data.
+        connection.sendPacket(privateDataSet);
+
+        // Wait up to five seconds for a response from the server.
+        IQ response = (IQ)collector.nextResult(5000);
+        // Stop queuing results
+        collector.cancel();
+        if (response == null) {
+            throw new XMPPException("No response from the server.");
+        }
+        // If the server replied with an error, throw an exception.
+        else if (response.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(response.getError());
+        }
+    }
+
+    /**
+     * Returns a String key for a given element name and namespace.
+     *
+     * @param elementName the element name.
+     * @param namespace the namespace.
+     * @return a unique key for the element name and namespace pair.
+     */
+    private static String getProviderKey(String elementName, String namespace) {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(elementName).append("/><").append(namespace).append("/>");
+        return buf.toString();
+    }
+
+    /**
+     * An IQ provider to parse IQ results containing private data.
+     */
+    public static class PrivateDataIQProvider implements IQProvider {
+        public IQ parseIQ(XmlPullParser parser) throws Exception {
+            PrivateData privateData = null;
+            boolean done = false;
+            while (!done) {
+                int eventType = parser.next();
+                if (eventType == XmlPullParser.START_TAG) {
+                    String elementName = parser.getName();
+                    String namespace = parser.getNamespace();
+                    // See if any objects are registered to handle this private data type.
+                    PrivateDataProvider provider = getPrivateDataProvider(elementName, namespace);
+                    // If there is a registered provider, use it.
+                    if (provider != null) {
+                        privateData = provider.parsePrivateData(parser);
+                    }
+                    // Otherwise, use a DefaultPrivateData instance to store the private data.
+                    else {
+                        DefaultPrivateData data = new DefaultPrivateData(elementName, namespace);
+                        boolean finished = false;
+                        while (!finished) {
+                            int event = parser.next();
+                            if (event == XmlPullParser.START_TAG) {
+                                String name = parser.getName();
+                                // If an empty element, set the value with the empty string.
+                                if (parser.isEmptyElementTag()) {
+                                    data.setValue(name,"");
+                                }
+                                // Otherwise, get the the element text.
+                                else {
+                                    event = parser.next();
+                                    if (event == XmlPullParser.TEXT) {
+                                        String value = parser.getText();
+                                        data.setValue(name, value);
+                                    }
+                                }
+                            }
+                            else if (event == XmlPullParser.END_TAG) {
+                                if (parser.getName().equals(elementName)) {
+                                    finished = true;
+                                }
+                            }
+                        }
+                        privateData = data;
+                    }
+                }
+                else if (eventType == XmlPullParser.END_TAG) {
+                    if (parser.getName().equals("query")) {
+                        done = true;
+                    }
+                }
+            }
+            return new PrivateDataResult(privateData);
+        }
+    }
+
+    /**
+     * An IQ packet to hold PrivateData GET results.
+     */
+    private static class PrivateDataResult extends IQ {
+
+        private PrivateData privateData;
+
+        PrivateDataResult(PrivateData privateData) {
+            this.privateData = privateData;
+        }
+
+        public PrivateData getPrivateData() {
+            return privateData;
+        }
+
+        public String getChildElementXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<query xmlns=\"jabber:iq:private\">");
+            if (privateData != null) {
+                privateData.toXML();
+            }
+            buf.append("</query>");
+            return buf.toString();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/RemoteRosterEntry.java b/src/org/jivesoftware/smackx/RemoteRosterEntry.java
new file mode 100644
index 0000000..5df3690
--- /dev/null
+++ b/src/org/jivesoftware/smackx/RemoteRosterEntry.java
@@ -0,0 +1,114 @@
+/**
+ * $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.smackx;
+
+import java.util.*;
+
+/**
+ * Represents a roster item, which consists of a JID and , their name and
+ * the groups the roster item belongs to. This roster item does not belong
+ * to the local roster. Therefore, it does not persist in the server.<p>
+ *
+ * The idea of a RemoteRosterEntry is to be used as part of a roster exchange.
+ *
+ * @author Gaston Dombiak
+ */
+public class RemoteRosterEntry {
+
+    private String user;
+    private String name;
+    private final List<String> groupNames = new ArrayList<String>();
+
+    /**
+     * Creates a new remote roster entry.
+     *
+     * @param user the user.
+     * @param name the user's name.
+     * @param groups the list of group names the entry will belong to, or <tt>null</tt> if the
+     *      the roster entry won't belong to a group.
+     */
+    public RemoteRosterEntry(String user, String name, String [] groups) {
+        this.user = user;
+        this.name = name;
+		if (groups != null) {
+            groupNames.addAll(Arrays.asList(groups));
+		}
+    }
+
+    /**
+     * Returns the user.
+     *
+     * @return the user.
+     */
+    public String getUser() {
+        return user;
+    }
+
+    /**
+     * Returns the user's name.
+     *
+     * @return the user's name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Returns an Iterator for the group names (as Strings) that the roster entry
+     * belongs to.
+     *
+     * @return an Iterator for the group names.
+     */
+    public Iterator<String> getGroupNames() {
+        synchronized (groupNames) {
+            return Collections.unmodifiableList(groupNames).iterator();
+        }
+    }
+
+    /**
+     * Returns a String array for the group names that the roster entry
+     * belongs to.
+     *
+     * @return a String[] for the group names.
+     */
+    public String[] getGroupArrayNames() {
+        synchronized (groupNames) {
+            return Collections.unmodifiableList(groupNames).toArray(new String[groupNames.size()]);
+        }
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<item jid=\"").append(user).append("\"");
+        if (name != null) {
+            buf.append(" name=\"").append(name).append("\"");
+        }
+        buf.append(">");
+        synchronized (groupNames) {
+            for (String groupName : groupNames) {
+                buf.append("<group>").append(groupName).append("</group>");
+            }
+        }
+        buf.append("</item>");
+        return buf.toString();
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/ReportedData.java b/src/org/jivesoftware/smackx/ReportedData.java
new file mode 100644
index 0000000..0d7b760
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ReportedData.java
@@ -0,0 +1,281 @@
+/**
+ * $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.smackx;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.packet.DataForm;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Represents a set of data results returned as part of a search. The report is structured 
+ * in columns and rows.
+ * 
+ * @author Gaston Dombiak
+ */
+public class ReportedData {
+    
+    private List<Column> columns = new ArrayList<Column>();
+    private List<Row> rows = new ArrayList<Row>();
+    private String title = "";
+    
+    /**
+     * Returns a new ReportedData if the packet is used for reporting data and includes an 
+     * extension that matches the elementName and namespace "x","jabber:x:data".
+     * 
+     * @param packet the packet used for reporting data.
+     */
+    public static ReportedData getReportedDataFrom(Packet packet) {
+        // Check if the packet includes the DataForm extension
+        PacketExtension packetExtension = packet.getExtension("x","jabber:x:data");
+        if (packetExtension != null) {
+            // Check if the existing DataForm is a result of a search
+            DataForm dataForm = (DataForm) packetExtension;
+            if (dataForm.getReportedData() != null)
+                return new ReportedData(dataForm);
+        }
+        // Otherwise return null
+        return null;
+    }
+
+
+    /**
+     * Creates a new ReportedData based on the returned dataForm from a search
+     *(namespace "jabber:iq:search").
+     *
+     * @param dataForm the dataForm returned from a search (namespace "jabber:iq:search").
+     */
+    private ReportedData(DataForm dataForm) {
+        // Add the columns to the report based on the reported data fields
+        for (Iterator fields = dataForm.getReportedData().getFields(); fields.hasNext();) {
+            FormField field = (FormField)fields.next();
+            columns.add(new Column(field.getLabel(), field.getVariable(), field.getType()));
+        }
+
+        // Add the rows to the report based on the form's items
+        for (Iterator items = dataForm.getItems(); items.hasNext();) {
+            DataForm.Item item = (DataForm.Item)items.next();
+            List<Field> fieldList = new ArrayList<Field>(columns.size());
+            FormField field;
+            for (Iterator fields = item.getFields(); fields.hasNext();) {
+                field = (FormField) fields.next();
+                // The field is created with all the values of the data form's field
+                List<String> values = new ArrayList<String>();
+                for (Iterator<String> it=field.getValues(); it.hasNext();) {
+                    values.add(it.next());
+                }
+                fieldList.add(new Field(field.getVariable(), values));
+            }
+            rows.add(new Row(fieldList));
+        }
+
+        // Set the report's title
+        this.title = dataForm.getTitle();
+    }
+
+
+    public ReportedData(){
+        // Allow for model creation of ReportedData.
+    }
+
+    /**
+     * Adds a new <code>Row</code>.
+     * @param row the new row to add.
+     */
+    public void addRow(Row row){
+        rows.add(row);
+    }
+
+    /**
+     * Adds a new <code>Column</code>
+     * @param column the column to add.
+     */
+    public void addColumn(Column column){
+        columns.add(column);
+    }
+
+
+    /**
+     * Returns an Iterator for the rows returned from a search.
+     *
+     * @return an Iterator for the rows returned from a search.
+     */
+    public Iterator<Row> getRows() {
+        return Collections.unmodifiableList(new ArrayList<Row>(rows)).iterator();
+    }
+
+    /**
+     * Returns an Iterator for the columns returned from a search.
+     *
+     * @return an Iterator for the columns returned from a search.
+     */
+    public Iterator<Column> getColumns() {
+        return Collections.unmodifiableList(new ArrayList<Column>(columns)).iterator();
+    }
+
+
+    /**
+     * Returns the report's title. It is similar to the title on a web page or an X
+     * window.
+     *
+     * @return title of the report.
+     */
+    public String getTitle() {
+        return title;
+    }
+
+    /**
+     *
+     * Represents the columns definition of the reported data.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Column {
+        private String label;
+        private String variable;
+        private String type;
+
+        /**
+         * Creates a new column with the specified definition.
+         *
+         * @param label the columns's label.
+         * @param variable the variable name of the column.
+         * @param type the format for the returned data.
+         */
+        public Column(String label, String variable, String type) {
+            this.label = label;
+            this.variable = variable;
+            this.type = type;
+        }
+
+        /**
+         * Returns the column's label.
+         *
+         * @return label of the column.
+         */
+        public String getLabel() {
+            return label;
+        }
+
+
+        /**
+         * Returns the column's data format. Valid formats are:
+         *
+         * <ul>
+         *  <li>text-single -> single line or word of text
+         *  <li>text-private -> instead of showing the user what they typed, you show ***** to
+         * protect it
+         *  <li>text-multi -> multiple lines of text entry
+         *  <li>list-single -> given a list of choices, pick one
+         *  <li>list-multi -> given a list of choices, pick one or more
+         *  <li>boolean -> 0 or 1, true or false, yes or no. Default value is 0
+         *  <li>fixed -> fixed for putting in text to show sections, or just advertise your web
+         * site in the middle of the form
+         *  <li>hidden -> is not given to the user at all, but returned with the questionnaire
+         *  <li>jid-single -> Jabber ID - choosing a JID from your roster, and entering one based
+         * on the rules for a JID.
+         *  <li>jid-multi -> multiple entries for JIDs
+         * </ul>
+         *
+         * @return format for the returned data.
+         */
+        public String getType() {
+            return type;
+        }
+
+
+        /**
+         * Returns the variable name that the column is showing.
+         *
+         * @return the variable name of the column.
+         */
+        public String getVariable() {
+            return variable;
+        }
+
+
+    }
+
+    public static class Row {
+        private List<Field> fields = new ArrayList<Field>();
+
+        public Row(List<Field> fields) {
+            this.fields = fields;
+        }
+
+        /**
+         * Returns the values of the field whose variable matches the requested variable.
+         *
+         * @param variable the variable to match.
+         * @return the values of the field whose variable matches the requested variable.
+         */
+        public Iterator getValues(String variable) {
+            for(Iterator<Field> it=getFields();it.hasNext();) {
+                Field field = it.next();
+                if (variable.equalsIgnoreCase(field.getVariable())) {
+                    return field.getValues();
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Returns the fields that define the data that goes with the item.
+         *
+         * @return the fields that define the data that goes with the item.
+         */
+        private Iterator<Field> getFields() {
+            return Collections.unmodifiableList(new ArrayList<Field>(fields)).iterator();
+        }
+    }
+
+    public static class Field {
+        private String variable;
+        private List<String> values;
+
+        public Field(String variable, List<String> values) {
+            this.variable = variable;
+            this.values = values;
+        }
+
+        /**
+         * Returns the variable name that the field represents.
+         * 
+         * @return the variable name of the field.
+         */
+        public String getVariable() {
+            return variable;
+        }
+
+        /**
+         * Returns an iterator on the values reported as part of the search.
+         * 
+         * @return the returned values of the search.
+         */
+        public Iterator<String> getValues() {
+            return Collections.unmodifiableList(values).iterator();
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/RosterExchangeListener.java b/src/org/jivesoftware/smackx/RosterExchangeListener.java
new file mode 100644
index 0000000..16ce559
--- /dev/null
+++ b/src/org/jivesoftware/smackx/RosterExchangeListener.java
@@ -0,0 +1,42 @@
+/**
+ * $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.smackx;
+
+import java.util.Iterator;
+
+/**
+ *
+ * A listener that is fired anytime a roster exchange is received.
+ *
+ * @author Gaston Dombiak
+ */
+public interface RosterExchangeListener {
+
+    /**
+     * Called when roster entries are received as part of a roster exchange.
+     *  
+     * @param from the user that sent the entries.
+     * @param remoteRosterEntries the entries sent by the user. The entries are instances of 
+     * RemoteRosterEntry.
+     */
+    public void entriesReceived(String from, Iterator<RemoteRosterEntry> remoteRosterEntries);
+
+}
diff --git a/src/org/jivesoftware/smackx/RosterExchangeManager.java b/src/org/jivesoftware/smackx/RosterExchangeManager.java
new file mode 100644
index 0000000..c1d193b
--- /dev/null
+++ b/src/org/jivesoftware/smackx/RosterExchangeManager.java
@@ -0,0 +1,187 @@
+/**
+ * $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.smackx;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Roster;
+import org.jivesoftware.smack.RosterEntry;
+import org.jivesoftware.smack.RosterGroup;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.filter.PacketExtensionFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.packet.RosterExchange;
+
+/**
+ *
+ * Manages Roster exchanges. A RosterExchangeManager provides a high level access to send 
+ * rosters, roster groups and roster entries to XMPP clients. It also provides an easy way
+ * to hook up custom logic when entries are received from another XMPP client through 
+ * RosterExchangeListeners.
+ *
+ * @author Gaston Dombiak
+ */
+public class RosterExchangeManager {
+
+    private List<RosterExchangeListener> rosterExchangeListeners = new ArrayList<RosterExchangeListener>();
+
+    private Connection con;
+
+    private PacketFilter packetFilter = new PacketExtensionFilter("x", "jabber:x:roster");
+    private PacketListener packetListener;
+
+    /**
+     * Creates a new roster exchange manager.
+     *
+     * @param con a Connection which is used to send and receive messages.
+     */
+    public RosterExchangeManager(Connection con) {
+        this.con = con;
+        init();
+    }
+
+    /**
+     * Adds a listener to roster exchanges. The listener will be fired anytime roster entries
+     * are received from remote XMPP clients.
+     *
+     * @param rosterExchangeListener a roster exchange listener.
+     */
+    public void addRosterListener(RosterExchangeListener rosterExchangeListener) {
+        synchronized (rosterExchangeListeners) {
+            if (!rosterExchangeListeners.contains(rosterExchangeListener)) {
+                rosterExchangeListeners.add(rosterExchangeListener);
+            }
+        }
+    }
+
+    /**
+     * Removes a listener from roster exchanges. The listener will be fired anytime roster 
+     * entries are received from remote XMPP clients.
+     *
+     * @param rosterExchangeListener a roster exchange listener..
+     */
+    public void removeRosterListener(RosterExchangeListener rosterExchangeListener) {
+        synchronized (rosterExchangeListeners) {
+            rosterExchangeListeners.remove(rosterExchangeListener);
+        }
+    }
+
+    /**
+     * Sends a roster to userID. All the entries of the roster will be sent to the
+     * target user.
+     * 
+     * @param roster the roster to send
+     * @param targetUserID the user that will receive the roster entries
+     */
+    public void send(Roster roster, String targetUserID) {
+        // Create a new message to send the roster
+        Message msg = new Message(targetUserID);
+        // Create a RosterExchange Package and add it to the message
+        RosterExchange rosterExchange = new RosterExchange(roster);
+        msg.addExtension(rosterExchange);
+
+        // Send the message that contains the roster
+        con.sendPacket(msg);
+    }
+
+    /**
+     * Sends a roster entry to userID.
+     * 
+     * @param rosterEntry the roster entry to send
+     * @param targetUserID the user that will receive the roster entries
+     */
+    public void send(RosterEntry rosterEntry, String targetUserID) {
+        // Create a new message to send the roster
+        Message msg = new Message(targetUserID);
+        // Create a RosterExchange Package and add it to the message
+        RosterExchange rosterExchange = new RosterExchange();
+        rosterExchange.addRosterEntry(rosterEntry);
+        msg.addExtension(rosterExchange);
+
+        // Send the message that contains the roster
+        con.sendPacket(msg);
+    }
+
+    /**
+     * Sends a roster group to userID. All the entries of the group will be sent to the 
+     * target user.
+     * 
+     * @param rosterGroup the roster group to send
+     * @param targetUserID the user that will receive the roster entries
+     */
+    public void send(RosterGroup rosterGroup, String targetUserID) {
+        // Create a new message to send the roster
+        Message msg = new Message(targetUserID);
+        // Create a RosterExchange Package and add it to the message
+        RosterExchange rosterExchange = new RosterExchange();
+        for (RosterEntry entry : rosterGroup.getEntries()) {
+            rosterExchange.addRosterEntry(entry);
+        }
+        msg.addExtension(rosterExchange);
+
+        // Send the message that contains the roster
+        con.sendPacket(msg);
+    }
+
+    /**
+     * Fires roster exchange listeners.
+     */
+    private void fireRosterExchangeListeners(String from, Iterator<RemoteRosterEntry> remoteRosterEntries) {
+        RosterExchangeListener[] listeners = null;
+        synchronized (rosterExchangeListeners) {
+            listeners = new RosterExchangeListener[rosterExchangeListeners.size()];
+            rosterExchangeListeners.toArray(listeners);
+        }
+        for (int i = 0; i < listeners.length; i++) {
+            listeners[i].entriesReceived(from, remoteRosterEntries);
+        }
+    }
+
+    private void init() {
+        // Listens for all roster exchange packets and fire the roster exchange listeners.
+        packetListener = new PacketListener() {
+            public void processPacket(Packet packet) {
+                Message message = (Message) packet;
+                RosterExchange rosterExchange =
+                    (RosterExchange) message.getExtension("x", "jabber:x:roster");
+                // Fire event for roster exchange listeners
+                fireRosterExchangeListeners(message.getFrom(), rosterExchange.getRosterEntries());
+            };
+
+        };
+        con.addPacketListener(packetListener, packetFilter);
+    }
+
+    public void destroy() {
+        if (con != null)
+            con.removePacketListener(packetListener);
+
+    }
+    protected void finalize() throws Throwable {
+        destroy();
+        super.finalize();
+    }
+}
diff --git a/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java b/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java
new file mode 100644
index 0000000..9e31f67
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ServiceDiscoveryManager.java
@@ -0,0 +1,708 @@
+/**
+ * $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.smackx;
+
+import org.jivesoftware.smack.*;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.packet.DataForm;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Manages discovery of services in XMPP entities. This class provides:
+ * <ol>
+ * <li>A registry of supported features in this XMPP entity.
+ * <li>Automatic response when this XMPP entity is queried for information.
+ * <li>Ability to discover items and information of remote XMPP entities.
+ * <li>Ability to publish publicly available items.
+ * </ol>  
+ * 
+ * @author Gaston Dombiak
+ */
+public class ServiceDiscoveryManager {
+
+    private static final String DEFAULT_IDENTITY_NAME = "Smack";
+    private static final String DEFAULT_IDENTITY_CATEGORY = "client";
+    private static final String DEFAULT_IDENTITY_TYPE = "pc";
+
+    private static List<DiscoverInfo.Identity> identities = new LinkedList<DiscoverInfo.Identity>();
+
+    private EntityCapsManager capsManager;
+
+    private static Map<Connection, ServiceDiscoveryManager> instances =
+            new ConcurrentHashMap<Connection, ServiceDiscoveryManager>();
+
+    private Connection connection;
+    private final Set<String> features = new HashSet<String>();
+    private DataForm extendedInfo = null;
+    private Map<String, NodeInformationProvider> nodeInformationProviders =
+            new ConcurrentHashMap<String, NodeInformationProvider>();
+
+    // Create a new ServiceDiscoveryManager on every established connection
+    static {
+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+            public void connectionCreated(Connection connection) {
+                new ServiceDiscoveryManager(connection);
+            }
+        });
+        identities.add(new Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE));
+    }
+
+    /**
+     * Creates a new ServiceDiscoveryManager for a given Connection. This means that the 
+     * service manager will respond to any service discovery request that the connection may
+     * receive. 
+     * 
+     * @param connection the connection to which a ServiceDiscoveryManager is going to be created.
+     */
+    public ServiceDiscoveryManager(Connection connection) {
+        this.connection = connection;
+
+        init();
+    }
+
+    /**
+     * Returns the ServiceDiscoveryManager instance associated with a given Connection.
+     * 
+     * @param connection the connection used to look for the proper ServiceDiscoveryManager.
+     * @return the ServiceDiscoveryManager associated with a given Connection.
+     */
+    public static ServiceDiscoveryManager getInstanceFor(Connection connection) {
+        return instances.get(connection);
+    }
+
+    /**
+     * Returns the name of the client that will be returned when asked for the client identity
+     * in a disco request. The name could be any value you need to identity this client.
+     * 
+     * @return the name of the client that will be returned when asked for the client identity
+     *          in a disco request.
+     */
+    public static String getIdentityName() {
+        DiscoverInfo.Identity identity = identities.get(0);
+        if (identity != null) {
+            return identity.getName();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Sets the name of the client that will be returned when asked for the client identity
+     * in a disco request. The name could be any value you need to identity this client.
+     * 
+     * @param name the name of the client that will be returned when asked for the client identity
+     *          in a disco request.
+     */
+    public static void setIdentityName(String name) {
+        DiscoverInfo.Identity identity = identities.remove(0);
+        identity = new DiscoverInfo.Identity(DEFAULT_IDENTITY_CATEGORY, name, DEFAULT_IDENTITY_TYPE);
+        identities.add(identity);
+    }
+
+    /**
+     * Returns the type of client that will be returned when asked for the client identity in a 
+     * disco request. The valid types are defined by the category client. Follow this link to learn 
+     * the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>.
+     * 
+     * @return the type of client that will be returned when asked for the client identity in a 
+     *          disco request.
+     */
+    public static String getIdentityType() {
+        DiscoverInfo.Identity identity = identities.get(0);
+        if (identity != null) {
+            return identity.getType();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Sets the type of client that will be returned when asked for the client identity in a 
+     * disco request. The valid types are defined by the category client. Follow this link to learn 
+     * the possible types: <a href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>.
+     * 
+     * @param type the type of client that will be returned when asked for the client identity in a 
+     *          disco request.
+     */
+    public static void setIdentityType(String type) {
+        DiscoverInfo.Identity identity = identities.get(0);
+        if (identity != null) {
+            identity.setType(type);
+        } else {
+            identity = new DiscoverInfo.Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, type);
+            identities.add(identity);
+        }
+    }
+
+    /**
+     * Returns all identities of this client as unmodifiable Collection
+     * 
+     * @return
+     */
+    public static List<DiscoverInfo.Identity> getIdentities() {
+        return Collections.unmodifiableList(identities);
+    }
+
+    /**
+     * Initializes the packet listeners of the connection that will answer to any
+     * service discovery request. 
+     */
+    private void init() {
+        // Register the new instance and associate it with the connection 
+        instances.put(connection, this);
+
+        addFeature(DiscoverInfo.NAMESPACE);
+        addFeature(DiscoverItems.NAMESPACE);
+
+        // Add a listener to the connection that removes the registered instance when
+        // the connection is closed
+        connection.addConnectionListener(new ConnectionListener() {
+            public void connectionClosed() {
+                // Unregister this instance since the connection has been closed
+                instances.remove(connection);
+            }
+
+            public void connectionClosedOnError(Exception e) {
+                // ignore
+            }
+
+            public void reconnectionFailed(Exception e) {
+                // ignore
+            }
+
+            public void reconnectingIn(int seconds) {
+                // ignore
+            }
+
+            public void reconnectionSuccessful() {
+                // ignore
+            }
+        });
+
+        // Listen for disco#items requests and answer with an empty result        
+        PacketFilter packetFilter = new PacketTypeFilter(DiscoverItems.class);
+        PacketListener packetListener = new PacketListener() {
+            public void processPacket(Packet packet) {
+                DiscoverItems discoverItems = (DiscoverItems) packet;
+                // Send back the items defined in the client if the request is of type GET
+                if (discoverItems != null && discoverItems.getType() == IQ.Type.GET) {
+                    DiscoverItems response = new DiscoverItems();
+                    response.setType(IQ.Type.RESULT);
+                    response.setTo(discoverItems.getFrom());
+                    response.setPacketID(discoverItems.getPacketID());
+                    response.setNode(discoverItems.getNode());
+
+                    // Add the defined items related to the requested node. Look for 
+                    // the NodeInformationProvider associated with the requested node.  
+                    NodeInformationProvider nodeInformationProvider =
+                            getNodeInformationProvider(discoverItems.getNode());
+                    if (nodeInformationProvider != null) {
+                        // Specified node was found, add node items
+                        response.addItems(nodeInformationProvider.getNodeItems());
+                        // Add packet extensions
+                        response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
+                    } else if(discoverItems.getNode() != null) {
+                        // Return <item-not-found/> error since client doesn't contain
+                        // the specified node
+                        response.setType(IQ.Type.ERROR);
+                        response.setError(new XMPPError(XMPPError.Condition.item_not_found));
+                    }
+                    connection.sendPacket(response);
+                }
+            }
+        };
+        connection.addPacketListener(packetListener, packetFilter);
+
+        // Listen for disco#info requests and answer the client's supported features 
+        // To add a new feature as supported use the #addFeature message        
+        packetFilter = new PacketTypeFilter(DiscoverInfo.class);
+        packetListener = new PacketListener() {
+            public void processPacket(Packet packet) {
+                DiscoverInfo discoverInfo = (DiscoverInfo) packet;
+                // Answer the client's supported features if the request is of the GET type
+                if (discoverInfo != null && discoverInfo.getType() == IQ.Type.GET) {
+                    DiscoverInfo response = new DiscoverInfo();
+                    response.setType(IQ.Type.RESULT);
+                    response.setTo(discoverInfo.getFrom());
+                    response.setPacketID(discoverInfo.getPacketID());
+                    response.setNode(discoverInfo.getNode());
+                    // Add the client's identity and features only if "node" is null
+                    // and if the request was not send to a node. If Entity Caps are
+                    // enabled the client's identity and features are may also added
+                    // if the right node is chosen
+                    if (discoverInfo.getNode() == null) {
+                        addDiscoverInfoTo(response);
+                    }
+                    else {
+                        // Disco#info was sent to a node. Check if we have information of the
+                        // specified node
+                        NodeInformationProvider nodeInformationProvider =
+                                getNodeInformationProvider(discoverInfo.getNode());
+                        if (nodeInformationProvider != null) {
+                            // Node was found. Add node features
+                            response.addFeatures(nodeInformationProvider.getNodeFeatures());
+                            // Add node identities
+                            response.addIdentities(nodeInformationProvider.getNodeIdentities());
+                            // Add packet extensions
+                            response.addExtensions(nodeInformationProvider.getNodePacketExtensions());
+                        }
+                        else {
+                            // Return <item-not-found/> error since specified node was not found
+                            response.setType(IQ.Type.ERROR);
+                            response.setError(new XMPPError(XMPPError.Condition.item_not_found));
+                        }
+                    }
+                    connection.sendPacket(response);
+                }
+            }
+        };
+        connection.addPacketListener(packetListener, packetFilter);
+    }
+
+    /**
+     * Add discover info response data.
+     * 
+     * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol; Example 2</a>
+     *
+     * @param response the discover info response packet
+     */
+    public void addDiscoverInfoTo(DiscoverInfo response) {
+        // First add the identities of the connection
+        response.addIdentities(identities);
+
+        // Add the registered features to the response
+        synchronized (features) {
+            for (Iterator<String> it = getFeatures(); it.hasNext();) {
+                response.addFeature(it.next());
+            }
+            response.addExtension(extendedInfo);
+        }
+    }
+
+    /**
+     * Returns the NodeInformationProvider responsible for providing information 
+     * (ie items) related to a given node or <tt>null</null> if none.<p>
+     * 
+     * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
+     * NodeInformationProvider will provide information about the rooms where the user has joined.
+     * 
+     * @param node the node that contains items associated with an entity not addressable as a JID.
+     * @return the NodeInformationProvider responsible for providing information related 
+     * to a given node.
+     */
+    private NodeInformationProvider getNodeInformationProvider(String node) {
+        if (node == null) {
+            return null;
+        }
+        return nodeInformationProviders.get(node);
+    }
+
+    /**
+     * Sets the NodeInformationProvider responsible for providing information 
+     * (ie items) related to a given node. Every time this client receives a disco request
+     * regarding the items of a given node, the provider associated to that node will be the 
+     * responsible for providing the requested information.<p>
+     * 
+     * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
+     * NodeInformationProvider will provide information about the rooms where the user has joined. 
+     * 
+     * @param node the node whose items will be provided by the NodeInformationProvider.
+     * @param listener the NodeInformationProvider responsible for providing items related
+     *      to the node.
+     */
+    public void setNodeInformationProvider(String node, NodeInformationProvider listener) {
+        nodeInformationProviders.put(node, listener);
+    }
+
+    /**
+     * Removes the NodeInformationProvider responsible for providing information 
+     * (ie items) related to a given node. This means that no more information will be
+     * available for the specified node.
+     * 
+     * In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the
+     * NodeInformationProvider will provide information about the rooms where the user has joined. 
+     * 
+     * @param node the node to remove the associated NodeInformationProvider.
+     */
+    public void removeNodeInformationProvider(String node) {
+        nodeInformationProviders.remove(node);
+    }
+
+    /**
+     * Returns the supported features by this XMPP entity.
+     * 
+     * @return an Iterator on the supported features by this XMPP entity.
+     */
+    public Iterator<String> getFeatures() {
+        synchronized (features) {
+            return Collections.unmodifiableList(new ArrayList<String>(features)).iterator();
+        }
+    }
+
+    /**
+     * Returns the supported features by this XMPP entity.
+     * 
+     * @return a copy of the List on the supported features by this XMPP entity.
+     */
+    public List<String> getFeaturesList() {
+        synchronized (features) {
+            return new LinkedList<String>(features);
+        }
+    }
+
+    /**
+     * Registers that a new feature is supported by this XMPP entity. When this client is 
+     * queried for its information the registered features will be answered.<p>
+     *
+     * Since no packet is actually sent to the server it is safe to perform this operation
+     * before logging to the server. In fact, you may want to configure the supported features
+     * before logging to the server so that the information is already available if it is required
+     * upon login.
+     *
+     * @param feature the feature to register as supported.
+     */
+    public void addFeature(String feature) {
+        synchronized (features) {
+            features.add(feature);
+            renewEntityCapsVersion();
+        }
+    }
+
+    /**
+     * Removes the specified feature from the supported features by this XMPP entity.<p>
+     *
+     * Since no packet is actually sent to the server it is safe to perform this operation
+     * before logging to the server.
+     *
+     * @param feature the feature to remove from the supported features.
+     */
+    public void removeFeature(String feature) {
+        synchronized (features) {
+            features.remove(feature);
+            renewEntityCapsVersion();
+        }
+    }
+
+    /**
+     * Returns true if the specified feature is registered in the ServiceDiscoveryManager.
+     *
+     * @param feature the feature to look for.
+     * @return a boolean indicating if the specified featured is registered or not.
+     */
+    public boolean includesFeature(String feature) {
+        synchronized (features) {
+            return features.contains(feature);
+        }
+    }
+
+    /**
+     * Registers extended discovery information of this XMPP entity. When this
+     * client is queried for its information this data form will be returned as
+     * specified by XEP-0128.
+     * <p>
+     *
+     * Since no packet is actually sent to the server it is safe to perform this
+     * operation before logging to the server. In fact, you may want to
+     * configure the extended info before logging to the server so that the
+     * information is already available if it is required upon login.
+     *
+     * @param info
+     *            the data form that contains the extend service discovery
+     *            information.
+     */
+    public void setExtendedInfo(DataForm info) {
+      extendedInfo = info;
+      renewEntityCapsVersion();
+    }
+
+    /**
+     * Returns the data form that is set as extended information for this Service Discovery instance (XEP-0128)
+     * 
+     * @see <a href="http://xmpp.org/extensions/xep-0128.html">XEP-128: Service Discovery Extensions</a>
+     * @return
+     */
+    public DataForm getExtendedInfo() {
+        return extendedInfo;
+    }
+
+    /**
+     * Returns the data form as List of PacketExtensions, or null if no data form is set.
+     * This representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider)
+     * 
+     * @return
+     */
+    public List<PacketExtension> getExtendedInfoAsList() {
+        List<PacketExtension> res = null;
+        if (extendedInfo != null) {
+            res = new ArrayList<PacketExtension>(1);
+            res.add(extendedInfo);
+        }
+        return res;
+    }
+
+    /**
+     * Removes the data form containing extended service discovery information
+     * from the information returned by this XMPP entity.<p>
+     *
+     * Since no packet is actually sent to the server it is safe to perform this
+     * operation before logging to the server.
+     */
+    public void removeExtendedInfo() {
+       extendedInfo = null;
+       renewEntityCapsVersion();
+    }
+
+    /**
+     * Returns the discovered information of a given XMPP entity addressed by its JID.
+     * Use null as entityID to query the server
+     * 
+     * @param entityID the address of the XMPP entity or null.
+     * @return the discovered information.
+     * @throws XMPPException if the operation failed for some reason.
+     */
+    public DiscoverInfo discoverInfo(String entityID) throws XMPPException {
+        if (entityID == null)
+            return discoverInfo(null, null);
+
+        // Check if the have it cached in the Entity Capabilities Manager
+        DiscoverInfo info = EntityCapsManager.getDiscoverInfoByUser(entityID);
+
+        if (info != null) {
+            // We were able to retrieve the information from Entity Caps and
+            // avoided a disco request, hurray!
+            return info;
+        }
+
+        // Try to get the newest node#version if it's known, otherwise null is
+        // returned
+        EntityCapsManager.NodeVerHash nvh = EntityCapsManager.getNodeVerHashByJid(entityID);
+
+        // Discover by requesting the information from the remote entity
+        // Note that wee need to use NodeVer as argument for Node if it exists
+        info = discoverInfo(entityID, nvh != null ? nvh.getNodeVer() : null);
+
+        // If the node version is known, store the new entry.
+        if (nvh != null) {
+            if (EntityCapsManager.verifyDiscoverInfoVersion(nvh.getVer(), nvh.getHash(), info))
+                EntityCapsManager.addDiscoverInfoByNode(nvh.getNodeVer(), info);
+        }
+
+        return info;
+    }
+
+    /**
+     * Returns the discovered information of a given XMPP entity addressed by its JID and
+     * note attribute. Use this message only when trying to query information which is not 
+     * directly addressable.
+     * 
+     * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a>
+     * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a>
+     * 
+     * @param entityID the address of the XMPP entity.
+     * @param node the optional attribute that supplements the 'jid' attribute.
+     * @return the discovered information.
+     * @throws XMPPException if the operation failed for some reason.
+     */
+    public DiscoverInfo discoverInfo(String entityID, String node) throws XMPPException {
+        // Discover the entity's info
+        DiscoverInfo disco = new DiscoverInfo();
+        disco.setType(IQ.Type.GET);
+        disco.setTo(entityID);
+        disco.setNode(node);
+
+        // Create a packet collector to listen for a response.
+        PacketCollector collector =
+            connection.createPacketCollector(new PacketIDFilter(disco.getPacketID()));
+
+        connection.sendPacket(disco);
+
+        // Wait up to 5 seconds for a result.
+        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        collector.cancel();
+        if (result == null) {
+            throw new XMPPException("No response from the server.");
+        }
+        if (result.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(result.getError());
+        }
+        return (DiscoverInfo) result;
+    }
+
+    /**
+     * Returns the discovered items of a given XMPP entity addressed by its JID.
+     * 
+     * @param entityID the address of the XMPP entity.
+     * @return the discovered information.
+     * @throws XMPPException if the operation failed for some reason.
+     */
+    public DiscoverItems discoverItems(String entityID) throws XMPPException {
+        return discoverItems(entityID, null);
+    }
+
+    /**
+     * Returns the discovered items of a given XMPP entity addressed by its JID and
+     * note attribute. Use this message only when trying to query information which is not 
+     * directly addressable.
+     * 
+     * @param entityID the address of the XMPP entity.
+     * @param node the optional attribute that supplements the 'jid' attribute.
+     * @return the discovered items.
+     * @throws XMPPException if the operation failed for some reason.
+     */
+    public DiscoverItems discoverItems(String entityID, String node) throws XMPPException {
+        // Discover the entity's items
+        DiscoverItems disco = new DiscoverItems();
+        disco.setType(IQ.Type.GET);
+        disco.setTo(entityID);
+        disco.setNode(node);
+
+        // Create a packet collector to listen for a response.
+        PacketCollector collector =
+            connection.createPacketCollector(new PacketIDFilter(disco.getPacketID()));
+
+        connection.sendPacket(disco);
+
+        // Wait up to 5 seconds for a result.
+        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        collector.cancel();
+        if (result == null) {
+            throw new XMPPException("No response from the server.");
+        }
+        if (result.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(result.getError());
+        }
+        return (DiscoverItems) result;
+    }
+
+    /**
+     * Returns true if the server supports publishing of items. A client may wish to publish items
+     * to the server so that the server can provide items associated to the client. These items will
+     * be returned by the server whenever the server receives a disco request targeted to the bare
+     * address of the client (i.e. user@host.com).
+     * 
+     * @param entityID the address of the XMPP entity.
+     * @return true if the server supports publishing of items.
+     * @throws XMPPException if the operation failed for some reason.
+     */
+    public boolean canPublishItems(String entityID) throws XMPPException {
+        DiscoverInfo info = discoverInfo(entityID);
+        return canPublishItems(info);
+     }
+
+     /**
+      * Returns true if the server supports publishing of items. A client may wish to publish items
+      * to the server so that the server can provide items associated to the client. These items will
+      * be returned by the server whenever the server receives a disco request targeted to the bare
+      * address of the client (i.e. user@host.com).
+      * 
+      * @param DiscoverInfo the discover info packet to check.
+      * @return true if the server supports publishing of items.
+      */
+     public static boolean canPublishItems(DiscoverInfo info) {
+         return info.containsFeature("http://jabber.org/protocol/disco#publish");
+     }
+
+    /**
+     * Publishes new items to a parent entity. The item elements to publish MUST have at least 
+     * a 'jid' attribute specifying the Entity ID of the item, and an action attribute which 
+     * specifies the action being taken for that item. Possible action values are: "update" and 
+     * "remove".
+     * 
+     * @param entityID the address of the XMPP entity.
+     * @param discoverItems the DiscoveryItems to publish.
+     * @throws XMPPException if the operation failed for some reason.
+     */
+    public void publishItems(String entityID, DiscoverItems discoverItems)
+            throws XMPPException {
+        publishItems(entityID, null, discoverItems);
+    }
+
+    /**
+     * Publishes new items to a parent entity and node. The item elements to publish MUST have at 
+     * least a 'jid' attribute specifying the Entity ID of the item, and an action attribute which 
+     * specifies the action being taken for that item. Possible action values are: "update" and 
+     * "remove".
+     * 
+     * @param entityID the address of the XMPP entity.
+     * @param node the attribute that supplements the 'jid' attribute.
+     * @param discoverItems the DiscoveryItems to publish.
+     * @throws XMPPException if the operation failed for some reason.
+     */
+    public void publishItems(String entityID, String node, DiscoverItems discoverItems)
+            throws XMPPException {
+        discoverItems.setType(IQ.Type.SET);
+        discoverItems.setTo(entityID);
+        discoverItems.setNode(node);
+
+        // Create a packet collector to listen for a response.
+        PacketCollector collector =
+            connection.createPacketCollector(new PacketIDFilter(discoverItems.getPacketID()));
+
+        connection.sendPacket(discoverItems);
+
+        // Wait up to 5 seconds for a result.
+        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        collector.cancel();
+        if (result == null) {
+            throw new XMPPException("No response from the server.");
+        }
+        if (result.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(result.getError());
+        }
+    }
+
+    /**
+     * Entity Capabilities
+     */
+
+    /**
+     * Loads the ServiceDiscoveryManager with an EntityCapsManger
+     * that speeds up certain lookups
+     * @param manager
+     */
+    public void setEntityCapsManager(EntityCapsManager manager) {
+        capsManager = manager;
+    }
+
+    /**
+     * Updates the Entity Capabilities Verification String
+     * if EntityCaps is enabled
+     */
+    private void renewEntityCapsVersion() {
+        if (capsManager != null && capsManager.entityCapsEnabled())
+            capsManager.updateLocalEntityCaps();
+    }
+}
diff --git a/src/org/jivesoftware/smackx/SharedGroupManager.java b/src/org/jivesoftware/smackx/SharedGroupManager.java
new file mode 100644
index 0000000..76cd527
--- /dev/null
+++ b/src/org/jivesoftware/smackx/SharedGroupManager.java
@@ -0,0 +1,72 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 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.smackx;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.PacketCollector;

+import org.jivesoftware.smack.SmackConfiguration;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.filter.PacketIDFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smackx.packet.SharedGroupsInfo;

+

+import java.util.List;

+

+/**

+ * A SharedGroupManager provides services for discovering the shared groups where a user belongs.<p>

+ *

+ * Important note: This functionality is not part of the XMPP spec and it will only work

+ * with Wildfire.

+ *

+ * @author Gaston Dombiak

+ */

+public class SharedGroupManager {

+

+    /**

+     * Returns the collection that will contain the name of the shared groups where the user

+     * logged in with the specified session belongs.

+     *

+     * @param connection connection to use to get the user's shared groups.

+     * @return collection with the shared groups' name of the logged user.

+     */

+    public static List<String> getSharedGroups(Connection connection) throws XMPPException {

+        // Discover the shared groups of the logged user

+        SharedGroupsInfo info = new SharedGroupsInfo();

+        info.setType(IQ.Type.GET);

+

+        // Create a packet collector to listen for a response.

+        PacketCollector collector =

+            connection.createPacketCollector(new PacketIDFilter(info.getPacketID()));

+

+        connection.sendPacket(info);

+

+        // Wait up to 5 seconds for a result.

+        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+        // Stop queuing results

+        collector.cancel();

+        if (result == null) {

+            throw new XMPPException("No response from the server.");

+        }

+        if (result.getType() == IQ.Type.ERROR) {

+            throw new XMPPException(result.getError());

+        }

+        return ((SharedGroupsInfo) result).getGroups();

+    }

+}

diff --git a/src/org/jivesoftware/smackx/XHTMLManager.java b/src/org/jivesoftware/smackx/XHTMLManager.java
new file mode 100644
index 0000000..a446819
--- /dev/null
+++ b/src/org/jivesoftware/smackx/XHTMLManager.java
@@ -0,0 +1,144 @@
+/**
+ * $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.smackx;
+
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.XHTMLExtension;
+
+import java.util.Iterator;
+
+/**
+ * Manages XHTML formatted texts within messages. A XHTMLManager provides a high level access to 
+ * get and set XHTML bodies to messages, enable and disable XHTML support and check if remote XMPP
+ * clients support XHTML.   
+ * 
+ * @author Gaston Dombiak
+ */
+public class XHTMLManager {
+
+    private final static String namespace = "http://jabber.org/protocol/xhtml-im";
+
+    // Enable the XHTML support on every established connection
+    // The ServiceDiscoveryManager class should have been already initialized
+    static {
+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+            public void connectionCreated(Connection connection) {
+                XHTMLManager.setServiceEnabled(connection, true);
+            }
+        });
+    }
+
+    /**
+     * Returns an Iterator for the XHTML bodies in the message. Returns null if 
+     * the message does not contain an XHTML extension.
+     *
+     * @param message an XHTML message
+     * @return an Iterator for the bodies in the message or null if none.
+     */
+    public static Iterator<String> getBodies(Message message) {
+        XHTMLExtension xhtmlExtension = (XHTMLExtension) message.getExtension("html", namespace);
+        if (xhtmlExtension != null)
+            return xhtmlExtension.getBodies();
+        else
+            return null;
+    }
+
+    /**
+     * Adds an XHTML body to the message.
+     *
+     * @param message the message that will receive the XHTML body
+     * @param body the string to add as an XHTML body to the message
+     */
+    public static void addBody(Message message, String body) {
+        XHTMLExtension xhtmlExtension = (XHTMLExtension) message.getExtension("html", namespace);
+        if (xhtmlExtension == null) {
+            // Create an XHTMLExtension and add it to the message
+            xhtmlExtension = new XHTMLExtension();
+            message.addExtension(xhtmlExtension);
+        }
+        // Add the required bodies to the message
+        xhtmlExtension.addBody(body);
+    }
+
+    /**
+     * Returns true if the message contains an XHTML extension.
+     *
+     * @param message the message to check if contains an XHTML extentsion or not
+     * @return a boolean indicating whether the message is an XHTML message
+     */
+    public static boolean isXHTMLMessage(Message message) {
+        return message.getExtension("html", namespace) != null;
+    }
+
+    /**
+     * Enables or disables the XHTML support on a given connection.<p>
+     *  
+     * Before starting to send XHTML messages to a user, check that the user can handle XHTML
+     * messages. Enable the XHTML support to indicate that this client handles XHTML messages.  
+     *
+     * @param connection the connection where the service will be enabled or disabled
+     * @param enabled indicates if the service will be enabled or disabled 
+     */
+    public synchronized static void setServiceEnabled(Connection connection, boolean enabled) {
+        if (isServiceEnabled(connection) == enabled)
+            return;
+
+        if (enabled) {
+            ServiceDiscoveryManager.getInstanceFor(connection).addFeature(namespace);
+        }
+        else {
+            ServiceDiscoveryManager.getInstanceFor(connection).removeFeature(namespace);
+        }
+    }
+
+    /**
+     * Returns true if the XHTML support is enabled for the given connection.
+     *
+     * @param connection the connection to look for XHTML support
+     * @return a boolean indicating if the XHTML support is enabled for the given connection
+     */
+    public static boolean isServiceEnabled(Connection connection) {
+        return ServiceDiscoveryManager.getInstanceFor(connection).includesFeature(namespace);
+    }
+
+    /**
+     * Returns true if the specified user handles XHTML messages.
+     *
+     * @param connection the connection to use to perform the service discovery
+     * @param userID the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com
+     * @return a boolean indicating whether the specified user handles XHTML messages
+     */
+    public static boolean isServiceEnabled(Connection connection, String userID) {
+        try {
+            DiscoverInfo result =
+                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(userID);
+            return result.containsFeature(namespace);
+        }
+        catch (XMPPException e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/XHTMLText.java b/src/org/jivesoftware/smackx/XHTMLText.java
new file mode 100644
index 0000000..201e530
--- /dev/null
+++ b/src/org/jivesoftware/smackx/XHTMLText.java
@@ -0,0 +1,429 @@
+/**
+ * $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.smackx;
+
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * An XHTMLText represents formatted text. This class also helps to build valid 
+ * XHTML tags.
+ *
+ * @author Gaston Dombiak
+ */
+public class XHTMLText {
+
+    private StringBuilder text = new StringBuilder(30);
+
+    /**
+     * Creates a new XHTMLText with body tag params.
+     * 
+     * @param style the XHTML style of the body
+     * @param lang the language of the body
+     */
+    public XHTMLText(String style, String lang) {
+        appendOpenBodyTag(style, lang);
+    }
+
+    /**
+     * Appends a tag that indicates that an anchor section begins.
+     * 
+     * @param href indicates the URL being linked to
+     * @param style the XHTML style of the anchor
+     */
+    public void appendOpenAnchorTag(String href, String style) {
+        StringBuilder sb = new StringBuilder("<a");
+        if (href != null) {
+            sb.append(" href=\"");
+            sb.append(href);
+            sb.append("\"");
+        }
+        if (style != null) {
+            sb.append(" style=\"");
+            sb.append(style);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates that an anchor section ends.
+     * 
+     */
+    public void appendCloseAnchorTag() {
+        text.append("</a>");
+    }
+
+    /**
+     * Appends a tag that indicates that a blockquote section begins.
+     * 
+     * @param style the XHTML style of the blockquote
+     */
+    public void appendOpenBlockQuoteTag(String style) {
+        StringBuilder sb = new StringBuilder("<blockquote");
+        if (style != null) {
+            sb.append(" style=\"");
+            sb.append(style);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates that a blockquote section ends.
+     * 
+     */
+    public void appendCloseBlockQuoteTag() {
+        text.append("</blockquote>");
+    }
+
+    /**
+     * Appends a tag that indicates that a body section begins.
+     * 
+     * @param style the XHTML style of the body
+     * @param lang the language of the body
+     */
+    private void appendOpenBodyTag(String style, String lang) {
+        StringBuilder sb = new StringBuilder("<body");
+        if (style != null) {
+            sb.append(" style=\"");
+            sb.append(style);
+            sb.append("\"");
+        }
+        if (lang != null) {
+            sb.append(" xml:lang=\"");
+            sb.append(lang);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates that a body section ends.
+     * 
+     */
+    private String closeBodyTag() {
+        return "</body>";
+    }
+
+    /**
+     * Appends a tag that inserts a single carriage return.
+     * 
+     */
+    public void appendBrTag() {
+        text.append("<br/>");
+    }
+
+    /**
+     * Appends a tag that indicates a reference to work, such as a book, report or web site.
+     * 
+     */
+    public void appendOpenCiteTag() {
+        text.append("<cite>");
+    }
+
+    /**
+     * Appends a tag that indicates text that is the code for a program.
+     * 
+     */
+    public void appendOpenCodeTag() {
+        text.append("<code>");
+    }
+
+    /**
+     * Appends a tag that indicates end of text that is the code for a program.
+     * 
+     */
+    public void appendCloseCodeTag() {
+        text.append("</code>");
+    }
+
+    /**
+     * Appends a tag that indicates emphasis.
+     * 
+     */
+    public void appendOpenEmTag() {
+        text.append("<em>");
+    }
+
+    /**
+     * Appends a tag that indicates end of emphasis.
+     * 
+     */
+    public void appendCloseEmTag() {
+        text.append("</em>");
+    }
+
+    /**
+     * Appends a tag that indicates a header, a title of a section of the message.
+     * 
+     * @param level the level of the Header. It should be a value between 1 and 3
+     * @param style the XHTML style of the blockquote
+     */
+    public void appendOpenHeaderTag(int level, String style) {
+        if (level > 3 || level < 1) {
+            return;
+        }
+        StringBuilder sb = new StringBuilder("<h");
+        sb.append(level);
+        if (style != null) {
+            sb.append(" style=\"");
+            sb.append(style);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates that a header section ends.
+     * 
+     * @param level the level of the Header. It should be a value between 1 and 3
+     */
+    public void appendCloseHeaderTag(int level) {
+        if (level > 3 || level < 1) {
+            return;
+        }
+        StringBuilder sb = new StringBuilder("</h");
+        sb.append(level);
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates an image.
+     * 
+     * @param align how text should flow around the picture
+     * @param alt the text to show if you don't show the picture
+     * @param height how tall is the picture
+     * @param src where to get the picture
+     * @param width how wide is the picture
+     */
+    public void appendImageTag(String align, String alt, String height, String src, String width) {
+        StringBuilder sb = new StringBuilder("<img");
+        if (align != null) {
+            sb.append(" align=\"");
+            sb.append(align);
+            sb.append("\"");
+        }
+        if (alt != null) {
+            sb.append(" alt=\"");
+            sb.append(alt);
+            sb.append("\"");
+        }
+        if (height != null) {
+            sb.append(" height=\"");
+            sb.append(height);
+            sb.append("\"");
+        }
+        if (src != null) {
+            sb.append(" src=\"");
+            sb.append(src);
+            sb.append("\"");
+        }
+        if (width != null) {
+            sb.append(" width=\"");
+            sb.append(width);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates the start of a new line item within a list.
+     * 
+     * @param style the style of the line item
+     */
+    public void appendLineItemTag(String style) {
+        StringBuilder sb = new StringBuilder("<li");
+        if (style != null) {
+            sb.append(" style=\"");
+            sb.append(style);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that creates an ordered list. "Ordered" means that the order of the items 
+     * in the list is important. To show this, browsers automatically number the list. 
+     * 
+     * @param style the style of the ordered list
+     */
+    public void appendOpenOrderedListTag(String style) {
+        StringBuilder sb = new StringBuilder("<ol");
+        if (style != null) {
+            sb.append(" style=\"");
+            sb.append(style);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates that an ordered list section ends.
+     * 
+     */
+    public void appendCloseOrderedListTag() {
+        text.append("</ol>");
+    }
+
+    /**
+     * Appends a tag that creates an unordered list. The unordered part means that the items 
+     * in the list are not in any particular order.
+     * 
+     * @param style the style of the unordered list
+     */
+    public void appendOpenUnorderedListTag(String style) {
+        StringBuilder sb = new StringBuilder("<ul");
+        if (style != null) {
+            sb.append(" style=\"");
+            sb.append(style);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates that an unordered list section ends.
+     * 
+     */
+    public void appendCloseUnorderedListTag() {
+        text.append("</ul>");
+    }
+
+    /**
+     * Appends a tag that indicates the start of a new paragraph. This is usually rendered 
+     * with two carriage returns, producing a single blank line in between the two paragraphs.
+     * 
+     * @param style the style of the paragraph
+     */
+    public void appendOpenParagraphTag(String style) {
+        StringBuilder sb = new StringBuilder("<p");
+        if (style != null) {
+            sb.append(" style=\"");
+            sb.append(style);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates the end of a new paragraph. This is usually rendered 
+     * with two carriage returns, producing a single blank line in between the two paragraphs.
+     * 
+     */
+    public void appendCloseParagraphTag() {
+        text.append("</p>");
+    }
+
+    /**
+     * Appends a tag that indicates that an inlined quote section begins.
+     * 
+     * @param style the style of the inlined quote
+     */
+    public void appendOpenInlinedQuoteTag(String style) {
+        StringBuilder sb = new StringBuilder("<q");
+        if (style != null) {
+            sb.append(" style=\"");
+            sb.append(style);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates that an inlined quote section ends.
+     * 
+     */
+    public void appendCloseInlinedQuoteTag() {
+        text.append("</q>");
+    }
+
+    /**
+     * Appends a tag that allows to set the fonts for a span of text.
+     * 
+     * @param style the style for a span of text
+     */
+    public void appendOpenSpanTag(String style) {
+        StringBuilder sb = new StringBuilder("<span");
+        if (style != null) {
+            sb.append(" style=\"");
+            sb.append(style);
+            sb.append("\"");
+        }
+        sb.append(">");
+        text.append(sb.toString());
+    }
+
+    /**
+     * Appends a tag that indicates that a span section ends.
+     * 
+     */
+    public void appendCloseSpanTag() {
+        text.append("</span>");
+    }
+
+    /**
+     * Appends a tag that indicates text which should be more forceful than surrounding text.
+     * 
+     */
+    public void appendOpenStrongTag() {
+        text.append("<strong>");
+    }
+
+    /**
+     * Appends a tag that indicates that a strong section ends.
+     * 
+     */
+    public void appendCloseStrongTag() {
+        text.append("</strong>");
+    }
+
+    /**
+     * Appends a given text to the XHTMLText.
+     * 
+     * @param textToAppend the text to append   
+     */
+    public void append(String textToAppend) {
+        text.append(StringUtils.escapeForXML(textToAppend));
+    }
+
+    /**
+     * Returns the text of the XHTMLText.
+     * 
+     * Note: Automatically adds the closing body tag.
+     * 
+     * @return the text of the XHTMLText   
+     */
+    public String toString() {
+        return text.toString().concat(closeBodyTag());
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/bookmark/BookmarkManager.java b/src/org/jivesoftware/smackx/bookmark/BookmarkManager.java
new file mode 100644
index 0000000..f85cc9c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bookmark/BookmarkManager.java
@@ -0,0 +1,224 @@
+/**
+ * $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.smackx.bookmark;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.PrivateDataManager;
+
+import java.util.*;
+
+/**
+ * Provides methods to manage bookmarks in accordance with JEP-0048. Methods for managing URLs and
+ * Conferences are provided.
+ * </p>
+ * It should be noted that some extensions have been made to the JEP. There is an attribute on URLs
+ * that marks a url as a news feed and also a sub-element can be added to either a URL or conference
+ * indicated that it is shared amongst all users on a server.
+ *
+ * @author Alexander Wenckus
+ */
+public class BookmarkManager {
+    private static final Map<Connection, BookmarkManager> bookmarkManagerMap = new HashMap<Connection, BookmarkManager>();
+    static {
+        PrivateDataManager.addPrivateDataProvider("storage", "storage:bookmarks",
+                new Bookmarks.Provider());
+    }
+
+    /**
+     * Returns the <i>BookmarkManager</i> for a connection, if it doesn't exist it is created.
+     *
+     * @param connection the connection for which the manager is desired.
+     * @return Returns the <i>BookmarkManager</i> for a connection, if it doesn't
+     * exist it is created.
+     * @throws XMPPException Thrown if the connection is null or has not yet been authenticated.
+     */
+    public synchronized static BookmarkManager getBookmarkManager(Connection connection)
+            throws XMPPException
+    {
+        BookmarkManager manager = (BookmarkManager) bookmarkManagerMap.get(connection);
+        if(manager == null) {
+            manager = new BookmarkManager(connection);
+            bookmarkManagerMap.put(connection, manager);
+        }
+        return manager;
+    }
+
+    private PrivateDataManager privateDataManager;
+    private Bookmarks bookmarks;
+    private final Object bookmarkLock = new Object();
+
+    /**
+     * Default constructor. Registers the data provider with the private data manager in the
+     * storage:bookmarks namespace.
+     *
+     * @param connection the connection for persisting and retrieving bookmarks.
+     * @throws XMPPException thrown when the connection is null or has not been authenticated.
+     */
+    private BookmarkManager(Connection connection) throws XMPPException {
+        if(connection == null || !connection.isAuthenticated()) {
+            throw new XMPPException("Invalid connection.");
+        }
+        this.privateDataManager = new PrivateDataManager(connection);
+    }
+
+    /**
+     * Returns all currently bookmarked conferences.
+     *
+     * @return returns all currently bookmarked conferences
+     * @throws XMPPException thrown when there was an error retrieving the current bookmarks from
+     * the server.
+     * @see BookmarkedConference
+     */
+    public Collection<BookmarkedConference> getBookmarkedConferences() throws XMPPException {
+        retrieveBookmarks();
+        return Collections.unmodifiableCollection(bookmarks.getBookmarkedConferences());
+    }
+
+    /**
+     * Adds or updates a conference in the bookmarks.
+     *
+     * @param name the name of the conference
+     * @param jid the jid of the conference
+     * @param isAutoJoin whether or not to join this conference automatically on login
+     * @param nickname the nickname to use for the user when joining the conference
+     * @param password the password to use for the user when joining the conference
+     * @throws XMPPException thrown when there is an issue retrieving the current bookmarks from
+     * the server.
+     */
+    public void addBookmarkedConference(String name, String jid, boolean isAutoJoin,
+            String nickname, String password) throws XMPPException
+    {
+        retrieveBookmarks();
+        BookmarkedConference bookmark
+                = new BookmarkedConference(name, jid, isAutoJoin, nickname, password);
+        List<BookmarkedConference> conferences = bookmarks.getBookmarkedConferences();
+        if(conferences.contains(bookmark)) {
+            BookmarkedConference oldConference = conferences.get(conferences.indexOf(bookmark));
+            if(oldConference.isShared()) {
+                throw new IllegalArgumentException("Cannot modify shared bookmark");
+            }
+            oldConference.setAutoJoin(isAutoJoin);
+            oldConference.setName(name);
+            oldConference.setNickname(nickname);
+            oldConference.setPassword(password);
+        }
+        else {
+            bookmarks.addBookmarkedConference(bookmark);
+        }
+        privateDataManager.setPrivateData(bookmarks);
+    }
+
+    /**
+     * Removes a conference from the bookmarks.
+     *
+     * @param jid the jid of the conference to be removed.
+     * @throws XMPPException thrown when there is a problem with the connection attempting to
+     * retrieve the bookmarks or persist the bookmarks.
+     * @throws IllegalArgumentException thrown when the conference being removed is a shared
+     * conference
+     */
+    public void removeBookmarkedConference(String jid) throws XMPPException {
+        retrieveBookmarks();
+        Iterator<BookmarkedConference> it = bookmarks.getBookmarkedConferences().iterator();
+        while(it.hasNext()) {
+            BookmarkedConference conference = it.next();
+            if(conference.getJid().equalsIgnoreCase(jid)) {
+                if(conference.isShared()) {
+                    throw new IllegalArgumentException("Conference is shared and can't be removed");
+                }
+                it.remove();
+                privateDataManager.setPrivateData(bookmarks);
+                return;
+            }
+        }
+    }
+
+    /**
+     * Returns an unmodifiable collection of all bookmarked urls.
+     *
+     * @return returns an unmodifiable collection of all bookmarked urls.
+     * @throws XMPPException thrown when there is a problem retriving bookmarks from the server.
+     */
+    public Collection<BookmarkedURL> getBookmarkedURLs() throws XMPPException {
+        retrieveBookmarks();
+        return Collections.unmodifiableCollection(bookmarks.getBookmarkedURLS());
+    }
+
+    /**
+     * Adds a new url or updates an already existing url in the bookmarks.
+     *
+     * @param URL the url of the bookmark
+     * @param name the name of the bookmark
+     * @param isRSS whether or not the url is an rss feed
+     * @throws XMPPException thrown when there is an error retriving or saving bookmarks from or to
+     * the server
+     */
+    public void addBookmarkedURL(String URL, String name, boolean isRSS) throws XMPPException {
+        retrieveBookmarks();
+        BookmarkedURL bookmark = new BookmarkedURL(URL, name, isRSS);
+        List<BookmarkedURL> urls = bookmarks.getBookmarkedURLS();
+        if(urls.contains(bookmark)) {
+            BookmarkedURL oldURL = urls.get(urls.indexOf(bookmark));
+            if(oldURL.isShared()) {
+                throw new IllegalArgumentException("Cannot modify shared bookmarks");
+            }
+            oldURL.setName(name);
+            oldURL.setRss(isRSS);
+        }
+        else {
+            bookmarks.addBookmarkedURL(bookmark);
+        }
+        privateDataManager.setPrivateData(bookmarks);
+    }
+
+    /**
+     *  Removes a url from the bookmarks.
+     *
+     * @param bookmarkURL the url of the bookmark to remove
+     * @throws XMPPException thrown if there is an error retriving or saving bookmarks from or to
+     * the server.
+     */
+    public void removeBookmarkedURL(String bookmarkURL) throws XMPPException {
+        retrieveBookmarks();
+        Iterator<BookmarkedURL> it = bookmarks.getBookmarkedURLS().iterator();
+        while(it.hasNext()) {
+            BookmarkedURL bookmark = it.next();
+            if(bookmark.getURL().equalsIgnoreCase(bookmarkURL)) {
+                if(bookmark.isShared()) {
+                    throw new IllegalArgumentException("Cannot delete a shared bookmark.");
+                }
+                it.remove();
+                privateDataManager.setPrivateData(bookmarks);
+                return;
+            }
+        }
+    }
+
+    private Bookmarks retrieveBookmarks() throws XMPPException {
+        synchronized(bookmarkLock) {
+            if(bookmarks == null) {
+                bookmarks = (Bookmarks) privateDataManager.getPrivateData("storage",
+                        "storage:bookmarks");
+            }
+            return bookmarks;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/bookmark/BookmarkedConference.java b/src/org/jivesoftware/smackx/bookmark/BookmarkedConference.java
new file mode 100644
index 0000000..5dac202
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bookmark/BookmarkedConference.java
@@ -0,0 +1,130 @@
+/**
+ * $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.smackx.bookmark;
+
+/**
+ * Respresents a Conference Room bookmarked on the server using JEP-0048 Bookmark Storage JEP.
+ *
+ * @author Derek DeMoro
+ */
+public class BookmarkedConference implements SharedBookmark {
+
+    private String name;
+    private boolean autoJoin;
+    private final String jid;
+
+    private String nickname;
+    private String password;
+    private boolean isShared;
+
+    protected BookmarkedConference(String jid) {
+        this.jid = jid;
+    }
+
+    protected BookmarkedConference(String name, String jid, boolean autoJoin, String nickname,
+            String password)
+    {
+        this.name = name;
+        this.jid = jid;
+        this.autoJoin = autoJoin;
+        this.nickname = nickname;
+        this.password = password;
+    }
+
+
+    /**
+     * Returns the display label representing the Conference room.
+     *
+     * @return the name of the conference room.
+     */
+    public String getName() {
+        return name;
+    }
+
+    protected void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Returns true if this conference room should be auto-joined on startup.
+     *
+     * @return true if room should be joined on startup, otherwise false.
+     */
+    public boolean isAutoJoin() {
+        return autoJoin;
+    }
+
+    protected void setAutoJoin(boolean autoJoin) {
+        this.autoJoin = autoJoin;
+    }
+
+    /**
+     * Returns the full JID of this conference room. (ex.dev@conference.jivesoftware.com)
+     *
+     * @return the full JID of  this conference room.
+     */
+    public String getJid() {
+        return jid;
+    }
+
+    /**
+     * Returns the nickname to use when joining this conference room. This is an optional
+     * value and may return null.
+     *
+     * @return the nickname to use when joining, null may be returned.
+     */
+    public String getNickname() {
+        return nickname;
+    }
+
+    protected void setNickname(String nickname) {
+        this.nickname = nickname;
+    }
+
+    /**
+     * Returns the password to use when joining this conference room. This is an optional
+     * value and may return null.
+     *
+     * @return the password to use when joining this conference room, null may be returned.
+     */
+    public String getPassword() {
+        return password;
+    }
+
+    protected void setPassword(String password) {
+        this.password = password;
+    }
+
+    public boolean equals(Object obj) {
+        if(obj == null || !(obj instanceof BookmarkedConference)) {
+            return false;
+        }
+        BookmarkedConference conference = (BookmarkedConference)obj;
+        return conference.getJid().equalsIgnoreCase(jid);
+    }
+
+    protected void setShared(boolean isShared) {
+        this.isShared = isShared;
+    }
+
+    public boolean isShared() {
+        return isShared;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/bookmark/BookmarkedURL.java b/src/org/jivesoftware/smackx/bookmark/BookmarkedURL.java
new file mode 100644
index 0000000..f3d6d9d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bookmark/BookmarkedURL.java
@@ -0,0 +1,104 @@
+/**
+ * $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.smackx.bookmark;
+
+/**
+ * Respresents one instance of a URL defined using JEP-0048 Bookmark Storage JEP.
+ *
+ * @author Derek DeMoro
+ */
+public class BookmarkedURL implements SharedBookmark {
+
+    private String name;
+    private final String URL;
+    private boolean isRss;
+    private boolean isShared;
+
+    protected BookmarkedURL(String URL) {
+        this.URL = URL;
+    }
+
+    protected BookmarkedURL(String URL, String name, boolean isRss) {
+        this.URL = URL;
+        this.name = name;
+        this.isRss = isRss;
+    }
+
+    /**
+     * Returns the name representing the URL (eg. Jive Software). This can be used in as a label, or
+     * identifer in applications.
+     *
+     * @return the name reprenting the URL.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the name representing the URL.
+     *
+     * @param name the name.
+     */
+    protected void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Returns the URL.
+     *
+     * @return the url.
+     */
+    public String getURL() {
+        return URL;
+    }
+    /**
+     * Set to true if this URL is an RSS or news feed.
+     *
+     * @param isRss True if the URL is a news feed and false if it is not.
+     */
+    protected void setRss(boolean isRss) {
+        this.isRss = isRss;
+    }
+
+    /**
+     * Returns true if this URL is a news feed.
+     *
+     * @return Returns true if this URL is a news feed.
+     */
+    public boolean isRss() {
+        return isRss;
+    }
+
+    public boolean equals(Object obj) {
+        if(!(obj instanceof BookmarkedURL)) {
+            return false;
+        }
+        BookmarkedURL url = (BookmarkedURL)obj;
+        return url.getURL().equalsIgnoreCase(URL);
+    }
+
+    protected void setShared(boolean shared) {
+        this.isShared = shared;
+    }
+
+    public boolean isShared() {
+        return isShared;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/bookmark/Bookmarks.java b/src/org/jivesoftware/smackx/bookmark/Bookmarks.java
new file mode 100644
index 0000000..100fa46
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bookmark/Bookmarks.java
@@ -0,0 +1,310 @@
+/**
+ * $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.smackx.bookmark;
+
+import org.jivesoftware.smackx.packet.PrivateData;
+import org.jivesoftware.smackx.provider.PrivateDataProvider;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Bookmarks is used for storing and retrieving URLS and Conference rooms.
+ * Bookmark Storage (JEP-0048) defined a protocol for the storage of bookmarks to conference rooms and other entities
+ * in a Jabber user's account.
+ * See the following code sample for saving Bookmarks:
+ * <p/>
+ * <pre>
+ * Connection con = new XMPPConnection("jabber.org");
+ * con.login("john", "doe");
+ * Bookmarks bookmarks = new Bookmarks();
+ * <p/>
+ * // Bookmark a URL
+ * BookmarkedURL url = new BookmarkedURL();
+ * url.setName("Google");
+ * url.setURL("http://www.jivesoftware.com");
+ * bookmarks.addURL(url);
+ * // Bookmark a Conference room.
+ * BookmarkedConference conference = new BookmarkedConference();
+ * conference.setName("My Favorite Room");
+ * conference.setAutoJoin("true");
+ * conference.setJID("dev@conference.jivesoftware.com");
+ * bookmarks.addConference(conference);
+ * // Save Bookmarks using PrivateDataManager.
+ * PrivateDataManager manager = new PrivateDataManager(con);
+ * manager.setPrivateData(bookmarks);
+ * <p/>
+ * <p/>
+ * LastActivity activity = LastActivity.getLastActivity(con, "xray@jabber.org");
+ * </pre>
+ *
+ * @author Derek DeMoro
+ */
+public class Bookmarks implements PrivateData {
+
+    private List<BookmarkedURL> bookmarkedURLS;
+    private List<BookmarkedConference> bookmarkedConferences;
+
+    /**
+     * Required Empty Constructor to use Bookmarks.
+     */
+    public Bookmarks() {
+        bookmarkedURLS = new ArrayList<BookmarkedURL>();
+        bookmarkedConferences = new ArrayList<BookmarkedConference>();
+    }
+
+    /**
+     * Adds a BookmarkedURL.
+     *
+     * @param bookmarkedURL the bookmarked bookmarkedURL.
+     */
+    public void addBookmarkedURL(BookmarkedURL bookmarkedURL) {
+        bookmarkedURLS.add(bookmarkedURL);
+    }
+
+    /**
+     * Removes a bookmarked bookmarkedURL.
+     *
+     * @param bookmarkedURL the bookmarked bookmarkedURL to remove.
+     */
+    public void removeBookmarkedURL(BookmarkedURL bookmarkedURL) {
+        bookmarkedURLS.remove(bookmarkedURL);
+    }
+
+    /**
+     * Removes all BookmarkedURLs from user's bookmarks.
+     */
+    public void clearBookmarkedURLS() {
+        bookmarkedURLS.clear();
+    }
+
+    /**
+     * Add a BookmarkedConference to bookmarks.
+     *
+     * @param bookmarkedConference the conference to remove.
+     */
+    public void addBookmarkedConference(BookmarkedConference bookmarkedConference) {
+        bookmarkedConferences.add(bookmarkedConference);
+    }
+
+    /**
+     * Removes a BookmarkedConference.
+     *
+     * @param bookmarkedConference the BookmarkedConference to remove.
+     */
+    public void removeBookmarkedConference(BookmarkedConference bookmarkedConference) {
+        bookmarkedConferences.remove(bookmarkedConference);
+    }
+
+    /**
+     * Removes all BookmarkedConferences from Bookmarks.
+     */
+    public void clearBookmarkedConferences() {
+        bookmarkedConferences.clear();
+    }
+
+    /**
+     * Returns a Collection of all Bookmarked URLs for this user.
+     *
+     * @return a collection of all Bookmarked URLs.
+     */
+    public List<BookmarkedURL> getBookmarkedURLS() {
+        return bookmarkedURLS;
+    }
+
+    /**
+     * Returns a Collection of all Bookmarked Conference for this user.
+     *
+     * @return a collection of all Bookmarked Conferences.
+     */
+    public List<BookmarkedConference> getBookmarkedConferences() {
+        return bookmarkedConferences;
+    }
+
+
+    /**
+     * Returns the root element name.
+     *
+     * @return the element name.
+     */
+    public String getElementName() {
+        return "storage";
+    }
+
+    /**
+     * Returns the root element XML namespace.
+     *
+     * @return the namespace.
+     */
+    public String getNamespace() {
+        return "storage:bookmarks";
+    }
+
+    /**
+     * Returns the XML reppresentation of the PrivateData.
+     *
+     * @return the private data as XML.
+     */
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<storage xmlns=\"storage:bookmarks\">");
+
+        final Iterator<BookmarkedURL> urls = getBookmarkedURLS().iterator();
+        while (urls.hasNext()) {
+            BookmarkedURL urlStorage = urls.next();
+            if(urlStorage.isShared()) {
+                continue;
+            }
+            buf.append("<url name=\"").append(urlStorage.getName()).
+                    append("\" url=\"").append(urlStorage.getURL()).append("\"");
+            if(urlStorage.isRss()) {
+                buf.append(" rss=\"").append(true).append("\"");
+            }
+            buf.append(" />");
+        }
+
+        // Add Conference additions
+        final Iterator<BookmarkedConference> conferences = getBookmarkedConferences().iterator();
+        while (conferences.hasNext()) {
+            BookmarkedConference conference = conferences.next();
+            if(conference.isShared()) {
+                continue;
+            }
+            buf.append("<conference ");
+            buf.append("name=\"").append(conference.getName()).append("\" ");
+            buf.append("autojoin=\"").append(conference.isAutoJoin()).append("\" ");
+            buf.append("jid=\"").append(conference.getJid()).append("\" ");
+            buf.append(">");
+
+            if (conference.getNickname() != null) {
+                buf.append("<nick>").append(conference.getNickname()).append("</nick>");
+            }
+
+
+            if (conference.getPassword() != null) {
+                buf.append("<password>").append(conference.getPassword()).append("</password>");
+            }
+            buf.append("</conference>");
+        }
+
+
+        buf.append("</storage>");
+        return buf.toString();
+    }
+
+    /**
+     * The IQ Provider for BookmarkStorage.
+     *
+     * @author Derek DeMoro
+     */
+    public static class Provider implements PrivateDataProvider {
+
+        /**
+         * Empty Constructor for PrivateDataProvider.
+         */
+        public Provider() {
+            super();
+        }
+
+        public PrivateData parsePrivateData(XmlPullParser parser) throws Exception {
+            Bookmarks storage = new Bookmarks();
+
+            boolean done = false;
+            while (!done) {
+                int eventType = parser.next();
+                if (eventType == XmlPullParser.START_TAG && "url".equals(parser.getName())) {
+                    final BookmarkedURL urlStorage = getURLStorage(parser);
+                    if (urlStorage != null) {
+                        storage.addBookmarkedURL(urlStorage);
+                    }
+                }
+                else if (eventType == XmlPullParser.START_TAG &&
+                        "conference".equals(parser.getName()))
+                {
+                    final BookmarkedConference conference = getConferenceStorage(parser);
+                    storage.addBookmarkedConference(conference);
+                }
+                else if (eventType == XmlPullParser.END_TAG && "storage".equals(parser.getName()))
+                {
+                    done = true;
+                }
+            }
+
+
+            return storage;
+        }
+    }
+
+    private static BookmarkedURL getURLStorage(XmlPullParser parser) throws IOException, XmlPullParserException {
+        String name = parser.getAttributeValue("", "name");
+        String url = parser.getAttributeValue("", "url");
+        String rssString = parser.getAttributeValue("", "rss");
+        boolean rss = rssString != null && "true".equals(rssString);
+
+        BookmarkedURL urlStore = new BookmarkedURL(url, name, rss);
+        boolean done = false;
+        while (!done) {
+            int eventType = parser.next();
+            if(eventType == XmlPullParser.START_TAG
+                        && "shared_bookmark".equals(parser.getName())) {
+                    urlStore.setShared(true);
+            }
+            else if (eventType == XmlPullParser.END_TAG && "url".equals(parser.getName())) {
+                done = true;
+            }
+        }
+        return urlStore;
+    }
+
+    private static BookmarkedConference getConferenceStorage(XmlPullParser parser) throws Exception {
+        String name = parser.getAttributeValue("", "name");
+        String autojoin = parser.getAttributeValue("", "autojoin");
+        String jid = parser.getAttributeValue("", "jid");
+
+        BookmarkedConference conf = new BookmarkedConference(jid);
+        conf.setName(name);
+        conf.setAutoJoin(Boolean.valueOf(autojoin).booleanValue());
+
+        // Check for nickname
+        boolean done = false;
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG && "nick".equals(parser.getName())) {
+                conf.setNickname(parser.nextText());
+            }
+            else if (eventType == XmlPullParser.START_TAG && "password".equals(parser.getName())) {
+                conf.setPassword(parser.nextText());
+            }
+            else if(eventType == XmlPullParser.START_TAG
+                        && "shared_bookmark".equals(parser.getName())) {
+                    conf.setShared(true);
+            }
+            else if (eventType == XmlPullParser.END_TAG && "conference".equals(parser.getName())) {
+                done = true;
+            }
+        }
+
+
+        return conf;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/bookmark/SharedBookmark.java b/src/org/jivesoftware/smackx/bookmark/SharedBookmark.java
new file mode 100644
index 0000000..f672bc1
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bookmark/SharedBookmark.java
@@ -0,0 +1,35 @@
+/**
+ * $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.smackx.bookmark;
+
+/**
+ *  Interface to indicate if a bookmark is shared across the server.
+ *
+ * @author Alexander Wenckus
+ */
+public interface SharedBookmark {
+
+    /**
+     * Returns true if this bookmark is shared.
+     *
+     * @return returns true if this bookmark is shared.
+     */
+    public boolean isShared();
+}
diff --git a/src/org/jivesoftware/smackx/bytestreams/BytestreamListener.java b/src/org/jivesoftware/smackx/bytestreams/BytestreamListener.java
new file mode 100644
index 0000000..be78255
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/BytestreamListener.java
@@ -0,0 +1,47 @@
+/**

+ * 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.smackx.bytestreams;

+

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamListener;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;

+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamListener;

+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;

+

+/**

+ * BytestreamListener are notified if a remote user wants to initiate a bytestream. Implement this

+ * interface to handle incoming bytestream requests.

+ * <p>

+ * BytestreamListener can be registered at the {@link Socks5BytestreamManager} or the

+ * {@link InBandBytestreamManager}.

+ * <p>

+ * There are two ways to add this listener. See

+ * {@link BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and

+ * {@link BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for further

+ * details.

+ * <p>

+ * {@link Socks5BytestreamListener} or {@link InBandBytestreamListener} provide a more specific

+ * interface of the BytestreamListener.

+ * 

+ * @author Henning Staib

+ */

+public interface BytestreamListener {

+

+    /**

+     * This listener is notified if a bytestream request from another user has been received.

+     * 

+     * @param request the incoming bytestream request

+     */

+    public void incomingBytestreamRequest(BytestreamRequest request);

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/BytestreamManager.java b/src/org/jivesoftware/smackx/bytestreams/BytestreamManager.java
new file mode 100644
index 0000000..ca6bbc6
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/BytestreamManager.java
@@ -0,0 +1,114 @@
+/**

+ * 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.smackx.bytestreams;

+

+import java.io.IOException;

+

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;

+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;

+

+/**

+ * BytestreamManager provides a generic interface for bytestream managers.

+ * <p>

+ * There are two implementations of the interface. See {@link Socks5BytestreamManager} and

+ * {@link InBandBytestreamManager}.

+ * 

+ * @author Henning Staib

+ */

+public interface BytestreamManager {

+

+    /**

+     * Adds {@link BytestreamListener} that is called for every incoming bytestream request unless

+     * there is a user specific {@link BytestreamListener} registered.

+     * <p>

+     * See {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and

+     * {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener)} for further

+     * details.

+     * 

+     * @param listener the listener to register

+     */

+    public void addIncomingBytestreamListener(BytestreamListener listener);

+

+    /**

+     * Removes the given listener from the list of listeners for all incoming bytestream requests.

+     * 

+     * @param listener the listener to remove

+     */

+    public void removeIncomingBytestreamListener(BytestreamListener listener);

+

+    /**

+     * Adds {@link BytestreamListener} that is called for every incoming bytestream request unless

+     * there is a user specific {@link BytestreamListener} registered.

+     * <p>

+     * Use this method if you are awaiting an incoming bytestream request from a specific user.

+     * <p>

+     * See {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)}

+     * and {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)}

+     * for further details.

+     * 

+     * @param listener the listener to register

+     * @param initiatorJID the JID of the user that wants to establish a bytestream

+     */

+    public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID);

+

+    /**

+     * Removes the listener for the given user.

+     * 

+     * @param initiatorJID the JID of the user the listener should be removed

+     */

+    public void removeIncomingBytestreamListener(String initiatorJID);

+

+    /**

+     * Establishes a bytestream with the given user and returns the session to send/receive data

+     * to/from the user.

+     * <p>

+     * Use this method to establish bytestreams to users accepting all incoming bytestream requests

+     * since this method doesn't provide a way to tell the user something about the data to be sent.

+     * <p>

+     * To establish a bytestream after negotiation the kind of data to be sent (e.g. file transfer)

+     * use {@link #establishSession(String, String)}.

+     * <p>

+     * See {@link Socks5BytestreamManager#establishSession(String)} and

+     * {@link InBandBytestreamManager#establishSession(String)} for further details.

+     * 

+     * @param targetJID the JID of the user a bytestream should be established

+     * @return the session to send/receive data to/from the user

+     * @throws XMPPException if an error occurred while establishing the session

+     * @throws IOException if an IO error occurred while establishing the session

+     * @throws InterruptedException if the thread was interrupted while waiting in a blocking

+     *         operation

+     */

+    public BytestreamSession establishSession(String targetJID) throws XMPPException, IOException,

+                    InterruptedException;

+

+    /**

+     * Establishes a bytestream with the given user and returns the session to send/receive data

+     * to/from the user.

+     * <p>

+     * See {@link Socks5BytestreamManager#establishSession(String)} and

+     * {@link InBandBytestreamManager#establishSession(String)} for further details.

+     * 

+     * @param targetJID the JID of the user a bytestream should be established

+     * @param sessionID the session ID for the bytestream request

+     * @return the session to send/receive data to/from the user

+     * @throws XMPPException if an error occurred while establishing the session

+     * @throws IOException if an IO error occurred while establishing the session

+     * @throws InterruptedException if the thread was interrupted while waiting in a blocking

+     *         operation

+     */

+    public BytestreamSession establishSession(String targetJID, String sessionID)

+                    throws XMPPException, IOException, InterruptedException;

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java b/src/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java
new file mode 100644
index 0000000..e368bad
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/BytestreamRequest.java
@@ -0,0 +1,59 @@
+/**

+ * 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.smackx.bytestreams;

+

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamRequest;

+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamRequest;

+

+/**

+ * BytestreamRequest provides an interface to handle incoming bytestream requests.

+ * <p>

+ * There are two implementations of the interface. See {@link Socks5BytestreamRequest} and

+ * {@link InBandBytestreamRequest}.

+ * 

+ * @author Henning Staib

+ */

+public interface BytestreamRequest {

+

+    /**

+     * Returns the sender of the bytestream open request.

+     * 

+     * @return the sender of the bytestream open request

+     */

+    public String getFrom();

+

+    /**

+     * Returns the session ID of the bytestream open request.

+     * 

+     * @return the session ID of the bytestream open request

+     */

+    public String getSessionID();

+

+    /**

+     * Accepts the bytestream open request and returns the session to send/receive data.

+     * 

+     * @return the session to send/receive data

+     * @throws XMPPException if an error occurred while accepting the bytestream request

+     * @throws InterruptedException if the thread was interrupted while waiting in a blocking

+     *         operation

+     */

+    public BytestreamSession accept() throws XMPPException, InterruptedException;

+

+    /**

+     * Rejects the bytestream request by sending a reject error to the initiator.

+     */

+    public void reject();

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/BytestreamSession.java b/src/org/jivesoftware/smackx/bytestreams/BytestreamSession.java
new file mode 100644
index 0000000..7aafc35
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/BytestreamSession.java
@@ -0,0 +1,81 @@
+/**

+ * 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.smackx.bytestreams;

+

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.OutputStream;

+

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession;

+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession;

+

+/**

+ * BytestreamSession provides an interface for established bytestream sessions.

+ * <p>

+ * There are two implementations of the interface. See {@link Socks5BytestreamSession} and

+ * {@link InBandBytestreamSession}.

+ * 

+ * @author Henning Staib

+ */

+public interface BytestreamSession {

+

+    /**

+     * Returns the InputStream associated with this session to send data.

+     * 

+     * @return the InputStream associated with this session to send data

+     * @throws IOException if an error occurs while retrieving the input stream

+     */

+    public InputStream getInputStream() throws IOException;

+

+    /**

+     * Returns the OutputStream associated with this session to receive data.

+     * 

+     * @return the OutputStream associated with this session to receive data

+     * @throws IOException if an error occurs while retrieving the output stream

+     */

+    public OutputStream getOutputStream() throws IOException;

+

+    /**

+     * Closes the bytestream session.

+     * <p>

+     * Closing the session will also close the input stream and the output stream associated to this

+     * session.

+     * 

+     * @throws IOException if an error occurs while closing the session

+     */

+    public void close() throws IOException;

+

+    /**

+     * Returns the timeout for read operations of the input stream associated with this session. 0

+     * returns implies that the option is disabled (i.e., timeout of infinity). Default is 0.

+     * 

+     * @return the timeout for read operations

+     * @throws IOException if there is an error in the underlying protocol

+     */

+    public int getReadTimeout() throws IOException;

+

+    /**

+     * Sets the specified timeout, in milliseconds. With this option set to a non-zero timeout, a

+     * read() call on the input stream associated with this session will block for only this amount

+     * of time. If the timeout expires, a java.net.SocketTimeoutException is raised, though the

+     * session is still valid. The option must be enabled prior to entering the blocking operation

+     * to have effect. The timeout must be > 0. A timeout of zero is interpreted as an infinite

+     * timeout. Default is 0.

+     * 

+     * @param timeout the specified timeout, in milliseconds

+     * @throws IOException if there is an error in the underlying protocol

+     */

+    public void setReadTimeout(int timeout) throws IOException;

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/CloseListener.java b/src/org/jivesoftware/smackx/bytestreams/ibb/CloseListener.java
new file mode 100644
index 0000000..7690e95
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/CloseListener.java
@@ -0,0 +1,75 @@
+/**

+ * 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.smackx.bytestreams.ibb;

+

+import org.jivesoftware.smack.PacketListener;

+import org.jivesoftware.smack.filter.AndFilter;

+import org.jivesoftware.smack.filter.IQTypeFilter;

+import org.jivesoftware.smack.filter.PacketFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;

+

+/**

+ * CloseListener handles all In-Band Bytestream close requests.

+ * <p>

+ * If a close request is received it looks if a stored In-Band Bytestream

+ * session exists and closes it. If no session with the given session ID exists

+ * an &lt;item-not-found/&gt; error is returned to the sender.

+ * 

+ * @author Henning Staib

+ */

+class CloseListener implements PacketListener {

+

+    /* manager containing the listeners and the XMPP connection */

+    private final InBandBytestreamManager manager;

+

+    /* packet filter for all In-Band Bytestream close requests */

+    private final PacketFilter closeFilter = new AndFilter(new PacketTypeFilter(

+                    Close.class), new IQTypeFilter(IQ.Type.SET));

+

+    /**

+     * Constructor.

+     * 

+     * @param manager the In-Band Bytestream manager

+     */

+    protected CloseListener(InBandBytestreamManager manager) {

+        this.manager = manager;

+    }

+

+    public void processPacket(Packet packet) {

+        Close closeRequest = (Close) packet;

+        InBandBytestreamSession ibbSession = this.manager.getSessions().get(

+                        closeRequest.getSessionID());

+        if (ibbSession == null) {

+            this.manager.replyItemNotFoundPacket(closeRequest);

+        }

+        else {

+            ibbSession.closeByPeer(closeRequest);

+            this.manager.getSessions().remove(closeRequest.getSessionID());

+        }

+

+    }

+

+    /**

+     * Returns the packet filter for In-Band Bytestream close requests.

+     * 

+     * @return the packet filter for In-Band Bytestream close requests

+     */

+    protected PacketFilter getFilter() {

+        return this.closeFilter;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/DataListener.java b/src/org/jivesoftware/smackx/bytestreams/ibb/DataListener.java
new file mode 100644
index 0000000..166c146
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/DataListener.java
@@ -0,0 +1,73 @@
+/**

+ * 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.smackx.bytestreams.ibb;

+

+import org.jivesoftware.smack.PacketListener;

+import org.jivesoftware.smack.filter.AndFilter;

+import org.jivesoftware.smack.filter.PacketFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;

+

+/**

+ * DataListener handles all In-Band Bytestream IQ stanzas containing a data

+ * packet extension that don't belong to an existing session.

+ * <p>

+ * If a data packet is received it looks if a stored In-Band Bytestream session

+ * exists. If no session with the given session ID exists an

+ * &lt;item-not-found/&gt; error is returned to the sender.

+ * <p>

+ * Data packets belonging to a running In-Band Bytestream session are processed

+ * by more specific listeners registered when an {@link InBandBytestreamSession}

+ * is created.

+ * 

+ * @author Henning Staib

+ */

+class DataListener implements PacketListener {

+

+    /* manager containing the listeners and the XMPP connection */

+    private final InBandBytestreamManager manager;

+

+    /* packet filter for all In-Band Bytestream data packets */

+    private final PacketFilter dataFilter = new AndFilter(

+                    new PacketTypeFilter(Data.class));

+

+    /**

+     * Constructor.

+     * 

+     * @param manager the In-Band Bytestream manager

+     */

+    public DataListener(InBandBytestreamManager manager) {

+        this.manager = manager;

+    }

+

+    public void processPacket(Packet packet) {

+        Data data = (Data) packet;

+        InBandBytestreamSession ibbSession = this.manager.getSessions().get(

+                        data.getDataPacketExtension().getSessionID());

+        if (ibbSession == null) {

+            this.manager.replyItemNotFoundPacket(data);

+        }

+    }

+

+    /**

+     * Returns the packet filter for In-Band Bytestream data packets.

+     * 

+     * @return the packet filter for In-Band Bytestream data packets

+     */

+    protected PacketFilter getFilter() {

+        return this.dataFilter;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamListener.java b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamListener.java
new file mode 100644
index 0000000..68791a6
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamListener.java
@@ -0,0 +1,46 @@
+/**

+ * 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.smackx.bytestreams.ibb;

+

+import org.jivesoftware.smackx.bytestreams.BytestreamListener;

+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;

+

+/**

+ * InBandBytestreamListener are informed if a remote user wants to initiate an In-Band Bytestream.

+ * Implement this interface to handle incoming In-Band Bytestream requests.

+ * <p>

+ * There are two ways to add this listener. See

+ * {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and

+ * {@link InBandBytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for

+ * further details.

+ * 

+ * @author Henning Staib

+ */

+public abstract class InBandBytestreamListener implements BytestreamListener {

+

+    

+    

+    public void incomingBytestreamRequest(BytestreamRequest request) {

+        incomingBytestreamRequest((InBandBytestreamRequest) request);

+    }

+

+    /**

+     * This listener is notified if an In-Band Bytestream request from another user has been

+     * received.

+     * 

+     * @param request the incoming In-Band Bytestream request

+     */

+    public abstract void incomingBytestreamRequest(InBandBytestreamRequest request);

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java
new file mode 100644
index 0000000..ef52531
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamManager.java
@@ -0,0 +1,546 @@
+/**

+ * 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.smackx.bytestreams.ibb;

+

+import java.util.Collections;

+import java.util.HashMap;

+import java.util.LinkedList;

+import java.util.List;

+import java.util.Map;

+import java.util.Random;

+import java.util.concurrent.ConcurrentHashMap;

+

+import org.jivesoftware.smack.AbstractConnectionListener;

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.ConnectionCreationListener;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.XMPPError;

+import org.jivesoftware.smack.util.SyncPacketSend;

+import org.jivesoftware.smackx.bytestreams.BytestreamListener;

+import org.jivesoftware.smackx.bytestreams.BytestreamManager;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;

+import org.jivesoftware.smackx.filetransfer.FileTransferManager;

+

+/**

+ * The InBandBytestreamManager class handles establishing In-Band Bytestreams as specified in the <a

+ * href="http://xmpp.org/extensions/xep-0047.html">XEP-0047</a>.

+ * <p>

+ * The In-Band Bytestreams (IBB) enables two entities to establish a virtual bytestream over which

+ * they can exchange Base64-encoded chunks of data over XMPP itself. It is the fall-back mechanism

+ * in case the Socks5 bytestream method of transferring data is not available.

+ * <p>

+ * There are two ways to send data over an In-Band Bytestream. It could either use IQ stanzas to

+ * send data packets or message stanzas. If IQ stanzas are used every data packet is acknowledged by

+ * the receiver. This is the recommended way to avoid possible rate-limiting penalties. Message

+ * stanzas are not acknowledged because most XMPP server implementation don't support stanza

+ * flow-control method like <a href="http://xmpp.org/extensions/xep-0079.html">Advanced Message

+ * Processing</a>. To set the stanza that should be used invoke {@link #setStanza(StanzaType)}.

+ * <p>

+ * To establish an In-Band Bytestream invoke the {@link #establishSession(String)} method. This will

+ * negotiate an in-band bytestream with the given target JID and return a session.

+ * <p>

+ * If a session ID for the In-Band Bytestream was already negotiated (e.g. while negotiating a file

+ * transfer) invoke {@link #establishSession(String, String)}.

+ * <p>

+ * To handle incoming In-Band Bytestream requests add an {@link InBandBytestreamListener} to the

+ * manager. There are two ways to add this listener. If you want to be informed about incoming

+ * In-Band Bytestreams from a specific user add the listener by invoking

+ * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should

+ * respond to all In-Band Bytestream requests invoke

+ * {@link #addIncomingBytestreamListener(BytestreamListener)}.

+ * <p>

+ * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming

+ * In-Band bytestream requests sent in the context of <a

+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See

+ * {@link FileTransferManager})

+ * <p>

+ * If no {@link InBandBytestreamListener}s are registered, all incoming In-Band bytestream requests

+ * will be rejected by returning a &lt;not-acceptable/&gt; error to the initiator.

+ * 

+ * @author Henning Staib

+ */

+public class InBandBytestreamManager implements BytestreamManager {

+

+    /**

+     * Stanzas that can be used to encapsulate In-Band Bytestream data packets.

+     */

+    public enum StanzaType {

+

+        /**

+         * IQ stanza.

+         */

+        IQ,

+

+        /**

+         * Message stanza.

+         */

+        MESSAGE

+    }

+

+    /*

+     * create a new InBandBytestreamManager and register its shutdown listener on every established

+     * connection

+     */

+    static {

+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {

+            public void connectionCreated(Connection connection) {

+                final InBandBytestreamManager manager;

+                manager = InBandBytestreamManager.getByteStreamManager(connection);

+

+                // register shutdown listener

+                connection.addConnectionListener(new AbstractConnectionListener() {

+

+                    public void connectionClosed() {

+                        manager.disableService();

+                    }

+

+                });

+

+            }

+        });

+    }

+

+    /**

+     * The XMPP namespace of the In-Band Bytestream

+     */

+    public static final String NAMESPACE = "http://jabber.org/protocol/ibb";

+

+    /**

+     * Maximum block size that is allowed for In-Band Bytestreams

+     */

+    public static final int MAXIMUM_BLOCK_SIZE = 65535;

+

+    /* prefix used to generate session IDs */

+    private static final String SESSION_ID_PREFIX = "jibb_";

+

+    /* random generator to create session IDs */

+    private final static Random randomGenerator = new Random();

+

+    /* stores one InBandBytestreamManager for each XMPP connection */

+    private final static Map<Connection, InBandBytestreamManager> managers = new HashMap<Connection, InBandBytestreamManager>();

+

+    /* XMPP connection */

+    private final Connection connection;

+

+    /*

+     * assigns a user to a listener that is informed if an In-Band Bytestream request for this user

+     * is received

+     */

+    private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();

+

+    /*

+     * list of listeners that respond to all In-Band Bytestream requests if there are no user

+     * specific listeners for that request

+     */

+    private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());

+

+    /* listener that handles all incoming In-Band Bytestream requests */

+    private final InitiationListener initiationListener;

+

+    /* listener that handles all incoming In-Band Bytestream IQ data packets */

+    private final DataListener dataListener;

+

+    /* listener that handles all incoming In-Band Bytestream close requests */

+    private final CloseListener closeListener;

+

+    /* assigns a session ID to the In-Band Bytestream session */

+    private final Map<String, InBandBytestreamSession> sessions = new ConcurrentHashMap<String, InBandBytestreamSession>();

+

+    /* block size used for new In-Band Bytestreams */

+    private int defaultBlockSize = 4096;

+

+    /* maximum block size allowed for this connection */

+    private int maximumBlockSize = MAXIMUM_BLOCK_SIZE;

+

+    /* the stanza used to send data packets */

+    private StanzaType stanza = StanzaType.IQ;

+

+    /*

+     * list containing session IDs of In-Band Bytestream open packets that should be ignored by the

+     * InitiationListener

+     */

+    private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());

+

+    /**

+     * Returns the InBandBytestreamManager to handle In-Band Bytestreams for a given

+     * {@link Connection}.

+     * 

+     * @param connection the XMPP connection

+     * @return the InBandBytestreamManager for the given XMPP connection

+     */

+    public static synchronized InBandBytestreamManager getByteStreamManager(Connection connection) {

+        if (connection == null)

+            return null;

+        InBandBytestreamManager manager = managers.get(connection);

+        if (manager == null) {

+            manager = new InBandBytestreamManager(connection);

+            managers.put(connection, manager);

+        }

+        return manager;

+    }

+

+    /**

+     * Constructor.

+     * 

+     * @param connection the XMPP connection

+     */

+    private InBandBytestreamManager(Connection connection) {

+        this.connection = connection;

+

+        // register bytestream open packet listener

+        this.initiationListener = new InitiationListener(this);

+        this.connection.addPacketListener(this.initiationListener,

+                        this.initiationListener.getFilter());

+

+        // register bytestream data packet listener

+        this.dataListener = new DataListener(this);

+        this.connection.addPacketListener(this.dataListener, this.dataListener.getFilter());

+

+        // register bytestream close packet listener

+        this.closeListener = new CloseListener(this);

+        this.connection.addPacketListener(this.closeListener, this.closeListener.getFilter());

+

+    }

+

+    /**

+     * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request

+     * unless there is a user specific InBandBytestreamListener registered.

+     * <p>

+     * If no listeners are registered all In-Band Bytestream request are rejected with a

+     * &lt;not-acceptable/&gt; error.

+     * <p>

+     * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming

+     * Socks5 bytestream requests sent in the context of <a

+     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See

+     * {@link FileTransferManager})

+     * 

+     * @param listener the listener to register

+     */

+    public void addIncomingBytestreamListener(BytestreamListener listener) {

+        this.allRequestListeners.add(listener);

+    }

+

+    /**

+     * Removes the given listener from the list of listeners for all incoming In-Band Bytestream

+     * requests.

+     * 

+     * @param listener the listener to remove

+     */

+    public void removeIncomingBytestreamListener(BytestreamListener listener) {

+        this.allRequestListeners.remove(listener);

+    }

+

+    /**

+     * Adds InBandBytestreamListener that is called for every incoming in-band bytestream request

+     * from the given user.

+     * <p>

+     * Use this method if you are awaiting an incoming Socks5 bytestream request from a specific

+     * user.

+     * <p>

+     * If no listeners are registered all In-Band Bytestream request are rejected with a

+     * &lt;not-acceptable/&gt; error.

+     * <p>

+     * Note that the registered {@link InBandBytestreamListener} will NOT be notified on incoming

+     * Socks5 bytestream requests sent in the context of <a

+     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See

+     * {@link FileTransferManager})

+     * 

+     * @param listener the listener to register

+     * @param initiatorJID the JID of the user that wants to establish an In-Band Bytestream

+     */

+    public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {

+        this.userListeners.put(initiatorJID, listener);

+    }

+

+    /**

+     * Removes the listener for the given user.

+     * 

+     * @param initiatorJID the JID of the user the listener should be removed

+     */

+    public void removeIncomingBytestreamListener(String initiatorJID) {

+        this.userListeners.remove(initiatorJID);

+    }

+

+    /**

+     * Use this method to ignore the next incoming In-Band Bytestream request containing the given

+     * session ID. No listeners will be notified for this request and and no error will be returned

+     * to the initiator.

+     * <p>

+     * This method should be used if you are awaiting an In-Band Bytestream request as a reply to

+     * another packet (e.g. file transfer).

+     * 

+     * @param sessionID to be ignored

+     */

+    public void ignoreBytestreamRequestOnce(String sessionID) {

+        this.ignoredBytestreamRequests.add(sessionID);

+    }

+

+    /**

+     * Returns the default block size that is used for all outgoing in-band bytestreams for this

+     * connection.

+     * <p>

+     * The recommended default block size is 4096 bytes. See <a

+     * href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a> Section 5.

+     * 

+     * @return the default block size

+     */

+    public int getDefaultBlockSize() {

+        return defaultBlockSize;

+    }

+

+    /**

+     * Sets the default block size that is used for all outgoing in-band bytestreams for this

+     * connection.

+     * <p>

+     * The default block size must be between 1 and 65535 bytes. The recommended default block size

+     * is 4096 bytes. See <a href="http://xmpp.org/extensions/xep-0047.html#usage">XEP-0047</a>

+     * Section 5.

+     * 

+     * @param defaultBlockSize the default block size to set

+     */

+    public void setDefaultBlockSize(int defaultBlockSize) {

+        if (defaultBlockSize <= 0 || defaultBlockSize > MAXIMUM_BLOCK_SIZE) {

+            throw new IllegalArgumentException("Default block size must be between 1 and "

+                            + MAXIMUM_BLOCK_SIZE);

+        }

+        this.defaultBlockSize = defaultBlockSize;

+    }

+

+    /**

+     * Returns the maximum block size that is allowed for In-Band Bytestreams for this connection.

+     * <p>

+     * Incoming In-Band Bytestream open request will be rejected with an

+     * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed

+     * block size.

+     * <p>

+     * The default maximum block size is 65535 bytes.

+     * 

+     * @return the maximum block size

+     */

+    public int getMaximumBlockSize() {

+        return maximumBlockSize;

+    }

+

+    /**

+     * Sets the maximum block size that is allowed for In-Band Bytestreams for this connection.

+     * <p>

+     * The maximum block size must be between 1 and 65535 bytes.

+     * <p>

+     * Incoming In-Band Bytestream open request will be rejected with an

+     * &lt;resource-constraint/&gt; error if the block size is greater then the maximum allowed

+     * block size.

+     * 

+     * @param maximumBlockSize the maximum block size to set

+     */

+    public void setMaximumBlockSize(int maximumBlockSize) {

+        if (maximumBlockSize <= 0 || maximumBlockSize > MAXIMUM_BLOCK_SIZE) {

+            throw new IllegalArgumentException("Maximum block size must be between 1 and "

+                            + MAXIMUM_BLOCK_SIZE);

+        }

+        this.maximumBlockSize = maximumBlockSize;

+    }

+

+    /**

+     * Returns the stanza used to send data packets.

+     * <p>

+     * Default is {@link StanzaType#IQ}. See <a

+     * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.

+     * 

+     * @return the stanza used to send data packets

+     */

+    public StanzaType getStanza() {

+        return stanza;

+    }

+

+    /**

+     * Sets the stanza used to send data packets.

+     * <p>

+     * The use of {@link StanzaType#IQ} is recommended. See <a

+     * href="http://xmpp.org/extensions/xep-0047.html#message">XEP-0047</a> Section 4.

+     * 

+     * @param stanza the stanza to set

+     */

+    public void setStanza(StanzaType stanza) {

+        this.stanza = stanza;

+    }

+

+    /**

+     * Establishes an In-Band Bytestream with the given user and returns the session to send/receive

+     * data to/from the user.

+     * <p>

+     * Use this method to establish In-Band Bytestreams to users accepting all incoming In-Band

+     * Bytestream requests since this method doesn't provide a way to tell the user something about

+     * the data to be sent.

+     * <p>

+     * To establish an In-Band Bytestream after negotiation the kind of data to be sent (e.g. file

+     * transfer) use {@link #establishSession(String, String)}.

+     * 

+     * @param targetJID the JID of the user an In-Band Bytestream should be established

+     * @return the session to send/receive data to/from the user

+     * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the

+     *         user prefers smaller block sizes

+     */

+    public InBandBytestreamSession establishSession(String targetJID) throws XMPPException {

+        String sessionID = getNextSessionID();

+        return establishSession(targetJID, sessionID);

+    }

+

+    /**

+     * Establishes an In-Band Bytestream with the given user using the given session ID and returns

+     * the session to send/receive data to/from the user.

+     * 

+     * @param targetJID the JID of the user an In-Band Bytestream should be established

+     * @param sessionID the session ID for the In-Band Bytestream request

+     * @return the session to send/receive data to/from the user

+     * @throws XMPPException if the user doesn't support or accept in-band bytestreams, or if the

+     *         user prefers smaller block sizes

+     */

+    public InBandBytestreamSession establishSession(String targetJID, String sessionID)

+                    throws XMPPException {

+        Open byteStreamRequest = new Open(sessionID, this.defaultBlockSize, this.stanza);

+        byteStreamRequest.setTo(targetJID);

+

+        // sending packet will throw exception on timeout or error reply

+        SyncPacketSend.getReply(this.connection, byteStreamRequest);

+

+        InBandBytestreamSession inBandBytestreamSession = new InBandBytestreamSession(

+                        this.connection, byteStreamRequest, targetJID);

+        this.sessions.put(sessionID, inBandBytestreamSession);

+

+        return inBandBytestreamSession;

+    }

+

+    /**

+     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream is

+     * not accepted.

+     * 

+     * @param request IQ packet that should be answered with a not-acceptable error

+     */

+    protected void replyRejectPacket(IQ request) {

+        XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);

+        IQ error = IQ.createErrorResponse(request, xmppError);

+        this.connection.sendPacket(error);

+    }

+

+    /**

+     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream open

+     * request is rejected because its block size is greater than the maximum allowed block size.

+     * 

+     * @param request IQ packet that should be answered with a resource-constraint error

+     */

+    protected void replyResourceConstraintPacket(IQ request) {

+        XMPPError xmppError = new XMPPError(XMPPError.Condition.resource_constraint);

+        IQ error = IQ.createErrorResponse(request, xmppError);

+        this.connection.sendPacket(error);

+    }

+

+    /**

+     * Responses to the given IQ packet's sender with an XMPP error that an In-Band Bytestream

+     * session could not be found.

+     * 

+     * @param request IQ packet that should be answered with a item-not-found error

+     */

+    protected void replyItemNotFoundPacket(IQ request) {

+        XMPPError xmppError = new XMPPError(XMPPError.Condition.item_not_found);

+        IQ error = IQ.createErrorResponse(request, xmppError);

+        this.connection.sendPacket(error);

+    }

+

+    /**

+     * Returns a new unique session ID.

+     * 

+     * @return a new unique session ID

+     */

+    private String getNextSessionID() {

+        StringBuilder buffer = new StringBuilder();

+        buffer.append(SESSION_ID_PREFIX);

+        buffer.append(Math.abs(randomGenerator.nextLong()));

+        return buffer.toString();

+    }

+

+    /**

+     * Returns the XMPP connection.

+     * 

+     * @return the XMPP connection

+     */

+    protected Connection getConnection() {

+        return this.connection;

+    }

+

+    /**

+     * Returns the {@link InBandBytestreamListener} that should be informed if a In-Band Bytestream

+     * request from the given initiator JID is received.

+     * 

+     * @param initiator the initiator's JID

+     * @return the listener

+     */

+    protected BytestreamListener getUserListener(String initiator) {

+        return this.userListeners.get(initiator);

+    }

+

+    /**

+     * Returns a list of {@link InBandBytestreamListener} that are informed if there are no

+     * listeners for a specific initiator.

+     * 

+     * @return list of listeners

+     */

+    protected List<BytestreamListener> getAllRequestListeners() {

+        return this.allRequestListeners;

+    }

+

+    /**

+     * Returns the sessions map.

+     * 

+     * @return the sessions map

+     */

+    protected Map<String, InBandBytestreamSession> getSessions() {

+        return sessions;

+    }

+

+    /**

+     * Returns the list of session IDs that should be ignored by the InitialtionListener

+     * 

+     * @return list of session IDs

+     */

+    protected List<String> getIgnoredBytestreamRequests() {

+        return ignoredBytestreamRequests;

+    }

+

+    /**

+     * Disables the InBandBytestreamManager by removing its packet listeners and resetting its

+     * internal status.

+     */

+    private void disableService() {

+

+        // remove manager from static managers map

+        managers.remove(connection);

+

+        // remove all listeners registered by this manager

+        this.connection.removePacketListener(this.initiationListener);

+        this.connection.removePacketListener(this.dataListener);

+        this.connection.removePacketListener(this.closeListener);

+

+        // shutdown threads

+        this.initiationListener.shutdown();

+

+        // reset internal status

+        this.userListeners.clear();

+        this.allRequestListeners.clear();

+        this.sessions.clear();

+        this.ignoredBytestreamRequests.clear();

+

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequest.java b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequest.java
new file mode 100644
index 0000000..5bc689a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamRequest.java
@@ -0,0 +1,92 @@
+/**

+ * 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.smackx.bytestreams.ibb;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;

+

+/**

+ * InBandBytestreamRequest class handles incoming In-Band Bytestream requests.

+ * 

+ * @author Henning Staib

+ */

+public class InBandBytestreamRequest implements BytestreamRequest {

+

+    /* the bytestream initialization request */

+    private final Open byteStreamRequest;

+

+    /*

+     * In-Band Bytestream manager containing the XMPP connection and helper

+     * methods

+     */

+    private final InBandBytestreamManager manager;

+

+    protected InBandBytestreamRequest(InBandBytestreamManager manager,

+                    Open byteStreamRequest) {

+        this.manager = manager;

+        this.byteStreamRequest = byteStreamRequest;

+    }

+

+    /**

+     * Returns the sender of the In-Band Bytestream open request.

+     * 

+     * @return the sender of the In-Band Bytestream open request

+     */

+    public String getFrom() {

+        return this.byteStreamRequest.getFrom();

+    }

+

+    /**

+     * Returns the session ID of the In-Band Bytestream open request.

+     * 

+     * @return the session ID of the In-Band Bytestream open request

+     */

+    public String getSessionID() {

+        return this.byteStreamRequest.getSessionID();

+    }

+

+    /**

+     * Accepts the In-Band Bytestream open request and returns the session to

+     * send/receive data.

+     * 

+     * @return the session to send/receive data

+     * @throws XMPPException if stream is invalid.

+     */

+    public InBandBytestreamSession accept() throws XMPPException {

+        Connection connection = this.manager.getConnection();

+

+        // create In-Band Bytestream session and store it

+        InBandBytestreamSession ibbSession = new InBandBytestreamSession(connection,

+                        this.byteStreamRequest, this.byteStreamRequest.getFrom());

+        this.manager.getSessions().put(this.byteStreamRequest.getSessionID(), ibbSession);

+

+        // acknowledge request

+        IQ resultIQ = IQ.createResultIQ(this.byteStreamRequest);

+        connection.sendPacket(resultIQ);

+

+        return ibbSession;

+    }

+

+    /**

+     * Rejects the In-Band Bytestream request by sending a reject error to the

+     * initiator.

+     */

+    public void reject() {

+        this.manager.replyRejectPacket(this.byteStreamRequest);

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSession.java b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSession.java
new file mode 100644
index 0000000..a33682c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/InBandBytestreamSession.java
@@ -0,0 +1,795 @@
+/**

+ * 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.smackx.bytestreams.ibb;

+

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.OutputStream;

+import java.net.SocketTimeoutException;

+import java.util.concurrent.BlockingQueue;

+import java.util.concurrent.LinkedBlockingQueue;

+import java.util.concurrent.TimeUnit;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.PacketListener;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.filter.AndFilter;

+import org.jivesoftware.smack.filter.PacketFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+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.XMPPError;

+import org.jivesoftware.smack.util.StringUtils;

+import org.jivesoftware.smack.util.SyncPacketSend;

+import org.jivesoftware.smackx.bytestreams.BytestreamSession;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;

+

+/**

+ * InBandBytestreamSession class represents an In-Band Bytestream session.

+ * <p>

+ * In-band bytestreams are bidirectional and this session encapsulates the streams for both

+ * directions.

+ * <p>

+ * Note that closing the In-Band Bytestream session will close both streams. If both streams are

+ * closed individually the session will be closed automatically once the second stream is closed.

+ * Use the {@link #setCloseBothStreamsEnabled(boolean)} method if both streams should be closed

+ * automatically if one of them is closed.

+ * 

+ * @author Henning Staib

+ */

+public class InBandBytestreamSession implements BytestreamSession {

+

+    /* XMPP connection */

+    private final Connection connection;

+

+    /* the In-Band Bytestream open request for this session */

+    private final Open byteStreamRequest;

+

+    /*

+     * the input stream for this session (either IQIBBInputStream or MessageIBBInputStream)

+     */

+    private IBBInputStream inputStream;

+

+    /*

+     * the output stream for this session (either IQIBBOutputStream or MessageIBBOutputStream)

+     */

+    private IBBOutputStream outputStream;

+

+    /* JID of the remote peer */

+    private String remoteJID;

+

+    /* flag to close both streams if one of them is closed */

+    private boolean closeBothStreamsEnabled = false;

+

+    /* flag to indicate if session is closed */

+    private boolean isClosed = false;

+

+    /**

+     * Constructor.

+     * 

+     * @param connection the XMPP connection

+     * @param byteStreamRequest the In-Band Bytestream open request for this session

+     * @param remoteJID JID of the remote peer

+     */

+    protected InBandBytestreamSession(Connection connection, Open byteStreamRequest,

+                    String remoteJID) {

+        this.connection = connection;

+        this.byteStreamRequest = byteStreamRequest;

+        this.remoteJID = remoteJID;

+

+        // initialize streams dependent to the uses stanza type

+        switch (byteStreamRequest.getStanza()) {

+        case IQ:

+            this.inputStream = new IQIBBInputStream();

+            this.outputStream = new IQIBBOutputStream();

+            break;

+        case MESSAGE:

+            this.inputStream = new MessageIBBInputStream();

+            this.outputStream = new MessageIBBOutputStream();

+            break;

+        }

+

+    }

+

+    public InputStream getInputStream() {

+        return this.inputStream;

+    }

+

+    public OutputStream getOutputStream() {

+        return this.outputStream;

+    }

+

+    public int getReadTimeout() {

+        return this.inputStream.readTimeout;

+    }

+

+    public void setReadTimeout(int timeout) {

+        if (timeout < 0) {

+            throw new IllegalArgumentException("Timeout must be >= 0");

+        }

+        this.inputStream.readTimeout = timeout;

+    }

+

+    /**

+     * Returns whether both streams should be closed automatically if one of the streams is closed.

+     * Default is <code>false</code>.

+     * 

+     * @return <code>true</code> if both streams will be closed if one of the streams is closed,

+     *         <code>false</code> if both streams can be closed independently.

+     */

+    public boolean isCloseBothStreamsEnabled() {

+        return closeBothStreamsEnabled;

+    }

+

+    /**

+     * Sets whether both streams should be closed automatically if one of the streams is closed.

+     * Default is <code>false</code>.

+     * 

+     * @param closeBothStreamsEnabled <code>true</code> if both streams should be closed if one of

+     *        the streams is closed, <code>false</code> if both streams should be closed

+     *        independently

+     */

+    public void setCloseBothStreamsEnabled(boolean closeBothStreamsEnabled) {

+        this.closeBothStreamsEnabled = closeBothStreamsEnabled;

+    }

+

+    public void close() throws IOException {

+        closeByLocal(true); // close input stream

+        closeByLocal(false); // close output stream

+    }

+

+    /**

+     * This method is invoked if a request to close the In-Band Bytestream has been received.

+     * 

+     * @param closeRequest the close request from the remote peer

+     */

+    protected void closeByPeer(Close closeRequest) {

+

+        /*

+         * close streams without flushing them, because stream is already considered closed on the

+         * remote peers side

+         */

+        this.inputStream.closeInternal();

+        this.inputStream.cleanup();

+        this.outputStream.closeInternal(false);

+

+        // acknowledge close request

+        IQ confirmClose = IQ.createResultIQ(closeRequest);

+        this.connection.sendPacket(confirmClose);

+

+    }

+

+    /**

+     * This method is invoked if one of the streams has been closed locally, if an error occurred

+     * locally or if the whole session should be closed.

+     * 

+     * @throws IOException if an error occurs while sending the close request

+     */

+    protected synchronized void closeByLocal(boolean in) throws IOException {

+        if (this.isClosed) {

+            return;

+        }

+

+        if (this.closeBothStreamsEnabled) {

+            this.inputStream.closeInternal();

+            this.outputStream.closeInternal(true);

+        }

+        else {

+            if (in) {

+                this.inputStream.closeInternal();

+            }

+            else {

+                // close stream but try to send any data left

+                this.outputStream.closeInternal(true);

+            }

+        }

+

+        if (this.inputStream.isClosed && this.outputStream.isClosed) {

+            this.isClosed = true;

+

+            // send close request

+            Close close = new Close(this.byteStreamRequest.getSessionID());

+            close.setTo(this.remoteJID);

+            try {

+                SyncPacketSend.getReply(this.connection, close);

+            }

+            catch (XMPPException e) {

+                throw new IOException("Error while closing stream: " + e.getMessage());

+            }

+

+            this.inputStream.cleanup();

+

+            // remove session from manager

+            InBandBytestreamManager.getByteStreamManager(this.connection).getSessions().remove(this);

+        }

+

+    }

+

+    /**

+     * IBBInputStream class is the base implementation of an In-Band Bytestream input stream.

+     * Subclasses of this input stream must provide a packet listener along with a packet filter to

+     * collect the In-Band Bytestream data packets.

+     */

+    private abstract class IBBInputStream extends InputStream {

+

+        /* the data packet listener to fill the data queue */

+        private final PacketListener dataPacketListener;

+

+        /* queue containing received In-Band Bytestream data packets */

+        protected final BlockingQueue<DataPacketExtension> dataQueue = new LinkedBlockingQueue<DataPacketExtension>();

+

+        /* buffer containing the data from one data packet */

+        private byte[] buffer;

+

+        /* pointer to the next byte to read from buffer */

+        private int bufferPointer = -1;

+

+        /* data packet sequence (range from 0 to 65535) */

+        private long seq = -1;

+

+        /* flag to indicate if input stream is closed */

+        private boolean isClosed = false;

+

+        /* flag to indicate if close method was invoked */

+        private boolean closeInvoked = false;

+

+        /* timeout for read operations */

+        private int readTimeout = 0;

+

+        /**

+         * Constructor.

+         */

+        public IBBInputStream() {

+            // add data packet listener to connection

+            this.dataPacketListener = getDataPacketListener();

+            connection.addPacketListener(this.dataPacketListener, getDataPacketFilter());

+        }

+

+        /**

+         * Returns the packet listener that processes In-Band Bytestream data packets.

+         * 

+         * @return the data packet listener

+         */

+        protected abstract PacketListener getDataPacketListener();

+

+        /**

+         * Returns the packet filter that accepts In-Band Bytestream data packets.

+         * 

+         * @return the data packet filter

+         */

+        protected abstract PacketFilter getDataPacketFilter();

+

+        public synchronized int read() throws IOException {

+            checkClosed();

+

+            // if nothing read yet or whole buffer has been read fill buffer

+            if (bufferPointer == -1 || bufferPointer >= buffer.length) {

+                // if no data available and stream was closed return -1

+                if (!loadBuffer()) {

+                    return -1;

+                }

+            }

+

+            // return byte and increment buffer pointer

+            return ((int) buffer[bufferPointer++]) & 0xff;

+        }

+

+        public synchronized int read(byte[] b, int off, int len) throws IOException {

+            if (b == null) {

+                throw new NullPointerException();

+            }

+            else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)

+                            || ((off + len) < 0)) {

+                throw new IndexOutOfBoundsException();

+            }

+            else if (len == 0) {

+                return 0;

+            }

+

+            checkClosed();

+

+            // if nothing read yet or whole buffer has been read fill buffer

+            if (bufferPointer == -1 || bufferPointer >= buffer.length) {

+                // if no data available and stream was closed return -1

+                if (!loadBuffer()) {

+                    return -1;

+                }

+            }

+

+            // if more bytes wanted than available return all available

+            int bytesAvailable = buffer.length - bufferPointer;

+            if (len > bytesAvailable) {

+                len = bytesAvailable;

+            }

+

+            System.arraycopy(buffer, bufferPointer, b, off, len);

+            bufferPointer += len;

+            return len;

+        }

+

+        public synchronized int read(byte[] b) throws IOException {

+            return read(b, 0, b.length);

+        }

+

+        /**

+         * This method blocks until a data packet is received, the stream is closed or the current

+         * thread is interrupted.

+         * 

+         * @return <code>true</code> if data was received, otherwise <code>false</code>

+         * @throws IOException if data packets are out of sequence

+         */

+        private synchronized boolean loadBuffer() throws IOException {

+

+            // wait until data is available or stream is closed

+            DataPacketExtension data = null;

+            try {

+                if (this.readTimeout == 0) {

+                    while (data == null) {

+                        if (isClosed && this.dataQueue.isEmpty()) {

+                            return false;

+                        }

+                        data = this.dataQueue.poll(1000, TimeUnit.MILLISECONDS);

+                    }

+                }

+                else {

+                    data = this.dataQueue.poll(this.readTimeout, TimeUnit.MILLISECONDS);

+                    if (data == null) {

+                        throw new SocketTimeoutException();

+                    }

+                }

+            }

+            catch (InterruptedException e) {

+                // Restore the interrupted status

+                Thread.currentThread().interrupt();

+                return false;

+            }

+

+            // handle sequence overflow

+            if (this.seq == 65535) {

+                this.seq = -1;

+            }

+

+            // check if data packets sequence is successor of last seen sequence

+            long seq = data.getSeq();

+            if (seq - 1 != this.seq) {

+                // packets out of order; close stream/session

+                InBandBytestreamSession.this.close();

+                throw new IOException("Packets out of sequence");

+            }

+            else {

+                this.seq = seq;

+            }

+

+            // set buffer to decoded data

+            buffer = data.getDecodedData();

+            bufferPointer = 0;

+            return true;

+        }

+

+        /**

+         * Checks if this stream is closed and throws an IOException if necessary

+         * 

+         * @throws IOException if stream is closed and no data should be read anymore

+         */

+        private void checkClosed() throws IOException {

+            /* throw no exception if there is data available, but not if close method was invoked */

+            if ((isClosed && this.dataQueue.isEmpty()) || closeInvoked) {

+                // clear data queue in case additional data was received after stream was closed

+                this.dataQueue.clear();

+                throw new IOException("Stream is closed");

+            }

+        }

+

+        public boolean markSupported() {

+            return false;

+        }

+

+        public void close() throws IOException {

+            if (isClosed) {

+                return;

+            }

+

+            this.closeInvoked = true;

+

+            InBandBytestreamSession.this.closeByLocal(true);

+        }

+

+        /**

+         * This method sets the close flag and removes the data packet listener.

+         */

+        private void closeInternal() {

+            if (isClosed) {

+                return;

+            }

+            isClosed = true;

+        }

+

+        /**

+         * Invoked if the session is closed.

+         */

+        private void cleanup() {

+            connection.removePacketListener(this.dataPacketListener);

+        }

+

+    }

+

+    /**

+     * IQIBBInputStream class implements IBBInputStream to be used with IQ stanzas encapsulating the

+     * data packets.

+     */

+    private class IQIBBInputStream extends IBBInputStream {

+

+        protected PacketListener getDataPacketListener() {

+            return new PacketListener() {

+

+                private long lastSequence = -1;

+

+                public void processPacket(Packet packet) {

+                    // get data packet extension

+                    DataPacketExtension data = (DataPacketExtension) packet.getExtension(

+                                    DataPacketExtension.ELEMENT_NAME,

+                                    InBandBytestreamManager.NAMESPACE);

+

+                    /*

+                     * check if sequence was not used already (see XEP-0047 Section 2.2)

+                     */

+                    if (data.getSeq() <= this.lastSequence) {

+                        IQ unexpectedRequest = IQ.createErrorResponse((IQ) packet, new XMPPError(

+                                        XMPPError.Condition.unexpected_request));

+                        connection.sendPacket(unexpectedRequest);

+                        return;

+

+                    }

+

+                    // check if encoded data is valid (see XEP-0047 Section 2.2)

+                    if (data.getDecodedData() == null) {

+                        // data is invalid; respond with bad-request error

+                        IQ badRequest = IQ.createErrorResponse((IQ) packet, new XMPPError(

+                                        XMPPError.Condition.bad_request));

+                        connection.sendPacket(badRequest);

+                        return;

+                    }

+

+                    // data is valid; add to data queue

+                    dataQueue.offer(data);

+

+                    // confirm IQ

+                    IQ confirmData = IQ.createResultIQ((IQ) packet);

+                    connection.sendPacket(confirmData);

+

+                    // set last seen sequence

+                    this.lastSequence = data.getSeq();

+                    if (this.lastSequence == 65535) {

+                        this.lastSequence = -1;

+                    }

+

+                }

+

+            };

+        }

+

+        protected PacketFilter getDataPacketFilter() {

+            /*

+             * filter all IQ stanzas having type 'SET' (represented by Data class), containing a

+             * data packet extension, matching session ID and recipient

+             */

+            return new AndFilter(new PacketTypeFilter(Data.class), new IBBDataPacketFilter());

+        }

+

+    }

+

+    /**

+     * MessageIBBInputStream class implements IBBInputStream to be used with message stanzas

+     * encapsulating the data packets.

+     */

+    private class MessageIBBInputStream extends IBBInputStream {

+

+        protected PacketListener getDataPacketListener() {

+            return new PacketListener() {

+

+                public void processPacket(Packet packet) {

+                    // get data packet extension

+                    DataPacketExtension data = (DataPacketExtension) packet.getExtension(

+                                    DataPacketExtension.ELEMENT_NAME,

+                                    InBandBytestreamManager.NAMESPACE);

+

+                    // check if encoded data is valid

+                    if (data.getDecodedData() == null) {

+                        /*

+                         * TODO once a majority of XMPP server implementation support XEP-0079

+                         * Advanced Message Processing the invalid message could be answered with an

+                         * appropriate error. For now we just ignore the packet. Subsequent packets

+                         * with an increased sequence will cause the input stream to close the

+                         * stream/session.

+                         */

+                        return;

+                    }

+

+                    // data is valid; add to data queue

+                    dataQueue.offer(data);

+

+                    // TODO confirm packet once XMPP servers support XEP-0079

+                }

+

+            };

+        }

+

+        @Override

+        protected PacketFilter getDataPacketFilter() {

+            /*

+             * filter all message stanzas containing a data packet extension, matching session ID

+             * and recipient

+             */

+            return new AndFilter(new PacketTypeFilter(Message.class), new IBBDataPacketFilter());

+        }

+

+    }

+

+    /**

+     * IBBDataPacketFilter class filters all packets from the remote peer of this session,

+     * containing an In-Band Bytestream data packet extension whose session ID matches this sessions

+     * ID.

+     */

+    private class IBBDataPacketFilter implements PacketFilter {

+

+        public boolean accept(Packet packet) {

+            // sender equals remote peer

+            if (!packet.getFrom().equalsIgnoreCase(remoteJID)) {

+                return false;

+            }

+

+            // stanza contains data packet extension

+            PacketExtension packetExtension = packet.getExtension(DataPacketExtension.ELEMENT_NAME,

+                            InBandBytestreamManager.NAMESPACE);

+            if (packetExtension == null || !(packetExtension instanceof DataPacketExtension)) {

+                return false;

+            }

+

+            // session ID equals this session ID

+            DataPacketExtension data = (DataPacketExtension) packetExtension;

+            if (!data.getSessionID().equals(byteStreamRequest.getSessionID())) {

+                return false;

+            }

+

+            return true;

+        }

+

+    }

+

+    /**

+     * IBBOutputStream class is the base implementation of an In-Band Bytestream output stream.

+     * Subclasses of this output stream must provide a method to send data over XMPP stream.

+     */

+    private abstract class IBBOutputStream extends OutputStream {

+

+        /* buffer with the size of this sessions block size */

+        protected final byte[] buffer;

+

+        /* pointer to next byte to write to buffer */

+        protected int bufferPointer = 0;

+

+        /* data packet sequence (range from 0 to 65535) */

+        protected long seq = 0;

+

+        /* flag to indicate if output stream is closed */

+        protected boolean isClosed = false;

+

+        /**

+         * Constructor.

+         */

+        public IBBOutputStream() {

+            this.buffer = new byte[(byteStreamRequest.getBlockSize()/4)*3];

+        }

+

+        /**

+         * Writes the given data packet to the XMPP stream.

+         * 

+         * @param data the data packet

+         * @throws IOException if an I/O error occurred while sending or if the stream is closed

+         */

+        protected abstract void writeToXML(DataPacketExtension data) throws IOException;

+

+        public synchronized void write(int b) throws IOException {

+            if (this.isClosed) {

+                throw new IOException("Stream is closed");

+            }

+

+            // if buffer is full flush buffer

+            if (bufferPointer >= buffer.length) {

+                flushBuffer();

+            }

+

+            buffer[bufferPointer++] = (byte) b;

+        }

+

+        public synchronized void write(byte b[], int off, int len) throws IOException {

+            if (b == null) {

+                throw new NullPointerException();

+            }

+            else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length)

+                            || ((off + len) < 0)) {

+                throw new IndexOutOfBoundsException();

+            }

+            else if (len == 0) {

+                return;

+            }

+

+            if (this.isClosed) {

+                throw new IOException("Stream is closed");

+            }

+

+            // is data to send greater than buffer size

+            if (len >= buffer.length) {

+

+                // "byte" off the first chunk to write out

+                writeOut(b, off, buffer.length);

+

+                // recursively call this method with the lesser amount

+                write(b, off + buffer.length, len - buffer.length);

+            }

+            else {

+                writeOut(b, off, len);

+            }

+        }

+

+        public synchronized void write(byte[] b) throws IOException {

+            write(b, 0, b.length);

+        }

+

+        /**

+         * Fills the buffer with the given data and sends it over the XMPP stream if the buffers

+         * capacity has been reached. This method is only called from this class so it is assured

+         * that the amount of data to send is <= buffer capacity

+         * 

+         * @param b the data

+         * @param off the data

+         * @param len the number of bytes to write

+         * @throws IOException if an I/O error occurred while sending or if the stream is closed

+         */

+        private synchronized void writeOut(byte b[], int off, int len) throws IOException {

+            if (this.isClosed) {

+                throw new IOException("Stream is closed");

+            }

+

+            // set to 0 in case the next 'if' block is not executed

+            int available = 0;

+

+            // is data to send greater that buffer space left

+            if (len > buffer.length - bufferPointer) {

+                // fill buffer to capacity and send it

+                available = buffer.length - bufferPointer;

+                System.arraycopy(b, off, buffer, bufferPointer, available);

+                bufferPointer += available;

+                flushBuffer();

+            }

+

+            // copy the data left to buffer

+            System.arraycopy(b, off + available, buffer, bufferPointer, len - available);

+            bufferPointer += len - available;

+        }

+

+        public synchronized void flush() throws IOException {

+            if (this.isClosed) {

+                throw new IOException("Stream is closed");

+            }

+            flushBuffer();

+        }

+

+        private synchronized void flushBuffer() throws IOException {

+

+            // do nothing if no data to send available

+            if (bufferPointer == 0) {

+                return;

+            }

+

+            // create data packet

+            String enc = StringUtils.encodeBase64(buffer, 0, bufferPointer, false);

+            DataPacketExtension data = new DataPacketExtension(byteStreamRequest.getSessionID(),

+                            this.seq, enc);

+

+            // write to XMPP stream

+            writeToXML(data);

+

+            // reset buffer pointer

+            bufferPointer = 0;

+

+            // increment sequence, considering sequence overflow

+            this.seq = (this.seq + 1 == 65535 ? 0 : this.seq + 1);

+

+        }

+

+        public void close() throws IOException {

+            if (isClosed) {

+                return;

+            }

+            InBandBytestreamSession.this.closeByLocal(false);

+        }

+

+        /**

+         * Sets the close flag and optionally flushes the stream.

+         * 

+         * @param flush if <code>true</code> flushes the stream

+         */

+        protected void closeInternal(boolean flush) {

+            if (this.isClosed) {

+                return;

+            }

+            this.isClosed = true;

+

+            try {

+                if (flush) {

+                    flushBuffer();

+                }

+            }

+            catch (IOException e) {

+                /*

+                 * ignore, because writeToXML() will not throw an exception if stream is already

+                 * closed

+                 */

+            }

+        }

+

+    }

+

+    /**

+     * IQIBBOutputStream class implements IBBOutputStream to be used with IQ stanzas encapsulating

+     * the data packets.

+     */

+    private class IQIBBOutputStream extends IBBOutputStream {

+

+        @Override

+        protected synchronized void writeToXML(DataPacketExtension data) throws IOException {

+            // create IQ stanza containing data packet

+            IQ iq = new Data(data);

+            iq.setTo(remoteJID);

+

+            try {

+                SyncPacketSend.getReply(connection, iq);

+            }

+            catch (XMPPException e) {

+                // close session unless it is already closed

+                if (!this.isClosed) {

+                    InBandBytestreamSession.this.close();

+                    throw new IOException("Error while sending Data: " + e.getMessage());

+                }

+            }

+

+        }

+

+    }

+

+    /**

+     * MessageIBBOutputStream class implements IBBOutputStream to be used with message stanzas

+     * encapsulating the data packets.

+     */

+    private class MessageIBBOutputStream extends IBBOutputStream {

+

+        @Override

+        protected synchronized void writeToXML(DataPacketExtension data) {

+            // create message stanza containing data packet

+            Message message = new Message(remoteJID);

+            message.addExtension(data);

+

+            connection.sendPacket(message);

+

+        }

+

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/InitiationListener.java b/src/org/jivesoftware/smackx/bytestreams/ibb/InitiationListener.java
new file mode 100644
index 0000000..0ecb081
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/InitiationListener.java
@@ -0,0 +1,127 @@
+/**

+ * 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.smackx.bytestreams.ibb;

+

+import java.util.concurrent.ExecutorService;

+import java.util.concurrent.Executors;

+

+import org.jivesoftware.smack.PacketListener;

+import org.jivesoftware.smack.filter.AndFilter;

+import org.jivesoftware.smack.filter.IQTypeFilter;

+import org.jivesoftware.smack.filter.PacketFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smackx.bytestreams.BytestreamListener;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;

+

+/**

+ * InitiationListener handles all incoming In-Band Bytestream open requests. If there are no

+ * listeners for a In-Band Bytestream request InitiationListener will always refuse the request and

+ * reply with a &lt;not-acceptable/&gt; error (<a

+ * href="http://xmpp.org/extensions/xep-0047.html#example-5" >XEP-0047</a> Section 2.1).

+ * <p>

+ * All In-Band Bytestream request having a block size greater than the maximum allowed block size

+ * for this connection are rejected with an &lt;resource-constraint/&gt; error. The maximum block

+ * size can be set by invoking {@link InBandBytestreamManager#setMaximumBlockSize(int)}.

+ * 

+ * @author Henning Staib

+ */

+class InitiationListener implements PacketListener {

+

+    /* manager containing the listeners and the XMPP connection */

+    private final InBandBytestreamManager manager;

+

+    /* packet filter for all In-Band Bytestream requests */

+    private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Open.class),

+                    new IQTypeFilter(IQ.Type.SET));

+

+    /* executor service to process incoming requests concurrently */

+    private final ExecutorService initiationListenerExecutor;

+

+    /**

+     * Constructor.

+     * 

+     * @param manager the In-Band Bytestream manager

+     */

+    protected InitiationListener(InBandBytestreamManager manager) {

+        this.manager = manager;

+        initiationListenerExecutor = Executors.newCachedThreadPool();

+    }

+

+    public void processPacket(final Packet packet) {

+        initiationListenerExecutor.execute(new Runnable() {

+

+            public void run() {

+                processRequest(packet);

+            }

+        });

+    }

+

+    private void processRequest(Packet packet) {

+        Open ibbRequest = (Open) packet;

+

+        // validate that block size is within allowed range

+        if (ibbRequest.getBlockSize() > this.manager.getMaximumBlockSize()) {

+            this.manager.replyResourceConstraintPacket(ibbRequest);

+            return;

+        }

+

+        // ignore request if in ignore list

+        if (this.manager.getIgnoredBytestreamRequests().remove(ibbRequest.getSessionID()))

+            return;

+

+        // build bytestream request from packet

+        InBandBytestreamRequest request = new InBandBytestreamRequest(this.manager, ibbRequest);

+

+        // notify listeners for bytestream initiation from a specific user

+        BytestreamListener userListener = this.manager.getUserListener(ibbRequest.getFrom());

+        if (userListener != null) {

+            userListener.incomingBytestreamRequest(request);

+

+        }

+        else if (!this.manager.getAllRequestListeners().isEmpty()) {

+            /*

+             * if there is no user specific listener inform listeners for all initiation requests

+             */

+            for (BytestreamListener listener : this.manager.getAllRequestListeners()) {

+                listener.incomingBytestreamRequest(request);

+            }

+

+        }

+        else {

+            /*

+             * if there is no listener for this initiation request, reply with reject message

+             */

+            this.manager.replyRejectPacket(ibbRequest);

+        }

+    }

+

+    /**

+     * Returns the packet filter for In-Band Bytestream open requests.

+     * 

+     * @return the packet filter for In-Band Bytestream open requests

+     */

+    protected PacketFilter getFilter() {

+        return this.initFilter;

+    }

+

+    /**

+     * Shuts down the listeners executor service.

+     */

+    protected void shutdown() {

+        this.initiationListenerExecutor.shutdownNow();

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java
new file mode 100644
index 0000000..9a78d73
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Close.java
@@ -0,0 +1,65 @@
+/**

+ * 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.smackx.bytestreams.ibb.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;

+

+/**

+ * Represents a request to close an In-Band Bytestream.

+ * 

+ * @author Henning Staib

+ */

+public class Close extends IQ {

+

+    /* unique session ID identifying this In-Band Bytestream */

+    private final String sessionID;

+

+    /**

+     * Creates a new In-Band Bytestream close request packet.

+     * 

+     * @param sessionID unique session ID identifying this In-Band Bytestream

+     */

+    public Close(String sessionID) {

+        if (sessionID == null || "".equals(sessionID)) {

+            throw new IllegalArgumentException("Session ID must not be null or empty");

+        }

+        this.sessionID = sessionID;

+        setType(Type.SET);

+    }

+

+    /**

+     * Returns the unique session ID identifying this In-Band Bytestream.

+     * 

+     * @return the unique session ID identifying this In-Band Bytestream

+     */

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    @Override

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<close ");

+        buf.append("xmlns=\"");

+        buf.append(InBandBytestreamManager.NAMESPACE);

+        buf.append("\" ");

+        buf.append("sid=\"");

+        buf.append(sessionID);

+        buf.append("\"");

+        buf.append("/>");

+        return buf.toString();

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java
new file mode 100644
index 0000000..696fa75
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Data.java
@@ -0,0 +1,64 @@
+/**

+ * 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.smackx.bytestreams.ibb.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+

+/**

+ * Represents a chunk of data sent over an In-Band Bytestream encapsulated in an

+ * IQ stanza.

+ * 

+ * @author Henning Staib

+ */

+public class Data extends IQ {

+

+    /* the data packet extension */

+    private final DataPacketExtension dataPacketExtension;

+

+    /**

+     * Constructor.

+     * 

+     * @param data data packet extension containing the encoded data

+     */

+    public Data(DataPacketExtension data) {

+        if (data == null) {

+            throw new IllegalArgumentException("Data must not be null");

+        }

+        this.dataPacketExtension = data;

+

+        /*

+         * also set as packet extension so that data packet extension can be

+         * retrieved from IQ stanza and message stanza in the same way

+         */

+        addExtension(data);

+        setType(IQ.Type.SET);

+    }

+

+    /**

+     * Returns the data packet extension.

+     * <p>

+     * Convenience method for <code>packet.getExtension("data",

+     * "http://jabber.org/protocol/ibb")</code>.

+     * 

+     * @return the data packet extension

+     */

+    public DataPacketExtension getDataPacketExtension() {

+        return this.dataPacketExtension;

+    }

+

+    public String getChildElementXML() {

+        return this.dataPacketExtension.toXML();

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtension.java b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtension.java
new file mode 100644
index 0000000..80ed1e1
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/DataPacketExtension.java
@@ -0,0 +1,149 @@
+/**

+ * 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.smackx.bytestreams.ibb.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.util.StringUtils;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;

+

+/**

+ * Represents a chunk of data of an In-Band Bytestream within an IQ stanza or a

+ * message stanza

+ * 

+ * @author Henning Staib

+ */

+public class DataPacketExtension implements PacketExtension {

+

+    /**

+     * The element name of the data packet extension.

+     */

+    public final static String ELEMENT_NAME = "data";

+

+    /* unique session ID identifying this In-Band Bytestream */

+    private final String sessionID;

+

+    /* sequence of this packet in regard to the other data packets */

+    private final long seq;

+

+    /* the data contained in this packet */

+    private final String data;

+

+    private byte[] decodedData;

+

+    /**

+     * Creates a new In-Band Bytestream data packet.

+     * 

+     * @param sessionID unique session ID identifying this In-Band Bytestream

+     * @param seq sequence of this packet in regard to the other data packets

+     * @param data the base64 encoded data contained in this packet

+     */

+    public DataPacketExtension(String sessionID, long seq, String data) {

+        if (sessionID == null || "".equals(sessionID)) {

+            throw new IllegalArgumentException("Session ID must not be null or empty");

+        }

+        if (seq < 0 || seq > 65535) {

+            throw new IllegalArgumentException("Sequence must not be between 0 and 65535");

+        }

+        if (data == null) {

+            throw new IllegalArgumentException("Data must not be null");

+        }

+        this.sessionID = sessionID;

+        this.seq = seq;

+        this.data = data;

+    }

+

+    /**

+     * Returns the unique session ID identifying this In-Band Bytestream.

+     * 

+     * @return the unique session ID identifying this In-Band Bytestream

+     */

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    /**

+     * Returns the sequence of this packet in regard to the other data packets.

+     * 

+     * @return the sequence of this packet in regard to the other data packets.

+     */

+    public long getSeq() {

+        return seq;

+    }

+

+    /**

+     * Returns the data contained in this packet.

+     * 

+     * @return the data contained in this packet.

+     */

+    public String getData() {

+        return data;

+    }

+

+    /**

+     * Returns the decoded data or null if data could not be decoded.

+     * <p>

+     * The encoded data is invalid if it contains bad Base64 input characters or

+     * if it contains the pad ('=') character on a position other than the last

+     * character(s) of the data. See <a

+     * href="http://xmpp.org/extensions/xep-0047.html#sec">XEP-0047</a> Section

+     * 6.

+     * 

+     * @return the decoded data

+     */

+    public byte[] getDecodedData() {

+        // return cached decoded data

+        if (this.decodedData != null) {

+            return this.decodedData;

+        }

+

+        // data must not contain the pad (=) other than end of data

+        if (data.matches(".*={1,2}+.+")) {

+            return null;

+        }

+

+        // decodeBase64 will return null if bad characters are included

+        this.decodedData = StringUtils.decodeBase64(data);

+        return this.decodedData;

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return InBandBytestreamManager.NAMESPACE;

+    }

+

+    public String toXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<");

+        buf.append(getElementName());

+        buf.append(" ");

+        buf.append("xmlns=\"");

+        buf.append(InBandBytestreamManager.NAMESPACE);

+        buf.append("\" ");

+        buf.append("seq=\"");

+        buf.append(seq);

+        buf.append("\" ");

+        buf.append("sid=\"");

+        buf.append(sessionID);

+        buf.append("\">");

+        buf.append(data);

+        buf.append("</");

+        buf.append(getElementName());

+        buf.append(">");

+        return buf.toString();

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java
new file mode 100644
index 0000000..94a7a9b
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/packet/Open.java
@@ -0,0 +1,126 @@
+/**

+ * 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.smackx.bytestreams.ibb.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType;

+

+/**

+ * Represents a request to open an In-Band Bytestream.

+ * 

+ * @author Henning Staib

+ */

+public class Open extends IQ {

+

+    /* unique session ID identifying this In-Band Bytestream */

+    private final String sessionID;

+

+    /* block size in which the data will be fragmented */

+    private final int blockSize;

+

+    /* stanza type used to encapsulate the data */

+    private final StanzaType stanza;

+

+    /**

+     * Creates a new In-Band Bytestream open request packet.

+     * <p>

+     * The data sent over this In-Band Bytestream will be fragmented in blocks

+     * with the given block size. The block size should not be greater than

+     * 65535. A recommended default value is 4096.

+     * <p>

+     * The data can be sent using IQ stanzas or message stanzas.

+     * 

+     * @param sessionID unique session ID identifying this In-Band Bytestream

+     * @param blockSize block size in which the data will be fragmented

+     * @param stanza stanza type used to encapsulate the data

+     */

+    public Open(String sessionID, int blockSize, StanzaType stanza) {

+        if (sessionID == null || "".equals(sessionID)) {

+            throw new IllegalArgumentException("Session ID must not be null or empty");

+        }

+        if (blockSize <= 0) {

+            throw new IllegalArgumentException("Block size must be greater than zero");

+        }

+

+        this.sessionID = sessionID;

+        this.blockSize = blockSize;

+        this.stanza = stanza;

+        setType(Type.SET);

+    }

+

+    /**

+     * Creates a new In-Band Bytestream open request packet.

+     * <p>

+     * The data sent over this In-Band Bytestream will be fragmented in blocks

+     * with the given block size. The block size should not be greater than

+     * 65535. A recommended default value is 4096.

+     * <p>

+     * The data will be sent using IQ stanzas.

+     * 

+     * @param sessionID unique session ID identifying this In-Band Bytestream

+     * @param blockSize block size in which the data will be fragmented

+     */

+    public Open(String sessionID, int blockSize) {

+        this(sessionID, blockSize, StanzaType.IQ);

+    }

+

+    /**

+     * Returns the unique session ID identifying this In-Band Bytestream.

+     * 

+     * @return the unique session ID identifying this In-Band Bytestream

+     */

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    /**

+     * Returns the block size in which the data will be fragmented.

+     * 

+     * @return the block size in which the data will be fragmented

+     */

+    public int getBlockSize() {

+        return blockSize;

+    }

+

+    /**

+     * Returns the stanza type used to encapsulate the data.

+     * 

+     * @return the stanza type used to encapsulate the data

+     */

+    public StanzaType getStanza() {

+        return stanza;

+    }

+

+    @Override

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<open ");

+        buf.append("xmlns=\"");

+        buf.append(InBandBytestreamManager.NAMESPACE);

+        buf.append("\" ");

+        buf.append("block-size=\"");

+        buf.append(blockSize);

+        buf.append("\" ");

+        buf.append("sid=\"");

+        buf.append(sessionID);

+        buf.append("\" ");

+        buf.append("stanza=\"");

+        buf.append(stanza.toString().toLowerCase());

+        buf.append("\"");

+        buf.append("/>");

+        return buf.toString();

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/provider/CloseIQProvider.java b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/CloseIQProvider.java
new file mode 100644
index 0000000..566724c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/CloseIQProvider.java
@@ -0,0 +1,33 @@
+/**

+ * 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.smackx.bytestreams.ibb.provider;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Close;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Parses a close In-Band Bytestream packet.

+ * 

+ * @author Henning Staib

+ */

+public class CloseIQProvider implements IQProvider {

+

+    public IQ parseIQ(XmlPullParser parser) throws Exception {

+        String sid = parser.getAttributeValue("", "sid");

+        return new Close(sid);

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/provider/DataPacketProvider.java b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/DataPacketProvider.java
new file mode 100644
index 0000000..5abed08
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/DataPacketProvider.java
@@ -0,0 +1,45 @@
+/**

+ * 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.smackx.bytestreams.ibb.provider;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Data;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.DataPacketExtension;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Parses an In-Band Bytestream data packet which can be a packet extension of

+ * either an IQ stanza or a message stanza.

+ * 

+ * @author Henning Staib

+ */

+public class DataPacketProvider implements PacketExtensionProvider, IQProvider {

+

+    public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+        String sessionID = parser.getAttributeValue("", "sid");

+        long seq = Long.parseLong(parser.getAttributeValue("", "seq"));

+        String data = parser.nextText();

+        return new DataPacketExtension(sessionID, seq, data);

+    }

+

+    public IQ parseIQ(XmlPullParser parser) throws Exception {

+        DataPacketExtension data = (DataPacketExtension) parseExtension(parser);

+        IQ iq = new Data(data);

+        return iq;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/ibb/provider/OpenIQProvider.java b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/OpenIQProvider.java
new file mode 100644
index 0000000..3cc725a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/ibb/provider/OpenIQProvider.java
@@ -0,0 +1,45 @@
+/**

+ * 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.smackx.bytestreams.ibb.provider;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager.StanzaType;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Parses an In-Band Bytestream open packet.

+ * 

+ * @author Henning Staib

+ */

+public class OpenIQProvider implements IQProvider {

+

+    public IQ parseIQ(XmlPullParser parser) throws Exception {

+        String sessionID = parser.getAttributeValue("", "sid");

+        int blockSize = Integer.parseInt(parser.getAttributeValue("", "block-size"));

+

+        String stanzaValue = parser.getAttributeValue("", "stanza");

+        StanzaType stanza = null;

+        if (stanzaValue == null) {

+            stanza = StanzaType.IQ;

+        }

+        else {

+            stanza = StanzaType.valueOf(stanzaValue.toUpperCase());

+        }

+

+        return new Open(sessionID, blockSize, stanza);

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java b/src/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java
new file mode 100644
index 0000000..2a78250
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/InitiationListener.java
@@ -0,0 +1,119 @@
+/**

+ * 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.smackx.bytestreams.socks5;

+

+import java.util.concurrent.ExecutorService;

+import java.util.concurrent.Executors;

+

+import org.jivesoftware.smack.PacketListener;

+import org.jivesoftware.smack.filter.AndFilter;

+import org.jivesoftware.smack.filter.IQTypeFilter;

+import org.jivesoftware.smack.filter.PacketFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smackx.bytestreams.BytestreamListener;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;

+

+/**

+ * InitiationListener handles all incoming SOCKS5 Bytestream initiation requests. If there are no

+ * listeners for a SOCKS5 bytestream request InitiationListener will always refuse the request and

+ * reply with a &lt;not-acceptable/&gt; error (<a

+ * href="http://xmpp.org/extensions/xep-0065.html#usecase-alternate">XEP-0065</a> Section 5.2.A2).

+ * 

+ * @author Henning Staib

+ */

+final class InitiationListener implements PacketListener {

+

+    /* manager containing the listeners and the XMPP connection */

+    private final Socks5BytestreamManager manager;

+

+    /* packet filter for all SOCKS5 Bytestream requests */

+    private final PacketFilter initFilter = new AndFilter(new PacketTypeFilter(Bytestream.class),

+                    new IQTypeFilter(IQ.Type.SET));

+

+    /* executor service to process incoming requests concurrently */

+    private final ExecutorService initiationListenerExecutor;

+

+    /**

+     * Constructor

+     * 

+     * @param manager the SOCKS5 Bytestream manager

+     */

+    protected InitiationListener(Socks5BytestreamManager manager) {

+        this.manager = manager;

+        initiationListenerExecutor = Executors.newCachedThreadPool();

+    }

+

+    public void processPacket(final Packet packet) {

+        initiationListenerExecutor.execute(new Runnable() {

+

+            public void run() {

+                processRequest(packet);

+            }

+        });

+    }

+

+    private void processRequest(Packet packet) {

+        Bytestream byteStreamRequest = (Bytestream) packet;

+

+        // ignore request if in ignore list

+        if (this.manager.getIgnoredBytestreamRequests().remove(byteStreamRequest.getSessionID())) {

+            return;

+        }

+

+        // build bytestream request from packet

+        Socks5BytestreamRequest request = new Socks5BytestreamRequest(this.manager,

+                        byteStreamRequest);

+

+        // notify listeners for bytestream initiation from a specific user

+        BytestreamListener userListener = this.manager.getUserListener(byteStreamRequest.getFrom());

+        if (userListener != null) {

+            userListener.incomingBytestreamRequest(request);

+

+        }

+        else if (!this.manager.getAllRequestListeners().isEmpty()) {

+            /*

+             * if there is no user specific listener inform listeners for all initiation requests

+             */

+            for (BytestreamListener listener : this.manager.getAllRequestListeners()) {

+                listener.incomingBytestreamRequest(request);

+            }

+

+        }

+        else {

+            /*

+             * if there is no listener for this initiation request, reply with reject message

+             */

+            this.manager.replyRejectPacket(byteStreamRequest);

+        }

+    }

+

+    /**

+     * Returns the packet filter for SOCKS5 Bytestream initialization requests.

+     * 

+     * @return the packet filter for SOCKS5 Bytestream initialization requests

+     */

+    protected PacketFilter getFilter() {

+        return this.initFilter;

+    }

+

+    /**

+     * Shuts down the listeners executor service.

+     */

+    protected void shutdown() {

+        this.initiationListenerExecutor.shutdownNow();

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java
new file mode 100644
index 0000000..1430b1d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamListener.java
@@ -0,0 +1,43 @@
+/**

+ * 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.smackx.bytestreams.socks5;

+

+import org.jivesoftware.smackx.bytestreams.BytestreamListener;

+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;

+

+/**

+ * Socks5BytestreamListener are informed if a remote user wants to initiate a SOCKS5 Bytestream.

+ * Implement this interface to handle incoming SOCKS5 Bytestream requests.

+ * <p>

+ * There are two ways to add this listener. See

+ * {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener)} and

+ * {@link Socks5BytestreamManager#addIncomingBytestreamListener(BytestreamListener, String)} for

+ * further details.

+ * 

+ * @author Henning Staib

+ */

+public abstract class Socks5BytestreamListener implements BytestreamListener {

+

+    public void incomingBytestreamRequest(BytestreamRequest request) {

+        incomingBytestreamRequest((Socks5BytestreamRequest) request);

+    }

+

+    /**

+     * This listener is notified if a SOCKS5 Bytestream request from another user has been received.

+     * 

+     * @param request the incoming SOCKS5 Bytestream request

+     */

+    public abstract void incomingBytestreamRequest(Socks5BytestreamRequest request);

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java
new file mode 100644
index 0000000..1383495
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamManager.java
@@ -0,0 +1,777 @@
+/**

+ * 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.smackx.bytestreams.socks5;

+

+import java.io.IOException;

+import java.lang.ref.WeakReference;

+import java.net.Socket;

+import java.util.ArrayList;

+import java.util.Collections;

+import java.util.Iterator;

+import java.util.LinkedList;

+import java.util.List;

+import java.util.Map;

+import java.util.Random;

+import java.util.WeakHashMap;

+import java.util.concurrent.ConcurrentHashMap;

+import java.util.concurrent.TimeoutException;

+

+import org.jivesoftware.smack.AbstractConnectionListener;

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.ConnectionCreationListener;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.XMPPError;

+import org.jivesoftware.smack.util.SyncPacketSend;

+import org.jivesoftware.smackx.ServiceDiscoveryManager;

+import org.jivesoftware.smackx.bytestreams.BytestreamListener;

+import org.jivesoftware.smackx.bytestreams.BytestreamManager;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHostUsed;

+import org.jivesoftware.smackx.filetransfer.FileTransferManager;

+import org.jivesoftware.smackx.packet.DiscoverInfo;

+import org.jivesoftware.smackx.packet.DiscoverItems;

+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;

+import org.jivesoftware.smackx.packet.DiscoverItems.Item;

+

+/**

+ * The Socks5BytestreamManager class handles establishing SOCKS5 Bytestreams as specified in the <a

+ * href="http://xmpp.org/extensions/xep-0065.html">XEP-0065</a>.

+ * <p>

+ * A SOCKS5 Bytestream is negotiated partly over the XMPP XML stream and partly over a separate

+ * socket. The actual transfer though takes place over a separately created socket.

+ * <p>

+ * A SOCKS5 Bytestream generally has three parties, the initiator, the target, and the stream host.

+ * The stream host is a specialized SOCKS5 proxy setup on a server, or, the initiator can act as the

+ * stream host.

+ * <p>

+ * To establish a SOCKS5 Bytestream invoke the {@link #establishSession(String)} method. This will

+ * negotiate a SOCKS5 Bytestream with the given target JID and return a socket.

+ * <p>

+ * If a session ID for the SOCKS5 Bytestream was already negotiated (e.g. while negotiating a file

+ * transfer) invoke {@link #establishSession(String, String)}.

+ * <p>

+ * To handle incoming SOCKS5 Bytestream requests add an {@link Socks5BytestreamListener} to the

+ * manager. There are two ways to add this listener. If you want to be informed about incoming

+ * SOCKS5 Bytestreams from a specific user add the listener by invoking

+ * {@link #addIncomingBytestreamListener(BytestreamListener, String)}. If the listener should

+ * respond to all SOCKS5 Bytestream requests invoke

+ * {@link #addIncomingBytestreamListener(BytestreamListener)}.

+ * <p>

+ * Note that the registered {@link Socks5BytestreamListener} will NOT be notified on incoming Socks5

+ * bytestream requests sent in the context of <a

+ * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See

+ * {@link FileTransferManager})

+ * <p>

+ * If no {@link Socks5BytestreamListener}s are registered, all incoming SOCKS5 Bytestream requests

+ * will be rejected by returning a &lt;not-acceptable/&gt; error to the initiator.

+ * 

+ * @author Henning Staib

+ */

+public final class Socks5BytestreamManager implements BytestreamManager {

+

+    /*

+     * create a new Socks5BytestreamManager and register a shutdown listener on every established

+     * connection

+     */

+    static {

+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {

+

+            public void connectionCreated(final Connection connection) {

+                final Socks5BytestreamManager manager;

+                manager = Socks5BytestreamManager.getBytestreamManager(connection);

+

+                // register shutdown listener

+                connection.addConnectionListener(new AbstractConnectionListener() {

+

+                    public void connectionClosed() {

+                        manager.disableService();

+                    }

+                    

+                    public void connectionClosedOnError(Exception e) {

+                        manager.disableService();

+                    }

+                    

+                    public void reconnectionSuccessful() {

+                        managers.put(connection, manager);

+                    }

+

+                });

+            }

+

+        });

+    }

+

+    /**

+     * The XMPP namespace of the SOCKS5 Bytestream

+     */

+    public static final String NAMESPACE = "http://jabber.org/protocol/bytestreams";

+

+    /* prefix used to generate session IDs */

+    private static final String SESSION_ID_PREFIX = "js5_";

+

+    /* random generator to create session IDs */

+    private final static Random randomGenerator = new Random();

+

+    /* stores one Socks5BytestreamManager for each XMPP connection */

+    private final static Map<Connection, Socks5BytestreamManager> managers = new WeakHashMap<Connection, Socks5BytestreamManager>();

+

+    /* XMPP connection */

+    private final Connection connection;

+

+    /*

+     * assigns a user to a listener that is informed if a bytestream request for this user is

+     * received

+     */

+    private final Map<String, BytestreamListener> userListeners = new ConcurrentHashMap<String, BytestreamListener>();

+

+    /*

+     * list of listeners that respond to all bytestream requests if there are not user specific

+     * listeners for that request

+     */

+    private final List<BytestreamListener> allRequestListeners = Collections.synchronizedList(new LinkedList<BytestreamListener>());

+

+    /* listener that handles all incoming bytestream requests */

+    private final InitiationListener initiationListener;

+

+    /* timeout to wait for the response to the SOCKS5 Bytestream initialization request */

+    private int targetResponseTimeout = 10000;

+

+    /* timeout for connecting to the SOCKS5 proxy selected by the target */

+    private int proxyConnectionTimeout = 10000;

+

+    /* blacklist of errornous SOCKS5 proxies */

+    private final List<String> proxyBlacklist = Collections.synchronizedList(new LinkedList<String>());

+

+    /* remember the last proxy that worked to prioritize it */

+    private String lastWorkingProxy = null;

+

+    /* flag to enable/disable prioritization of last working proxy */

+    private boolean proxyPrioritizationEnabled = true;

+

+    /*

+     * list containing session IDs of SOCKS5 Bytestream initialization packets that should be

+     * ignored by the InitiationListener

+     */

+    private List<String> ignoredBytestreamRequests = Collections.synchronizedList(new LinkedList<String>());

+

+    /**

+     * Returns the Socks5BytestreamManager to handle SOCKS5 Bytestreams for a given

+     * {@link Connection}.

+     * <p>

+     * If no manager exists a new is created and initialized.

+     * 

+     * @param connection the XMPP connection or <code>null</code> if given connection is

+     *        <code>null</code>

+     * @return the Socks5BytestreamManager for the given XMPP connection

+     */

+    public static synchronized Socks5BytestreamManager getBytestreamManager(Connection connection) {

+        if (connection == null) {

+            return null;

+        }

+        Socks5BytestreamManager manager = managers.get(connection);

+        if (manager == null) {

+            manager = new Socks5BytestreamManager(connection);

+            managers.put(connection, manager);

+            manager.activate();

+        }

+        return manager;

+    }

+

+    /**

+     * Private constructor.

+     * 

+     * @param connection the XMPP connection

+     */

+    private Socks5BytestreamManager(Connection connection) {

+        this.connection = connection;

+        this.initiationListener = new InitiationListener(this);

+    }

+

+    /**

+     * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request unless

+     * there is a user specific BytestreamListener registered.

+     * <p>

+     * If no listeners are registered all SOCKS5 Bytestream request are rejected with a

+     * &lt;not-acceptable/&gt; error.

+     * <p>

+     * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5

+     * bytestream requests sent in the context of <a

+     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See

+     * {@link FileTransferManager})

+     * 

+     * @param listener the listener to register

+     */

+    public void addIncomingBytestreamListener(BytestreamListener listener) {

+        this.allRequestListeners.add(listener);

+    }

+

+    /**

+     * Removes the given listener from the list of listeners for all incoming SOCKS5 Bytestream

+     * requests.

+     * 

+     * @param listener the listener to remove

+     */

+    public void removeIncomingBytestreamListener(BytestreamListener listener) {

+        this.allRequestListeners.remove(listener);

+    }

+

+    /**

+     * Adds BytestreamListener that is called for every incoming SOCKS5 Bytestream request from the

+     * given user.

+     * <p>

+     * Use this method if you are awaiting an incoming SOCKS5 Bytestream request from a specific

+     * user.

+     * <p>

+     * If no listeners are registered all SOCKS5 Bytestream request are rejected with a

+     * &lt;not-acceptable/&gt; error.

+     * <p>

+     * Note that the registered {@link BytestreamListener} will NOT be notified on incoming Socks5

+     * bytestream requests sent in the context of <a

+     * href="http://xmpp.org/extensions/xep-0096.html">XEP-0096</a> file transfer. (See

+     * {@link FileTransferManager})

+     * 

+     * @param listener the listener to register

+     * @param initiatorJID the JID of the user that wants to establish a SOCKS5 Bytestream

+     */

+    public void addIncomingBytestreamListener(BytestreamListener listener, String initiatorJID) {

+        this.userListeners.put(initiatorJID, listener);

+    }

+

+    /**

+     * Removes the listener for the given user.

+     * 

+     * @param initiatorJID the JID of the user the listener should be removed

+     */

+    public void removeIncomingBytestreamListener(String initiatorJID) {

+        this.userListeners.remove(initiatorJID);

+    }

+

+    /**

+     * Use this method to ignore the next incoming SOCKS5 Bytestream request containing the given

+     * session ID. No listeners will be notified for this request and and no error will be returned

+     * to the initiator.

+     * <p>

+     * This method should be used if you are awaiting a SOCKS5 Bytestream request as a reply to

+     * another packet (e.g. file transfer).

+     * 

+     * @param sessionID to be ignored

+     */

+    public void ignoreBytestreamRequestOnce(String sessionID) {

+        this.ignoredBytestreamRequests.add(sessionID);

+    }

+

+    /**

+     * Disables the SOCKS5 Bytestream manager by removing the SOCKS5 Bytestream feature from the

+     * service discovery, disabling the listener for SOCKS5 Bytestream initiation requests and

+     * resetting its internal state.

+     * <p>

+     * To re-enable the SOCKS5 Bytestream feature invoke {@link #getBytestreamManager(Connection)}.

+     * Using the file transfer API will automatically re-enable the SOCKS5 Bytestream feature.

+     */

+    public synchronized void disableService() {

+

+        // remove initiation packet listener

+        this.connection.removePacketListener(this.initiationListener);

+

+        // shutdown threads

+        this.initiationListener.shutdown();

+

+        // clear listeners

+        this.allRequestListeners.clear();

+        this.userListeners.clear();

+

+        // reset internal state

+        this.lastWorkingProxy = null;

+        this.proxyBlacklist.clear();

+        this.ignoredBytestreamRequests.clear();

+

+        // remove manager from static managers map

+        managers.remove(this.connection);

+

+        // shutdown local SOCKS5 proxy if there are no more managers for other connections

+        if (managers.size() == 0) {

+            Socks5Proxy.getSocks5Proxy().stop();

+        }

+

+        // remove feature from service discovery

+        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);

+

+        // check if service discovery is not already disposed by connection shutdown

+        if (serviceDiscoveryManager != null) {

+            serviceDiscoveryManager.removeFeature(NAMESPACE);

+        }

+

+    }

+

+    /**

+     * Returns the timeout to wait for the response to the SOCKS5 Bytestream initialization request.

+     * Default is 10000ms.

+     * 

+     * @return the timeout to wait for the response to the SOCKS5 Bytestream initialization request

+     */

+    public int getTargetResponseTimeout() {

+        if (this.targetResponseTimeout <= 0) {

+            this.targetResponseTimeout = 10000;

+        }

+        return targetResponseTimeout;

+    }

+

+    /**

+     * Sets the timeout to wait for the response to the SOCKS5 Bytestream initialization request.

+     * Default is 10000ms.

+     * 

+     * @param targetResponseTimeout the timeout to set

+     */

+    public void setTargetResponseTimeout(int targetResponseTimeout) {

+        this.targetResponseTimeout = targetResponseTimeout;

+    }

+

+    /**

+     * Returns the timeout for connecting to the SOCKS5 proxy selected by the target. Default is

+     * 10000ms.

+     * 

+     * @return the timeout for connecting to the SOCKS5 proxy selected by the target

+     */

+    public int getProxyConnectionTimeout() {

+        if (this.proxyConnectionTimeout <= 0) {

+            this.proxyConnectionTimeout = 10000;

+        }

+        return proxyConnectionTimeout;

+    }

+

+    /**

+     * Sets the timeout for connecting to the SOCKS5 proxy selected by the target. Default is

+     * 10000ms.

+     * 

+     * @param proxyConnectionTimeout the timeout to set

+     */

+    public void setProxyConnectionTimeout(int proxyConnectionTimeout) {

+        this.proxyConnectionTimeout = proxyConnectionTimeout;

+    }

+

+    /**

+     * Returns if the prioritization of the last working SOCKS5 proxy on successive SOCKS5

+     * Bytestream connections is enabled. Default is <code>true</code>.

+     * 

+     * @return <code>true</code> if prioritization is enabled, <code>false</code> otherwise

+     */

+    public boolean isProxyPrioritizationEnabled() {

+        return proxyPrioritizationEnabled;

+    }

+

+    /**

+     * Enable/disable the prioritization of the last working SOCKS5 proxy on successive SOCKS5

+     * Bytestream connections.

+     * 

+     * @param proxyPrioritizationEnabled enable/disable the prioritization of the last working

+     *        SOCKS5 proxy

+     */

+    public void setProxyPrioritizationEnabled(boolean proxyPrioritizationEnabled) {

+        this.proxyPrioritizationEnabled = proxyPrioritizationEnabled;

+    }

+

+    /**

+     * Establishes a SOCKS5 Bytestream with the given user and returns the Socket to send/receive

+     * data to/from the user.

+     * <p>

+     * Use this method to establish SOCKS5 Bytestreams to users accepting all incoming Socks5

+     * bytestream requests since this method doesn't provide a way to tell the user something about

+     * the data to be sent.

+     * <p>

+     * To establish a SOCKS5 Bytestream after negotiation the kind of data to be sent (e.g. file

+     * transfer) use {@link #establishSession(String, String)}.

+     * 

+     * @param targetJID the JID of the user a SOCKS5 Bytestream should be established

+     * @return the Socket to send/receive data to/from the user

+     * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5

+     *         Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies

+     * @throws IOException if the bytestream could not be established

+     * @throws InterruptedException if the current thread was interrupted while waiting

+     */

+    public Socks5BytestreamSession establishSession(String targetJID) throws XMPPException,

+                    IOException, InterruptedException {

+        String sessionID = getNextSessionID();

+        return establishSession(targetJID, sessionID);

+    }

+

+    /**

+     * Establishes a SOCKS5 Bytestream with the given user using the given session ID and returns

+     * the Socket to send/receive data to/from the user.

+     * 

+     * @param targetJID the JID of the user a SOCKS5 Bytestream should be established

+     * @param sessionID the session ID for the SOCKS5 Bytestream request

+     * @return the Socket to send/receive data to/from the user

+     * @throws XMPPException if the user doesn't support or accept SOCKS5 Bytestreams, if no Socks5

+     *         Proxy could be found, if the user couldn't connect to any of the SOCKS5 Proxies

+     * @throws IOException if the bytestream could not be established

+     * @throws InterruptedException if the current thread was interrupted while waiting

+     */

+    public Socks5BytestreamSession establishSession(String targetJID, String sessionID)

+                    throws XMPPException, IOException, InterruptedException {

+

+        XMPPException discoveryException = null;

+        // check if target supports SOCKS5 Bytestream

+        if (!supportsSocks5(targetJID)) {

+            throw new XMPPException(targetJID + " doesn't support SOCKS5 Bytestream");

+        }

+

+        List<String> proxies = new ArrayList<String>();

+        // determine SOCKS5 proxies from XMPP-server

+        try {

+            proxies.addAll(determineProxies());

+        } catch (XMPPException e) {

+            // don't abort here, just remember the exception thrown by determineProxies()

+            // determineStreamHostInfos() will at least add the local Socks5 proxy (if enabled)

+            discoveryException = e;

+        }

+

+        // determine address and port of each proxy

+        List<StreamHost> streamHosts = determineStreamHostInfos(proxies);

+

+        if (streamHosts.isEmpty()) {

+            throw discoveryException != null ? discoveryException : new XMPPException("no SOCKS5 proxies available");

+        }

+

+        // compute digest

+        String digest = Socks5Utils.createDigest(sessionID, this.connection.getUser(), targetJID);

+

+        // prioritize last working SOCKS5 proxy if exists

+        if (this.proxyPrioritizationEnabled && this.lastWorkingProxy != null) {

+            StreamHost selectedStreamHost = null;

+            for (StreamHost streamHost : streamHosts) {

+                if (streamHost.getJID().equals(this.lastWorkingProxy)) {

+                    selectedStreamHost = streamHost;

+                    break;

+                }

+            }

+            if (selectedStreamHost != null) {

+                streamHosts.remove(selectedStreamHost);

+                streamHosts.add(0, selectedStreamHost);

+            }

+

+        }

+

+        Socks5Proxy socks5Proxy = Socks5Proxy.getSocks5Proxy();

+        try {

+

+            // add transfer digest to local proxy to make transfer valid

+            socks5Proxy.addTransfer(digest);

+

+            // create initiation packet

+            Bytestream initiation = createBytestreamInitiation(sessionID, targetJID, streamHosts);

+

+            // send initiation packet

+            Packet response = SyncPacketSend.getReply(this.connection, initiation,

+                            getTargetResponseTimeout());

+

+            // extract used stream host from response

+            StreamHostUsed streamHostUsed = ((Bytestream) response).getUsedHost();

+            StreamHost usedStreamHost = initiation.getStreamHost(streamHostUsed.getJID());

+

+            if (usedStreamHost == null) {

+                throw new XMPPException("Remote user responded with unknown host");

+            }

+

+            // build SOCKS5 client

+            Socks5Client socks5Client = new Socks5ClientForInitiator(usedStreamHost, digest,

+                            this.connection, sessionID, targetJID);

+

+            // establish connection to proxy

+            Socket socket = socks5Client.getSocket(getProxyConnectionTimeout());

+

+            // remember last working SOCKS5 proxy to prioritize it for next request

+            this.lastWorkingProxy = usedStreamHost.getJID();

+

+            // negotiation successful, return the output stream

+            return new Socks5BytestreamSession(socket, usedStreamHost.getJID().equals(

+                            this.connection.getUser()));

+

+        }

+        catch (TimeoutException e) {

+            throw new IOException("Timeout while connecting to SOCKS5 proxy");

+        }

+        finally {

+

+            // remove transfer digest if output stream is returned or an exception

+            // occurred

+            socks5Proxy.removeTransfer(digest);

+

+        }

+    }

+

+    /**

+     * Returns <code>true</code> if the given target JID supports feature SOCKS5 Bytestream.

+     * 

+     * @param targetJID the target JID

+     * @return <code>true</code> if the given target JID supports feature SOCKS5 Bytestream

+     *         otherwise <code>false</code>

+     * @throws XMPPException if there was an error querying target for supported features

+     */

+    private boolean supportsSocks5(String targetJID) throws XMPPException {

+        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);

+        DiscoverInfo discoverInfo = serviceDiscoveryManager.discoverInfo(targetJID);

+        return discoverInfo.containsFeature(NAMESPACE);

+    }

+

+    /**

+     * Returns a list of JIDs of SOCKS5 proxies by querying the XMPP server. The SOCKS5 proxies are

+     * in the same order as returned by the XMPP server.

+     * 

+     * @return list of JIDs of SOCKS5 proxies

+     * @throws XMPPException if there was an error querying the XMPP server for SOCKS5 proxies

+     */

+    private List<String> determineProxies() throws XMPPException {

+        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager.getInstanceFor(this.connection);

+

+        List<String> proxies = new ArrayList<String>();

+

+        // get all items form XMPP server

+        DiscoverItems discoverItems = serviceDiscoveryManager.discoverItems(this.connection.getServiceName());

+        Iterator<Item> itemIterator = discoverItems.getItems();

+

+        // query all items if they are SOCKS5 proxies

+        while (itemIterator.hasNext()) {

+            Item item = itemIterator.next();

+

+            // skip blacklisted servers

+            if (this.proxyBlacklist.contains(item.getEntityID())) {

+                continue;

+            }

+

+            try {

+                DiscoverInfo proxyInfo;

+                proxyInfo = serviceDiscoveryManager.discoverInfo(item.getEntityID());

+                Iterator<Identity> identities = proxyInfo.getIdentities();

+

+                // item must have category "proxy" and type "bytestream"

+                while (identities.hasNext()) {

+                    Identity identity = identities.next();

+

+                    if ("proxy".equalsIgnoreCase(identity.getCategory())

+                                    && "bytestreams".equalsIgnoreCase(identity.getType())) {

+                        proxies.add(item.getEntityID());

+                        break;

+                    }

+

+                    /*

+                     * server is not a SOCKS5 proxy, blacklist server to skip next time a Socks5

+                     * bytestream should be established

+                     */

+                    this.proxyBlacklist.add(item.getEntityID());

+

+                }

+            }

+            catch (XMPPException e) {

+                // blacklist errornous server

+                this.proxyBlacklist.add(item.getEntityID());

+            }

+        }

+

+        return proxies;

+    }

+

+    /**

+     * Returns a list of stream hosts containing the IP address an the port for the given list of

+     * SOCKS5 proxy JIDs. The order of the returned list is the same as the given list of JIDs

+     * excluding all SOCKS5 proxies who's network settings could not be determined. If a local

+     * SOCKS5 proxy is running it will be the first item in the list returned.

+     * 

+     * @param proxies a list of SOCKS5 proxy JIDs

+     * @return a list of stream hosts containing the IP address an the port

+     */

+    private List<StreamHost> determineStreamHostInfos(List<String> proxies) {

+        List<StreamHost> streamHosts = new ArrayList<StreamHost>();

+

+        // add local proxy on first position if exists

+        List<StreamHost> localProxies = getLocalStreamHost();

+        if (localProxies != null) {

+            streamHosts.addAll(localProxies);

+        }

+

+        // query SOCKS5 proxies for network settings

+        for (String proxy : proxies) {

+            Bytestream streamHostRequest = createStreamHostRequest(proxy);

+            try {

+                Bytestream response = (Bytestream) SyncPacketSend.getReply(this.connection,

+                                streamHostRequest);

+                streamHosts.addAll(response.getStreamHosts());

+            }

+            catch (XMPPException e) {

+                // blacklist errornous proxies

+                this.proxyBlacklist.add(proxy);

+            }

+        }

+

+        return streamHosts;

+    }

+

+    /**

+     * Returns a IQ packet to query a SOCKS5 proxy its network settings.

+     * 

+     * @param proxy the proxy to query

+     * @return IQ packet to query a SOCKS5 proxy its network settings

+     */

+    private Bytestream createStreamHostRequest(String proxy) {

+        Bytestream request = new Bytestream();

+        request.setType(IQ.Type.GET);

+        request.setTo(proxy);

+        return request;

+    }

+

+    /**

+     * Returns the stream host information of the local SOCKS5 proxy containing the IP address and

+     * the port or null if local SOCKS5 proxy is not running.

+     * 

+     * @return the stream host information of the local SOCKS5 proxy or null if local SOCKS5 proxy

+     *         is not running

+     */

+    private List<StreamHost> getLocalStreamHost() {

+

+        // get local proxy singleton

+        Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();

+

+        if (socks5Server.isRunning()) {

+            List<String> addresses = socks5Server.getLocalAddresses();

+            int port = socks5Server.getPort();

+

+            if (addresses.size() >= 1) {

+                List<StreamHost> streamHosts = new ArrayList<StreamHost>();

+                for (String address : addresses) {

+                    StreamHost streamHost = new StreamHost(this.connection.getUser(), address);

+                    streamHost.setPort(port);

+                    streamHosts.add(streamHost);

+                }

+                return streamHosts;

+            }

+

+        }

+

+        // server is not running or local address could not be determined

+        return null;

+    }

+

+    /**

+     * Returns a SOCKS5 Bytestream initialization request packet with the given session ID

+     * containing the given stream hosts for the given target JID.

+     * 

+     * @param sessionID the session ID for the SOCKS5 Bytestream

+     * @param targetJID the target JID of SOCKS5 Bytestream request

+     * @param streamHosts a list of SOCKS5 proxies the target should connect to

+     * @return a SOCKS5 Bytestream initialization request packet

+     */

+    private Bytestream createBytestreamInitiation(String sessionID, String targetJID,

+                    List<StreamHost> streamHosts) {

+        Bytestream initiation = new Bytestream(sessionID);

+

+        // add all stream hosts

+        for (StreamHost streamHost : streamHosts) {

+            initiation.addStreamHost(streamHost);

+        }

+

+        initiation.setType(IQ.Type.SET);

+        initiation.setTo(targetJID);

+

+        return initiation;

+    }

+

+    /**

+     * Responses to the given packet's sender with a XMPP error that a SOCKS5 Bytestream is not

+     * accepted.

+     * 

+     * @param packet Packet that should be answered with a not-acceptable error

+     */

+    protected void replyRejectPacket(IQ packet) {

+        XMPPError xmppError = new XMPPError(XMPPError.Condition.no_acceptable);

+        IQ errorIQ = IQ.createErrorResponse(packet, xmppError);

+        this.connection.sendPacket(errorIQ);

+    }

+

+    /**

+     * Activates the Socks5BytestreamManager by registering the SOCKS5 Bytestream initialization

+     * listener and enabling the SOCKS5 Bytestream feature.

+     */

+    private void activate() {

+        // register bytestream initiation packet listener

+        this.connection.addPacketListener(this.initiationListener,

+                        this.initiationListener.getFilter());

+

+        // enable SOCKS5 feature

+        enableService();

+    }

+

+    /**

+     * Adds the SOCKS5 Bytestream feature to the service discovery.

+     */

+    private void enableService() {

+        ServiceDiscoveryManager manager = ServiceDiscoveryManager.getInstanceFor(this.connection);

+        if (!manager.includesFeature(NAMESPACE)) {

+            manager.addFeature(NAMESPACE);

+        }

+    }

+

+    /**

+     * Returns a new unique session ID.

+     * 

+     * @return a new unique session ID

+     */

+    private String getNextSessionID() {

+        StringBuilder buffer = new StringBuilder();

+        buffer.append(SESSION_ID_PREFIX);

+        buffer.append(Math.abs(randomGenerator.nextLong()));

+        return buffer.toString();

+    }

+

+    /**

+     * Returns the XMPP connection.

+     * 

+     * @return the XMPP connection

+     */

+    protected Connection getConnection() {

+        return this.connection;

+    }

+

+    /**

+     * Returns the {@link BytestreamListener} that should be informed if a SOCKS5 Bytestream request

+     * from the given initiator JID is received.

+     * 

+     * @param initiator the initiator's JID

+     * @return the listener

+     */

+    protected BytestreamListener getUserListener(String initiator) {

+        return this.userListeners.get(initiator);

+    }

+

+    /**

+     * Returns a list of {@link BytestreamListener} that are informed if there are no listeners for

+     * a specific initiator.

+     * 

+     * @return list of listeners

+     */

+    protected List<BytestreamListener> getAllRequestListeners() {

+        return this.allRequestListeners;

+    }

+

+    /**

+     * Returns the list of session IDs that should be ignored by the InitialtionListener

+     * 

+     * @return list of session IDs

+     */

+    protected List<String> getIgnoredBytestreamRequests() {

+        return ignoredBytestreamRequests;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java
new file mode 100644
index 0000000..0b2fdeb
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamRequest.java
@@ -0,0 +1,316 @@
+/**

+ * 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.smackx.bytestreams.socks5;

+

+import java.io.IOException;

+import java.net.Socket;

+import java.util.Collection;

+import java.util.concurrent.TimeoutException;

+

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.XMPPError;

+import org.jivesoftware.smack.util.Cache;

+import org.jivesoftware.smackx.bytestreams.BytestreamRequest;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;

+

+/**

+ * Socks5BytestreamRequest class handles incoming SOCKS5 Bytestream requests.

+ * 

+ * @author Henning Staib

+ */

+public class Socks5BytestreamRequest implements BytestreamRequest {

+

+    /* lifetime of an Item in the blacklist */

+    private static final long BLACKLIST_LIFETIME = 60 * 1000 * 120;

+

+    /* size of the blacklist */

+    private static final int BLACKLIST_MAX_SIZE = 100;

+

+    /* blacklist of addresses of SOCKS5 proxies */

+    private static final Cache<String, Integer> ADDRESS_BLACKLIST = new Cache<String, Integer>(

+                    BLACKLIST_MAX_SIZE, BLACKLIST_LIFETIME);

+

+    /*

+     * The number of connection failures it takes for a particular SOCKS5 proxy to be blacklisted.

+     * When a proxy is blacklisted no more connection attempts will be made to it for a period of 2

+     * hours.

+     */

+    private static int CONNECTION_FAILURE_THRESHOLD = 2;

+

+    /* the bytestream initialization request */

+    private Bytestream bytestreamRequest;

+

+    /* SOCKS5 Bytestream manager containing the XMPP connection and helper methods */

+    private Socks5BytestreamManager manager;

+

+    /* timeout to connect to all SOCKS5 proxies */

+    private int totalConnectTimeout = 10000;

+

+    /* minimum timeout to connect to one SOCKS5 proxy */

+    private int minimumConnectTimeout = 2000;

+

+    /**

+     * Returns the number of connection failures it takes for a particular SOCKS5 proxy to be

+     * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a

+     * period of 2 hours. Default is 2.

+     * 

+     * @return the number of connection failures it takes for a particular SOCKS5 proxy to be

+     *         blacklisted

+     */

+    public static int getConnectFailureThreshold() {

+        return CONNECTION_FAILURE_THRESHOLD;

+    }

+

+    /**

+     * Sets the number of connection failures it takes for a particular SOCKS5 proxy to be

+     * blacklisted. When a proxy is blacklisted no more connection attempts will be made to it for a

+     * period of 2 hours. Default is 2.

+     * <p>

+     * Setting the connection failure threshold to zero disables the blacklisting.

+     * 

+     * @param connectFailureThreshold the number of connection failures it takes for a particular

+     *        SOCKS5 proxy to be blacklisted

+     */

+    public static void setConnectFailureThreshold(int connectFailureThreshold) {

+        CONNECTION_FAILURE_THRESHOLD = connectFailureThreshold;

+    }

+

+    /**

+     * Creates a new Socks5BytestreamRequest.

+     * 

+     * @param manager the SOCKS5 Bytestream manager

+     * @param bytestreamRequest the SOCKS5 Bytestream initialization packet

+     */

+    protected Socks5BytestreamRequest(Socks5BytestreamManager manager, Bytestream bytestreamRequest) {

+        this.manager = manager;

+        this.bytestreamRequest = bytestreamRequest;

+    }

+

+    /**

+     * Returns the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.

+     * <p>

+     * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given

+     * by the initiator until a connection is established. This timeout divided by the number of

+     * SOCKS5 proxies determines the timeout for every connection attempt.

+     * <p>

+     * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking

+     * {@link #setMinimumConnectTimeout(int)}.

+     * 

+     * @return the maximum timeout to connect to SOCKS5 proxies

+     */

+    public int getTotalConnectTimeout() {

+        if (this.totalConnectTimeout <= 0) {

+            return 10000;

+        }

+        return this.totalConnectTimeout;

+    }

+

+    /**

+     * Sets the maximum timeout to connect to SOCKS5 proxies. Default is 10000ms.

+     * <p>

+     * When accepting a SOCKS5 Bytestream request Smack tries to connect to all SOCKS5 proxies given

+     * by the initiator until a connection is established. This timeout divided by the number of

+     * SOCKS5 proxies determines the timeout for every connection attempt.

+     * <p>

+     * You can set the minimum timeout for establishing a connection to one SOCKS5 proxy by invoking

+     * {@link #setMinimumConnectTimeout(int)}.

+     * 

+     * @param totalConnectTimeout the maximum timeout to connect to SOCKS5 proxies

+     */

+    public void setTotalConnectTimeout(int totalConnectTimeout) {

+        this.totalConnectTimeout = totalConnectTimeout;

+    }

+

+    /**

+     * Returns the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream

+     * request. Default is 2000ms.

+     * 

+     * @return the timeout to connect to one SOCKS5 proxy

+     */

+    public int getMinimumConnectTimeout() {

+        if (this.minimumConnectTimeout <= 0) {

+            return 2000;

+        }

+        return this.minimumConnectTimeout;

+    }

+

+    /**

+     * Sets the timeout to connect to one SOCKS5 proxy while accepting the SOCKS5 Bytestream

+     * request. Default is 2000ms.

+     * 

+     * @param minimumConnectTimeout the timeout to connect to one SOCKS5 proxy

+     */

+    public void setMinimumConnectTimeout(int minimumConnectTimeout) {

+        this.minimumConnectTimeout = minimumConnectTimeout;

+    }

+

+    /**

+     * Returns the sender of the SOCKS5 Bytestream initialization request.

+     * 

+     * @return the sender of the SOCKS5 Bytestream initialization request.

+     */

+    public String getFrom() {

+        return this.bytestreamRequest.getFrom();

+    }

+

+    /**

+     * Returns the session ID of the SOCKS5 Bytestream initialization request.

+     * 

+     * @return the session ID of the SOCKS5 Bytestream initialization request.

+     */

+    public String getSessionID() {

+        return this.bytestreamRequest.getSessionID();

+    }

+

+    /**

+     * Accepts the SOCKS5 Bytestream initialization request and returns the socket to send/receive

+     * data.

+     * <p>

+     * Before accepting the SOCKS5 Bytestream request you can set timeouts by invoking

+     * {@link #setTotalConnectTimeout(int)} and {@link #setMinimumConnectTimeout(int)}.

+     * 

+     * @return the socket to send/receive data

+     * @throws XMPPException if connection to all SOCKS5 proxies failed or if stream is invalid.

+     * @throws InterruptedException if the current thread was interrupted while waiting

+     */

+    public Socks5BytestreamSession accept() throws XMPPException, InterruptedException {

+        Collection<StreamHost> streamHosts = this.bytestreamRequest.getStreamHosts();

+

+        // throw exceptions if request contains no stream hosts

+        if (streamHosts.size() == 0) {

+            cancelRequest();

+        }

+

+        StreamHost selectedHost = null;

+        Socket socket = null;

+

+        String digest = Socks5Utils.createDigest(this.bytestreamRequest.getSessionID(),

+                        this.bytestreamRequest.getFrom(), this.manager.getConnection().getUser());

+

+        /*

+         * determine timeout for each connection attempt; each SOCKS5 proxy has the same amount of

+         * time so that the first does not consume the whole timeout

+         */

+        int timeout = Math.max(getTotalConnectTimeout() / streamHosts.size(),

+                        getMinimumConnectTimeout());

+

+        for (StreamHost streamHost : streamHosts) {

+            String address = streamHost.getAddress() + ":" + streamHost.getPort();

+

+            // check to see if this address has been blacklisted

+            int failures = getConnectionFailures(address);

+            if (CONNECTION_FAILURE_THRESHOLD > 0 && failures >= CONNECTION_FAILURE_THRESHOLD) {

+                continue;

+            }

+

+            // establish socket

+            try {

+

+                // build SOCKS5 client

+                final Socks5Client socks5Client = new Socks5Client(streamHost, digest);

+

+                // connect to SOCKS5 proxy with a timeout

+                socket = socks5Client.getSocket(timeout);

+

+                // set selected host

+                selectedHost = streamHost;

+                break;

+

+            }

+            catch (TimeoutException e) {

+                incrementConnectionFailures(address);

+            }

+            catch (IOException e) {

+                incrementConnectionFailures(address);

+            }

+            catch (XMPPException e) {

+                incrementConnectionFailures(address);

+            }

+

+        }

+

+        // throw exception if connecting to all SOCKS5 proxies failed

+        if (selectedHost == null || socket == null) {

+            cancelRequest();

+        }

+

+        // send used-host confirmation

+        Bytestream response = createUsedHostResponse(selectedHost);

+        this.manager.getConnection().sendPacket(response);

+

+        return new Socks5BytestreamSession(socket, selectedHost.getJID().equals(

+                        this.bytestreamRequest.getFrom()));

+

+    }

+

+    /**

+     * Rejects the SOCKS5 Bytestream request by sending a reject error to the initiator.

+     */

+    public void reject() {

+        this.manager.replyRejectPacket(this.bytestreamRequest);

+    }

+

+    /**

+     * Cancels the SOCKS5 Bytestream request by sending an error to the initiator and building a

+     * XMPP exception.

+     * 

+     * @throws XMPPException XMPP exception containing the XMPP error

+     */

+    private void cancelRequest() throws XMPPException {

+        String errorMessage = "Could not establish socket with any provided host";

+        XMPPError error = new XMPPError(XMPPError.Condition.item_not_found, errorMessage);

+        IQ errorIQ = IQ.createErrorResponse(this.bytestreamRequest, error);

+        this.manager.getConnection().sendPacket(errorIQ);

+        throw new XMPPException(errorMessage, error);

+    }

+

+    /**

+     * Returns the response to the SOCKS5 Bytestream request containing the SOCKS5 proxy used.

+     * 

+     * @param selectedHost the used SOCKS5 proxy

+     * @return the response to the SOCKS5 Bytestream request

+     */

+    private Bytestream createUsedHostResponse(StreamHost selectedHost) {

+        Bytestream response = new Bytestream(this.bytestreamRequest.getSessionID());

+        response.setTo(this.bytestreamRequest.getFrom());

+        response.setType(IQ.Type.RESULT);

+        response.setPacketID(this.bytestreamRequest.getPacketID());

+        response.setUsedHost(selectedHost.getJID());

+        return response;

+    }

+

+    /**

+     * Increments the connection failure counter by one for the given address.

+     * 

+     * @param address the address the connection failure counter should be increased

+     */

+    private void incrementConnectionFailures(String address) {

+        Integer count = ADDRESS_BLACKLIST.get(address);

+        ADDRESS_BLACKLIST.put(address, count == null ? 1 : count + 1);

+    }

+

+    /**

+     * Returns how often the connection to the given address failed.

+     * 

+     * @param address the address

+     * @return number of connection failures

+     */

+    private int getConnectionFailures(String address) {

+        Integer count = ADDRESS_BLACKLIST.get(address);

+        return count != null ? count : 0;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java
new file mode 100644
index 0000000..41ab142
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5BytestreamSession.java
@@ -0,0 +1,98 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * 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.smackx.bytestreams.socks5;

+

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.OutputStream;

+import java.net.Socket;

+import java.net.SocketException;

+

+import org.jivesoftware.smackx.bytestreams.BytestreamSession;

+

+/**

+ * Socks5BytestreamSession class represents a SOCKS5 Bytestream session.

+ * 

+ * @author Henning Staib

+ */

+public class Socks5BytestreamSession implements BytestreamSession {

+

+    /* the underlying socket of the SOCKS5 Bytestream */

+    private final Socket socket;

+

+    /* flag to indicate if this session is a direct or mediated connection */

+    private final boolean isDirect;

+

+    protected Socks5BytestreamSession(Socket socket, boolean isDirect) {

+        this.socket = socket;

+        this.isDirect = isDirect;

+    }

+

+    /**

+     * Returns <code>true</code> if the session is established through a direct connection between

+     * the initiator and target, <code>false</code> if the session is mediated over a SOCKS proxy.

+     * 

+     * @return <code>true</code> if session is a direct connection, <code>false</code> if session is

+     *         mediated over a SOCKS5 proxy

+     */

+    public boolean isDirect() {

+        return this.isDirect;

+    }

+

+    /**

+     * Returns <code>true</code> if the session is mediated over a SOCKS proxy, <code>false</code>

+     * if this session is established through a direct connection between the initiator and target.

+     * 

+     * @return <code>true</code> if session is mediated over a SOCKS5 proxy, <code>false</code> if

+     *         session is a direct connection

+     */

+    public boolean isMediated() {

+        return !this.isDirect;

+    }

+

+    public InputStream getInputStream() throws IOException {

+        return this.socket.getInputStream();

+    }

+

+    public OutputStream getOutputStream() throws IOException {

+        return this.socket.getOutputStream();

+    }

+

+    public int getReadTimeout() throws IOException {

+        try {

+            return this.socket.getSoTimeout();

+        }

+        catch (SocketException e) {

+            throw new IOException("Error on underlying Socket");

+        }

+    }

+

+    public void setReadTimeout(int timeout) throws IOException {

+        try {

+            this.socket.setSoTimeout(timeout);

+        }

+        catch (SocketException e) {

+            throw new IOException("Error on underlying Socket");

+        }

+    }

+

+    public void close() throws IOException {

+        this.socket.close();

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java
new file mode 100644
index 0000000..664ea59
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Client.java
@@ -0,0 +1,204 @@
+/**

+ * 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.smackx.bytestreams.socks5;

+

+import java.io.DataInputStream;

+import java.io.DataOutputStream;

+import java.io.IOException;

+import java.net.InetSocketAddress;

+import java.net.Socket;

+import java.net.SocketAddress;

+import java.util.Arrays;

+import java.util.concurrent.Callable;

+import java.util.concurrent.ExecutionException;

+import java.util.concurrent.FutureTask;

+import java.util.concurrent.TimeUnit;

+import java.util.concurrent.TimeoutException;

+

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;

+

+/**

+ * The SOCKS5 client class handles establishing a connection to a SOCKS5 proxy. Connecting to a

+ * SOCKS5 proxy requires authentication. This implementation only supports the no-authentication

+ * authentication method.

+ * 

+ * @author Henning Staib

+ */

+class Socks5Client {

+

+    /* stream host containing network settings and name of the SOCKS5 proxy */

+    protected StreamHost streamHost;

+

+    /* SHA-1 digest identifying the SOCKS5 stream */

+    protected String digest;

+

+    /**

+     * Constructor for a SOCKS5 client.

+     * 

+     * @param streamHost containing network settings of the SOCKS5 proxy

+     * @param digest identifying the SOCKS5 Bytestream

+     */

+    public Socks5Client(StreamHost streamHost, String digest) {

+        this.streamHost = streamHost;

+        this.digest = digest;

+    }

+

+    /**

+     * Returns the initialized socket that can be used to transfer data between peers via the SOCKS5

+     * proxy.

+     * 

+     * @param timeout timeout to connect to SOCKS5 proxy in milliseconds

+     * @return socket the initialized socket

+     * @throws IOException if initializing the socket failed due to a network error

+     * @throws XMPPException if establishing connection to SOCKS5 proxy failed

+     * @throws TimeoutException if connecting to SOCKS5 proxy timed out

+     * @throws InterruptedException if the current thread was interrupted while waiting

+     */

+    public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,

+                    TimeoutException {

+

+        // wrap connecting in future for timeout

+        FutureTask<Socket> futureTask = new FutureTask<Socket>(new Callable<Socket>() {

+

+            public Socket call() throws Exception {

+

+                // initialize socket

+                Socket socket = new Socket();

+                SocketAddress socketAddress = new InetSocketAddress(streamHost.getAddress(),

+                                streamHost.getPort());

+                socket.connect(socketAddress);

+

+                // initialize connection to SOCKS5 proxy

+                if (!establish(socket)) {

+

+                    // initialization failed, close socket

+                    socket.close();

+                    throw new XMPPException("establishing connection to SOCKS5 proxy failed");

+

+                }

+

+                return socket;

+            }

+

+        });

+        Thread executor = new Thread(futureTask);

+        executor.start();

+

+        // get connection to initiator with timeout

+        try {

+            return futureTask.get(timeout, TimeUnit.MILLISECONDS);

+        }

+        catch (ExecutionException e) {

+            Throwable cause = e.getCause();

+            if (cause != null) {

+                // case exceptions to comply with method signature

+                if (cause instanceof IOException) {

+                    throw (IOException) cause;

+                }

+                if (cause instanceof XMPPException) {

+                    throw (XMPPException) cause;

+                }

+            }

+

+            // throw generic IO exception if unexpected exception was thrown

+            throw new IOException("Error while connection to SOCKS5 proxy");

+        }

+

+    }

+

+    /**

+     * Initializes the connection to the SOCKS5 proxy by negotiating authentication method and

+     * requesting a stream for the given digest. Currently only the no-authentication method is

+     * supported by the Socks5Client.

+     * <p>

+     * Returns <code>true</code> if a stream could be established, otherwise <code>false</code>. If

+     * <code>false</code> is returned the given Socket should be closed.

+     * 

+     * @param socket connected to a SOCKS5 proxy

+     * @return <code>true</code> if if a stream could be established, otherwise <code>false</code>.

+     *         If <code>false</code> is returned the given Socket should be closed.

+     * @throws IOException if a network error occurred

+     */

+    protected boolean establish(Socket socket) throws IOException {

+

+        /*

+         * use DataInputStream/DataOutpuStream to assure read and write is completed in a single

+         * statement

+         */

+        DataInputStream in = new DataInputStream(socket.getInputStream());

+        DataOutputStream out = new DataOutputStream(socket.getOutputStream());

+

+        // authentication negotiation

+        byte[] cmd = new byte[3];

+

+        cmd[0] = (byte) 0x05; // protocol version 5

+        cmd[1] = (byte) 0x01; // number of authentication methods supported

+        cmd[2] = (byte) 0x00; // authentication method: no-authentication required

+

+        out.write(cmd);

+        out.flush();

+

+        byte[] response = new byte[2];

+        in.readFully(response);

+

+        // check if server responded with correct version and no-authentication method

+        if (response[0] != (byte) 0x05 || response[1] != (byte) 0x00) {

+            return false;

+        }

+

+        // request SOCKS5 connection with given address/digest

+        byte[] connectionRequest = createSocks5ConnectRequest();

+        out.write(connectionRequest);

+        out.flush();

+

+        // receive response

+        byte[] connectionResponse;

+        try {

+            connectionResponse = Socks5Utils.receiveSocks5Message(in);

+        }

+        catch (XMPPException e) {

+            return false; // server answered in an unsupported way

+        }

+

+        // verify response

+        connectionRequest[1] = (byte) 0x00; // set expected return status to 0

+        return Arrays.equals(connectionRequest, connectionResponse);

+    }

+

+    /**

+     * Returns a SOCKS5 connection request message. It contains the command "connect", the address

+     * type "domain" and the digest as address.

+     * <p>

+     * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)

+     * 

+     * @return SOCKS5 connection request message

+     */

+    private byte[] createSocks5ConnectRequest() {

+        byte addr[] = this.digest.getBytes();

+

+        byte[] data = new byte[7 + addr.length];

+        data[0] = (byte) 0x05; // version (SOCKS5)

+        data[1] = (byte) 0x01; // command (1 - connect)

+        data[2] = (byte) 0x00; // reserved byte (always 0)

+        data[3] = (byte) 0x03; // address type (3 - domain name)

+        data[4] = (byte) addr.length; // address length

+        System.arraycopy(addr, 0, data, 5, addr.length); // address

+        data[data.length - 2] = (byte) 0; // address port (2 bytes always 0)

+        data[data.length - 1] = (byte) 0;

+

+        return data;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java
new file mode 100644
index 0000000..0d90791
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5ClientForInitiator.java
@@ -0,0 +1,117 @@
+/**

+ * 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.smackx.bytestreams.socks5;

+

+import java.io.IOException;

+import java.net.Socket;

+import java.util.concurrent.TimeoutException;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.util.SyncPacketSend;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream.StreamHost;

+

+/**

+ * Implementation of a SOCKS5 client used on the initiators side. This is needed because connecting

+ * to the local SOCKS5 proxy differs form the regular way to connect to a SOCKS5 proxy. Additionally

+ * a remote SOCKS5 proxy has to be activated by the initiator before data can be transferred between

+ * the peers.

+ * 

+ * @author Henning Staib

+ */

+class Socks5ClientForInitiator extends Socks5Client {

+

+    /* the XMPP connection used to communicate with the SOCKS5 proxy */

+    private Connection connection;

+

+    /* the session ID used to activate SOCKS5 stream */

+    private String sessionID;

+

+    /* the target JID used to activate SOCKS5 stream */

+    private String target;

+

+    /**

+     * Creates a new SOCKS5 client for the initiators side.

+     * 

+     * @param streamHost containing network settings of the SOCKS5 proxy

+     * @param digest identifying the SOCKS5 Bytestream

+     * @param connection the XMPP connection

+     * @param sessionID the session ID of the SOCKS5 Bytestream

+     * @param target the target JID of the SOCKS5 Bytestream

+     */

+    public Socks5ClientForInitiator(StreamHost streamHost, String digest, Connection connection,

+                    String sessionID, String target) {

+        super(streamHost, digest);

+        this.connection = connection;

+        this.sessionID = sessionID;

+        this.target = target;

+    }

+

+    public Socket getSocket(int timeout) throws IOException, XMPPException, InterruptedException,

+                    TimeoutException {

+        Socket socket = null;

+

+        // check if stream host is the local SOCKS5 proxy

+        if (this.streamHost.getJID().equals(this.connection.getUser())) {

+            Socks5Proxy socks5Server = Socks5Proxy.getSocks5Proxy();

+            socket = socks5Server.getSocket(this.digest);

+            if (socket == null) {

+                throw new XMPPException("target is not connected to SOCKS5 proxy");

+            }

+        }

+        else {

+            socket = super.getSocket(timeout);

+

+            try {

+                activate();

+            }

+            catch (XMPPException e) {

+                socket.close();

+                throw new XMPPException("activating SOCKS5 Bytestream failed", e);

+            }

+

+        }

+

+        return socket;

+    }

+

+    /**

+     * Activates the SOCKS5 Bytestream by sending a XMPP SOCKS5 Bytestream activation packet to the

+     * SOCKS5 proxy.

+     */

+    private void activate() throws XMPPException {

+        Bytestream activate = createStreamHostActivation();

+        // if activation fails #getReply throws an exception

+        SyncPacketSend.getReply(this.connection, activate);

+    }

+

+    /**

+     * Returns a SOCKS5 Bytestream activation packet.

+     * 

+     * @return SOCKS5 Bytestream activation packet

+     */

+    private Bytestream createStreamHostActivation() {

+        Bytestream activate = new Bytestream(this.sessionID);

+        activate.setMode(null);

+        activate.setType(IQ.Type.SET);

+        activate.setTo(this.streamHost.getJID());

+

+        activate.setToActivate(this.target);

+

+        return activate;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java
new file mode 100644
index 0000000..11ef7a9
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Proxy.java
@@ -0,0 +1,423 @@
+/**

+ * 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.smackx.bytestreams.socks5;

+

+import java.io.DataInputStream;

+import java.io.DataOutputStream;

+import java.io.IOException;

+import java.net.InetAddress;

+import java.net.ServerSocket;

+import java.net.Socket;

+import java.net.SocketException;

+import java.net.UnknownHostException;

+import java.util.ArrayList;

+import java.util.Collections;

+import java.util.LinkedHashSet;

+import java.util.LinkedList;

+import java.util.List;

+import java.util.Map;

+import java.util.Set;

+import java.util.concurrent.ConcurrentHashMap;

+

+import org.jivesoftware.smack.SmackConfiguration;

+import org.jivesoftware.smack.XMPPException;

+

+/**

+ * The Socks5Proxy class represents a local SOCKS5 proxy server. It can be enabled/disabled by

+ * setting the <code>localSocks5ProxyEnabled</code> flag in the <code>smack-config.xml</code> or by

+ * invoking {@link SmackConfiguration#setLocalSocks5ProxyEnabled(boolean)}. The proxy is enabled by

+ * default.

+ * <p>

+ * The port of the local SOCKS5 proxy can be configured by setting <code>localSocks5ProxyPort</code>

+ * in the <code>smack-config.xml</code> or by invoking

+ * {@link SmackConfiguration#setLocalSocks5ProxyPort(int)}. Default port is 7777. If you set the

+ * port to a negative value Smack tries to the absolute value and all following until it finds an

+ * open port.

+ * <p>

+ * If your application is running on a machine with multiple network interfaces or if you want to

+ * provide your public address in case you are behind a NAT router, invoke

+ * {@link #addLocalAddress(String)} or {@link #replaceLocalAddresses(List)} to modify the list of

+ * local network addresses used for outgoing SOCKS5 Bytestream requests.

+ * <p>

+ * The local SOCKS5 proxy server refuses all connections except the ones that are explicitly allowed

+ * in the process of establishing a SOCKS5 Bytestream (

+ * {@link Socks5BytestreamManager#establishSession(String)}).

+ * <p>

+ * This Implementation has the following limitations:

+ * <ul>

+ * <li>only supports the no-authentication authentication method</li>

+ * <li>only supports the <code>connect</code> command and will not answer correctly to other

+ * commands</li>

+ * <li>only supports requests with the domain address type and will not correctly answer to requests

+ * with other address types</li>

+ * </ul>

+ * (see <a href="http://tools.ietf.org/html/rfc1928">RFC 1928</a>)

+ * 

+ * @author Henning Staib

+ */

+public class Socks5Proxy {

+

+    /* SOCKS5 proxy singleton */

+    private static Socks5Proxy socks5Server;

+

+    /* reusable implementation of a SOCKS5 proxy server process */

+    private Socks5ServerProcess serverProcess;

+

+    /* thread running the SOCKS5 server process */

+    private Thread serverThread;

+

+    /* server socket to accept SOCKS5 connections */

+    private ServerSocket serverSocket;

+

+    /* assigns a connection to a digest */

+    private final Map<String, Socket> connectionMap = new ConcurrentHashMap<String, Socket>();

+

+    /* list of digests connections should be stored */

+    private final List<String> allowedConnections = Collections.synchronizedList(new LinkedList<String>());

+

+    private final Set<String> localAddresses = Collections.synchronizedSet(new LinkedHashSet<String>());

+

+    /**

+     * Private constructor.

+     */

+    private Socks5Proxy() {

+        this.serverProcess = new Socks5ServerProcess();

+

+        // add default local address

+        try {

+            this.localAddresses.add(InetAddress.getLocalHost().getHostAddress());

+        }

+        catch (UnknownHostException e) {

+            // do nothing

+        }

+

+    }

+

+    /**

+     * Returns the local SOCKS5 proxy server.

+     * 

+     * @return the local SOCKS5 proxy server

+     */

+    public static synchronized Socks5Proxy getSocks5Proxy() {

+        if (socks5Server == null) {

+            socks5Server = new Socks5Proxy();

+        }

+        if (SmackConfiguration.isLocalSocks5ProxyEnabled()) {

+            socks5Server.start();

+        }

+        return socks5Server;

+    }

+

+    /**

+     * Starts the local SOCKS5 proxy server. If it is already running, this method does nothing.

+     */

+    public synchronized void start() {

+        if (isRunning()) {

+            return;

+        }

+        try {

+            if (SmackConfiguration.getLocalSocks5ProxyPort() < 0) {

+                int port = Math.abs(SmackConfiguration.getLocalSocks5ProxyPort());

+                for (int i = 0; i < 65535 - port; i++) {

+                    try {

+                        this.serverSocket = new ServerSocket(port + i);

+                        break;

+                    }

+                    catch (IOException e) {

+                        // port is used, try next one

+                    }

+                }

+            }

+            else {

+                this.serverSocket = new ServerSocket(SmackConfiguration.getLocalSocks5ProxyPort());

+            }

+

+            if (this.serverSocket != null) {

+                this.serverThread = new Thread(this.serverProcess);

+                this.serverThread.start();

+            }

+        }

+        catch (IOException e) {

+            // couldn't setup server

+            System.err.println("couldn't setup local SOCKS5 proxy on port "

+                            + SmackConfiguration.getLocalSocks5ProxyPort() + ": " + e.getMessage());

+        }

+    }

+

+    /**

+     * Stops the local SOCKS5 proxy server. If it is not running this method does nothing.

+     */

+    public synchronized void stop() {

+        if (!isRunning()) {

+            return;

+        }

+

+        try {

+            this.serverSocket.close();

+        }

+        catch (IOException e) {

+            // do nothing

+        }

+

+        if (this.serverThread != null && this.serverThread.isAlive()) {

+            try {

+                this.serverThread.interrupt();

+                this.serverThread.join();

+            }

+            catch (InterruptedException e) {

+                // do nothing

+            }

+        }

+        this.serverThread = null;

+        this.serverSocket = null;

+

+    }

+

+    /**

+     * Adds the given address to the list of local network addresses.

+     * <p>

+     * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request.

+     * This may be necessary if your application is running on a machine with multiple network

+     * interfaces or if you want to provide your public address in case you are behind a NAT router.

+     * <p>

+     * The order of the addresses used is determined by the order you add addresses.

+     * <p>

+     * Note that the list of addresses initially contains the address returned by

+     * <code>InetAddress.getLocalHost().getHostAddress()</code>. You can replace the list of

+     * addresses by invoking {@link #replaceLocalAddresses(List)}.

+     * 

+     * @param address the local network address to add

+     */

+    public void addLocalAddress(String address) {

+        if (address == null) {

+            throw new IllegalArgumentException("address may not be null");

+        }

+        this.localAddresses.add(address);

+    }

+

+    /**

+     * Removes the given address from the list of local network addresses. This address will then no

+     * longer be used of outgoing SOCKS5 Bytestream requests.

+     * 

+     * @param address the local network address to remove

+     */

+    public void removeLocalAddress(String address) {

+        this.localAddresses.remove(address);

+    }

+

+    /**

+     * Returns an unmodifiable list of the local network addresses that will be used for streamhost

+     * candidates of outgoing SOCKS5 Bytestream requests.

+     * 

+     * @return unmodifiable list of the local network addresses

+     */

+    public List<String> getLocalAddresses() {

+        return Collections.unmodifiableList(new ArrayList<String>(this.localAddresses));

+    }

+

+    /**

+     * Replaces the list of local network addresses.

+     * <p>

+     * Use this method if you want to provide multiple addresses in a SOCKS5 Bytestream request and

+     * want to define their order. This may be necessary if your application is running on a machine

+     * with multiple network interfaces or if you want to provide your public address in case you

+     * are behind a NAT router.

+     * 

+     * @param addresses the new list of local network addresses

+     */

+    public void replaceLocalAddresses(List<String> addresses) {

+        if (addresses == null) {

+            throw new IllegalArgumentException("list must not be null");

+        }

+        this.localAddresses.clear();

+        this.localAddresses.addAll(addresses);

+

+    }

+

+    /**

+     * Returns the port of the local SOCKS5 proxy server. If it is not running -1 will be returned.

+     * 

+     * @return the port of the local SOCKS5 proxy server or -1 if proxy is not running

+     */

+    public int getPort() {

+        if (!isRunning()) {

+            return -1;

+        }

+        return this.serverSocket.getLocalPort();

+    }

+

+    /**

+     * Returns the socket for the given digest. A socket will be returned if the given digest has

+     * been in the list of allowed transfers (see {@link #addTransfer(String)}) while the peer

+     * connected to the SOCKS5 proxy.

+     * 

+     * @param digest identifying the connection

+     * @return socket or null if there is no socket for the given digest

+     */

+    protected Socket getSocket(String digest) {

+        return this.connectionMap.get(digest);

+    }

+

+    /**

+     * Add the given digest to the list of allowed transfers. Only connections for allowed transfers

+     * are stored and can be retrieved by invoking {@link #getSocket(String)}. All connections to

+     * the local SOCKS5 proxy that don't contain an allowed digest are discarded.

+     * 

+     * @param digest to be added to the list of allowed transfers

+     */

+    protected void addTransfer(String digest) {

+        this.allowedConnections.add(digest);

+    }

+

+    /**

+     * Removes the given digest from the list of allowed transfers. After invoking this method

+     * already stored connections with the given digest will be removed.

+     * <p>

+     * The digest should be removed after establishing the SOCKS5 Bytestream is finished, an error

+     * occurred while establishing the connection or if the connection is not allowed anymore.

+     * 

+     * @param digest to be removed from the list of allowed transfers

+     */

+    protected void removeTransfer(String digest) {

+        this.allowedConnections.remove(digest);

+        this.connectionMap.remove(digest);

+    }

+

+    /**

+     * Returns <code>true</code> if the local SOCKS5 proxy server is running, otherwise

+     * <code>false</code>.

+     * 

+     * @return <code>true</code> if the local SOCKS5 proxy server is running, otherwise

+     *         <code>false</code>

+     */

+    public boolean isRunning() {

+        return this.serverSocket != null;

+    }

+

+    /**

+     * Implementation of a simplified SOCKS5 proxy server.

+     */

+    private class Socks5ServerProcess implements Runnable {

+

+        public void run() {

+            while (true) {

+                Socket socket = null;

+

+                try {

+

+                    if (Socks5Proxy.this.serverSocket.isClosed()

+                                    || Thread.currentThread().isInterrupted()) {

+                        return;

+                    }

+

+                    // accept connection

+                    socket = Socks5Proxy.this.serverSocket.accept();

+

+                    // initialize connection

+                    establishConnection(socket);

+

+                }

+                catch (SocketException e) {

+                    /*

+                     * do nothing, if caused by closing the server socket, thread will terminate in

+                     * next loop

+                     */

+                }

+                catch (Exception e) {

+                    try {

+                        if (socket != null) {

+                            socket.close();

+                        }

+                    }

+                    catch (IOException e1) {

+                        /* do nothing */

+                    }

+                }

+            }

+

+        }

+

+        /**

+         * Negotiates a SOCKS5 connection and stores it on success.

+         * 

+         * @param socket connection to the client

+         * @throws XMPPException if client requests a connection in an unsupported way

+         * @throws IOException if a network error occurred

+         */

+        private void establishConnection(Socket socket) throws XMPPException, IOException {

+            DataOutputStream out = new DataOutputStream(socket.getOutputStream());

+            DataInputStream in = new DataInputStream(socket.getInputStream());

+

+            // first byte is version should be 5

+            int b = in.read();

+            if (b != 5) {

+                throw new XMPPException("Only SOCKS5 supported");

+            }

+

+            // second byte number of authentication methods supported

+            b = in.read();

+

+            // read list of supported authentication methods

+            byte[] auth = new byte[b];

+            in.readFully(auth);

+

+            byte[] authMethodSelectionResponse = new byte[2];

+            authMethodSelectionResponse[0] = (byte) 0x05; // protocol version

+

+            // only authentication method 0, no authentication, supported

+            boolean noAuthMethodFound = false;

+            for (int i = 0; i < auth.length; i++) {

+                if (auth[i] == (byte) 0x00) {

+                    noAuthMethodFound = true;

+                    break;

+                }

+            }

+

+            if (!noAuthMethodFound) {

+                authMethodSelectionResponse[1] = (byte) 0xFF; // no acceptable methods

+                out.write(authMethodSelectionResponse);

+                out.flush();

+                throw new XMPPException("Authentication method not supported");

+            }

+

+            authMethodSelectionResponse[1] = (byte) 0x00; // no-authentication method

+            out.write(authMethodSelectionResponse);

+            out.flush();

+

+            // receive connection request

+            byte[] connectionRequest = Socks5Utils.receiveSocks5Message(in);

+

+            // extract digest

+            String responseDigest = new String(connectionRequest, 5, connectionRequest[4]);

+

+            // return error if digest is not allowed

+            if (!Socks5Proxy.this.allowedConnections.contains(responseDigest)) {

+                connectionRequest[1] = (byte) 0x05; // set return status to 5 (connection refused)

+                out.write(connectionRequest);

+                out.flush();

+

+                throw new XMPPException("Connection is not allowed");

+            }

+

+            connectionRequest[1] = (byte) 0x00; // set return status to 0 (success)

+            out.write(connectionRequest);

+            out.flush();

+

+            // store connection

+            Socks5Proxy.this.connectionMap.put(responseDigest, socket);

+        }

+

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java
new file mode 100644
index 0000000..9c92563
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/Socks5Utils.java
@@ -0,0 +1,73 @@
+/**

+ * 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.smackx.bytestreams.socks5;

+

+import java.io.DataInputStream;

+import java.io.IOException;

+

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.util.StringUtils;

+

+/**

+ * A collection of utility methods for SOcKS5 messages.

+ * 

+ * @author Henning Staib

+ */

+class Socks5Utils {

+

+    /**

+     * Returns a SHA-1 digest of the given parameters as specified in <a

+     * href="http://xmpp.org/extensions/xep-0065.html#impl-socks5">XEP-0065</a>.

+     * 

+     * @param sessionID for the SOCKS5 Bytestream

+     * @param initiatorJID JID of the initiator of a SOCKS5 Bytestream

+     * @param targetJID JID of the target of a SOCKS5 Bytestream

+     * @return SHA-1 digest of the given parameters

+     */

+    public static String createDigest(String sessionID, String initiatorJID, String targetJID) {

+        StringBuilder b = new StringBuilder();

+        b.append(sessionID).append(initiatorJID).append(targetJID);

+        return StringUtils.hash(b.toString());

+    }

+

+    /**

+     * Reads a SOCKS5 message from the given InputStream. The message can either be a SOCKS5 request

+     * message or a SOCKS5 response message.

+     * <p>

+     * (see <a href="http://tools.ietf.org/html/rfc1928">RFC1928</a>)

+     * 

+     * @param in the DataInputStream to read the message from

+     * @return the SOCKS5 message

+     * @throws IOException if a network error occurred

+     * @throws XMPPException if the SOCKS5 message contains an unsupported address type

+     */

+    public static byte[] receiveSocks5Message(DataInputStream in) throws IOException, XMPPException {

+        byte[] header = new byte[5];

+        in.readFully(header, 0, 5);

+

+        if (header[3] != (byte) 0x03) {

+            throw new XMPPException("Unsupported SOCKS5 address type");

+        }

+

+        int addressLength = header[4];

+

+        byte[] response = new byte[7 + addressLength];

+        System.arraycopy(header, 0, response, 0, header.length);

+

+        in.readFully(response, header.length, addressLength + 2);

+

+        return response;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java b/src/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java
new file mode 100644
index 0000000..9e07fc3
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/packet/Bytestream.java
@@ -0,0 +1,474 @@
+/**

+ * 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.smackx.bytestreams.socks5.packet;

+

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.Collections;

+import java.util.List;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.PacketExtension;

+

+/**

+ * A packet representing part of a SOCKS5 Bytestream negotiation.

+ * 

+ * @author Alexander Wenckus

+ */

+public class Bytestream extends IQ {

+

+    private String sessionID;

+

+    private Mode mode = Mode.tcp;

+

+    private final List<StreamHost> streamHosts = new ArrayList<StreamHost>();

+

+    private StreamHostUsed usedHost;

+

+    private Activate toActivate;

+

+    /**

+     * The default constructor

+     */

+    public Bytestream() {

+        super();

+    }

+

+    /**

+     * A constructor where the session ID can be specified.

+     * 

+     * @param SID The session ID related to the negotiation.

+     * @see #setSessionID(String)

+     */

+    public Bytestream(final String SID) {

+        super();

+        setSessionID(SID);

+    }

+

+    /**

+     * Set the session ID related to the bytestream. The session ID is a unique identifier used to

+     * differentiate between stream negotiations.

+     * 

+     * @param sessionID the unique session ID that identifies the transfer.

+     */

+    public void setSessionID(final String sessionID) {

+        this.sessionID = sessionID;

+    }

+

+    /**

+     * Returns the session ID related to the bytestream negotiation.

+     * 

+     * @return Returns the session ID related to the bytestream negotiation.

+     * @see #setSessionID(String)

+     */

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    /**

+     * Set the transport mode. This should be put in the initiation of the interaction.

+     * 

+     * @param mode the transport mode, either UDP or TCP

+     * @see Mode

+     */

+    public void setMode(final Mode mode) {

+        this.mode = mode;

+    }

+

+    /**

+     * Returns the transport mode.

+     * 

+     * @return Returns the transport mode.

+     * @see #setMode(Mode)

+     */

+    public Mode getMode() {

+        return mode;

+    }

+

+    /**

+     * Adds a potential stream host that the remote user can connect to to receive the file.

+     * 

+     * @param JID The JID of the stream host.

+     * @param address The internet address of the stream host.

+     * @return The added stream host.

+     */

+    public StreamHost addStreamHost(final String JID, final String address) {

+        return addStreamHost(JID, address, 0);

+    }

+

+    /**

+     * Adds a potential stream host that the remote user can connect to to receive the file.

+     * 

+     * @param JID The JID of the stream host.

+     * @param address The internet address of the stream host.

+     * @param port The port on which the remote host is seeking connections.

+     * @return The added stream host.

+     */

+    public StreamHost addStreamHost(final String JID, final String address, final int port) {

+        StreamHost host = new StreamHost(JID, address);

+        host.setPort(port);

+        addStreamHost(host);

+

+        return host;

+    }

+

+    /**

+     * Adds a potential stream host that the remote user can transfer the file through.

+     * 

+     * @param host The potential stream host.

+     */

+    public void addStreamHost(final StreamHost host) {

+        streamHosts.add(host);

+    }

+

+    /**

+     * Returns the list of stream hosts contained in the packet.

+     * 

+     * @return Returns the list of stream hosts contained in the packet.

+     */

+    public Collection<StreamHost> getStreamHosts() {

+        return Collections.unmodifiableCollection(streamHosts);

+    }

+

+    /**

+     * Returns the stream host related to the given JID, or null if there is none.

+     * 

+     * @param JID The JID of the desired stream host.

+     * @return Returns the stream host related to the given JID, or null if there is none.

+     */

+    public StreamHost getStreamHost(final String JID) {

+        if (JID == null) {

+            return null;

+        }

+        for (StreamHost host : streamHosts) {

+            if (host.getJID().equals(JID)) {

+                return host;

+            }

+        }

+

+        return null;

+    }

+

+    /**

+     * Returns the count of stream hosts contained in this packet.

+     * 

+     * @return Returns the count of stream hosts contained in this packet.

+     */

+    public int countStreamHosts() {

+        return streamHosts.size();

+    }

+

+    /**

+     * Upon connecting to the stream host the target of the stream replies to the initiator with the

+     * JID of the SOCKS5 host that they used.

+     * 

+     * @param JID The JID of the used host.

+     */

+    public void setUsedHost(final String JID) {

+        this.usedHost = new StreamHostUsed(JID);

+    }

+

+    /**

+     * Returns the SOCKS5 host connected to by the remote user.

+     * 

+     * @return Returns the SOCKS5 host connected to by the remote user.

+     */

+    public StreamHostUsed getUsedHost() {

+        return usedHost;

+    }

+

+    /**

+     * Returns the activate element of the packet sent to the proxy host to verify the identity of

+     * the initiator and match them to the appropriate stream.

+     * 

+     * @return Returns the activate element of the packet sent to the proxy host to verify the

+     *         identity of the initiator and match them to the appropriate stream.

+     */

+    public Activate getToActivate() {

+        return toActivate;

+    }

+

+    /**

+     * Upon the response from the target of the used host the activate packet is sent to the SOCKS5

+     * proxy. The proxy will activate the stream or return an error after verifying the identity of

+     * the initiator, using the activate packet.

+     * 

+     * @param targetID The JID of the target of the file transfer.

+     */

+    public void setToActivate(final String targetID) {

+        this.toActivate = new Activate(targetID);

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<query xmlns=\"http://jabber.org/protocol/bytestreams\"");

+        if (this.getType().equals(IQ.Type.SET)) {

+            if (getSessionID() != null) {

+                buf.append(" sid=\"").append(getSessionID()).append("\"");

+            }

+            if (getMode() != null) {

+                buf.append(" mode = \"").append(getMode()).append("\"");

+            }

+            buf.append(">");

+            if (getToActivate() == null) {

+                for (StreamHost streamHost : getStreamHosts()) {

+                    buf.append(streamHost.toXML());

+                }

+            }

+            else {

+                buf.append(getToActivate().toXML());

+            }

+        }

+        else if (this.getType().equals(IQ.Type.RESULT)) {

+            buf.append(">");

+            if (getUsedHost() != null) {

+                buf.append(getUsedHost().toXML());

+            }

+            // A result from the server can also contain stream hosts

+            else if (countStreamHosts() > 0) {

+                for (StreamHost host : streamHosts) {

+                    buf.append(host.toXML());

+                }

+            }

+        }

+        else if (this.getType().equals(IQ.Type.GET)) {

+            return buf.append("/>").toString();

+        }

+        else {

+            return null;

+        }

+        buf.append("</query>");

+

+        return buf.toString();

+    }

+

+    /**

+     * Packet extension that represents a potential SOCKS5 proxy for the file transfer. Stream hosts

+     * are forwarded to the target of the file transfer who then chooses and connects to one.

+     * 

+     * @author Alexander Wenckus

+     */

+    public static class StreamHost implements PacketExtension {

+

+        public static String NAMESPACE = "";

+

+        public static String ELEMENTNAME = "streamhost";

+

+        private final String JID;

+

+        private final String addy;

+

+        private int port = 0;

+

+        /**

+         * Default constructor.

+         * 

+         * @param JID The JID of the stream host.

+         * @param address The internet address of the stream host.

+         */

+        public StreamHost(final String JID, final String address) {

+            this.JID = JID;

+            this.addy = address;

+        }

+

+        /**

+         * Returns the JID of the stream host.

+         * 

+         * @return Returns the JID of the stream host.

+         */

+        public String getJID() {

+            return JID;

+        }

+

+        /**

+         * Returns the internet address of the stream host.

+         * 

+         * @return Returns the internet address of the stream host.

+         */

+        public String getAddress() {

+            return addy;

+        }

+

+        /**

+         * Sets the port of the stream host.

+         * 

+         * @param port The port on which the potential stream host would accept the connection.

+         */

+        public void setPort(final int port) {

+            this.port = port;

+        }

+

+        /**

+         * Returns the port on which the potential stream host would accept the connection.

+         * 

+         * @return Returns the port on which the potential stream host would accept the connection.

+         */

+        public int getPort() {

+            return port;

+        }

+

+        public String getNamespace() {

+            return NAMESPACE;

+        }

+

+        public String getElementName() {

+            return ELEMENTNAME;

+        }

+

+        public String toXML() {

+            StringBuilder buf = new StringBuilder();

+

+            buf.append("<").append(getElementName()).append(" ");

+            buf.append("jid=\"").append(getJID()).append("\" ");

+            buf.append("host=\"").append(getAddress()).append("\" ");

+            if (getPort() != 0) {

+                buf.append("port=\"").append(getPort()).append("\"");

+            }

+            else {

+                buf.append("zeroconf=\"_jabber.bytestreams\"");

+            }

+            buf.append("/>");

+

+            return buf.toString();

+        }

+    }

+

+    /**

+     * After selected a SOCKS5 stream host and successfully connecting, the target of the file

+     * transfer returns a byte stream packet with the stream host used extension.

+     * 

+     * @author Alexander Wenckus

+     */

+    public static class StreamHostUsed implements PacketExtension {

+

+        public String NAMESPACE = "";

+

+        public static String ELEMENTNAME = "streamhost-used";

+

+        private final String JID;

+

+        /**

+         * Default constructor.

+         * 

+         * @param JID The JID of the selected stream host.

+         */

+        public StreamHostUsed(final String JID) {

+            this.JID = JID;

+        }

+

+        /**

+         * Returns the JID of the selected stream host.

+         * 

+         * @return Returns the JID of the selected stream host.

+         */

+        public String getJID() {

+            return JID;

+        }

+

+        public String getNamespace() {

+            return NAMESPACE;

+        }

+

+        public String getElementName() {

+            return ELEMENTNAME;

+        }

+

+        public String toXML() {

+            StringBuilder buf = new StringBuilder();

+            buf.append("<").append(getElementName()).append(" ");

+            buf.append("jid=\"").append(getJID()).append("\" ");

+            buf.append("/>");

+            return buf.toString();

+        }

+    }

+

+    /**

+     * The packet sent by the stream initiator to the stream proxy to activate the connection.

+     * 

+     * @author Alexander Wenckus

+     */

+    public static class Activate implements PacketExtension {

+

+        public String NAMESPACE = "";

+

+        public static String ELEMENTNAME = "activate";

+

+        private final String target;

+

+        /**

+         * Default constructor specifying the target of the stream.

+         * 

+         * @param target The target of the stream.

+         */

+        public Activate(final String target) {

+            this.target = target;

+        }

+

+        /**

+         * Returns the target of the activation.

+         * 

+         * @return Returns the target of the activation.

+         */

+        public String getTarget() {

+            return target;

+        }

+

+        public String getNamespace() {

+            return NAMESPACE;

+        }

+

+        public String getElementName() {

+            return ELEMENTNAME;

+        }

+

+        public String toXML() {

+            StringBuilder buf = new StringBuilder();

+            buf.append("<").append(getElementName()).append(">");

+            buf.append(getTarget());

+            buf.append("</").append(getElementName()).append(">");

+            return buf.toString();

+        }

+    }

+

+    /**

+     * The stream can be either a TCP stream or a UDP stream.

+     * 

+     * @author Alexander Wenckus

+     */

+    public enum Mode {

+

+        /**

+         * A TCP based stream.

+         */

+        tcp,

+

+        /**

+         * A UDP based stream.

+         */

+        udp;

+

+        public static Mode fromName(String name) {

+            Mode mode;

+            try {

+                mode = Mode.valueOf(name);

+            }

+            catch (Exception ex) {

+                mode = tcp;

+            }

+

+            return mode;

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java b/src/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java
new file mode 100644
index 0000000..76f9b0c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/bytestreams/socks5/provider/BytestreamsProvider.java
@@ -0,0 +1,82 @@
+/**

+ * 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.smackx.bytestreams.socks5.provider;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Parses a bytestream packet.

+ * 

+ * @author Alexander Wenckus

+ */

+public class BytestreamsProvider implements IQProvider {

+

+    public IQ parseIQ(XmlPullParser parser) throws Exception {

+        boolean done = false;

+

+        Bytestream toReturn = new Bytestream();

+

+        String id = parser.getAttributeValue("", "sid");

+        String mode = parser.getAttributeValue("", "mode");

+

+        // streamhost

+        String JID = null;

+        String host = null;

+        String port = null;

+

+        int eventType;

+        String elementName;

+        while (!done) {

+            eventType = parser.next();

+            elementName = parser.getName();

+            if (eventType == XmlPullParser.START_TAG) {

+                if (elementName.equals(Bytestream.StreamHost.ELEMENTNAME)) {

+                    JID = parser.getAttributeValue("", "jid");

+                    host = parser.getAttributeValue("", "host");

+                    port = parser.getAttributeValue("", "port");

+                }

+                else if (elementName.equals(Bytestream.StreamHostUsed.ELEMENTNAME)) {

+                    toReturn.setUsedHost(parser.getAttributeValue("", "jid"));

+                }

+                else if (elementName.equals(Bytestream.Activate.ELEMENTNAME)) {

+                    toReturn.setToActivate(parser.getAttributeValue("", "jid"));

+                }

+            }

+            else if (eventType == XmlPullParser.END_TAG) {

+                if (elementName.equals("streamhost")) {

+                    if (port == null) {

+                        toReturn.addStreamHost(JID, host);

+                    }

+                    else {

+                        toReturn.addStreamHost(JID, host, Integer.parseInt(port));

+                    }

+                    JID = null;

+                    host = null;

+                    port = null;

+                }

+                else if (elementName.equals("query")) {

+                    done = true;

+                }

+            }

+        }

+

+        toReturn.setMode((Bytestream.Mode.fromName(mode)));

+        toReturn.setSessionID(id);

+        return toReturn;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/carbons/Carbon.java b/src/org/jivesoftware/smackx/carbons/Carbon.java
new file mode 100644
index 0000000..588688a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/carbons/Carbon.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright 2013 Georg Lukas
+ *
+ * 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.smackx.carbons;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.forward.Forwarded;
+import org.jivesoftware.smackx.packet.DelayInfo;
+import org.jivesoftware.smackx.provider.DelayInfoProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Packet extension for XEP-0280: Message Carbons. This class implements
+ * the packet extension and a {@link PacketExtensionProvider} to parse
+ * message carbon copies from a packet. The extension
+ * <a href="http://xmpp.org/extensions/xep-0280.html">XEP-0280</a> is
+ * meant to synchronize a message flow to multiple presences of a user.
+ *
+ * <p>The {@link Carbon.Provider} must be registered in the
+ * <b>smack.properties</b> file for the elements <b>sent</b> and
+ * <b>received</b> with namespace <b>urn:xmpp:carbons:2</b></p> to be used.
+ *
+ * @author Georg Lukas
+ */
+public class Carbon implements PacketExtension {
+    public static final String NAMESPACE = "urn:xmpp:carbons:2";
+
+    private Direction dir;
+    private Forwarded fwd;
+
+    public Carbon(Direction dir, Forwarded fwd) {
+        this.dir = dir;
+        this.fwd = fwd;
+    }
+
+    /**
+     * get the direction (sent or received) of the carbon.
+     *
+     * @return the {@link Direction} of the carbon.
+     */
+    public Direction getDirection() {
+        return dir;
+    }
+
+    /**
+     * get the forwarded packet.
+     *
+     * @return the {@link Forwarded} message contained in this Carbon.
+     */
+    public Forwarded getForwarded() {
+        return fwd;
+    }
+
+    @Override
+    public String getElementName() {
+        return dir.toString();
+    }
+
+    @Override
+    public String getNamespace() {
+        return NAMESPACE;
+    }
+
+    @Override
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"")
+                .append(getNamespace()).append("\">");
+
+        buf.append(fwd.toXML());
+
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+    /**
+     * An enum to display the direction of a {@link Carbon} message.
+     */
+    public static enum Direction {
+        received,
+        sent
+    }
+
+    public static class Provider implements PacketExtensionProvider {
+
+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+            Direction dir = Direction.valueOf(parser.getName());
+            Forwarded fwd = null;
+
+            boolean done = false;
+            while (!done) {
+                int eventType = parser.next();
+                if (eventType == XmlPullParser.START_TAG && parser.getName().equals("forwarded")) {
+                    fwd = (Forwarded)new Forwarded.Provider().parseExtension(parser);
+                }
+                else if (eventType == XmlPullParser.END_TAG && dir == Direction.valueOf(parser.getName()))
+                    done = true;
+            }
+            if (fwd == null)
+                throw new Exception("sent/received must contain exactly one <forwarded> tag");
+            return new Carbon(dir, fwd);
+        }
+    }
+
+    /**
+     * Packet extension indicating that a message may not be carbon-copied.
+     */
+    public static class Private implements PacketExtension {
+        public static final String ELEMENT = "private";
+
+        public String getElementName() {
+            return ELEMENT;
+        }
+
+        public String getNamespace() {
+            return Carbon.NAMESPACE;
+        }
+
+        public String toXML() {
+            return "<" + ELEMENT + " xmlns=\"" + Carbon.NAMESPACE + "\"/>";
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/carbons/CarbonManager.java b/src/org/jivesoftware/smackx/carbons/CarbonManager.java
new file mode 100644
index 0000000..f44701a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/carbons/CarbonManager.java
@@ -0,0 +1,213 @@
+/**
+ * Copyright 2013 Georg Lukas
+ *
+ * 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.smackx.carbons;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+
+/**
+ * Packet extension for XEP-0280: Message Carbons. This class implements
+ * the manager for registering {@link Carbon} support, enabling and disabling
+ * message carbons.
+ *
+ * You should call enableCarbons() before sending your first undirected
+ * presence.
+ *
+ * @author Georg Lukas
+ */
+public class CarbonManager {
+
+    private static Map<Connection, CarbonManager> instances =
+            Collections.synchronizedMap(new WeakHashMap<Connection, CarbonManager>());
+
+    static {
+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+            public void connectionCreated(Connection connection) {
+                new CarbonManager(connection);
+            }
+        });
+    }
+    
+    private Connection connection;
+    private volatile boolean enabled_state = false;
+
+    private CarbonManager(Connection connection) {
+        ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
+        sdm.addFeature(Carbon.NAMESPACE);
+        this.connection = connection;
+        instances.put(connection, this);
+    }
+
+    /**
+     * Obtain the CarbonManager responsible for a connection.
+     *
+     * @param connection the connection object.
+     *
+     * @return a CarbonManager instance
+     */
+    public static CarbonManager getInstanceFor(Connection connection) {
+        CarbonManager carbonManager = instances.get(connection);
+
+        if (carbonManager == null) {
+            carbonManager = new CarbonManager(connection);
+        }
+
+        return carbonManager;
+    }
+
+    private IQ carbonsEnabledIQ(final boolean new_state) {
+        IQ setIQ = new IQ() {
+            public String getChildElementXML() {
+                return "<" + (new_state? "enable" : "disable") + " xmlns='" + Carbon.NAMESPACE + "'/>";
+            }
+        };
+        setIQ.setType(IQ.Type.SET);
+        return setIQ;
+    }
+
+    /**
+     * Returns true if XMPP Carbons are supported by the server.
+     * 
+     * @return true if supported
+     */
+    public boolean isSupportedByServer() {
+        try {
+            DiscoverInfo result = ServiceDiscoveryManager
+                .getInstanceFor(connection).discoverInfo(connection.getServiceName());
+            return result.containsFeature(Carbon.NAMESPACE);
+        }
+        catch (XMPPException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Notify server to change the carbons state. This method returns
+     * immediately and changes the variable when the reply arrives.
+     *
+     * You should first check for support using isSupportedByServer().
+     *
+     * @param new_state whether carbons should be enabled or disabled
+     */
+    public void sendCarbonsEnabled(final boolean new_state) {
+        IQ setIQ = carbonsEnabledIQ(new_state);
+
+        connection.addPacketListener(new PacketListener() {
+            public void processPacket(Packet packet) {
+                IQ result = (IQ)packet;
+                if (result.getType() == IQ.Type.RESULT) {
+                    enabled_state = new_state;
+                }
+                connection.removePacketListener(this);
+            }
+        }, new PacketIDFilter(setIQ.getPacketID()));
+
+        connection.sendPacket(setIQ);
+    }
+
+    /**
+     * Notify server to change the carbons state. This method blocks
+     * some time until the server replies to the IQ and returns true on
+     * success.
+     *
+     * You should first check for support using isSupportedByServer().
+     *
+     * @param new_state whether carbons should be enabled or disabled
+     *
+     * @return true if the operation was successful
+     */
+    public boolean setCarbonsEnabled(final boolean new_state) {
+        if (enabled_state == new_state)
+            return true;
+
+        IQ setIQ = carbonsEnabledIQ(new_state);
+
+        PacketCollector collector =
+                connection.createPacketCollector(new PacketIDFilter(setIQ.getPacketID()));
+        connection.sendPacket(setIQ);
+        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        collector.cancel();
+
+        if (result != null && result.getType() == IQ.Type.RESULT) {
+            enabled_state = new_state;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Helper method to enable carbons.
+     *
+     * @return true if the operation was successful
+     */
+    public boolean enableCarbons() {
+        return setCarbonsEnabled(true);
+    }
+
+    /**
+     * Helper method to disable carbons.
+     *
+     * @return true if the operation was successful
+     */
+    public boolean disableCarbons() {
+        return setCarbonsEnabled(false);
+    }
+
+    /**
+     * Check if carbons are enabled on this connection.
+     */
+    public boolean getCarbonsEnabled() {
+        return this.enabled_state;
+    }
+
+    /**
+     * Obtain a Carbon from a message, if available.
+     *
+     * @param msg Message object to check for carbons
+     *
+     * @return a Carbon if available, null otherwise.
+     */
+    public static Carbon getCarbon(Message msg) {
+        Carbon cc = (Carbon)msg.getExtension("received", Carbon.NAMESPACE);
+        if (cc == null)
+            cc = (Carbon)msg.getExtension("sent", Carbon.NAMESPACE);
+        return cc;
+    }
+
+    /**
+     * Mark a message as "private", so it will not be carbon-copied.
+     *
+     * @param msg Message object to mark private
+     */
+    public static void disableCarbons(Message msg) {
+        msg.addExtension(new Carbon.Private());
+    }
+}
diff --git a/src/org/jivesoftware/smackx/commands/AdHocCommand.java b/src/org/jivesoftware/smackx/commands/AdHocCommand.java
new file mode 100755
index 0000000..3077d08
--- /dev/null
+++ b/src/org/jivesoftware/smackx/commands/AdHocCommand.java
@@ -0,0 +1,450 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2005-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.smackx.commands;

+

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.packet.XMPPError;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.packet.AdHocCommandData;

+

+import java.util.List;

+

+/**

+ * An ad-hoc command is responsible for executing the provided service and

+ * storing the result of the execution. Each new request will create a new

+ * instance of the command, allowing information related to executions to be

+ * stored in it. For example suppose that a command that retrieves the list of

+ * users on a server is implemented. When the command is executed it gets that

+ * list and the result is stored as a form in the command instance, i.e. the

+ * <code>getForm</code> method retrieves a form with all the users.

+ * <p>

+ * Each command has a <tt>node</tt> that should be unique within a given JID.

+ * <p>

+ * Commands may have zero or more stages. Each stage is usually used for

+ * gathering information required for the command execution. Users are able to

+ * move forward or backward across the different stages. Commands may not be

+ * cancelled while they are being executed. However, users may request the

+ * "cancel" action when submitting a stage response indicating that the command

+ * execution should be aborted. Thus, releasing any collected information.

+ * Commands that require user interaction (i.e. have more than one stage) will

+ * have to provide the data forms the user must complete in each stage and the

+ * allowed actions the user might perform during each stage (e.g. go to the

+ * previous stage or go to the next stage).

+ * <p>

+ * All the actions may throw an XMPPException if there is a problem executing

+ * them. The <code>XMPPError</code> of that exception may have some specific

+ * information about the problem. The possible extensions are:

+ * 

+ * <li><i>malformed-action</i>. Extension of a <i>bad-request</i> error.</li>

+ * <li><i>bad-action</i>. Extension of a <i>bad-request</i> error.</li>

+ * <li><i>bad-locale</i>. Extension of a <i>bad-request</i> error.</li>

+ * <li><i>bad-payload</i>. Extension of a <i>bad-request</i> error.</li>

+ * <li><i>bad-sessionid</i>. Extension of a <i>bad-request</i> error.</li>

+ * <li><i>session-expired</i>. Extension of a <i>not-allowed</i> error.</li>

+ * <p>

+ * See the <code>SpecificErrorCondition</code> class for detailed description

+ * of each one.

+ * <p>

+ * Use the <code>getSpecificErrorConditionFrom</code> to obtain the specific

+ * information from an <code>XMPPError</code>.

+ * 

+ * @author Gabriel Guardincerri

+ * 

+ */

+public abstract class AdHocCommand {

+    // TODO: Analyze the redesign of command by having an ExecutionResponse as a

+    // TODO: result to the execution of every action. That result should have all the

+    // TODO: information related to the execution, e.g. the form to fill. Maybe this

+    // TODO: design is more intuitive and simpler than the current one that has all in

+    // TODO: one class.

+

+    private AdHocCommandData data;

+

+    public AdHocCommand() {

+        super();

+        data = new AdHocCommandData();

+    }

+

+    /**

+     * Returns the specific condition of the <code>error</code> or <tt>null</tt> if the

+     * error doesn't have any.

+     * 

+     * @param error the error the get the specific condition from.

+     * @return the specific condition of this error, or null if it doesn't have

+     *         any.

+     */

+    public static SpecificErrorCondition getSpecificErrorCondition(XMPPError error) {

+        // This method is implemented to provide an easy way of getting a packet

+        // extension of the XMPPError.

+        for (SpecificErrorCondition condition : SpecificErrorCondition.values()) {

+            if (error.getExtension(condition.toString(),

+                    AdHocCommandData.SpecificError.namespace) != null) {

+                return condition;

+            }

+        }

+        return null;

+    }

+

+    /**

+     * Set the the human readable name of the command, usually used for

+     * displaying in a UI.

+     * 

+     * @param name the name.

+     */

+    public void setName(String name) {

+        data.setName(name);

+    }

+

+    /**

+     * Returns the human readable name of the command.

+     * 

+     * @return the human readable name of the command

+     */

+    public String getName() {

+        return data.getName();

+    }

+

+    /**

+     * Sets the unique identifier of the command. This value must be unique for

+     * the <code>OwnerJID</code>.

+     * 

+     * @param node the unique identifier of the command.

+     */

+    public void setNode(String node) {

+        data.setNode(node);

+    }

+

+    /**

+     * Returns the unique identifier of the command. It is unique for the

+     * <code>OwnerJID</code>.

+     * 

+     * @return the unique identifier of the command.

+     */

+    public String getNode() {

+        return data.getNode();

+    }

+

+    /**

+     * Returns the full JID of the owner of this command. This JID is the "to" of a

+     * execution request.

+     * 

+     * @return the owner JID.

+     */

+    public abstract String getOwnerJID();

+

+    /**

+     * Returns the notes that the command has at the current stage.

+     * 

+     * @return a list of notes.

+     */

+    public List<AdHocCommandNote> getNotes() {

+        return data.getNotes();

+    }

+

+    /**

+     * Adds a note to the current stage. This should be used when setting a

+     * response to the execution of an action. All the notes added here are

+     * returned by the {@link #getNotes} method during the current stage.

+     * Once the stage changes all the notes are discarded.

+     * 

+     * @param note the note.

+     */

+    protected void addNote(AdHocCommandNote note) {

+        data.addNote(note);

+    }

+

+    public String getRaw() {

+        return data.getChildElementXML();

+    }

+

+    /**

+     * Returns the form of the current stage. Usually it is the form that must

+     * be answered to execute the next action. If that is the case it should be

+     * used by the requester to fill all the information that the executor needs

+     * to continue to the next stage. It can also be the result of the

+     * execution.

+     * 

+     * @return the form of the current stage to fill out or the result of the

+     *         execution.

+     */

+    public Form getForm() {

+        if (data.getForm() == null) {

+            return null;

+        }

+        else {

+            return new Form(data.getForm());

+        }

+    }

+

+    /**

+     * Sets the form of the current stage. This should be used when setting a

+     * response. It could be a form to fill out the information needed to go to

+     * the next stage or the result of an execution.

+     * 

+     * @param form the form of the current stage to fill out or the result of the

+     *      execution.

+     */

+    protected void setForm(Form form) {

+        data.setForm(form.getDataFormToSend());

+    }

+

+    /**

+     * Executes the command. This is invoked only on the first stage of the

+     * command. It is invoked on every command. If there is a problem executing

+     * the command it throws an XMPPException.

+     * 

+     * @throws XMPPException if there is an error executing the command.

+     */

+    public abstract void execute() throws XMPPException;

+

+    /**

+     * Executes the next action of the command with the information provided in

+     * the <code>response</code>. This form must be the answer form of the

+     * previous stage. This method will be only invoked for commands that have one

+     * or more stages. If there is a problem executing the command it throws an

+     * XMPPException.

+     * 

+     * @param response the form answer of the previous stage.

+     * @throws XMPPException if there is a problem executing the command.

+     */

+    public abstract void next(Form response) throws XMPPException;

+

+    /**

+     * Completes the command execution with the information provided in the

+     * <code>response</code>. This form must be the answer form of the

+     * previous stage. This method will be only invoked for commands that have one

+     * or more stages. If there is a problem executing the command it throws an

+     * XMPPException.

+     * 

+     * @param response the form answer of the previous stage.

+     * @throws XMPPException if there is a problem executing the command.

+     */

+    public abstract void complete(Form response) throws XMPPException;

+

+    /**

+     * Goes to the previous stage. The requester is asking to re-send the

+     * information of the previous stage. The command must change it state to

+     * the previous one. If there is a problem executing the command it throws

+     * an XMPPException.

+     * 

+     * @throws XMPPException if there is a problem executing the command.

+     */

+    public abstract void prev() throws XMPPException;

+

+    /**

+     * Cancels the execution of the command. This can be invoked on any stage of

+     * the execution. If there is a problem executing the command it throws an

+     * XMPPException.

+     * 

+     * @throws XMPPException if there is a problem executing the command.

+     */

+    public abstract void cancel() throws XMPPException;

+

+    /**

+     * Returns a collection with the allowed actions based on the current stage.

+     * Possible actions are: {@link Action#prev prev}, {@link Action#next next} and

+     * {@link Action#complete complete}. This method will be only invoked for commands that

+     * have one or more stages.

+     * 

+     * @return a collection with the allowed actions based on the current stage

+     *      as defined in the SessionData.

+     */

+    protected List<Action> getActions() {

+        return data.getActions();

+    }

+

+    /**

+     * Add an action to the current stage available actions. This should be used

+     * when creating a response.

+     * 

+     * @param action the action.

+     */

+    protected void addActionAvailable(Action action) {

+        data.addAction(action);

+    }

+

+    /**

+     * Returns the action available for the current stage which is

+     * considered the equivalent to "execute". When the requester sends his

+     * reply, if no action was defined in the command then the action will be

+     * assumed "execute" thus assuming the action returned by this method. This

+     * method will never be invoked for commands that have no stages.

+     * 

+     * @return the action available for the current stage which is considered

+     *      the equivalent to "execute".

+     */

+    protected Action getExecuteAction() {

+        return data.getExecuteAction();

+    }

+

+    /**

+     * Sets which of the actions available for the current stage is

+     * considered the equivalent to "execute". This should be used when setting

+     * a response. When the requester sends his reply, if no action was defined

+     * in the command then the action will be assumed "execute" thus assuming

+     * the action returned by this method.

+     * 

+     * @param action the action.

+     */

+    protected void setExecuteAction(Action action) {

+        data.setExecuteAction(action);

+    }

+

+    /**

+     * Returns the status of the current stage.

+     * 

+     * @return the current status.

+     */

+    public Status getStatus() {

+        return data.getStatus();

+    }

+

+    /**

+     * Sets the data of the current stage. This should not used.

+     * 

+     * @param data the data.

+     */

+    void setData(AdHocCommandData data) {

+        this.data = data;

+    }

+

+    /**

+     * Gets the data of the current stage. This should not used.

+     *

+     * @return the data.

+     */

+    AdHocCommandData getData() {

+        return data;

+    }

+

+    /**

+     * Returns true if the <code>action</code> is available in the current stage.

+     * The {@link Action#cancel cancel} action is always allowed. To define the

+     * available actions use the <code>addActionAvailable</code> method.

+     * 

+     * @param action

+     *            The action to check if it is available.

+     * @return True if the action is available for the current stage.

+     */

+    protected boolean isValidAction(Action action) {

+        return getActions().contains(action) || Action.cancel.equals(action);

+    }

+

+    /**

+     * The status of the stage in the adhoc command.

+     */

+    public enum Status {

+

+        /**

+         * The command is being executed.

+         */

+        executing,

+

+        /**

+         * The command has completed. The command session has ended.

+         */

+        completed,

+

+        /**

+         * The command has been canceled. The command session has ended.

+         */

+        canceled

+    }

+

+    public enum Action {

+

+        /**

+         * The command should be executed or continue to be executed. This is

+         * the default value.

+         */

+        execute,

+

+        /**

+         * The command should be canceled.

+         */

+        cancel,

+

+        /**

+         * The command should be digress to the previous stage of execution.

+         */

+        prev,

+

+        /**

+         * The command should progress to the next stage of execution.

+         */

+        next,

+

+        /**

+         * The command should be completed (if possible).

+         */

+        complete,

+

+        /**

+         * The action is unknow. This is used when a recieved message has an

+         * unknown action. It must not be used to send an execution request.

+         */

+        unknown

+    }

+

+    public enum SpecificErrorCondition {

+

+        /**

+         * The responding JID cannot accept the specified action.

+         */

+        badAction("bad-action"),

+

+        /**

+         * The responding JID does not understand the specified action.

+         */

+        malformedAction("malformed-action"),

+

+        /**

+         * The responding JID cannot accept the specified language/locale.

+         */

+        badLocale("bad-locale"),

+

+        /**

+         * The responding JID cannot accept the specified payload (e.g. the data

+         * form did not provide one or more required fields).

+         */

+        badPayload("bad-payload"),

+

+        /**

+         * The responding JID cannot accept the specified sessionid.

+         */

+        badSessionid("bad-sessionid"),

+

+        /**

+         * The requesting JID specified a sessionid that is no longer active

+         * (either because it was completed, canceled, or timed out).

+         */

+        sessionExpired("session-expired");

+

+        private String value;

+

+        SpecificErrorCondition(String value) {

+            this.value = value;

+        }

+

+        public String toString() {

+            return value;

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/commands/AdHocCommandManager.java b/src/org/jivesoftware/smackx/commands/AdHocCommandManager.java
new file mode 100755
index 0000000..8dac999
--- /dev/null
+++ b/src/org/jivesoftware/smackx/commands/AdHocCommandManager.java
@@ -0,0 +1,753 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2005-2008 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.smackx.commands;

+

+import org.jivesoftware.smack.*;

+import org.jivesoftware.smack.filter.PacketFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.packet.XMPPError;

+import org.jivesoftware.smack.util.StringUtils;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.NodeInformationProvider;

+import org.jivesoftware.smackx.ServiceDiscoveryManager;

+import org.jivesoftware.smackx.commands.AdHocCommand.Action;

+import org.jivesoftware.smackx.commands.AdHocCommand.Status;

+import org.jivesoftware.smackx.packet.AdHocCommandData;

+import org.jivesoftware.smackx.packet.DiscoverInfo;

+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;

+import org.jivesoftware.smackx.packet.DiscoverItems;

+

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.Collections;

+import java.util.List;

+import java.util.Map;

+import java.util.WeakHashMap;

+import java.util.concurrent.ConcurrentHashMap;

+

+/**

+ * An AdHocCommandManager is responsible for keeping the list of available

+ * commands offered by a service and for processing commands requests.

+ *

+ * Pass in a Connection instance to

+ * {@link #getAddHocCommandsManager(org.jivesoftware.smack.Connection)} in order to

+ * get an instance of this class. 

+ * 

+ * @author Gabriel Guardincerri

+ */

+public class AdHocCommandManager {

+

+    private static final String DISCO_NAMESPACE = "http://jabber.org/protocol/commands";

+

+    private static final String discoNode = DISCO_NAMESPACE;

+

+    /**

+     * The session time out in seconds.

+     */

+    private static final int SESSION_TIMEOUT = 2 * 60;

+

+    /**

+     * Map a Connection with it AdHocCommandManager. This map have a key-value

+     * pair for every active connection.

+     */

+    private static Map<Connection, AdHocCommandManager> instances =

+            new ConcurrentHashMap<Connection, AdHocCommandManager>();

+

+    /**

+     * Register the listener for all the connection creations. When a new

+     * connection is created a new AdHocCommandManager is also created and

+     * related to that connection.

+     */

+    static {

+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {

+            public void connectionCreated(Connection connection) {

+                new AdHocCommandManager(connection);

+            }

+        });

+    }

+

+    /**

+     * Returns the <code>AdHocCommandManager</code> related to the

+     * <code>connection</code>.

+     *

+     * @param connection the XMPP connection.

+     * @return the AdHocCommandManager associated with the connection.

+     */

+    public static AdHocCommandManager getAddHocCommandsManager(Connection connection) {

+        return instances.get(connection);

+    }

+

+    /**

+     * Thread that reaps stale sessions.

+     */

+    private Thread sessionsSweeper;

+

+    /**

+     * The Connection that this instances of AdHocCommandManager manages

+     */

+    private Connection connection;

+

+    /**

+     * Map a command node with its AdHocCommandInfo. Note: Key=command node,

+     * Value=command. Command node matches the node attribute sent by command

+     * requesters.

+     */

+    private Map<String, AdHocCommandInfo> commands = Collections

+            .synchronizedMap(new WeakHashMap<String, AdHocCommandInfo>());

+

+    /**

+     * Map a command session ID with the instance LocalCommand. The LocalCommand

+     * is the an objects that has all the information of the current state of

+     * the command execution. Note: Key=session ID, Value=LocalCommand. Session

+     * ID matches the sessionid attribute sent by command responders.

+     */

+    private Map<String, LocalCommand> executingCommands = new ConcurrentHashMap<String, LocalCommand>();

+

+    private AdHocCommandManager(Connection connection) {

+        super();

+        this.connection = connection;

+        init();

+    }

+

+    /**

+     * Registers a new command with this command manager, which is related to a

+     * connection. The <tt>node</tt> is an unique identifier of that command for

+     * the connection related to this command manager. The <tt>name</tt> is the

+     * human readable name of the command. The <tt>class</tt> is the class of

+     * the command, which must extend {@link LocalCommand} and have a default

+     * constructor.

+     *

+     * @param node the unique identifier of the command.

+     * @param name the human readable name of the command.

+     * @param clazz the class of the command, which must extend {@link LocalCommand}.

+     */

+    public void registerCommand(String node, String name, final Class<? extends LocalCommand> clazz) {

+        registerCommand(node, name, new LocalCommandFactory() {

+            public LocalCommand getInstance() throws InstantiationException, IllegalAccessException  {

+                return clazz.newInstance();

+            }

+        });

+    }

+

+    /**

+     * Registers a new command with this command manager, which is related to a

+     * connection. The <tt>node</tt> is an unique identifier of that

+     * command for the connection related to this command manager. The <tt>name</tt>

+     * is the human readeale name of the command. The <tt>factory</tt> generates

+     * new instances of the command.

+     *

+     * @param node the unique identifier of the command.

+     * @param name the human readable name of the command.

+     * @param factory a factory to create new instances of the command.

+     */

+    public void registerCommand(String node, final String name, LocalCommandFactory factory) {

+        AdHocCommandInfo commandInfo = new AdHocCommandInfo(node, name, connection.getUser(), factory);

+

+        commands.put(node, commandInfo);

+        // Set the NodeInformationProvider that will provide information about

+        // the added command

+        ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(node,

+                new NodeInformationProvider() {

+                    public List<DiscoverItems.Item> getNodeItems() {

+                        return null;

+                    }

+

+                    public List<String> getNodeFeatures() {

+                        List<String> answer = new ArrayList<String>();

+                        answer.add(DISCO_NAMESPACE);

+                        // TODO: check if this service is provided by the

+                        // TODO: current connection.

+                        answer.add("jabber:x:data");

+                        return answer;

+                    }

+

+                    public List<DiscoverInfo.Identity> getNodeIdentities() {

+                        List<DiscoverInfo.Identity> answer = new ArrayList<DiscoverInfo.Identity>();

+                        DiscoverInfo.Identity identity = new DiscoverInfo.Identity(

+                                "automation", name, "command-node");

+                        answer.add(identity);

+                        return answer;

+                    }

+

+                    @Override

+                    public List<PacketExtension> getNodePacketExtensions() {

+                        return null;

+                    }

+

+                });

+    }

+

+    /**

+     * Discover the commands of an specific JID. The <code>jid</code> is a

+     * full JID.

+     *

+     * @param jid the full JID to retrieve the commands for.

+     * @return the discovered items.

+     * @throws XMPPException if the operation failed for some reason.

+     */

+    public DiscoverItems discoverCommands(String jid) throws XMPPException {

+        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager

+                .getInstanceFor(connection);

+        return serviceDiscoveryManager.discoverItems(jid, discoNode);

+    }

+

+    /**

+     * Publish the commands to an specific JID.

+     *

+     * @param jid the full JID to publish the commands to.

+     * @throws XMPPException if the operation failed for some reason.

+     */

+    public void publishCommands(String jid) throws XMPPException {

+        ServiceDiscoveryManager serviceDiscoveryManager = ServiceDiscoveryManager

+                .getInstanceFor(connection);

+

+        // Collects the commands to publish as items

+        DiscoverItems discoverItems = new DiscoverItems();

+        Collection<AdHocCommandInfo> xCommandsList = getRegisteredCommands();

+

+        for (AdHocCommandInfo info : xCommandsList) {

+            DiscoverItems.Item item = new DiscoverItems.Item(info.getOwnerJID());

+            item.setName(info.getName());

+            item.setNode(info.getNode());

+            discoverItems.addItem(item);

+        }

+

+        serviceDiscoveryManager.publishItems(jid, discoNode, discoverItems);

+    }

+

+    /**

+     * Returns a command that represents an instance of a command in a remote

+     * host. It is used to execute remote commands. The concept is similar to

+     * RMI. Every invocation on this command is equivalent to an invocation in

+     * the remote command.

+     *

+     * @param jid the full JID of the host of the remote command

+     * @param node the identifier of the command

+     * @return a local instance equivalent to the remote command.

+     */

+    public RemoteCommand getRemoteCommand(String jid, String node) {

+        return new RemoteCommand(connection, node, jid);

+    }

+

+    /**

+     * <ul>

+     * <li>Adds listeners to the connection</li>

+     * <li>Registers the ad-hoc command feature to the ServiceDiscoveryManager</li>

+     * <li>Registers the items of the feature</li>

+     * <li>Adds packet listeners to handle execution requests</li>

+     * <li>Creates and start the session sweeper</li>

+     * </ul>

+     */

+    private void init() {

+        // Register the new instance and associate it with the connection

+        instances.put(connection, this);

+

+        // Add a listener to the connection that removes the registered instance

+        // when the connection is closed

+        connection.addConnectionListener(new ConnectionListener() {

+            public void connectionClosed() {

+                // Unregister this instance since the connection has been closed

+                instances.remove(connection);

+            }

+

+            public void connectionClosedOnError(Exception e) {

+                // Unregister this instance since the connection has been closed

+                instances.remove(connection);

+            }

+

+            public void reconnectionSuccessful() {

+                // Register this instance since the connection has been

+                // reestablished

+                instances.put(connection, AdHocCommandManager.this);

+            }

+

+            public void reconnectingIn(int seconds) {

+                // Nothing to do

+            }

+

+            public void reconnectionFailed(Exception e) {

+                // Nothing to do

+            }

+        });

+

+        // Add the feature to the service discovery manage to show that this

+        // connection supports the AdHoc-Commands protocol.

+        // This information will be used when another client tries to

+        // discover whether this client supports AdHoc-Commands or not.

+        ServiceDiscoveryManager.getInstanceFor(connection).addFeature(

+                DISCO_NAMESPACE);

+

+        // Set the NodeInformationProvider that will provide information about

+        // which AdHoc-Commands are registered, whenever a disco request is

+        // received

+        ServiceDiscoveryManager.getInstanceFor(connection)

+                .setNodeInformationProvider(discoNode,

+                        new NodeInformationProvider() {

+                            public List<DiscoverItems.Item> getNodeItems() {

+

+                                List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();

+                                Collection<AdHocCommandInfo> commandsList = getRegisteredCommands();

+

+                                for (AdHocCommandInfo info : commandsList) {

+                                    DiscoverItems.Item item = new DiscoverItems.Item(

+                                            info.getOwnerJID());

+                                    item.setName(info.getName());

+                                    item.setNode(info.getNode());

+                                    answer.add(item);

+                                }

+

+                                return answer;

+                            }

+

+                            public List<String> getNodeFeatures() {

+                                return null;

+                            }

+

+                            public List<Identity> getNodeIdentities() {

+                                return null;

+                            }

+

+                            @Override

+                            public List<PacketExtension> getNodePacketExtensions() {

+                                return null;

+                            }

+                        });

+

+        // The packet listener and the filter for processing some AdHoc Commands

+        // Packets

+        PacketListener listener = new PacketListener() {

+            public void processPacket(Packet packet) {

+                AdHocCommandData requestData = (AdHocCommandData) packet;

+                processAdHocCommand(requestData);

+            }

+        };

+

+        PacketFilter filter = new PacketTypeFilter(AdHocCommandData.class);

+        connection.addPacketListener(listener, filter);

+

+        sessionsSweeper = null;

+    }

+

+    /**

+     * Process the AdHoc-Command packet that request the execution of some

+     * action of a command. If this is the first request, this method checks,

+     * before executing the command, if:

+     * <ul>

+     *  <li>The requested command exists</li>

+     *  <li>The requester has permissions to execute it</li>

+     *  <li>The command has more than one stage, if so, it saves the command and

+     *      session ID for further use</li>

+     * </ul>

+     * 

+     * <br>

+     * <br>

+     * If this is not the first request, this method checks, before executing

+     * the command, if:

+     * <ul>

+     *  <li>The session ID of the request was stored</li>

+     *  <li>The session life do not exceed the time out</li>

+     *  <li>The action to execute is one of the available actions</li>

+     * </ul>

+     *

+     * @param requestData

+     *            the packet to process.

+     */

+    private void processAdHocCommand(AdHocCommandData requestData) {

+        // Only process requests of type SET

+        if (requestData.getType() != IQ.Type.SET) {

+            return;

+        }

+

+        // Creates the response with the corresponding data

+        AdHocCommandData response = new AdHocCommandData();

+        response.setTo(requestData.getFrom());

+        response.setPacketID(requestData.getPacketID());

+        response.setNode(requestData.getNode());

+        response.setId(requestData.getTo());

+

+        String sessionId = requestData.getSessionID();

+        String commandNode = requestData.getNode();

+

+        if (sessionId == null) {

+            // A new execution request has been received. Check that the

+            // command exists

+            if (!commands.containsKey(commandNode)) {

+                // Requested command does not exist so return

+                // item_not_found error.

+                respondError(response, XMPPError.Condition.item_not_found);

+                return;

+            }

+

+            // Create new session ID

+            sessionId = StringUtils.randomString(15);

+

+            try {

+                // Create a new instance of the command with the

+                // corresponding sessioid

+                LocalCommand command = newInstanceOfCmd(commandNode, sessionId);

+

+                response.setType(IQ.Type.RESULT);

+                command.setData(response);

+

+                // Check that the requester has enough permission.

+                // Answer forbidden error if requester permissions are not

+                // enough to execute the requested command

+                if (!command.hasPermission(requestData.getFrom())) {

+                    respondError(response, XMPPError.Condition.forbidden);

+                    return;

+                }

+

+                Action action = requestData.getAction();

+

+                // If the action is unknown then respond an error.

+                if (action != null && action.equals(Action.unknown)) {

+                    respondError(response, XMPPError.Condition.bad_request,

+                            AdHocCommand.SpecificErrorCondition.malformedAction);

+                    return;

+                }

+

+                // If the action is not execute, then it is an invalid action.

+                if (action != null && !action.equals(Action.execute)) {

+                    respondError(response, XMPPError.Condition.bad_request,

+                            AdHocCommand.SpecificErrorCondition.badAction);

+                    return;

+                }

+

+                // Increase the state number, so the command knows in witch

+                // stage it is

+                command.incrementStage();

+                // Executes the command

+                command.execute();

+

+                if (command.isLastStage()) {

+                    // If there is only one stage then the command is completed

+                    response.setStatus(Status.completed);

+                }

+                else {

+                    // Else it is still executing, and is registered to be

+                    // available for the next call

+                    response.setStatus(Status.executing);

+                    executingCommands.put(sessionId, command);

+                    // See if the session reaping thread is started. If not, start it.

+                    if (sessionsSweeper == null) {

+                        sessionsSweeper = new Thread(new Runnable() {

+                            public void run() {

+                                while (true) {

+                                    for (String sessionId : executingCommands.keySet()) {

+                                        LocalCommand command = executingCommands.get(sessionId);

+                                        // Since the command could be removed in the meanwhile

+                                        // of getting the key and getting the value - by a

+                                        // processed packet. We must check if it still in the

+                                        // map.

+                                        if (command != null) {

+                                            long creationStamp = command.getCreationDate();

+                                            // Check if the Session data has expired (default is

+                                            // 10 minutes)

+                                            // To remove it from the session list it waits for

+                                            // the double of the of time out time. This is to

+                                            // let

+                                            // the requester know why his execution request is

+                                            // not accepted. If the session is removed just

+                                            // after the time out, then whe the user request to

+                                            // continue the execution he will recieved an

+                                            // invalid session error and not a time out error.

+                                            if (System.currentTimeMillis() - creationStamp > SESSION_TIMEOUT * 1000 * 2) {

+                                                // Remove the expired session

+                                                executingCommands.remove(sessionId);

+                                            }

+                                        }

+                                    }

+                                    try {

+                                        Thread.sleep(1000);

+                                    }

+                                    catch (InterruptedException ie) {

+                                        // Ignore.

+                                    }

+                                }

+                            }

+

+                        });

+                        sessionsSweeper.setDaemon(true);

+                        sessionsSweeper.start();

+                    }

+                }

+

+                // Sends the response packet

+                connection.sendPacket(response);

+

+            }

+            catch (XMPPException e) {

+                // If there is an exception caused by the next, complete,

+                // prev or cancel method, then that error is returned to the

+                // requester.

+                XMPPError error = e.getXMPPError();

+

+                // If the error type is cancel, then the execution is

+                // canceled therefore the status must show that, and the

+                // command be removed from the executing list.

+                if (XMPPError.Type.CANCEL.equals(error.getType())) {

+                    response.setStatus(Status.canceled);

+                    executingCommands.remove(sessionId);

+                }

+                respondError(response, error);

+                e.printStackTrace();

+            }

+        }

+        else {

+            LocalCommand command = executingCommands.get(sessionId);

+

+            // Check that a command exists for the specified sessionID

+            // This also handles if the command was removed in the meanwhile

+            // of getting the key and the value of the map.

+            if (command == null) {

+                respondError(response, XMPPError.Condition.bad_request,

+                        AdHocCommand.SpecificErrorCondition.badSessionid);

+                return;

+            }

+

+            // Check if the Session data has expired (default is 10 minutes)

+            long creationStamp = command.getCreationDate();

+            if (System.currentTimeMillis() - creationStamp > SESSION_TIMEOUT * 1000) {

+                // Remove the expired session

+                executingCommands.remove(sessionId);

+

+                // Answer a not_allowed error (session-expired)

+                respondError(response, XMPPError.Condition.not_allowed,

+                        AdHocCommand.SpecificErrorCondition.sessionExpired);

+                return;

+            }

+

+            /*

+             * Since the requester could send two requests for the same

+             * executing command i.e. the same session id, all the execution of

+             * the action must be synchronized to avoid inconsistencies.

+             */

+            synchronized (command) {

+                Action action = requestData.getAction();

+

+                // If the action is unknown the respond an error

+                if (action != null && action.equals(Action.unknown)) {

+                    respondError(response, XMPPError.Condition.bad_request,

+                            AdHocCommand.SpecificErrorCondition.malformedAction);

+                    return;

+                }

+

+                // If the user didn't specify an action or specify the execute

+                // action then follow the actual default execute action

+                if (action == null || Action.execute.equals(action)) {

+                    action = command.getExecuteAction();

+                }

+

+                // Check that the specified action was previously

+                // offered

+                if (!command.isValidAction(action)) {

+                    respondError(response, XMPPError.Condition.bad_request,

+                            AdHocCommand.SpecificErrorCondition.badAction);

+                    return;

+                }

+

+                try {

+                    // TODO: Check that all the requierd fields of the form are

+                    // TODO: filled, if not throw an exception. This will simplify the

+                    // TODO: construction of new commands

+

+                    // Since all errors were passed, the response is now a

+                    // result

+                    response.setType(IQ.Type.RESULT);

+

+                    // Set the new data to the command.

+                    command.setData(response);

+

+                    if (Action.next.equals(action)) {

+                        command.incrementStage();

+                        command.next(new Form(requestData.getForm()));

+                        if (command.isLastStage()) {

+                            // If it is the last stage then the command is

+                            // completed

+                            response.setStatus(Status.completed);

+                        }

+                        else {

+                            // Otherwise it is still executing

+                            response.setStatus(Status.executing);

+                        }

+                    }

+                    else if (Action.complete.equals(action)) {

+                        command.incrementStage();

+                        command.complete(new Form(requestData.getForm()));

+                        response.setStatus(Status.completed);

+                        // Remove the completed session

+                        executingCommands.remove(sessionId);

+                    }

+                    else if (Action.prev.equals(action)) {

+                        command.decrementStage();

+                        command.prev();

+                    }

+                    else if (Action.cancel.equals(action)) {

+                        command.cancel();

+                        response.setStatus(Status.canceled);

+                        // Remove the canceled session

+                        executingCommands.remove(sessionId);

+                    }

+

+                    connection.sendPacket(response);

+                }

+                catch (XMPPException e) {

+                    // If there is an exception caused by the next, complete,

+                    // prev or cancel method, then that error is returned to the

+                    // requester.

+                    XMPPError error = e.getXMPPError();

+

+                    // If the error type is cancel, then the execution is

+                    // canceled therefore the status must show that, and the

+                    // command be removed from the executing list.

+                    if (XMPPError.Type.CANCEL.equals(error.getType())) {

+                        response.setStatus(Status.canceled);

+                        executingCommands.remove(sessionId);

+                    }

+                    respondError(response, error);

+

+                    e.printStackTrace();

+                }

+            }

+        }

+    }

+

+    /**

+     * Responds an error with an specific condition.

+     * 

+     * @param response the response to send.

+     * @param condition the condition of the error.

+     */

+    private void respondError(AdHocCommandData response,

+            XMPPError.Condition condition) {

+        respondError(response, new XMPPError(condition));

+    }

+

+    /**

+     * Responds an error with an specific condition.

+     * 

+     * @param response the response to send.

+     * @param condition the condition of the error.

+     * @param specificCondition the adhoc command error condition.

+     */

+    private void respondError(AdHocCommandData response, XMPPError.Condition condition,

+            AdHocCommand.SpecificErrorCondition specificCondition)

+    {

+        XMPPError error = new XMPPError(condition);

+        error.addExtension(new AdHocCommandData.SpecificError(specificCondition));

+        respondError(response, error);

+    }

+

+    /**

+     * Responds an error with an specific error.

+     * 

+     * @param response the response to send.

+     * @param error the error to send.

+     */

+    private void respondError(AdHocCommandData response, XMPPError error) {

+        response.setType(IQ.Type.ERROR);

+        response.setError(error);

+        connection.sendPacket(response);

+    }

+

+    /**

+     * Creates a new instance of a command to be used by a new execution request

+     * 

+     * @param commandNode the command node that identifies it.

+     * @param sessionID the session id of this execution.

+     * @return the command instance to execute.

+     * @throws XMPPException if there is problem creating the new instance.

+     */

+    private LocalCommand newInstanceOfCmd(String commandNode, String sessionID)

+            throws XMPPException

+    {

+        AdHocCommandInfo commandInfo = commands.get(commandNode);

+        LocalCommand command;

+        try {

+            command = (LocalCommand) commandInfo.getCommandInstance();

+            command.setSessionID(sessionID);

+            command.setName(commandInfo.getName());

+            command.setNode(commandInfo.getNode());

+        }

+        catch (InstantiationException e) {

+            e.printStackTrace();

+            throw new XMPPException(new XMPPError(

+                    XMPPError.Condition.interna_server_error));

+        }

+        catch (IllegalAccessException e) {

+            e.printStackTrace();

+            throw new XMPPException(new XMPPError(

+                    XMPPError.Condition.interna_server_error));

+        }

+        return command;

+    }

+

+    /**

+     * Returns the registered commands of this command manager, which is related

+     * to a connection.

+     * 

+     * @return the registered commands.

+     */

+    private Collection<AdHocCommandInfo> getRegisteredCommands() {

+        return commands.values();

+    }

+

+    /**

+     * Stores ad-hoc command information.

+     */

+    private static class AdHocCommandInfo {

+

+        private String node;

+        private String name;

+        private String ownerJID;

+        private LocalCommandFactory factory;

+

+        public AdHocCommandInfo(String node, String name, String ownerJID,

+                LocalCommandFactory factory)

+        {

+            this.node = node;

+            this.name = name;

+            this.ownerJID = ownerJID;

+            this.factory = factory;

+        }

+

+        public LocalCommand getCommandInstance() throws InstantiationException,

+                IllegalAccessException

+        {

+            return factory.getInstance();

+        }

+

+        public String getName() {

+            return name;

+        }

+

+        public String getNode() {

+            return node;

+        }

+

+        public String getOwnerJID() {

+            return ownerJID;

+        }

+    }

+}
diff --git a/src/org/jivesoftware/smackx/commands/AdHocCommandNote.java b/src/org/jivesoftware/smackx/commands/AdHocCommandNote.java
new file mode 100755
index 0000000..10dedbe
--- /dev/null
+++ b/src/org/jivesoftware/smackx/commands/AdHocCommandNote.java
@@ -0,0 +1,86 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2005-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.smackx.commands;

+

+/**

+ * Notes can be added to a command execution response. A note has a type and value.

+ * 

+ * @author Gabriel Guardincerri

+ */

+public class AdHocCommandNote {

+

+    private Type type;

+    private String value;

+

+    /**

+     * Creates a new adhoc command note with the specified type and value.

+     *

+     * @param type the type of the note.

+     * @param value the value of the note.

+     */

+    public AdHocCommandNote(Type type, String value) {

+        this.type = type;

+        this.value = value;

+    }

+

+    /**

+     * Returns the value or message of the note.

+     * 

+     * @return the value or message of the note.

+     */

+    public String getValue() {

+        return value;

+    }

+

+    /**

+     * Return the type of the note.

+     * 

+     * @return the type of the note.

+     */

+    public Type getType() {

+        return type;

+    }

+

+    /**

+     * Represents a note type.

+     */

+    public enum Type {

+

+        /**

+         * The note is informational only. This is not really an exceptional

+         * condition.

+         */

+        info,

+

+        /**

+         * The note indicates a warning. Possibly due to illogical (yet valid)

+         * data.

+         */

+        warn,

+

+        /**

+         * The note indicates an error. The text should indicate the reason for

+         * the error.

+         */

+        error

+    }

+

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/commands/LocalCommand.java b/src/org/jivesoftware/smackx/commands/LocalCommand.java
new file mode 100755
index 0000000..627d30e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/commands/LocalCommand.java
@@ -0,0 +1,169 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2005-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.smackx.commands;

+

+import org.jivesoftware.smackx.packet.AdHocCommandData;

+

+/**

+ * Represents a command that can be executed locally from a remote location. This

+ * class must be extended to implement an specific ad-hoc command. This class

+ * provides some useful tools:<ul>

+ *      <li>Node</li>

+ *      <li>Name</li>

+ *      <li>Session ID</li>

+ *      <li>Current Stage</li>

+ *      <li>Available actions</li>

+ *      <li>Default action</li>

+ * </ul><p/>

+ * To implement a new command extend this class and implement all the abstract

+ * methods. When implementing the actions remember that they could be invoked

+ * several times, and that you must use the current stage number to know what to

+ * do.

+ * 

+ * @author Gabriel Guardincerri

+ */

+public abstract class LocalCommand extends AdHocCommand {

+

+    /**

+     * The time stamp of first invokation of the command. Used to implement the session timeout.

+     */

+    private long creationDate;

+

+    /**

+     * The unique ID of the execution of the command.

+     */

+    private String sessionID;

+

+    /**

+     * The full JID of the host of the command.

+     */

+    private String ownerJID;

+

+    /**

+     * The number of the current stage.

+     */

+    private int currenStage;

+

+    public LocalCommand() {

+        super();

+        this.creationDate = System.currentTimeMillis();

+        currenStage = -1;

+    }

+

+    /**

+     * The sessionID is an unique identifier of an execution request. This is

+     * automatically handled and should not be called.

+     * 

+     * @param sessionID the unique session id of this execution

+     */

+    public void setSessionID(String sessionID) {

+        this.sessionID = sessionID;

+        getData().setSessionID(sessionID);

+    }

+

+    /**

+     * Returns the session ID of this execution.

+     * 

+     * @return the unique session id of this execution

+     */

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    /**

+     * Sets the JID of the command host. This is automatically handled and should

+     * not be called.

+     * 

+     * @param ownerJID the JID of the owner.

+     */

+    public void setOwnerJID(String ownerJID) {

+        this.ownerJID = ownerJID;

+    }

+

+    @Override

+    public String getOwnerJID() {

+        return ownerJID;

+    }

+

+    /**

+     * Returns the date the command was created.

+     * 

+     * @return the date the command was created.

+     */

+    public long getCreationDate() {

+        return creationDate;

+    }

+

+    /**

+     * Returns true if the current stage is the last one. If it is then the

+     * execution of some action will complete the execution of the command.

+     * Commands that don't have multiple stages can always return <tt>true</tt>.

+     * 

+     * @return true if the command is in the last stage.

+     */

+    public abstract boolean isLastStage();

+

+    /**

+     * Returns true if the specified requester has permission to execute all the

+     * stages of this action. This is checked when the first request is received,

+     * if the permission is grant then the requester will be able to execute

+     * all the stages of the command. It is not checked again during the

+     * execution.

+     *

+     * @param jid the JID to check permissions on.

+     * @return true if the user has permission to execute this action.

+     */

+    public abstract boolean hasPermission(String jid);

+

+    /**

+     * Returns the currently executing stage number. The first stage number is

+     * 0. During the execution of the first action this method will answer 0.

+     *

+     * @return the current stage number.

+     */

+    public int getCurrentStage() {

+        return currenStage;

+    }

+

+    @Override

+    void setData(AdHocCommandData data) {

+        data.setSessionID(sessionID);

+        super.setData(data);

+    }

+

+    /**

+     * Increase the current stage number. This is automatically handled and should

+     * not be called.

+     * 

+     */

+    void incrementStage() {

+        currenStage++;

+    }

+

+    /**

+     * Decrease the current stage number. This is automatically handled and should

+     * not be called.

+     * 

+     */

+    void decrementStage() {

+        currenStage--;

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/commands/LocalCommandFactory.java b/src/org/jivesoftware/smackx/commands/LocalCommandFactory.java
new file mode 100644
index 0000000..83fc455
--- /dev/null
+++ b/src/org/jivesoftware/smackx/commands/LocalCommandFactory.java
@@ -0,0 +1,43 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2008 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.smackx.commands;
+
+/**
+ * A factory for creating local commands. It's useful in cases where instantiation
+ * of a command is more complicated than just using the default constructor. For example,
+ * when arguments must be passed into the constructor or when using a dependency injection
+ * framework. When a LocalCommandFactory isn't used, you can provide the AdHocCommandManager
+ * a Class object instead. For more details, see
+ * {@link AdHocCommandManager#registerCommand(String, String, LocalCommandFactory)}. 
+ *
+ * @author Matt Tucker
+ */
+public interface LocalCommandFactory {
+
+    /**
+     * Returns an instance of a LocalCommand.
+     *
+     * @return a LocalCommand instance.
+     * @throws InstantiationException if creating an instance failed.
+     * @throws IllegalAccessException if creating an instance is not allowed.
+     */
+    public LocalCommand getInstance() throws InstantiationException, IllegalAccessException;
+
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/commands/RemoteCommand.java b/src/org/jivesoftware/smackx/commands/RemoteCommand.java
new file mode 100755
index 0000000..3c2df1d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/commands/RemoteCommand.java
@@ -0,0 +1,201 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2005-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.smackx.commands;
+
+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.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.packet.AdHocCommandData;
+
+/**
+ * Represents a command that is in a remote location. Invoking one of the
+ * {@link AdHocCommand.Action#execute execute}, {@link AdHocCommand.Action#next next},
+ * {@link AdHocCommand.Action#prev prev}, {@link AdHocCommand.Action#cancel cancel} or
+ * {@link AdHocCommand.Action#complete complete} actions results in executing that
+ * action in the remote location. In response to that action the internal state
+ * of the this command instance will change. For example, if the command is a
+ * single stage command, then invoking the execute action will execute this
+ * action in the remote location. After that the local instance will have a
+ * state of "completed" and a form or notes that applies.
+ *
+ * @author Gabriel Guardincerri
+ *
+ */
+public class RemoteCommand extends AdHocCommand {
+
+    /**
+     * The connection that is used to execute this command
+     */
+    private Connection connection;
+
+    /**
+     * The full JID of the command host
+     */
+    private String jid;
+
+    /**
+     * The session ID of this execution.
+     */
+    private String sessionID;
+
+
+    /**
+     * The number of milliseconds to wait for a response from the server
+     * The default value is the default packet reply timeout (5000 ms).
+     */
+    private long packetReplyTimeout;
+
+    /**
+     * Creates a new RemoteCommand that uses an specific connection to execute a
+     * command identified by <code>node</code> in the host identified by
+     * <code>jid</code>
+     *
+     * @param connection the connection to use for the execution.
+     * @param node the identifier of the command.
+     * @param jid the JID of the host.
+     */
+    protected RemoteCommand(Connection connection, String node, String jid) {
+        super();
+        this.connection = connection;
+        this.jid = jid;
+        this.setNode(node);
+        this.packetReplyTimeout = SmackConfiguration.getPacketReplyTimeout();
+    }
+
+    @Override
+    public void cancel() throws XMPPException {
+        executeAction(Action.cancel, packetReplyTimeout);
+    }
+
+    @Override
+    public void complete(Form form) throws XMPPException {
+        executeAction(Action.complete, form, packetReplyTimeout);
+    }
+
+    @Override
+    public void execute() throws XMPPException {
+        executeAction(Action.execute, packetReplyTimeout);
+    }
+
+    /**
+     * Executes the default action of the command with the information provided
+     * in the Form. This form must be the anwser form of the previous stage. If
+     * there is a problem executing the command it throws an XMPPException.
+     *
+     * @param form the form anwser of the previous stage.
+     * @throws XMPPException if an error occurs.
+     */
+    public void execute(Form form) throws XMPPException {
+        executeAction(Action.execute, form, packetReplyTimeout);
+    }
+
+    @Override
+    public void next(Form form) throws XMPPException {
+        executeAction(Action.next, form, packetReplyTimeout);
+    }
+
+    @Override
+    public void prev() throws XMPPException {
+        executeAction(Action.prev, packetReplyTimeout);
+    }
+
+    private void executeAction(Action action, long packetReplyTimeout) throws XMPPException {
+        executeAction(action, null, packetReplyTimeout);
+    }
+
+    /**
+     * Executes the <code>action</codo> with the <code>form</code>.
+     * The action could be any of the available actions. The form must
+     * be the anwser of the previous stage. It can be <tt>null</tt> if it is the first stage.
+     *
+     * @param action the action to execute.
+     * @param form the form with the information.
+     * @param timeout the amount of time to wait for a reply.
+     * @throws XMPPException if there is a problem executing the command.
+     */
+    private void executeAction(Action action, Form form, long timeout) throws XMPPException {
+        // TODO: Check that all the required fields of the form were filled, if
+        // TODO: not throw the corresponding exeption. This will make a faster response,
+        // TODO: since the request is stoped before it's sent.
+        AdHocCommandData data = new AdHocCommandData();
+        data.setType(IQ.Type.SET);
+        data.setTo(getOwnerJID());
+        data.setNode(getNode());
+        data.setSessionID(sessionID);
+        data.setAction(action);
+
+        if (form != null) {
+            data.setForm(form.getDataFormToSend());
+        }
+
+        PacketCollector collector = connection.createPacketCollector(
+                new PacketIDFilter(data.getPacketID()));
+
+        connection.sendPacket(data);
+
+        Packet response = collector.nextResult(timeout);
+
+        // Cancel the collector.
+        collector.cancel();
+        if (response == null) {
+            throw new XMPPException("No response from server on status set.");
+        }
+        if (response.getError() != null) {
+            throw new XMPPException(response.getError());
+        }
+
+        AdHocCommandData responseData = (AdHocCommandData) response;
+        this.sessionID = responseData.getSessionID();
+        super.setData(responseData);
+    }
+
+    @Override
+    public String getOwnerJID() {
+        return jid;
+    }
+
+    /**
+     * Returns the number of milliseconds to wait for a respone. The
+     * {@link SmackConfiguration#getPacketReplyTimeout default} value
+     * should be adjusted for commands that can take a long time to execute.
+     *
+     * @return the number of milliseconds to wait for responses.
+     */
+    public long getPacketReplyTimeout() {
+        return packetReplyTimeout;
+    }
+
+    /**
+     * Returns the number of milliseconds to wait for a respone. The
+     * {@link SmackConfiguration#getPacketReplyTimeout default} value
+     * should be adjusted for commands that can take a long time to execute.
+     *
+     * @param packetReplyTimeout the number of milliseconds to wait for responses.
+     */
+    public void setPacketReplyTimeout(long packetReplyTimeout) {
+        this.packetReplyTimeout = packetReplyTimeout;
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/debugger/package.html b/src/org/jivesoftware/smackx/debugger/package.html
new file mode 100644
index 0000000..8ea20e0
--- /dev/null
+++ b/src/org/jivesoftware/smackx/debugger/package.html
@@ -0,0 +1 @@
+<body>Smack optional Debuggers.</body>
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java b/src/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java
new file mode 100644
index 0000000..1da222e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/entitycaps/EntityCapsManager.java
@@ -0,0 +1,713 @@
+/**
+ * Copyright 2009 Jonas Ådahl.
+ * Copyright 2011-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.smackx.entitycaps;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketInterceptor;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPConnection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.filter.NotFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.filter.PacketExtensionFilter;
+import org.jivesoftware.smack.util.Base64;
+import org.jivesoftware.smack.util.Cache;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.NodeInformationProvider;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.entitycaps.cache.EntityCapsPersistentCache;
+import org.jivesoftware.smackx.entitycaps.packet.CapsExtension;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Feature;
+import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
+import org.jivesoftware.smackx.packet.DiscoverItems.Item;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Keeps track of entity capabilities.
+ * 
+ * @author Florian Schmaus
+ */
+public class EntityCapsManager {
+
+    public static final String NAMESPACE = "http://jabber.org/protocol/caps";
+    public static final String ELEMENT = "c";
+
+    private static final String ENTITY_NODE = "http://www.igniterealtime.org/projects/smack";
+    private static final Map<String, MessageDigest> SUPPORTED_HASHES = new HashMap<String, MessageDigest>();
+
+    protected static EntityCapsPersistentCache persistentCache;
+
+    private static Map<Connection, EntityCapsManager> instances = Collections
+            .synchronizedMap(new WeakHashMap<Connection, EntityCapsManager>());
+
+    /**
+     * Map of (node + '#" + hash algorithm) to DiscoverInfo data
+     */
+    protected static Map<String, DiscoverInfo> caps = new Cache<String, DiscoverInfo>(1000, -1);
+
+    /**
+     * Map of Full JID -&gt; DiscoverInfo/null. In case of c2s connection the
+     * key is formed as user@server/resource (resource is required) In case of
+     * link-local connection the key is formed as user@host (no resource) In
+     * case of a server or component the key is formed as domain
+     */
+    protected static Map<String, NodeVerHash> jidCaps = new Cache<String, NodeVerHash>(10000, -1);
+
+    static {
+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+            public void connectionCreated(Connection connection) {
+                if (connection instanceof XMPPConnection)
+                    new EntityCapsManager(connection);
+            }
+        });
+
+        try {
+            MessageDigest sha1MessageDigest = MessageDigest.getInstance("SHA-1");
+            SUPPORTED_HASHES.put("sha-1", sha1MessageDigest);
+        } catch (NoSuchAlgorithmException e) {
+            // Ignore
+        }
+    }
+
+    private WeakReference<Connection> weakRefConnection;
+    private ServiceDiscoveryManager sdm;
+    private boolean entityCapsEnabled;
+    private String currentCapsVersion;
+    private boolean presenceSend = false;
+    private Queue<String> lastLocalCapsVersions = new ConcurrentLinkedQueue<String>();
+
+    /**
+     * Add DiscoverInfo to the database.
+     * 
+     * @param nodeVer
+     *            The node and verification String (e.g.
+     *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
+     * @param info
+     *            DiscoverInfo for the specified node.
+     */
+    public static void addDiscoverInfoByNode(String nodeVer, DiscoverInfo info) {
+        caps.put(nodeVer, info);
+
+        if (persistentCache != null)
+            persistentCache.addDiscoverInfoByNodePersistent(nodeVer, info);
+    }
+
+    /**
+     * Get the Node version (node#ver) of a JID. Returns a String or null if
+     * EntiyCapsManager does not have any information.
+     * 
+     * @param user
+     *            the user (Full JID)
+     * @return the node version (node#ver) or null
+     */
+    public static String getNodeVersionByJid(String jid) {
+        NodeVerHash nvh = jidCaps.get(jid);
+        if (nvh != null) {
+            return nvh.nodeVer;
+        } else {
+            return null;
+        }
+    }
+
+    public static NodeVerHash getNodeVerHashByJid(String jid) {
+        return jidCaps.get(jid);
+    }
+
+    /**
+     * Get the discover info given a user name. The discover info is returned if
+     * the user has a node#ver associated with it and the node#ver has a
+     * discover info associated with it.
+     * 
+     * @param user
+     *            user name (Full JID)
+     * @return the discovered info
+     */
+    public static DiscoverInfo getDiscoverInfoByUser(String user) {
+        NodeVerHash nvh = jidCaps.get(user);
+        if (nvh == null)
+            return null;
+
+        return getDiscoveryInfoByNodeVer(nvh.nodeVer);
+    }
+
+    /**
+     * Retrieve DiscoverInfo for a specific node.
+     * 
+     * @param nodeVer
+     *            The node name (e.g.
+     *            "http://psi-im.org#q07IKJEyjvHSyhy//CH0CxmKi8w=").
+     * @return The corresponding DiscoverInfo or null if none is known.
+     */
+    public static DiscoverInfo getDiscoveryInfoByNodeVer(String nodeVer) {
+        DiscoverInfo info = caps.get(nodeVer);
+        if (info != null)
+            info = new DiscoverInfo(info);
+
+        return info;
+    }
+
+    /**
+     * Set the persistent cache implementation
+     * 
+     * @param cache
+     * @throws IOException
+     */
+    public static void setPersistentCache(EntityCapsPersistentCache cache) throws IOException {
+        if (persistentCache != null)
+            throw new IllegalStateException("Entity Caps Persistent Cache was already set");
+        persistentCache = cache;
+        persistentCache.replay();
+    }
+
+    /**
+     * Sets the maximum Cache size for the JID to nodeVer Cache
+     * 
+     * @param maxCacheSize
+     */
+    @SuppressWarnings("rawtypes")
+    public static void setJidCapsMaxCacheSize(int maxCacheSize) {
+        ((Cache) jidCaps).setMaxCacheSize(maxCacheSize);
+    }
+
+    /**
+     * Sets the maximum Cache size for the nodeVer to DiscoverInfo Cache
+     * 
+     * @param maxCacheSize
+     */
+    @SuppressWarnings("rawtypes")
+    public static void setCapsMaxCacheSize(int maxCacheSize) {
+        ((Cache) caps).setMaxCacheSize(maxCacheSize);
+    }
+
+    private EntityCapsManager(Connection connection) {
+        this.weakRefConnection = new WeakReference<Connection>(connection);
+        this.sdm = ServiceDiscoveryManager.getInstanceFor(connection);
+        init();
+    }
+
+    private void init() {
+        Connection connection = weakRefConnection.get();
+        instances.put(connection, this);
+
+        connection.addConnectionListener(new ConnectionListener() {
+            public void connectionClosed() {
+                // Unregister this instance since the connection has been closed
+                presenceSend = false;
+                instances.remove(weakRefConnection.get());
+            }
+
+            public void connectionClosedOnError(Exception e) {
+                presenceSend = false;
+            }
+
+            public void reconnectionFailed(Exception e) {
+                // ignore
+            }
+
+            public void reconnectingIn(int seconds) {
+                // ignore
+            }
+
+            public void reconnectionSuccessful() {
+                // ignore
+            }
+        });
+
+        // This calculates the local entity caps version
+        updateLocalEntityCaps();
+
+        if (SmackConfiguration.autoEnableEntityCaps())
+            enableEntityCaps();
+
+        PacketFilter packetFilter = new AndFilter(new PacketTypeFilter(Presence.class), new PacketExtensionFilter(
+                ELEMENT, NAMESPACE));
+        connection.addPacketListener(new PacketListener() {
+            // Listen for remote presence stanzas with the caps extension
+            // If we receive such a stanza, record the JID and nodeVer
+            @Override
+            public void processPacket(Packet packet) {
+                if (!entityCapsEnabled())
+                    return;
+
+                CapsExtension ext = (CapsExtension) packet.getExtension(EntityCapsManager.ELEMENT,
+                        EntityCapsManager.NAMESPACE);
+
+                String hash = ext.getHash().toLowerCase();
+                if (!SUPPORTED_HASHES.containsKey(hash))
+                    return;
+
+                String from = packet.getFrom();
+                String node = ext.getNode();
+                String ver = ext.getVer();
+
+                jidCaps.put(from, new NodeVerHash(node, ver, hash));
+            }
+
+        }, packetFilter);
+
+        packetFilter = new AndFilter(new PacketTypeFilter(Presence.class), new NotFilter(new PacketExtensionFilter(
+                ELEMENT, NAMESPACE)));
+        connection.addPacketListener(new PacketListener() {
+            @Override
+            public void processPacket(Packet packet) {
+                // always remove the JID from the map, even if entityCaps are
+                // disabled
+                String from = packet.getFrom();
+                jidCaps.remove(from);
+            }
+        }, packetFilter);
+
+        packetFilter = new PacketTypeFilter(Presence.class);
+        connection.addPacketSendingListener(new PacketListener() {
+            @Override
+            public void processPacket(Packet packet) {
+                presenceSend = true;
+            }
+        }, packetFilter);
+
+        // Intercept presence packages and add caps data when intended.
+        // XEP-0115 specifies that a client SHOULD include entity capabilities
+        // with every presence notification it sends.
+        PacketFilter capsPacketFilter = new PacketTypeFilter(Presence.class);
+        PacketInterceptor packetInterceptor = new PacketInterceptor() {
+            public void interceptPacket(Packet packet) {
+                if (!entityCapsEnabled)
+                    return;
+
+                CapsExtension caps = new CapsExtension(ENTITY_NODE, getCapsVersion(), "sha-1");
+                packet.addExtension(caps);
+            }
+        };
+        connection.addPacketInterceptor(packetInterceptor, capsPacketFilter);
+        // It's important to do this as last action. Since it changes the
+        // behavior of the SDM in some ways
+        sdm.setEntityCapsManager(this);
+    }
+
+    public static synchronized EntityCapsManager getInstanceFor(Connection connection) {
+        // For testing purposed forbid EntityCaps for non XMPPConnections
+        // it may work on BOSH connections too
+        if (!(connection instanceof XMPPConnection))
+            return null;
+
+        if (SUPPORTED_HASHES.size() <= 0)
+            return null;
+
+        EntityCapsManager entityCapsManager = instances.get(connection);
+
+        if (entityCapsManager == null) {
+            entityCapsManager = new EntityCapsManager(connection);
+        }
+
+        return entityCapsManager;
+    }
+
+    public void enableEntityCaps() {
+        // Add Entity Capabilities (XEP-0115) feature node.
+        sdm.addFeature(NAMESPACE);
+        updateLocalEntityCaps();
+        entityCapsEnabled = true;
+    }
+
+    public void disableEntityCaps() {
+        entityCapsEnabled = false;
+        sdm.removeFeature(NAMESPACE);
+    }
+
+    public boolean entityCapsEnabled() {
+        return entityCapsEnabled;
+    }
+
+    /**
+     * Remove a record telling what entity caps node a user has.
+     * 
+     * @param user
+     *            the user (Full JID)
+     */
+    public void removeUserCapsNode(String user) {
+        jidCaps.remove(user);
+    }
+
+    /**
+     * Get our own caps version. The version depends on the enabled features. A
+     * caps version looks like '66/0NaeaBKkwk85efJTGmU47vXI='
+     * 
+     * @return our own caps version
+     */
+    public String getCapsVersion() {
+        return currentCapsVersion;
+    }
+
+    /**
+     * Returns the local entity's NodeVer (e.g.
+     * "http://www.igniterealtime.org/projects/smack/#66/0NaeaBKkwk85efJTGmU47vXI=
+     * )
+     * 
+     * @return
+     */
+    public String getLocalNodeVer() {
+        return ENTITY_NODE + '#' + getCapsVersion();
+    }
+
+    /**
+     * Returns true if Entity Caps are supported by a given JID
+     * 
+     * @param jid
+     * @return
+     */
+    public boolean areEntityCapsSupported(String jid) {
+        if (jid == null)
+            return false;
+
+        try {
+            DiscoverInfo result = sdm.discoverInfo(jid);
+            return result.containsFeature(NAMESPACE);
+        } catch (XMPPException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Returns true if Entity Caps are supported by the local service/server
+     * 
+     * @return
+     */
+    public boolean areEntityCapsSupportedByServer() {
+        return areEntityCapsSupported(weakRefConnection.get().getServiceName());
+    }
+
+    /**
+     * Updates the local user Entity Caps information with the data provided
+     * 
+     * If we are connected and there was already a presence send, another
+     * presence is send to inform others about your new Entity Caps node string.
+     * 
+     * @param discoverInfo
+     *            the local users discover info (mostly the service discovery
+     *            features)
+     * @param identityType
+     *            the local users identity type
+     * @param identityName
+     *            the local users identity name
+     * @param extendedInfo
+     *            the local users extended info
+     */
+    public void updateLocalEntityCaps() {
+        Connection connection = weakRefConnection.get();
+
+        DiscoverInfo discoverInfo = new DiscoverInfo();
+        discoverInfo.setType(IQ.Type.RESULT);
+        discoverInfo.setNode(getLocalNodeVer());
+        if (connection != null)
+            discoverInfo.setFrom(connection.getUser());
+        sdm.addDiscoverInfoTo(discoverInfo);
+
+        currentCapsVersion = generateVerificationString(discoverInfo, "sha-1");
+        addDiscoverInfoByNode(ENTITY_NODE + '#' + currentCapsVersion, discoverInfo);
+        if (lastLocalCapsVersions.size() > 10) {
+            String oldCapsVersion = lastLocalCapsVersions.poll();
+            sdm.removeNodeInformationProvider(ENTITY_NODE + '#' + oldCapsVersion);
+        }
+        lastLocalCapsVersions.add(currentCapsVersion);
+
+        caps.put(currentCapsVersion, discoverInfo);
+        if (connection != null)
+            jidCaps.put(connection.getUser(), new NodeVerHash(ENTITY_NODE, currentCapsVersion, "sha-1"));
+
+        sdm.setNodeInformationProvider(ENTITY_NODE + '#' + currentCapsVersion, new NodeInformationProvider() {
+            List<String> features = sdm.getFeaturesList();
+            List<Identity> identities = new LinkedList<Identity>(ServiceDiscoveryManager.getIdentities());
+            List<PacketExtension> packetExtensions = sdm.getExtendedInfoAsList();
+
+            @Override
+            public List<Item> getNodeItems() {
+                return null;
+            }
+
+            @Override
+            public List<String> getNodeFeatures() {
+                return features;
+            }
+
+            @Override
+            public List<Identity> getNodeIdentities() {
+                return identities;
+            }
+
+            @Override
+            public List<PacketExtension> getNodePacketExtensions() {
+                return packetExtensions;
+            }
+        });
+
+        // Send an empty presence, and let the packet intercepter
+        // add a <c/> node to it.
+        // See http://xmpp.org/extensions/xep-0115.html#advertise
+        // We only send a presence packet if there was already one send
+        // to respect ConnectionConfiguration.isSendPresence()
+        if (connection != null && connection.isAuthenticated() && presenceSend) {
+            Presence presence = new Presence(Presence.Type.available);
+            connection.sendPacket(presence);
+        }
+    }
+
+    /**
+     * Verify DisoverInfo and Caps Node as defined in XEP-0115 5.4 Processing
+     * Method
+     * 
+     * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0115
+     *      5.4 Processing Method</a>
+     * 
+     * @param capsNode
+     *            the caps node (i.e. node#ver)
+     * @param info
+     * @return true if it's valid and should be cache, false if not
+     */
+    public static boolean verifyDiscoverInfoVersion(String ver, String hash, DiscoverInfo info) {
+        // step 3.3 check for duplicate identities
+        if (info.containsDuplicateIdentities())
+            return false;
+
+        // step 3.4 check for duplicate features
+        if (info.containsDuplicateFeatures())
+            return false;
+
+        // step 3.5 check for well-formed packet extensions
+        if (verifyPacketExtensions(info))
+            return false;
+
+        String calculatedVer = generateVerificationString(info, hash);
+
+        if (!ver.equals(calculatedVer))
+            return false;
+
+        return true;
+    }
+
+    /**
+     * 
+     * @param info
+     * @return true if the packet extensions is ill-formed
+     */
+    protected static boolean verifyPacketExtensions(DiscoverInfo info) {
+        List<FormField> foundFormTypes = new LinkedList<FormField>();
+        for (Iterator<PacketExtension> i = info.getExtensions().iterator(); i.hasNext();) {
+            PacketExtension pe = i.next();
+            if (pe.getNamespace().equals(Form.NAMESPACE)) {
+                DataForm df = (DataForm) pe;
+                for (Iterator<FormField> it = df.getFields(); it.hasNext();) {
+                    FormField f = it.next();
+                    if (f.getVariable().equals("FORM_TYPE")) {
+                        for (FormField fft : foundFormTypes) {
+                            if (f.equals(fft))
+                                return true;
+                        }
+                        foundFormTypes.add(f);
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Generates a XEP-115 Verification String
+     * 
+     * @see <a href="http://xmpp.org/extensions/xep-0115.html#ver">XEP-115
+     *      Verification String</a>
+     * 
+     * @param discoverInfo
+     * @param hash
+     *            the used hash function
+     * @return The generated verification String or null if the hash is not
+     *         supported
+     */
+    protected static String generateVerificationString(DiscoverInfo discoverInfo, String hash) {
+        MessageDigest md = SUPPORTED_HASHES.get(hash.toLowerCase());
+        if (md == null)
+            return null;
+
+        DataForm extendedInfo = (DataForm) discoverInfo.getExtension(Form.ELEMENT, Form.NAMESPACE);
+
+        // 1. Initialize an empty string S ('sb' in this method).
+        StringBuilder sb = new StringBuilder(); // Use StringBuilder as we don't
+                                                // need thread-safe StringBuffer
+
+        // 2. Sort the service discovery identities by category and then by
+        // type and then by xml:lang
+        // (if it exists), formatted as CATEGORY '/' [TYPE] '/' [LANG] '/'
+        // [NAME]. Note that each slash is included even if the LANG or
+        // NAME is not included (in accordance with XEP-0030, the category and
+        // type MUST be included.
+        SortedSet<DiscoverInfo.Identity> sortedIdentities = new TreeSet<DiscoverInfo.Identity>();
+
+        for (Iterator<DiscoverInfo.Identity> it = discoverInfo.getIdentities(); it.hasNext();)
+            sortedIdentities.add(it.next());
+
+        // 3. For each identity, append the 'category/type/lang/name' to S,
+        // followed by the '<' character.
+        for (Iterator<DiscoverInfo.Identity> it = sortedIdentities.iterator(); it.hasNext();) {
+            DiscoverInfo.Identity identity = it.next();
+            sb.append(identity.getCategory());
+            sb.append("/");
+            sb.append(identity.getType());
+            sb.append("/");
+            sb.append(identity.getLanguage() == null ? "" : identity.getLanguage());
+            sb.append("/");
+            sb.append(identity.getName() == null ? "" : identity.getName());
+            sb.append("<");
+        }
+
+        // 4. Sort the supported service discovery features.
+        SortedSet<String> features = new TreeSet<String>();
+        for (Iterator<Feature> it = discoverInfo.getFeatures(); it.hasNext();)
+            features.add(it.next().getVar());
+
+        // 5. For each feature, append the feature to S, followed by the '<'
+        // character
+        for (String f : features) {
+            sb.append(f);
+            sb.append("<");
+        }
+
+        // only use the data form for calculation is it has a hidden FORM_TYPE
+        // field
+        // see XEP-0115 5.4 step 3.6
+        if (extendedInfo != null && extendedInfo.hasHiddenFormTypeField()) {
+            synchronized (extendedInfo) {
+                // 6. If the service discovery information response includes
+                // XEP-0128 data forms, sort the forms by the FORM_TYPE (i.e.,
+                // by the XML character data of the <value/> element).
+                SortedSet<FormField> fs = new TreeSet<FormField>(new Comparator<FormField>() {
+                    public int compare(FormField f1, FormField f2) {
+                        return f1.getVariable().compareTo(f2.getVariable());
+                    }
+                });
+
+                FormField ft = null;
+
+                for (Iterator<FormField> i = extendedInfo.getFields(); i.hasNext();) {
+                    FormField f = i.next();
+                    if (!f.getVariable().equals("FORM_TYPE")) {
+                        fs.add(f);
+                    } else {
+                        ft = f;
+                    }
+                }
+
+                // Add FORM_TYPE values
+                if (ft != null) {
+                    formFieldValuesToCaps(ft.getValues(), sb);
+                }
+
+                // 7. 3. For each field other than FORM_TYPE:
+                // 1. Append the value of the "var" attribute, followed by the
+                // '<' character.
+                // 2. Sort values by the XML character data of the <value/>
+                // element.
+                // 3. For each <value/> element, append the XML character data,
+                // followed by the '<' character.
+                for (FormField f : fs) {
+                    sb.append(f.getVariable());
+                    sb.append("<");
+                    formFieldValuesToCaps(f.getValues(), sb);
+                }
+            }
+        }
+        // 8. Ensure that S is encoded according to the UTF-8 encoding (RFC
+        // 3269).
+        // 9. Compute the verification string by hashing S using the algorithm
+        // specified in the 'hash' attribute (e.g., SHA-1 as defined in RFC
+        // 3174).
+        // The hashed data MUST be generated with binary output and
+        // encoded using Base64 as specified in Section 4 of RFC 4648
+        // (note: the Base64 output MUST NOT include whitespace and MUST set
+        // padding bits to zero).
+        byte[] digest = md.digest(sb.toString().getBytes());
+        return Base64.encodeBytes(digest);
+    }
+
+    private static void formFieldValuesToCaps(Iterator<String> i, StringBuilder sb) {
+        SortedSet<String> fvs = new TreeSet<String>();
+        while (i.hasNext()) {
+            fvs.add(i.next());
+        }
+        for (String fv : fvs) {
+            sb.append(fv);
+            sb.append("<");
+        }
+    }
+
+    public static class NodeVerHash {
+        private String node;
+        private String hash;
+        private String ver;
+        private String nodeVer;
+
+        NodeVerHash(String node, String ver, String hash) {
+            this.node = node;
+            this.ver = ver;
+            this.hash = hash;
+            nodeVer = node + "#" + ver;
+        }
+
+        public String getNodeVer() {
+            return nodeVer;
+        }
+
+        public String getNode() {
+            return node;
+        }
+
+        public String getHash() {
+            return hash;
+        }
+
+        public String getVer() {
+            return ver;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java b/src/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java
new file mode 100644
index 0000000..0441043
--- /dev/null
+++ b/src/org/jivesoftware/smackx/entitycaps/cache/EntityCapsPersistentCache.java
@@ -0,0 +1,38 @@
+/**
+ * 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.smackx.entitycaps.cache;
+
+import java.io.IOException;
+
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+
+public interface EntityCapsPersistentCache {
+    /**
+     * Add an DiscoverInfo to the persistent Cache
+     * 
+     * @param node
+     * @param info
+     */
+    void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info);
+
+    /**
+     * Replay the Caches data into EntityCapsManager
+     */
+    void replay() throws IOException;
+
+    /**
+     * Empty the Cache
+     */
+    void emptyCache();
+}
diff --git a/src/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java b/src/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java
new file mode 100644
index 0000000..0312c7e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/entitycaps/cache/SimpleDirectoryPersistentCache.java
@@ -0,0 +1,194 @@
+/**
+ *  Copyright 2011 Florian Schmaus
+ *
+ *  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.smackx.entitycaps.cache;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.Base32Encoder;
+import org.jivesoftware.smack.util.Base64Encoder;
+import org.jivesoftware.smack.util.StringEncoder;
+import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.provider.DiscoverInfoProvider;
+import org.xmlpull.v1.XmlPullParserFactory;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * Simple implementation of an EntityCapsPersistentCache that uses a directory
+ * to store the Caps information for every known node. Every node is represented
+ * by an file.
+ * 
+ * @author Florian Schmaus
+ * 
+ */
+public class SimpleDirectoryPersistentCache implements EntityCapsPersistentCache {
+
+    private File cacheDir;
+    private StringEncoder filenameEncoder;
+
+    /**
+     * Creates a new SimpleDirectoryPersistentCache Object. Make sure that the
+     * cacheDir exists and that it's an directory.
+     * <p>
+     * Default filename encoder {@link Base32Encoder}, as this will work on all 
+     * filesystems, both case sensitive and case insensitive.  It does however 
+     * produce longer filenames.
+     * 
+     * @param cacheDir
+     */
+    public SimpleDirectoryPersistentCache(File cacheDir) {
+        this(cacheDir, Base32Encoder.getInstance());
+    }
+
+    /**
+     * Creates a new SimpleDirectoryPersistentCache Object. Make sure that the
+     * cacheDir exists and that it's an directory.
+     * 
+     * If your cacheDir is case insensitive then make sure to set the
+     * StringEncoder to {@link Base32Encoder} (which is the default).
+     * 
+     * @param cacheDir The directory where the cache will be stored.
+     * @param filenameEncoder Encodes the node string into a filename.
+     */
+    public SimpleDirectoryPersistentCache(File cacheDir, StringEncoder filenameEncoder) {
+        if (!cacheDir.exists())
+            throw new IllegalStateException("Cache directory \"" + cacheDir + "\" does not exist");
+        if (!cacheDir.isDirectory())
+            throw new IllegalStateException("Cache directory \"" + cacheDir + "\" is not a directory");
+
+        this.cacheDir = cacheDir;
+        this.filenameEncoder = filenameEncoder;
+    }
+
+    @Override
+    public void addDiscoverInfoByNodePersistent(String node, DiscoverInfo info) {
+        String filename = filenameEncoder.encode(node);
+        File nodeFile = new File(cacheDir, filename);
+        try {
+            if (nodeFile.createNewFile())
+                writeInfoToFile(nodeFile, info);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void replay() throws IOException {
+        File[] files = cacheDir.listFiles();
+        for (File f : files) {
+            String node = filenameEncoder.decode(f.getName());
+            DiscoverInfo info = restoreInfoFromFile(f);
+            if (info == null)
+                continue;
+
+            EntityCapsManager.addDiscoverInfoByNode(node, info);
+        }
+    }
+
+    public void emptyCache() {
+        File[] files = cacheDir.listFiles();
+        for (File f : files) {
+            f.delete();
+        }
+    }
+
+    /**
+     * Writes the DiscoverInfo packet to an file
+     * 
+     * @param file
+     * @param info
+     * @throws IOException
+     */
+    private static void writeInfoToFile(File file, DiscoverInfo info) throws IOException {
+        DataOutputStream dos = new DataOutputStream(new FileOutputStream(file));
+        try {
+            dos.writeUTF(info.toXML());
+        } finally {
+            dos.close();
+        }
+    }
+
+    /**
+     * Tries to restore an DiscoverInfo packet from a file.
+     * 
+     * @param file
+     * @return
+     * @throws IOException
+     */
+    private static DiscoverInfo restoreInfoFromFile(File file) throws IOException {
+        DataInputStream dis = new DataInputStream(new FileInputStream(file));
+        String fileContent = null;
+        String id;
+        String from;
+        String to;
+
+        try {
+            fileContent = dis.readUTF();
+        } finally {
+            dis.close();
+        }
+        if (fileContent == null)
+            return null;
+
+        Reader reader = new StringReader(fileContent);
+        XmlPullParser parser;
+        try {
+            parser = XmlPullParserFactory.newInstance().newPullParser();
+            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+            parser.setInput(reader);
+        } catch (XmlPullParserException xppe) {
+            xppe.printStackTrace();
+            return null;
+        }
+
+        DiscoverInfo iqPacket;
+        IQProvider provider = new DiscoverInfoProvider();
+
+        // Parse the IQ, we only need the id
+        try {
+            parser.next();
+            id = parser.getAttributeValue("", "id");
+            from = parser.getAttributeValue("", "from");
+            to = parser.getAttributeValue("", "to");
+            parser.next();
+        } catch (XmlPullParserException e1) {
+            return null;
+        }
+
+        try {
+            iqPacket = (DiscoverInfo) provider.parseIQ(parser);
+        } catch (Exception e) {
+            return null;
+        }
+
+        iqPacket.setPacketID(id);
+        iqPacket.setFrom(from);
+        iqPacket.setTo(to);
+        iqPacket.setType(IQ.Type.RESULT);
+        return iqPacket;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java b/src/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java
new file mode 100644
index 0000000..a87c86c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/entitycaps/packet/CapsExtension.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright 2009 Jonas Ådahl.
+ * Copyright 2011-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.smackx.entitycaps.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
+
+public class CapsExtension implements PacketExtension {
+
+    private String node, ver, hash;
+
+    public CapsExtension() {
+    }
+
+    public CapsExtension(String node, String version, String hash) {
+        this.node = node;
+        this.ver = version;
+        this.hash = hash;
+    }
+
+    public String getElementName() {
+        return EntityCapsManager.ELEMENT;
+    }
+
+    public String getNamespace() {
+        return EntityCapsManager.NAMESPACE;
+    }
+
+    public String getNode() {
+        return node;
+    }
+
+    public void setNode(String node) {
+        this.node = node;
+    }
+
+    public String getVer() {
+        return ver;
+    }
+
+    public void setVer(String ver) {
+        this.ver = ver;
+    }
+
+    public String getHash() {
+        return hash;
+    }
+
+    public void setHash(String hash) {
+        this.hash = hash;
+    }
+
+    /*
+     *  <c xmlns='http://jabber.org/protocol/caps'
+     *  hash='sha-1'
+     *  node='http://code.google.com/p/exodus'
+     *  ver='QgayPKawpkPSDYmwT/WM94uAlu0='/>
+     *
+     */
+    public String toXML() {
+        String xml = "<" + EntityCapsManager.ELEMENT + " xmlns=\"" + EntityCapsManager.NAMESPACE + "\" " +
+            "hash=\"" + hash + "\" " +
+            "node=\"" + node + "\" " +
+            "ver=\"" + ver + "\"/>";
+
+        return xml;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java b/src/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java
new file mode 100644
index 0000000..a112cd5
--- /dev/null
+++ b/src/org/jivesoftware/smackx/entitycaps/provider/CapsExtensionProvider.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright 2009 Jonas Ådahl.
+ * Copyright 2011-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.smackx.entitycaps.provider;
+
+import java.io.IOException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
+import org.jivesoftware.smackx.entitycaps.packet.CapsExtension;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public class CapsExtensionProvider implements PacketExtensionProvider {
+
+    public PacketExtension parseExtension(XmlPullParser parser) throws XmlPullParserException, IOException,
+            XMPPException {
+        String hash = null;
+        String version = null;
+        String node = null;
+        if (parser.getEventType() == XmlPullParser.START_TAG
+                && parser.getName().equalsIgnoreCase(EntityCapsManager.ELEMENT)) {
+            hash = parser.getAttributeValue(null, "hash");
+            version = parser.getAttributeValue(null, "ver");
+            node = parser.getAttributeValue(null, "node");
+        } else {
+            throw new XMPPException("Malformed Caps element");
+        }
+
+        parser.next();
+
+        if (!(parser.getEventType() == XmlPullParser.END_TAG
+                && parser.getName().equalsIgnoreCase(EntityCapsManager.ELEMENT))) {
+            throw new XMPPException("Malformed nested Caps element");
+        }
+
+        if (hash != null && version != null && node != null) {
+            return new CapsExtension(node, version, hash);
+        } else {
+            throw new XMPPException("Caps elment with missing attributes");
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java b/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java
new file mode 100644
index 0000000..22b5b1d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/FaultTolerantNegotiator.java
@@ -0,0 +1,186 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 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.smackx.filetransfer;
+
+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.OrFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.*;
+import java.util.List;
+import java.util.ArrayList;
+
+
+/**
+ * The fault tolerant negotiator takes two stream negotiators, the primary and the secondary
+ * negotiator. If the primary negotiator fails during the stream negotiaton process, the second
+ * negotiator is used.
+ */
+public class FaultTolerantNegotiator extends StreamNegotiator {
+
+    private StreamNegotiator primaryNegotiator;
+    private StreamNegotiator secondaryNegotiator;
+    private Connection connection;
+    private PacketFilter primaryFilter;
+    private PacketFilter secondaryFilter;
+
+    public FaultTolerantNegotiator(Connection connection, StreamNegotiator primary,
+            StreamNegotiator secondary) {
+        this.primaryNegotiator = primary;
+        this.secondaryNegotiator = secondary;
+        this.connection = connection;
+    }
+
+    public PacketFilter getInitiationPacketFilter(String from, String streamID) {
+        if (primaryFilter == null || secondaryFilter == null) {
+            primaryFilter = primaryNegotiator.getInitiationPacketFilter(from, streamID);
+            secondaryFilter = secondaryNegotiator.getInitiationPacketFilter(from, streamID);
+        }
+        return new OrFilter(primaryFilter, secondaryFilter);
+    }
+
+    InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException {
+        throw new UnsupportedOperationException("Negotiation only handled by create incoming " +
+                "stream method.");
+    }
+
+    final Packet initiateIncomingStream(Connection connection, StreamInitiation initiation) {
+        throw new UnsupportedOperationException("Initiation handled by createIncomingStream " +
+                "method");
+    }
+
+    public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException {
+        PacketCollector collector = connection.createPacketCollector(
+                getInitiationPacketFilter(initiation.getFrom(), initiation.getSessionID()));
+
+        connection.sendPacket(super.createInitiationAccept(initiation, getNamespaces()));
+
+        ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(2);
+        CompletionService<InputStream> service
+                = new ExecutorCompletionService<InputStream>(threadPoolExecutor);
+        List<Future<InputStream>> futures = new ArrayList<Future<InputStream>>();
+        InputStream stream = null;
+        XMPPException exception = null;
+        try {
+            futures.add(service.submit(new NegotiatorService(collector)));
+            futures.add(service.submit(new NegotiatorService(collector)));
+
+            int i = 0;
+            while (stream == null && i < futures.size()) {
+                Future<InputStream> future;
+                try {
+                    i++;
+                    future = service.poll(10, TimeUnit.SECONDS);
+                }
+                catch (InterruptedException e) {
+                    continue;
+                }
+
+                if (future == null) {
+                    continue;
+                }
+
+                try {
+                    stream = future.get();
+                }
+                catch (InterruptedException e) {
+                    /* Do Nothing */
+                }
+                catch (ExecutionException e) {
+                    exception = new XMPPException(e.getCause());
+                }
+            }
+        }
+        finally {
+            for (Future<InputStream> future : futures) {
+                future.cancel(true);
+            }
+            collector.cancel();
+            threadPoolExecutor.shutdownNow();
+        }
+        if (stream == null) {
+            if (exception != null) {
+                throw exception;
+            }
+            else {
+                throw new XMPPException("File transfer negotiation failed.");
+            }
+        }
+
+        return stream;
+    }
+
+    private StreamNegotiator determineNegotiator(Packet streamInitiation) {
+        return primaryFilter.accept(streamInitiation) ? primaryNegotiator : secondaryNegotiator;
+    }
+
+    public OutputStream createOutgoingStream(String streamID, String initiator, String target)
+            throws XMPPException {
+        OutputStream stream;
+        try {
+            stream = primaryNegotiator.createOutgoingStream(streamID, initiator, target);
+        }
+        catch (XMPPException ex) {
+            stream = secondaryNegotiator.createOutgoingStream(streamID, initiator, target);
+        }
+
+        return stream;
+    }
+
+    public String[] getNamespaces() {
+        String[] primary = primaryNegotiator.getNamespaces();
+        String[] secondary = secondaryNegotiator.getNamespaces();
+
+        String[] namespaces = new String[primary.length + secondary.length];
+        System.arraycopy(primary, 0, namespaces, 0, primary.length);
+        System.arraycopy(secondary, 0, namespaces, primary.length, secondary.length);
+
+        return namespaces;
+    }
+
+    public void cleanup() {
+    }
+
+    private class NegotiatorService implements Callable<InputStream> {
+
+        private PacketCollector collector;
+
+        NegotiatorService(PacketCollector collector) {
+            this.collector = collector;
+        }
+
+        public InputStream call() throws Exception {
+            Packet streamInitiation = collector.nextResult(
+                    SmackConfiguration.getPacketReplyTimeout() * 2);
+            if (streamInitiation == null) {
+                throw new XMPPException("No response from remote client");
+            }
+            StreamNegotiator negotiator = determineNegotiator(streamInitiation);
+            return negotiator.negotiateIncomingStream(streamInitiation);
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java b/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java
new file mode 100644
index 0000000..b840fd5
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/FileTransfer.java
@@ -0,0 +1,380 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.filetransfer;

+

+import org.jivesoftware.smack.XMPPException;

+

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.OutputStream;

+

+/**

+ * Contains the generic file information and progress related to a particular

+ * file transfer.

+ *

+ * @author Alexander Wenckus

+ *

+ */

+public abstract class FileTransfer {

+

+	private String fileName;

+

+	private String filePath;

+

+	private long fileSize;

+

+	private String peer;

+

+	private Status status = Status.initial;

+

+    private final Object statusMonitor = new Object();

+

+	protected FileTransferNegotiator negotiator;

+

+	protected String streamID;

+

+	protected long amountWritten = -1;

+

+	private Error error;

+

+	private Exception exception;

+

+    /**

+     * Buffer size between input and output

+     */

+    private static final int BUFFER_SIZE = 8192;

+

+    protected FileTransfer(String peer, String streamID,

+			FileTransferNegotiator negotiator) {

+		this.peer = peer;

+		this.streamID = streamID;

+		this.negotiator = negotiator;

+	}

+

+	protected void setFileInfo(String fileName, long fileSize) {

+		this.fileName = fileName;

+		this.fileSize = fileSize;

+	}

+

+	protected void setFileInfo(String path, String fileName, long fileSize) {

+		this.filePath = path;

+		this.fileName = fileName;

+		this.fileSize = fileSize;

+	}

+

+	/**

+	 * Returns the size of the file being transfered.

+	 *

+	 * @return Returns the size of the file being transfered.

+	 */

+	public long getFileSize() {

+		return fileSize;

+	}

+

+	/**

+	 * Returns the name of the file being transfered.

+	 *

+	 * @return Returns the name of the file being transfered.

+	 */

+	public String getFileName() {

+		return fileName;

+	}

+

+	/**

+	 * Returns the local path of the file.

+	 *

+	 * @return Returns the local path of the file.

+	 */

+	public String getFilePath() {

+		return filePath;

+	}

+

+	/**

+	 * Returns the JID of the peer for this file transfer.

+	 *

+	 * @return Returns the JID of the peer for this file transfer.

+	 */

+	public String getPeer() {

+		return peer;

+	}

+

+	/**

+	 * Returns the progress of the file transfer as a number between 0 and 1.

+	 *

+	 * @return Returns the progress of the file transfer as a number between 0

+	 *         and 1.

+	 */

+	public double getProgress() {

+        if (amountWritten <= 0 || fileSize <= 0) {

+            return 0;

+        }

+        return (double) amountWritten / (double) fileSize;

+	}

+

+	/**

+	 * Returns true if the transfer has been cancelled, if it has stopped because

+	 * of a an error, or the transfer completed successfully.

+	 *

+	 * @return Returns true if the transfer has been cancelled, if it has stopped

+	 *         because of a an error, or the transfer completed successfully.

+	 */

+	public boolean isDone() {

+		return status == Status.cancelled || status == Status.error

+				|| status == Status.complete || status == Status.refused;

+	}

+

+	/**

+	 * Returns the current status of the file transfer.

+	 *

+	 * @return Returns the current status of the file transfer.

+	 */

+	public Status getStatus() {

+		return status;

+	}

+

+	protected void setError(Error type) {

+		this.error = type;

+	}

+

+	/**

+	 * When {@link #getStatus()} returns that there was an {@link Status#error}

+	 * during the transfer, the type of error can be retrieved through this

+	 * method.

+	 *

+	 * @return Returns the type of error that occurred if one has occurred.

+	 */

+	public Error getError() {

+		return error;

+	}

+

+	/**

+	 * If an exception occurs asynchronously it will be stored for later

+	 * retrieval. If there is an error there maybe an exception set.

+	 *

+	 * @return The exception that occurred or null if there was no exception.

+	 * @see #getError()

+	 */

+	public Exception getException() {

+		return exception;

+	}

+

+    public String getStreamID() {

+        return streamID;

+    }

+

+	/**

+	 * Cancels the file transfer.

+	 */

+	public abstract void cancel();

+

+	protected void setException(Exception exception) {

+		this.exception = exception;

+	}

+

+	protected void setStatus(Status status) {

+        synchronized (statusMonitor) {

+		    this.status = status;

+	    }

+    }

+

+    protected boolean updateStatus(Status oldStatus, Status newStatus) {

+        synchronized (statusMonitor) {

+            if (oldStatus != status) {

+                return false;

+            }

+            status = newStatus;

+            return true;

+        }

+    }

+

+	protected void writeToStream(final InputStream in, final OutputStream out)

+			throws XMPPException

+    {

+		final byte[] b = new byte[BUFFER_SIZE];

+		int count = 0;

+		amountWritten = 0;

+

+        do {

+			// write to the output stream

+			try {

+				out.write(b, 0, count);

+			} catch (IOException e) {

+				throw new XMPPException("error writing to output stream", e);

+			}

+

+			amountWritten += count;

+

+			// read more bytes from the input stream

+			try {

+				count = in.read(b);

+			} catch (IOException e) {

+				throw new XMPPException("error reading from input stream", e);

+			}

+		} while (count != -1 && !getStatus().equals(Status.cancelled));

+

+		// the connection was likely terminated abrubtly if these are not equal

+		if (!getStatus().equals(Status.cancelled) && getError() == Error.none

+				&& amountWritten != fileSize) {

+            setStatus(Status.error);

+			this.error = Error.connection;

+		}

+	}

+

+	/**

+	 * A class to represent the current status of the file transfer.

+	 *

+	 * @author Alexander Wenckus

+	 *

+	 */

+	public enum Status {

+

+		/**

+		 * An error occurred during the transfer.

+		 *

+		 * @see FileTransfer#getError()

+		 */

+		error("Error"),

+

+		/**

+         * The initial status of the file transfer.

+         */

+        initial("Initial"),

+

+        /**

+		 * The file transfer is being negotiated with the peer. The party

+		 * Receiving the file has the option to accept or refuse a file transfer

+		 * request. If they accept, then the process of stream negotiation will

+		 * begin. If they refuse the file will not be transfered.

+		 *

+		 * @see #negotiating_stream

+		 */

+		negotiating_transfer("Negotiating Transfer"),

+

+		/**

+		 * The peer has refused the file transfer request halting the file

+		 * transfer negotiation process.

+		 */

+		refused("Refused"),

+

+		/**

+		 * The stream to transfer the file is being negotiated over the chosen

+		 * stream type. After the stream negotiating process is complete the

+		 * status becomes negotiated.

+		 *

+		 * @see #negotiated

+		 */

+		negotiating_stream("Negotiating Stream"),

+

+		/**

+		 * After the stream negotiation has completed the intermediate state

+		 * between the time when the negotiation is finished and the actual

+		 * transfer begins.

+		 */

+		negotiated("Negotiated"),

+

+		/**

+		 * The transfer is in progress.

+		 *

+		 * @see FileTransfer#getProgress()

+		 */

+		in_progress("In Progress"),

+

+		/**

+		 * The transfer has completed successfully.

+		 */

+		complete("Complete"),

+

+		/**

+		 * The file transfer was cancelled

+		 */

+		cancelled("Cancelled");

+

+        private String status;

+

+        private Status(String status) {

+            this.status = status;

+        }

+

+        public String toString() {

+            return status;

+        }

+    }

+

+    /**

+     * Return the length of bytes written out to the stream.

+     * @return the amount in bytes written out.

+     */

+    public long getAmountWritten(){

+        return amountWritten;

+    }

+

+    public enum Error {

+		/**

+		 * No error

+		 */

+		none("No error"),

+

+		/**

+		 * The peer did not find any of the provided stream mechanisms

+		 * acceptable.

+		 */

+		not_acceptable("The peer did not find any of the provided stream mechanisms acceptable."),

+

+		/**

+		 * The provided file to transfer does not exist or could not be read.

+		 */

+		bad_file("The provided file to transfer does not exist or could not be read."),

+

+		/**

+		 * The remote user did not respond or the connection timed out.

+		 */

+		no_response("The remote user did not respond or the connection timed out."),

+

+		/**

+		 * An error occurred over the socket connected to send the file.

+		 */

+		connection("An error occured over the socket connected to send the file."),

+

+		/**

+		 * An error occurred while sending or receiving the file

+		 */

+		stream("An error occured while sending or recieving the file.");

+

+		private final String msg;

+

+		private Error(String msg) {

+			this.msg = msg;

+		}

+

+		/**

+		 * Returns a String representation of this error.

+		 *

+		 * @return Returns a String representation of this error.

+		 */

+		public String getMessage() {

+			return msg;

+		}

+

+		public String toString() {

+			return msg;

+		}

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java b/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java
new file mode 100644
index 0000000..8e07543
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/FileTransferListener.java
@@ -0,0 +1,36 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.filetransfer;

+

+/**

+ * File transfers can cause several events to be raised. These events can be

+ * monitored through this interface.

+ * 

+ * @author Alexander Wenckus

+ */

+public interface FileTransferListener {

+	/**

+	 * A request to send a file has been recieved from another user.

+	 * 

+	 * @param request

+	 *            The request from the other user.

+	 */

+	public void fileTransferRequest(final FileTransferRequest request);

+}

diff --git a/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java b/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java
new file mode 100644
index 0000000..6e413fa
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/FileTransferManager.java
@@ -0,0 +1,182 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.filetransfer;

+

+import org.jivesoftware.smack.PacketListener;

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.filter.AndFilter;

+import org.jivesoftware.smack.filter.IQTypeFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.XMPPError;

+import org.jivesoftware.smack.util.StringUtils;

+import org.jivesoftware.smackx.packet.StreamInitiation;

+

+import java.util.ArrayList;

+import java.util.List;

+

+/**

+ * The file transfer manager class handles the sending and recieving of files.

+ * To send a file invoke the {@link #createOutgoingFileTransfer(String)} method.

+ * <p>

+ * And to recieve a file add a file transfer listener to the manager. The

+ * listener will notify you when there is a new file transfer request. To create

+ * the {@link IncomingFileTransfer} object accept the transfer, or, if the

+ * transfer is not desirable reject it.

+ * 

+ * @author Alexander Wenckus

+ * 

+ */

+public class FileTransferManager {

+

+	private final FileTransferNegotiator fileTransferNegotiator;

+

+	private List<FileTransferListener> listeners;

+

+	private Connection connection;

+

+	/**

+	 * Creates a file transfer manager to initiate and receive file transfers.

+	 * 

+	 * @param connection

+	 *            The Connection that the file transfers will use.

+	 */

+	public FileTransferManager(Connection connection) {

+		this.connection = connection;

+		this.fileTransferNegotiator = FileTransferNegotiator

+				.getInstanceFor(connection);

+	}

+

+	/**

+	 * Add a file transfer listener to listen to incoming file transfer

+	 * requests.

+	 * 

+	 * @param li

+	 *            The listener

+	 * @see #removeFileTransferListener(FileTransferListener)

+	 * @see FileTransferListener

+	 */

+	public void addFileTransferListener(final FileTransferListener li) {

+		if (listeners == null) {

+			initListeners();

+		}

+		synchronized (this.listeners) {

+			listeners.add(li);

+		}

+	}

+

+	private void initListeners() {

+		listeners = new ArrayList<FileTransferListener>();

+

+		connection.addPacketListener(new PacketListener() {

+			public void processPacket(Packet packet) {

+				fireNewRequest((StreamInitiation) packet);

+			}

+		}, new AndFilter(new PacketTypeFilter(StreamInitiation.class),

+				new IQTypeFilter(IQ.Type.SET)));

+	}

+

+	protected void fireNewRequest(StreamInitiation initiation) {

+		FileTransferListener[] listeners = null;

+		synchronized (this.listeners) {

+			listeners = new FileTransferListener[this.listeners.size()];

+			this.listeners.toArray(listeners);

+		}

+		FileTransferRequest request = new FileTransferRequest(this, initiation);

+		for (int i = 0; i < listeners.length; i++) {

+			listeners[i].fileTransferRequest(request);

+		}

+	}

+

+	/**

+	 * Removes a file transfer listener.

+	 * 

+	 * @param li

+	 *            The file transfer listener to be removed

+	 * @see FileTransferListener

+	 */

+	public void removeFileTransferListener(final FileTransferListener li) {

+		if (listeners == null) {

+			return;

+		}

+		synchronized (this.listeners) {

+			listeners.remove(li);

+		}

+	}

+

+	/**

+	 * Creates an OutgoingFileTransfer to send a file to another user.

+	 * 

+	 * @param userID

+	 *            The fully qualified jabber ID (i.e. full JID) with resource of the user to

+	 *            send the file to.

+	 * @return The send file object on which the negotiated transfer can be run.

+	 * @exception IllegalArgumentException if userID is null or not a full JID

+	 */

+	public OutgoingFileTransfer createOutgoingFileTransfer(String userID) {

+        if (userID == null) {

+            throw new IllegalArgumentException("userID was null");

+        }

+        // We need to create outgoing file transfers with a full JID since this method will later

+        // use XEP-0095 to negotiate the stream. This is done with IQ stanzas that need to be addressed to a full JID

+        // in order to reach an client entity.

+        else if (!StringUtils.isFullJID(userID)) {

+            throw new IllegalArgumentException("The provided user id was not a full JID (i.e. with resource part)");

+        }

+

+		return new OutgoingFileTransfer(connection.getUser(), userID,

+				fileTransferNegotiator.getNextStreamID(),

+				fileTransferNegotiator);

+	}

+

+	/**

+	 * When the file transfer request is acceptable, this method should be

+	 * invoked. It will create an IncomingFileTransfer which allows the

+	 * transmission of the file to procede.

+	 * 

+	 * @param request

+	 *            The remote request that is being accepted.

+	 * @return The IncomingFileTransfer which manages the download of the file

+	 *         from the transfer initiator.

+	 */

+	protected IncomingFileTransfer createIncomingFileTransfer(

+			FileTransferRequest request) {

+		if (request == null) {

+			throw new NullPointerException("RecieveRequest cannot be null");

+		}

+

+		IncomingFileTransfer transfer = new IncomingFileTransfer(request,

+                fileTransferNegotiator);

+		transfer.setFileInfo(request.getFileName(), request.getFileSize());

+

+		return transfer;

+	}

+

+	protected void rejectIncomingFileTransfer(FileTransferRequest request) {

+		StreamInitiation initiation = request.getStreamInitiation();

+

+		IQ rejection = FileTransferNegotiator.createIQ(

+				initiation.getPacketID(), initiation.getFrom(), initiation

+						.getTo(), IQ.Type.ERROR);

+		rejection.setError(new XMPPError(XMPPError.Condition.no_acceptable));

+		connection.sendPacket(rejection);

+	}

+}

diff --git a/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java b/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java
new file mode 100644
index 0000000..d1fb7bf
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/FileTransferNegotiator.java
@@ -0,0 +1,485 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.filetransfer;

+

+import java.net.URLConnection;

+import java.util.ArrayList;

+import java.util.Arrays;

+import java.util.Collection;

+import java.util.Collections;

+import java.util.Iterator;

+import java.util.List;

+import java.util.Map;

+import java.util.Random;

+import java.util.concurrent.ConcurrentHashMap;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.ConnectionListener;

+import org.jivesoftware.smack.PacketCollector;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.filter.PacketIDFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.XMPPError;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.FormField;

+import org.jivesoftware.smackx.ServiceDiscoveryManager;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;

+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;

+import org.jivesoftware.smackx.packet.DataForm;

+import org.jivesoftware.smackx.packet.StreamInitiation;

+

+/**

+ * Manages the negotiation of file transfers according to JEP-0096. If a file is

+ * being sent the remote user chooses the type of stream under which the file

+ * will be sent.

+ *

+ * @author Alexander Wenckus

+ * @see <a href="http://xmpp.org/extensions/xep-0096.html">XEP-0096: SI File Transfer</a>

+ */

+public class FileTransferNegotiator {

+

+    // Static

+

+    private static final String[] NAMESPACE = {

+            "http://jabber.org/protocol/si/profile/file-transfer",

+            "http://jabber.org/protocol/si"};

+

+    private static final Map<Connection, FileTransferNegotiator> transferObject =

+            new ConcurrentHashMap<Connection, FileTransferNegotiator>();

+

+    private static final String STREAM_INIT_PREFIX = "jsi_";

+

+    protected static final String STREAM_DATA_FIELD_NAME = "stream-method";

+

+    private static final Random randomGenerator = new Random();

+

+    /**

+     * A static variable to use only offer IBB for file transfer. It is generally recommend to only

+     * set this variable to true for testing purposes as IBB is the backup file transfer method

+     * and shouldn't be used as the only transfer method in production systems.

+     */

+    public static boolean IBB_ONLY = (System.getProperty("ibb") != null);//true;

+

+    /**

+     * Returns the file transfer negotiator related to a particular connection.

+     * When this class is requested on a particular connection the file transfer

+     * service is automatically enabled.

+     *

+     * @param connection The connection for which the transfer manager is desired

+     * @return The IMFileTransferManager

+     */

+    public static FileTransferNegotiator getInstanceFor(

+            final Connection connection) {

+        if (connection == null) {

+            throw new IllegalArgumentException("Connection cannot be null");

+        }

+        if (!connection.isConnected()) {

+            return null;

+        }

+

+        if (transferObject.containsKey(connection)) {

+            return transferObject.get(connection);

+        }

+        else {

+            FileTransferNegotiator transfer = new FileTransferNegotiator(

+                    connection);

+            setServiceEnabled(connection, true);

+            transferObject.put(connection, transfer);

+            return transfer;

+        }

+    }

+

+    /**

+     * Enable the Jabber services related to file transfer on the particular

+     * connection.

+     *

+     * @param connection The connection on which to enable or disable the services.

+     * @param isEnabled  True to enable, false to disable.

+     */

+    public static void setServiceEnabled(final Connection connection,

+            final boolean isEnabled) {

+        ServiceDiscoveryManager manager = ServiceDiscoveryManager

+                .getInstanceFor(connection);

+

+        List<String> namespaces = new ArrayList<String>();

+        namespaces.addAll(Arrays.asList(NAMESPACE));

+        namespaces.add(InBandBytestreamManager.NAMESPACE);

+        if (!IBB_ONLY) {

+            namespaces.add(Socks5BytestreamManager.NAMESPACE);

+        }

+

+        for (String namespace : namespaces) {

+            if (isEnabled) {

+                if (!manager.includesFeature(namespace)) {

+                    manager.addFeature(namespace);

+                }

+            } else {

+                manager.removeFeature(namespace);

+            }

+        }

+        

+    }

+

+    /**

+     * Checks to see if all file transfer related services are enabled on the

+     * connection.

+     *

+     * @param connection The connection to check

+     * @return True if all related services are enabled, false if they are not.

+     */

+    public static boolean isServiceEnabled(final Connection connection) {

+        ServiceDiscoveryManager manager = ServiceDiscoveryManager

+                .getInstanceFor(connection);

+

+        List<String> namespaces = new ArrayList<String>();

+        namespaces.addAll(Arrays.asList(NAMESPACE));

+        namespaces.add(InBandBytestreamManager.NAMESPACE);

+        if (!IBB_ONLY) {

+            namespaces.add(Socks5BytestreamManager.NAMESPACE);

+        }

+

+        for (String namespace : namespaces) {

+            if (!manager.includesFeature(namespace)) {

+                return false;

+            }

+        }

+        return true;

+    }

+

+    /**

+     * A convenience method to create an IQ packet.

+     *

+     * @param ID   The packet ID of the

+     * @param to   To whom the packet is addressed.

+     * @param from From whom the packet is sent.

+     * @param type The IQ type of the packet.

+     * @return The created IQ packet.

+     */

+    public static IQ createIQ(final String ID, final String to,

+            final String from, final IQ.Type type) {

+        IQ iqPacket = new IQ() {

+            public String getChildElementXML() {

+                return null;

+            }

+        };

+        iqPacket.setPacketID(ID);

+        iqPacket.setTo(to);

+        iqPacket.setFrom(from);

+        iqPacket.setType(type);

+

+        return iqPacket;

+    }

+

+    /**

+     * Returns a collection of the supported transfer protocols.

+     *

+     * @return Returns a collection of the supported transfer protocols.

+     */

+    public static Collection<String> getSupportedProtocols() {

+        List<String> protocols = new ArrayList<String>();

+        protocols.add(InBandBytestreamManager.NAMESPACE);

+        if (!IBB_ONLY) {

+            protocols.add(Socks5BytestreamManager.NAMESPACE);

+        }

+        return Collections.unmodifiableList(protocols);

+    }

+

+    // non-static

+

+    private final Connection connection;

+

+    private final StreamNegotiator byteStreamTransferManager;

+

+    private final StreamNegotiator inbandTransferManager;

+

+    private FileTransferNegotiator(final Connection connection) {

+        configureConnection(connection);

+

+        this.connection = connection;

+        byteStreamTransferManager = new Socks5TransferNegotiator(connection);

+        inbandTransferManager = new IBBTransferNegotiator(connection);

+    }

+

+    private void configureConnection(final Connection connection) {

+        connection.addConnectionListener(new ConnectionListener() {

+            public void connectionClosed() {

+                cleanup(connection);

+            }

+

+            public void connectionClosedOnError(Exception e) {

+                cleanup(connection);

+            }

+

+            public void reconnectionFailed(Exception e) {

+                // ignore

+            }

+

+            public void reconnectionSuccessful() {

+                // ignore

+            }

+

+            public void reconnectingIn(int seconds) {

+                // ignore

+            }

+        });

+    }

+

+    private void cleanup(final Connection connection) {

+        if (transferObject.remove(connection) != null) {

+            inbandTransferManager.cleanup();

+        }

+    }

+

+    /**

+     * Selects an appropriate stream negotiator after examining the incoming file transfer request.

+     *

+     * @param request The related file transfer request.

+     * @return The file transfer object that handles the transfer

+     * @throws XMPPException If there are either no stream methods contained in the packet, or

+     *                       there is not an appropriate stream method.

+     */

+    public StreamNegotiator selectStreamNegotiator(

+            FileTransferRequest request) throws XMPPException {

+        StreamInitiation si = request.getStreamInitiation();

+        FormField streamMethodField = getStreamMethodField(si

+                .getFeatureNegotiationForm());

+

+        if (streamMethodField == null) {

+            String errorMessage = "No stream methods contained in packet.";

+            XMPPError error = new XMPPError(XMPPError.Condition.bad_request, errorMessage);

+            IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),

+                    IQ.Type.ERROR);

+            iqPacket.setError(error);

+            connection.sendPacket(iqPacket);

+            throw new XMPPException(errorMessage, error);

+        }

+

+        // select the appropriate protocol

+

+        StreamNegotiator selectedStreamNegotiator;

+        try {

+            selectedStreamNegotiator = getNegotiator(streamMethodField);

+        }

+        catch (XMPPException e) {

+            IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),

+                    IQ.Type.ERROR);

+            iqPacket.setError(e.getXMPPError());

+            connection.sendPacket(iqPacket);

+            throw e;

+        }

+

+        // return the appropriate negotiator

+

+        return selectedStreamNegotiator;

+    }

+

+    private FormField getStreamMethodField(DataForm form) {

+        FormField field = null;

+        for (Iterator<FormField> it = form.getFields(); it.hasNext();) {

+            field = it.next();

+            if (field.getVariable().equals(STREAM_DATA_FIELD_NAME)) {

+                break;

+            }

+            field = null;

+        }

+        return field;

+    }

+

+    private StreamNegotiator getNegotiator(final FormField field)

+            throws XMPPException {

+        String variable;

+        boolean isByteStream = false;

+        boolean isIBB = false;

+        for (Iterator<FormField.Option> it = field.getOptions(); it.hasNext();) {

+            variable = it.next().getValue();

+            if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {

+                isByteStream = true;

+            }

+            else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {

+                isIBB = true;

+            }

+        }

+

+        if (!isByteStream && !isIBB) {

+            XMPPError error = new XMPPError(XMPPError.Condition.bad_request,

+                    "No acceptable transfer mechanism");

+            throw new XMPPException(error.getMessage(), error);

+        }

+

+       //if (isByteStream && isIBB && field.getType().equals(FormField.TYPE_LIST_MULTI)) {

+        if (isByteStream && isIBB) { 

+            return new FaultTolerantNegotiator(connection,

+                    byteStreamTransferManager,

+                    inbandTransferManager);

+        }

+        else if (isByteStream) {

+            return byteStreamTransferManager;

+        }

+        else {

+            return inbandTransferManager;

+        }

+    }

+

+    /**

+     * Reject a stream initiation request from a remote user.

+     *

+     * @param si The Stream Initiation request to reject.

+     */

+    public void rejectStream(final StreamInitiation si) {

+        XMPPError error = new XMPPError(XMPPError.Condition.forbidden, "Offer Declined");

+        IQ iqPacket = createIQ(si.getPacketID(), si.getFrom(), si.getTo(),

+                IQ.Type.ERROR);

+        iqPacket.setError(error);

+        connection.sendPacket(iqPacket);

+    }

+

+    /**

+     * Returns a new, unique, stream ID to identify a file transfer.

+     *

+     * @return Returns a new, unique, stream ID to identify a file transfer.

+     */

+    public String getNextStreamID() {

+        StringBuilder buffer = new StringBuilder();

+        buffer.append(STREAM_INIT_PREFIX);

+        buffer.append(Math.abs(randomGenerator.nextLong()));

+

+        return buffer.toString();

+    }

+

+    /**

+     * Send a request to another user to send them a file. The other user has

+     * the option of, accepting, rejecting, or not responding to a received file

+     * transfer request.

+     * <p/>

+     * If they accept, the packet will contain the other user's chosen stream

+     * type to send the file across. The two choices this implementation

+     * provides to the other user for file transfer are <a

+     * href="http://www.jabber.org/jeps/jep-0065.html">SOCKS5 Bytestreams</a>,

+     * which is the preferred method of transfer, and <a

+     * href="http://www.jabber.org/jeps/jep-0047.html">In-Band Bytestreams</a>,

+     * which is the fallback mechanism.

+     * <p/>

+     * The other user may choose to decline the file request if they do not

+     * desire the file, their client does not support JEP-0096, or if there are

+     * no acceptable means to transfer the file.

+     * <p/>

+     * Finally, if the other user does not respond this method will return null

+     * after the specified timeout.

+     *

+     * @param userID          The userID of the user to whom the file will be sent.

+     * @param streamID        The unique identifier for this file transfer.

+     * @param fileName        The name of this file. Preferably it should include an

+     *                        extension as it is used to determine what type of file it is.

+     * @param size            The size, in bytes, of the file.

+     * @param desc            A description of the file.

+     * @param responseTimeout The amount of time, in milliseconds, to wait for the remote

+     *                        user to respond. If they do not respond in time, this

+     * @return Returns the stream negotiator selected by the peer.

+     * @throws XMPPException Thrown if there is an error negotiating the file transfer.

+     */

+    public StreamNegotiator negotiateOutgoingTransfer(final String userID,

+            final String streamID, final String fileName, final long size,

+            final String desc, int responseTimeout) throws XMPPException {

+        StreamInitiation si = new StreamInitiation();

+        si.setSesssionID(streamID);

+        si.setMimeType(URLConnection.guessContentTypeFromName(fileName));

+

+        StreamInitiation.File siFile = new StreamInitiation.File(fileName, size);

+        siFile.setDesc(desc);

+        si.setFile(siFile);

+

+        si.setFeatureNegotiationForm(createDefaultInitiationForm());

+

+        si.setFrom(connection.getUser());

+        si.setTo(userID);

+        si.setType(IQ.Type.SET);

+

+        PacketCollector collector = connection

+                .createPacketCollector(new PacketIDFilter(si.getPacketID()));

+        connection.sendPacket(si);

+        Packet siResponse = collector.nextResult(responseTimeout);

+        collector.cancel();

+

+        if (siResponse instanceof IQ) {

+            IQ iqResponse = (IQ) siResponse;

+            if (iqResponse.getType().equals(IQ.Type.RESULT)) {

+                StreamInitiation response = (StreamInitiation) siResponse;

+                return getOutgoingNegotiator(getStreamMethodField(response

+                        .getFeatureNegotiationForm()));

+

+            }

+            else if (iqResponse.getType().equals(IQ.Type.ERROR)) {

+                throw new XMPPException(iqResponse.getError());

+            }

+            else {

+                throw new XMPPException("File transfer response unreadable");

+            }

+        }

+        else {

+            return null;

+        }

+    }

+

+    private StreamNegotiator getOutgoingNegotiator(final FormField field)

+            throws XMPPException {

+        String variable;

+        boolean isByteStream = false;

+        boolean isIBB = false;

+        for (Iterator<String> it = field.getValues(); it.hasNext();) {

+            variable = it.next();

+            if (variable.equals(Socks5BytestreamManager.NAMESPACE) && !IBB_ONLY) {

+                isByteStream = true;

+            }

+            else if (variable.equals(InBandBytestreamManager.NAMESPACE)) {

+                isIBB = true;

+            }

+        }

+

+        if (!isByteStream && !isIBB) {

+            XMPPError error = new XMPPError(XMPPError.Condition.bad_request,

+                    "No acceptable transfer mechanism");

+            throw new XMPPException(error.getMessage(), error);

+        }

+

+        if (isByteStream && isIBB) {

+            return new FaultTolerantNegotiator(connection,

+                    byteStreamTransferManager, inbandTransferManager);

+        }

+        else if (isByteStream) {

+            return byteStreamTransferManager;

+        }

+        else {

+            return inbandTransferManager;

+        }

+    }

+

+    private DataForm createDefaultInitiationForm() {

+        DataForm form = new DataForm(Form.TYPE_FORM);

+        FormField field = new FormField(STREAM_DATA_FIELD_NAME);

+        field.setType(FormField.TYPE_LIST_SINGLE);

+        if (!IBB_ONLY) {

+            field.addOption(new FormField.Option(Socks5BytestreamManager.NAMESPACE));

+        }

+        field.addOption(new FormField.Option(InBandBytestreamManager.NAMESPACE));

+        form.addField(field);

+        return form;

+    }

+}

diff --git a/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java b/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java
new file mode 100644
index 0000000..6b5ccd8
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/FileTransferRequest.java
@@ -0,0 +1,138 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.filetransfer;

+

+import org.jivesoftware.smackx.packet.StreamInitiation;

+

+/**

+ * A request to send a file recieved from another user.

+ * 

+ * @author Alexander Wenckus

+ * 

+ */

+public class FileTransferRequest {

+	private final StreamInitiation streamInitiation;

+

+	private final FileTransferManager manager;

+

+	/**

+	 * A recieve request is constructed from the Stream Initiation request

+	 * received from the initator.

+	 * 

+	 * @param manager

+	 *            The manager handling this file transfer

+	 * 

+	 * @param si

+	 *            The Stream initiaton recieved from the initiator.

+	 */

+	public FileTransferRequest(FileTransferManager manager, StreamInitiation si) {

+		this.streamInitiation = si;

+		this.manager = manager;

+	}

+

+	/**

+	 * Returns the name of the file.

+	 * 

+	 * @return Returns the name of the file.

+	 */

+	public String getFileName() {

+		return streamInitiation.getFile().getName();

+	}

+

+	/**

+	 * Returns the size in bytes of the file.

+	 * 

+	 * @return Returns the size in bytes of the file.

+	 */

+	public long getFileSize() {

+		return streamInitiation.getFile().getSize();

+	}

+

+	/**

+	 * Returns the description of the file provided by the requestor.

+	 * 

+	 * @return Returns the description of the file provided by the requestor.

+	 */

+	public String getDescription() {

+		return streamInitiation.getFile().getDesc();

+	}

+

+	/**

+	 * Returns the mime-type of the file.

+	 * 

+	 * @return Returns the mime-type of the file.

+	 */

+	public String getMimeType() {

+		return streamInitiation.getMimeType();

+	}

+

+	/**

+	 * Returns the fully-qualified jabber ID of the user that requested this

+	 * file transfer.

+	 * 

+	 * @return Returns the fully-qualified jabber ID of the user that requested

+	 *         this file transfer.

+	 */

+	public String getRequestor() {

+		return streamInitiation.getFrom();

+	}

+

+	/**

+	 * Returns the stream ID that uniquely identifies this file transfer.

+	 * 

+	 * @return Returns the stream ID that uniquely identifies this file

+	 *         transfer.

+	 */

+	public String getStreamID() {

+		return streamInitiation.getSessionID();

+	}

+

+	/**

+	 * Returns the stream initiation packet that was sent by the requestor which

+	 * contains the parameters of the file transfer being transfer and also the

+	 * methods available to transfer the file.

+	 * 

+	 * @return Returns the stream initiation packet that was sent by the

+	 *         requestor which contains the parameters of the file transfer

+	 *         being transfer and also the methods available to transfer the

+	 *         file.

+	 */

+	protected StreamInitiation getStreamInitiation() {

+		return streamInitiation;

+	}

+

+	/**

+	 * Accepts this file transfer and creates the incoming file transfer.

+	 * 

+	 * @return Returns the <b><i>IncomingFileTransfer</b></i> on which the

+	 *         file transfer can be carried out.

+	 */

+	public IncomingFileTransfer accept() {

+		return manager.createIncomingFileTransfer(this);

+	}

+

+	/**

+	 * Rejects the file transfer request.

+	 */

+	public void reject() {

+		manager.rejectIncomingFileTransfer(this);

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java b/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java
new file mode 100644
index 0000000..b32f49a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/IBBTransferNegotiator.java
@@ -0,0 +1,152 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.filetransfer;

+

+import java.io.InputStream;

+import java.io.OutputStream;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.filter.AndFilter;

+import org.jivesoftware.smack.filter.FromContainsFilter;

+import org.jivesoftware.smack.filter.PacketFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamManager;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamRequest;

+import org.jivesoftware.smackx.bytestreams.ibb.InBandBytestreamSession;

+import org.jivesoftware.smackx.bytestreams.ibb.packet.Open;

+import org.jivesoftware.smackx.packet.StreamInitiation;

+

+/**

+ * The In-Band Bytestream file transfer method, or IBB for short, transfers the

+ * file over the same XML Stream used by XMPP. It is the fall-back mechanism in

+ * case the SOCKS5 bytestream method of transferring files is not available.

+ * 

+ * @author Alexander Wenckus

+ * @author Henning Staib

+ * @see <a href="http://xmpp.org/extensions/xep-0047.html">XEP-0047: In-Band

+ *      Bytestreams (IBB)</a>

+ */

+public class IBBTransferNegotiator extends StreamNegotiator {

+

+    private Connection connection;

+

+    private InBandBytestreamManager manager;

+

+    /**

+     * The default constructor for the In-Band Bytestream Negotiator.

+     * 

+     * @param connection The connection which this negotiator works on.

+     */

+    protected IBBTransferNegotiator(Connection connection) {

+        this.connection = connection;

+        this.manager = InBandBytestreamManager.getByteStreamManager(connection);

+    }

+

+    public OutputStream createOutgoingStream(String streamID, String initiator,

+                    String target) throws XMPPException {

+        InBandBytestreamSession session = this.manager.establishSession(target, streamID);

+        session.setCloseBothStreamsEnabled(true);

+        return session.getOutputStream();

+    }

+

+    public InputStream createIncomingStream(StreamInitiation initiation)

+                    throws XMPPException {

+        /*

+         * In-Band Bytestream initiation listener must ignore next in-band

+         * bytestream request with given session ID

+         */

+        this.manager.ignoreBytestreamRequestOnce(initiation.getSessionID());

+

+        Packet streamInitiation = initiateIncomingStream(this.connection, initiation);

+        return negotiateIncomingStream(streamInitiation);

+    }

+

+    public PacketFilter getInitiationPacketFilter(String from, String streamID) {

+        /*

+         * this method is always called prior to #negotiateIncomingStream() so

+         * the In-Band Bytestream initiation listener must ignore the next

+         * In-Band Bytestream request with the given session ID

+         */

+        this.manager.ignoreBytestreamRequestOnce(streamID);

+

+        return new AndFilter(new FromContainsFilter(from), new IBBOpenSidFilter(streamID));

+    }

+

+    public String[] getNamespaces() {

+        return new String[] { InBandBytestreamManager.NAMESPACE };

+    }

+

+    InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException {

+        // build In-Band Bytestream request

+        InBandBytestreamRequest request = new ByteStreamRequest(this.manager,

+                        (Open) streamInitiation);

+

+        // always accept the request

+        InBandBytestreamSession session = request.accept();

+        session.setCloseBothStreamsEnabled(true);

+        return session.getInputStream();

+    }

+

+    public void cleanup() {

+    }

+

+    /**

+     * This PacketFilter accepts an incoming In-Band Bytestream open request

+     * with a specified session ID.

+     */

+    private static class IBBOpenSidFilter extends PacketTypeFilter {

+

+        private String sessionID;

+

+        public IBBOpenSidFilter(String sessionID) {

+            super(Open.class);

+            if (sessionID == null) {

+                throw new IllegalArgumentException("StreamID cannot be null");

+            }

+            this.sessionID = sessionID;

+        }

+

+        public boolean accept(Packet packet) {

+            if (super.accept(packet)) {

+                Open bytestream = (Open) packet;

+

+                // packet must by of type SET and contains the given session ID

+                return this.sessionID.equals(bytestream.getSessionID())

+                                && IQ.Type.SET.equals(bytestream.getType());

+            }

+            return false;

+        }

+    }

+

+    /**

+     * Derive from InBandBytestreamRequest to access protected constructor.

+     */

+    private static class ByteStreamRequest extends InBandBytestreamRequest {

+

+        private ByteStreamRequest(InBandBytestreamManager manager, Open byteStreamRequest) {

+            super(manager, byteStreamRequest);

+        }

+

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java b/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java
new file mode 100644
index 0000000..91a5a0d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/IncomingFileTransfer.java
@@ -0,0 +1,215 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.filetransfer;

+

+import org.jivesoftware.smack.XMPPException;

+

+import java.io.*;

+import java.util.concurrent.*;

+

+/**

+ * An incoming file transfer is created when the

+ * {@link FileTransferManager#createIncomingFileTransfer(FileTransferRequest)}

+ * method is invoked. It is a file being sent to the local user from another

+ * user on the jabber network. There are two stages of the file transfer to be

+ * concerned with and they can be handled in different ways depending upon the

+ * method that is invoked on this class.

+ * <p/>

+ * The first way that a file is recieved is by calling the

+ * {@link #recieveFile()} method. This method, negotiates the appropriate stream

+ * method and then returns the <b><i>InputStream</b></i> to read the file

+ * data from.

+ * <p/>

+ * The second way that a file can be recieved through this class is by invoking

+ * the {@link #recieveFile(File)} method. This method returns immediatly and

+ * takes as its parameter a file on the local file system where the file

+ * recieved from the transfer will be put.

+ *

+ * @author Alexander Wenckus

+ */

+public class IncomingFileTransfer extends FileTransfer {

+

+    private FileTransferRequest recieveRequest;

+

+    private InputStream inputStream;

+

+    protected IncomingFileTransfer(FileTransferRequest request,

+            FileTransferNegotiator transferNegotiator) {

+        super(request.getRequestor(), request.getStreamID(), transferNegotiator);

+        this.recieveRequest = request;

+    }

+

+    /**

+     * Negotiates the stream method to transfer the file over and then returns

+     * the negotiated stream.

+     *

+     * @return The negotiated InputStream from which to read the data.

+     * @throws XMPPException If there is an error in the negotiation process an exception

+     *                       is thrown.

+     */

+    public InputStream recieveFile() throws XMPPException {

+        if (inputStream != null) {

+            throw new IllegalStateException("Transfer already negotiated!");

+        }

+

+        try {

+            inputStream = negotiateStream();

+        }

+        catch (XMPPException e) {

+            setException(e);

+            throw e;

+        }

+

+        return inputStream;

+    }

+

+    /**

+     * This method negotitates the stream and then transfer's the file over the

+     * negotiated stream. The transfered file will be saved at the provided

+     * location.

+     * <p/>

+     * This method will return immedialtly, file transfer progress can be

+     * monitored through several methods:

+     * <p/>

+     * <UL>

+     * <LI>{@link FileTransfer#getStatus()}

+     * <LI>{@link FileTransfer#getProgress()}

+     * <LI>{@link FileTransfer#isDone()}

+     * </UL>

+     *

+     * @param file The location to save the file.

+     * @throws XMPPException            when the file transfer fails

+     * @throws IllegalArgumentException This exception is thrown when the the provided file is

+     *                                  either null, or cannot be written to.

+     */

+    public void recieveFile(final File file) throws XMPPException {

+        if (file != null) {

+            if (!file.exists()) {

+                try {

+                    file.createNewFile();

+                }

+                catch (IOException e) {

+                    throw new XMPPException(

+                            "Could not create file to write too", e);

+                }

+            }

+            if (!file.canWrite()) {

+                throw new IllegalArgumentException("Cannot write to provided file");

+            }

+        }

+        else {

+            throw new IllegalArgumentException("File cannot be null");

+        }

+

+        Thread transferThread = new Thread(new Runnable() {

+            public void run() {

+                try {

+                    inputStream = negotiateStream();

+                }

+                catch (XMPPException e) {

+                    handleXMPPException(e);

+                    return;

+                }

+

+                OutputStream outputStream = null;

+                try {

+                    outputStream = new FileOutputStream(file);

+                    setStatus(Status.in_progress);

+                    writeToStream(inputStream, outputStream);

+                }

+                catch (XMPPException e) {

+                    setStatus(Status.error);

+                    setError(Error.stream);

+                    setException(e);

+                }

+                catch (FileNotFoundException e) {

+                    setStatus(Status.error);

+                    setError(Error.bad_file);

+                    setException(e);

+                }

+

+                if (getStatus().equals(Status.in_progress)) {

+                    setStatus(Status.complete);

+                }

+                if (inputStream != null) {

+                    try {

+                        inputStream.close();

+                    }

+                    catch (Throwable io) {

+                        /* Ignore */

+                    }

+                }

+                if (outputStream != null) {

+                    try {

+                        outputStream.close();

+                    }

+                    catch (Throwable io) {

+                        /* Ignore */

+                    }

+                }

+            }

+        }, "File Transfer " + streamID);

+        transferThread.start();

+    }

+

+    private void handleXMPPException(XMPPException e) {

+        setStatus(FileTransfer.Status.error);

+        setException(e);

+    }

+

+    private InputStream negotiateStream() throws XMPPException {

+        setStatus(Status.negotiating_transfer);

+        final StreamNegotiator streamNegotiator = negotiator

+                .selectStreamNegotiator(recieveRequest);

+        setStatus(Status.negotiating_stream);

+        FutureTask<InputStream> streamNegotiatorTask = new FutureTask<InputStream>(

+                new Callable<InputStream>() {

+

+                    public InputStream call() throws Exception {

+                        return streamNegotiator

+                                .createIncomingStream(recieveRequest.getStreamInitiation());

+                    }

+                });

+        streamNegotiatorTask.run();

+        InputStream inputStream;

+        try {

+            inputStream = streamNegotiatorTask.get(15, TimeUnit.SECONDS);

+        }

+        catch (InterruptedException e) {

+            throw new XMPPException("Interruption while executing", e);

+        }

+        catch (ExecutionException e) {

+            throw new XMPPException("Error in execution", e);

+        }

+        catch (TimeoutException e) {

+            throw new XMPPException("Request timed out", e);

+        }

+        finally {

+            streamNegotiatorTask.cancel(true);

+        }

+        setStatus(Status.negotiated);

+        return inputStream;

+    }

+

+    public void cancel() {

+        setStatus(Status.cancelled);

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java b/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java
new file mode 100644
index 0000000..bba6c38
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/OutgoingFileTransfer.java
@@ -0,0 +1,456 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.filetransfer;

+

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.packet.XMPPError;

+

+import java.io.*;

+

+/**

+ * Handles the sending of a file to another user. File transfer's in jabber have

+ * several steps and there are several methods in this class that handle these

+ * steps differently.

+ *

+ * @author Alexander Wenckus

+ *

+ */

+public class OutgoingFileTransfer extends FileTransfer {

+

+	private static int RESPONSE_TIMEOUT = 60 * 1000;

+    private NegotiationProgress callback;

+

+    /**

+     * Returns the time in milliseconds after which the file transfer

+     * negotiation process will timeout if the other user has not responded.

+     *

+     * @return Returns the time in milliseconds after which the file transfer

+     *         negotiation process will timeout if the remote user has not

+     *         responded.

+     */

+    public static int getResponseTimeout() {

+        return RESPONSE_TIMEOUT;

+    }

+

+	/**

+	 * Sets the time in milliseconds after which the file transfer negotiation

+	 * process will timeout if the other user has not responded.

+	 *

+	 * @param responseTimeout

+	 *            The timeout time in milliseconds.

+	 */

+	public static void setResponseTimeout(int responseTimeout) {

+		RESPONSE_TIMEOUT = responseTimeout;

+	}

+

+	private OutputStream outputStream;

+

+	private String initiator;

+

+	private Thread transferThread;

+

+	protected OutgoingFileTransfer(String initiator, String target,

+			String streamID, FileTransferNegotiator transferNegotiator) {

+		super(target, streamID, transferNegotiator);

+		this.initiator = initiator;

+	}

+

+	protected void setOutputStream(OutputStream stream) {

+		if (outputStream == null) {

+			this.outputStream = stream;

+		}

+	}

+

+	/**

+	 * Returns the output stream connected to the peer to transfer the file. It

+	 * is only available after it has been successfully negotiated by the

+	 * {@link StreamNegotiator}.

+	 *

+	 * @return Returns the output stream connected to the peer to transfer the

+	 *         file.

+	 */

+	protected OutputStream getOutputStream() {

+		if (getStatus().equals(FileTransfer.Status.negotiated)) {

+			return outputStream;

+		} else {

+			return null;

+		}

+	}

+

+	/**

+	 * This method handles the negotiation of the file transfer and the stream,

+	 * it only returns the created stream after the negotiation has been completed.

+	 *

+	 * @param fileName

+	 *            The name of the file that will be transmitted. It is

+	 *            preferable for this name to have an extension as it will be

+	 *            used to determine the type of file it is.

+	 * @param fileSize

+	 *            The size in bytes of the file that will be transmitted.

+	 * @param description

+	 *            A description of the file that will be transmitted.

+	 * @return The OutputStream that is connected to the peer to transmit the

+	 *         file.

+	 * @throws XMPPException

+	 *             Thrown if an error occurs during the file transfer

+	 *             negotiation process.

+	 */

+	public synchronized OutputStream sendFile(String fileName, long fileSize,

+			String description) throws XMPPException {

+		if (isDone() || outputStream != null) {

+			throw new IllegalStateException(

+					"The negotation process has already"

+							+ " been attempted on this file transfer");

+		}

+		try {

+			setFileInfo(fileName, fileSize);

+			this.outputStream = negotiateStream(fileName, fileSize, description);

+		} catch (XMPPException e) {

+			handleXMPPException(e);

+			throw e;

+		}

+		return outputStream;

+	}

+

+	/**

+	 * This methods handles the transfer and stream negotiation process. It

+	 * returns immediately and its progress will be updated through the

+	 * {@link NegotiationProgress} callback.

+	 *

+	 * @param fileName

+	 *            The name of the file that will be transmitted. It is

+	 *            preferable for this name to have an extension as it will be

+	 *            used to determine the type of file it is.

+	 * @param fileSize

+	 *            The size in bytes of the file that will be transmitted.

+	 * @param description

+	 *            A description of the file that will be transmitted.

+	 * @param progress

+	 *            A callback to monitor the progress of the file transfer

+	 *            negotiation process and to retrieve the OutputStream when it

+	 *            is complete.

+	 */

+	public synchronized void sendFile(final String fileName,

+			final long fileSize, final String description,

+			final NegotiationProgress progress)

+    {

+        if(progress == null) {

+            throw new IllegalArgumentException("Callback progress cannot be null.");

+        }

+        checkTransferThread();

+		if (isDone() || outputStream != null) {

+			throw new IllegalStateException(

+					"The negotation process has already"

+							+ " been attempted for this file transfer");

+		}

+        setFileInfo(fileName, fileSize);

+        this.callback = progress;

+        transferThread = new Thread(new Runnable() {

+			public void run() {

+				try {

+					OutgoingFileTransfer.this.outputStream = negotiateStream(

+							fileName, fileSize, description);

+                    progress.outputStreamEstablished(OutgoingFileTransfer.this.outputStream);

+                }

+                catch (XMPPException e) {

+					handleXMPPException(e);

+				}

+			}

+		}, "File Transfer Negotiation " + streamID);

+		transferThread.start();

+	}

+

+	private void checkTransferThread() {

+		if (transferThread != null && transferThread.isAlive() || isDone()) {

+			throw new IllegalStateException(

+					"File transfer in progress or has already completed.");

+		}

+	}

+

+    /**

+	 * This method handles the stream negotiation process and transmits the file

+	 * to the remote user. It returns immediately and the progress of the file

+	 * transfer can be monitored through several methods:

+	 *

+	 * <UL>

+	 * <LI>{@link FileTransfer#getStatus()}

+	 * <LI>{@link FileTransfer#getProgress()}

+	 * <LI>{@link FileTransfer#isDone()}

+	 * </UL>

+	 *

+     * @param file the file to transfer to the remote entity.

+     * @param description a description for the file to transfer.

+	 * @throws XMPPException

+	 *             If there is an error during the negotiation process or the

+	 *             sending of the file.

+	 */

+	public synchronized void sendFile(final File file, final String description)

+			throws XMPPException {

+		checkTransferThread();

+		if (file == null || !file.exists() || !file.canRead()) {

+			throw new IllegalArgumentException("Could not read file");

+		} else {

+			setFileInfo(file.getAbsolutePath(), file.getName(), file.length());

+		}

+

+		transferThread = new Thread(new Runnable() {

+			public void run() {

+				try {

+					outputStream = negotiateStream(file.getName(), file

+							.length(), description);

+				} catch (XMPPException e) {

+					handleXMPPException(e);

+					return;

+				}

+				if (outputStream == null) {

+					return;

+				}

+

+                if (!updateStatus(Status.negotiated, Status.in_progress)) {

+					return;

+				}

+

+				InputStream inputStream = null;

+				try {

+					inputStream = new FileInputStream(file);

+					writeToStream(inputStream, outputStream);

+				} catch (FileNotFoundException e) {

+					setStatus(FileTransfer.Status.error);

+					setError(Error.bad_file);

+					setException(e);

+				} catch (XMPPException e) {

+					setStatus(FileTransfer.Status.error);

+					setException(e);

+				} finally {

+					try {

+						if (inputStream != null) {

+							inputStream.close();

+						}

+

+						outputStream.flush();

+						outputStream.close();

+					} catch (IOException e) {

+                        /* Do Nothing */

+					}

+				}

+                updateStatus(Status.in_progress, FileTransfer.Status.complete);

+				}

+

+		}, "File Transfer " + streamID);

+		transferThread.start();

+	}

+

+    /**

+	 * This method handles the stream negotiation process and transmits the file

+	 * to the remote user. It returns immediately and the progress of the file

+	 * transfer can be monitored through several methods:

+	 *

+	 * <UL>

+	 * <LI>{@link FileTransfer#getStatus()}

+	 * <LI>{@link FileTransfer#getProgress()}

+	 * <LI>{@link FileTransfer#isDone()}

+	 * </UL>

+	 *

+     * @param in the stream to transfer to the remote entity.

+     * @param fileName the name of the file that is transferred

+     * @param fileSize the size of the file that is transferred

+     * @param description a description for the file to transfer.

+	 */

+	public synchronized void sendStream(final InputStream in, final String fileName, final long fileSize, final String description){

+		checkTransferThread();

+

+		setFileInfo(fileName, fileSize);

+		transferThread = new Thread(new Runnable() {

+			public void run() {

+                setFileInfo(fileName, fileSize);

+                //Create packet filter

+                try {

+					outputStream = negotiateStream(fileName, fileSize, description);

+				} catch (XMPPException e) {

+					handleXMPPException(e);

+					return;

+				} catch (IllegalStateException e) {

+					setStatus(FileTransfer.Status.error);

+					setException(e);

+				}

+				if (outputStream == null) {

+					return;

+				}

+

+                if (!updateStatus(Status.negotiated, Status.in_progress)) {

+					return;

+				}

+				try {

+					writeToStream(in, outputStream);

+				} catch (XMPPException e) {

+					setStatus(FileTransfer.Status.error);

+					setException(e);

+				} catch (IllegalStateException e) {

+					setStatus(FileTransfer.Status.error);

+					setException(e);

+				} finally {

+					try {

+						if (in != null) {

+							in.close();

+						}

+

+						outputStream.flush();

+						outputStream.close();

+					} catch (IOException e) {

+                        /* Do Nothing */

+					}

+				}

+                updateStatus(Status.in_progress, FileTransfer.Status.complete);

+				}

+

+		}, "File Transfer " + streamID);

+		transferThread.start();

+	}

+

+	private void handleXMPPException(XMPPException e) {

+		XMPPError error = e.getXMPPError();

+		if (error != null) {

+			int code = error.getCode();

+			if (code == 403) {

+				setStatus(Status.refused);

+				return;

+			}

+            else if (code == 400) {

+				setStatus(Status.error);

+				setError(Error.not_acceptable);

+            }

+            else {

+                setStatus(FileTransfer.Status.error);

+            }

+        }

+

+        setException(e);

+	}

+

+	/**

+	 * Returns the amount of bytes that have been sent for the file transfer. Or

+	 * -1 if the file transfer has not started.

+	 * <p>

+	 * Note: This method is only useful when the {@link #sendFile(File, String)}

+	 * method is called, as it is the only method that actually transmits the

+	 * file.

+	 *

+	 * @return Returns the amount of bytes that have been sent for the file

+	 *         transfer. Or -1 if the file transfer has not started.

+	 */

+	public long getBytesSent() {

+		return amountWritten;

+	}

+

+	private OutputStream negotiateStream(String fileName, long fileSize,

+			String description) throws XMPPException {

+		// Negotiate the file transfer profile

+

+        if (!updateStatus(Status.initial, Status.negotiating_transfer)) {

+            throw new XMPPException("Illegal state change");

+        }

+		StreamNegotiator streamNegotiator = negotiator.negotiateOutgoingTransfer(

+				getPeer(), streamID, fileName, fileSize, description,

+				RESPONSE_TIMEOUT);

+

+		if (streamNegotiator == null) {

+			setStatus(Status.error);

+			setError(Error.no_response);

+			return null;

+		}

+

+        // Negotiate the stream

+        if (!updateStatus(Status.negotiating_transfer, Status.negotiating_stream)) {

+            throw new XMPPException("Illegal state change");

+        }

+		outputStream = streamNegotiator.createOutgoingStream(streamID,

+                initiator, getPeer());

+

+        if (!updateStatus(Status.negotiating_stream, Status.negotiated)) {

+            throw new XMPPException("Illegal state change");

+		}

+		return outputStream;

+	}

+

+	public void cancel() {

+		setStatus(Status.cancelled);

+	}

+

+    @Override

+    protected boolean updateStatus(Status oldStatus, Status newStatus) {

+        boolean isUpdated = super.updateStatus(oldStatus, newStatus);

+        if(callback != null && isUpdated) {

+            callback.statusUpdated(oldStatus, newStatus);

+        }

+        return isUpdated;

+    }

+

+    @Override

+    protected void setStatus(Status status) {

+        Status oldStatus = getStatus();

+        super.setStatus(status);

+        if(callback != null) {

+            callback.statusUpdated(oldStatus, status);

+        }

+    }

+

+    @Override

+    protected void setException(Exception exception) {

+        super.setException(exception);

+        if(callback != null) {

+            callback.errorEstablishingStream(exception);

+        }

+    }

+

+    /**

+	 * A callback class to retrieve the status of an outgoing transfer

+	 * negotiation process.

+	 *

+	 * @author Alexander Wenckus

+	 *

+	 */

+	public interface NegotiationProgress {

+

+		/**

+		 * Called when the status changes

+         *

+         * @param oldStatus the previous status of the file transfer.

+         * @param newStatus the new status of the file transfer.

+         */

+		void statusUpdated(Status oldStatus, Status newStatus);

+

+		/**

+		 * Once the negotiation process is completed the output stream can be

+		 * retrieved.

+         *

+         * @param stream the established stream which can be used to transfer the file to the remote

+         * entity

+		 */

+		void outputStreamEstablished(OutputStream stream);

+

+        /**

+         * Called when an exception occurs during the negotiation progress.

+         *

+         * @param e the exception that occurred.

+         */

+        void errorEstablishingStream(Exception e);

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java b/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java
new file mode 100644
index 0000000..3c07fdc
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/Socks5TransferNegotiator.java
@@ -0,0 +1,164 @@
+/**

+ * 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.smackx.filetransfer;

+

+import java.io.IOException;

+import java.io.InputStream;

+import java.io.OutputStream;

+import java.io.PushbackInputStream;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.filter.AndFilter;

+import org.jivesoftware.smack.filter.FromMatchesFilter;

+import org.jivesoftware.smack.filter.PacketFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamManager;

+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamRequest;

+import org.jivesoftware.smackx.bytestreams.socks5.Socks5BytestreamSession;

+import org.jivesoftware.smackx.bytestreams.socks5.packet.Bytestream;

+import org.jivesoftware.smackx.packet.StreamInitiation;

+

+/**

+ * Negotiates a SOCKS5 Bytestream to be used for file transfers. The implementation is based on the

+ * {@link Socks5BytestreamManager} and the {@link Socks5BytestreamRequest}.

+ * 

+ * @author Henning Staib

+ * @see <a href="http://xmpp.org/extensions/xep-0065.html">XEP-0065: SOCKS5 Bytestreams</a>

+ */

+public class Socks5TransferNegotiator extends StreamNegotiator {

+

+    private Connection connection;

+

+    private Socks5BytestreamManager manager;

+

+    Socks5TransferNegotiator(Connection connection) {

+        this.connection = connection;

+        this.manager = Socks5BytestreamManager.getBytestreamManager(this.connection);

+    }

+

+    @Override

+    public OutputStream createOutgoingStream(String streamID, String initiator, String target)

+                    throws XMPPException {

+        try {

+            return this.manager.establishSession(target, streamID).getOutputStream();

+        }

+        catch (IOException e) {

+            throw new XMPPException("error establishing SOCKS5 Bytestream", e);

+        }

+        catch (InterruptedException e) {

+            throw new XMPPException("error establishing SOCKS5 Bytestream", e);

+        }

+    }

+

+    @Override

+    public InputStream createIncomingStream(StreamInitiation initiation) throws XMPPException,

+                    InterruptedException {

+        /*

+         * SOCKS5 initiation listener must ignore next SOCKS5 Bytestream request with given session

+         * ID

+         */

+        this.manager.ignoreBytestreamRequestOnce(initiation.getSessionID());

+

+        Packet streamInitiation = initiateIncomingStream(this.connection, initiation);

+        return negotiateIncomingStream(streamInitiation);

+    }

+

+    @Override

+    public PacketFilter getInitiationPacketFilter(final String from, String streamID) {

+        /*

+         * this method is always called prior to #negotiateIncomingStream() so the SOCKS5

+         * InitiationListener must ignore the next SOCKS5 Bytestream request with the given session

+         * ID

+         */

+        this.manager.ignoreBytestreamRequestOnce(streamID);

+

+        return new AndFilter(new FromMatchesFilter(from), new BytestreamSIDFilter(streamID));

+    }

+

+    @Override

+    public String[] getNamespaces() {

+        return new String[] { Socks5BytestreamManager.NAMESPACE };

+    }

+

+    @Override

+    InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException,

+                    InterruptedException {

+        // build SOCKS5 Bytestream request

+        Socks5BytestreamRequest request = new ByteStreamRequest(this.manager,

+                        (Bytestream) streamInitiation);

+

+        // always accept the request

+        Socks5BytestreamSession session = request.accept();

+

+        // test input stream

+        try {

+            PushbackInputStream stream = new PushbackInputStream(session.getInputStream());

+            int firstByte = stream.read();

+            stream.unread(firstByte);

+            return stream;

+        }

+        catch (IOException e) {

+            throw new XMPPException("Error establishing input stream", e);

+        }

+    }

+

+    @Override

+    public void cleanup() {

+        /* do nothing */

+    }

+

+    /**

+     * This PacketFilter accepts an incoming SOCKS5 Bytestream request with a specified session ID.

+     */

+    private static class BytestreamSIDFilter extends PacketTypeFilter {

+

+        private String sessionID;

+

+        public BytestreamSIDFilter(String sessionID) {

+            super(Bytestream.class);

+            if (sessionID == null) {

+                throw new IllegalArgumentException("StreamID cannot be null");

+            }

+            this.sessionID = sessionID;

+        }

+

+        @Override

+        public boolean accept(Packet packet) {

+            if (super.accept(packet)) {

+                Bytestream bytestream = (Bytestream) packet;

+

+                // packet must by of type SET and contains the given session ID

+                return this.sessionID.equals(bytestream.getSessionID())

+                                && IQ.Type.SET.equals(bytestream.getType());

+            }

+            return false;

+        }

+

+    }

+

+    /**

+     * Derive from Socks5BytestreamRequest to access protected constructor.

+     */

+    private static class ByteStreamRequest extends Socks5BytestreamRequest {

+

+        private ByteStreamRequest(Socks5BytestreamManager manager, Bytestream byteStreamRequest) {

+            super(manager, byteStreamRequest);

+        }

+

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java b/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java
new file mode 100644
index 0000000..5eefe43
--- /dev/null
+++ b/src/org/jivesoftware/smackx/filetransfer/StreamNegotiator.java
@@ -0,0 +1,167 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.filetransfer;

+

+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.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.XMPPError;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.FormField;

+import org.jivesoftware.smackx.packet.DataForm;

+import org.jivesoftware.smackx.packet.StreamInitiation;

+

+import java.io.InputStream;

+import java.io.OutputStream;

+

+/**

+ * After the file transfer negotiation process is completed according to

+ * JEP-0096, the negotiation process is passed off to a particular stream

+ * negotiator. The stream negotiator will then negotiate the chosen stream and

+ * return the stream to transfer the file.

+ *

+ * @author Alexander Wenckus

+ */

+public abstract class StreamNegotiator {

+

+    /**

+     * Creates the initiation acceptance packet to forward to the stream

+     * initiator.

+     *

+     * @param streamInitiationOffer The offer from the stream initiator to connect for a stream.

+     * @param namespaces            The namespace that relates to the accepted means of transfer.

+     * @return The response to be forwarded to the initiator.

+     */

+    public StreamInitiation createInitiationAccept(

+            StreamInitiation streamInitiationOffer, String[] namespaces)

+    {

+        StreamInitiation response = new StreamInitiation();

+        response.setTo(streamInitiationOffer.getFrom());

+        response.setFrom(streamInitiationOffer.getTo());

+        response.setType(IQ.Type.RESULT);

+        response.setPacketID(streamInitiationOffer.getPacketID());

+

+        DataForm form = new DataForm(Form.TYPE_SUBMIT);

+        FormField field = new FormField(

+                FileTransferNegotiator.STREAM_DATA_FIELD_NAME);

+        for (String namespace : namespaces) {

+            field.addValue(namespace);

+        }

+        form.addField(field);

+

+        response.setFeatureNegotiationForm(form);

+        return response;

+    }

+

+

+    public IQ createError(String from, String to, String packetID, XMPPError xmppError) {

+        IQ iq = FileTransferNegotiator.createIQ(packetID, to, from, IQ.Type.ERROR);

+        iq.setError(xmppError);

+        return iq;

+    }

+

+    Packet initiateIncomingStream(Connection connection, StreamInitiation initiation) throws XMPPException {

+        StreamInitiation response = createInitiationAccept(initiation,

+                getNamespaces());

+

+        // establish collector to await response

+        PacketCollector collector = connection

+                .createPacketCollector(getInitiationPacketFilter(initiation.getFrom(), initiation.getSessionID()));

+        connection.sendPacket(response);

+

+        Packet streamMethodInitiation = collector

+                .nextResult(SmackConfiguration.getPacketReplyTimeout());

+        collector.cancel();

+        if (streamMethodInitiation == null) {

+            throw new XMPPException("No response from file transfer initiator");

+        }

+

+        return streamMethodInitiation;

+    }

+

+    /**

+     * Returns the packet filter that will return the initiation packet for the appropriate stream

+     * initiation.

+     *

+     * @param from     The initiator of the file transfer.

+     * @param streamID The stream ID related to the transfer.

+     * @return The <b><i>PacketFilter</b></i> that will return the packet relatable to the stream

+     *         initiation.

+     */

+    public abstract PacketFilter getInitiationPacketFilter(String from, String streamID);

+

+

+    abstract InputStream negotiateIncomingStream(Packet streamInitiation) throws XMPPException,

+            InterruptedException;

+

+    /**

+     * This method handles the file stream download negotiation process. The

+     * appropriate stream negotiator's initiate incoming stream is called after

+     * an appropriate file transfer method is selected. The manager will respond

+     * to the initiator with the selected means of transfer, then it will handle

+     * any negotiation specific to the particular transfer method. This method

+     * returns the InputStream, ready to transfer the file.

+     *

+     * @param initiation The initiation that triggered this download.

+     * @return After the negotiation process is complete, the InputStream to

+     *         write a file to is returned.

+     * @throws XMPPException If an error occurs during this process an XMPPException is

+     *                       thrown.

+     * @throws InterruptedException If thread is interrupted.

+     */

+    public abstract InputStream createIncomingStream(StreamInitiation initiation)

+            throws XMPPException, InterruptedException;

+

+    /**

+     * This method handles the file upload stream negotiation process. The

+     * particular stream negotiator is determined during the file transfer

+     * negotiation process. This method returns the OutputStream to transmit the

+     * file to the remote user.

+     *

+     * @param streamID  The streamID that uniquely identifies the file transfer.

+     * @param initiator The fully-qualified JID of the initiator of the file transfer.

+     * @param target    The fully-qualified JID of the target or receiver of the file

+     *                  transfer.

+     * @return The negotiated stream ready for data.

+     * @throws XMPPException If an error occurs during the negotiation process an

+     *                       exception will be thrown.

+     */

+    public abstract OutputStream createOutgoingStream(String streamID,

+            String initiator, String target) throws XMPPException;

+

+    /**

+     * Returns the XMPP namespace reserved for this particular type of file

+     * transfer.

+     *

+     * @return Returns the XMPP namespace reserved for this particular type of

+     *         file transfer.

+     */

+    public abstract String[] getNamespaces();

+

+    /**

+     * Cleanup any and all resources associated with this negotiator.

+     */

+    public abstract void cleanup();

+

+}

diff --git a/src/org/jivesoftware/smackx/forward/Forwarded.java b/src/org/jivesoftware/smackx/forward/Forwarded.java
new file mode 100644
index 0000000..817ee27
--- /dev/null
+++ b/src/org/jivesoftware/smackx/forward/Forwarded.java
@@ -0,0 +1,125 @@
+/**
+ * Copyright 2013 Georg Lukas
+ *
+ * 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.smackx.forward;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.packet.DelayInfo;
+import org.jivesoftware.smackx.provider.DelayInfoProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Packet extension for XEP-0297: Stanza Forwarding. This class implements
+ * the packet extension and a {@link PacketExtensionProvider} to parse
+ * forwarded messages from a packet. The extension
+ * <a href="http://xmpp.org/extensions/xep-0297.html">XEP-0297</a> is
+ * a prerequisite for XEP-0280 (Message Carbons).
+ *
+ * <p>The {@link Forwarded.Provider} must be registered in the
+ * <b>smack.properties</b> file for the element <b>forwarded</b> with
+ * namespace <b>urn:xmpp:forwarded:0</b></p> to be used.
+ *
+ * @author Georg Lukas
+ */
+public class Forwarded implements PacketExtension {
+    public static final String NAMESPACE = "urn:xmpp:forward:0";
+    public static final String ELEMENT_NAME = "forwarded";
+
+    private DelayInfo delay;
+    private Packet forwardedPacket;
+
+    /**
+     * Creates a new Forwarded packet extension.
+     *
+     * @param delay an optional {@link DelayInfo} timestamp of the packet.
+     * @param fwdPacket the packet that is forwarded (required).
+     */
+    public Forwarded(DelayInfo delay, Packet fwdPacket) {
+        this.delay = delay;
+        this.forwardedPacket = fwdPacket;
+    }
+
+    @Override
+    public String getElementName() {
+        return ELEMENT_NAME;
+    }
+
+    @Override
+    public String getNamespace() {
+        return NAMESPACE;
+    }
+
+    @Override
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"")
+                .append(getNamespace()).append("\">");
+
+        if (delay != null)
+            buf.append(delay.toXML());
+        buf.append(forwardedPacket.toXML());
+
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+    /**
+     * get the packet forwarded by this stanza.
+     *
+     * @return the {@link Packet} instance (typically a message) that was forwarded.
+     */
+    public Packet getForwardedPacket() {
+        return forwardedPacket;
+    }
+
+    /**
+     * get the timestamp of the forwarded packet.
+     *
+     * @return the {@link DelayInfo} representing the time when the original packet was sent. May be null.
+     */
+    public DelayInfo getDelayInfo() {
+        return delay;
+    }
+
+    public static class Provider implements PacketExtensionProvider {
+        DelayInfoProvider dip = new DelayInfoProvider();
+
+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+            DelayInfo di = null;
+            Packet packet = null;
+
+            boolean done = false;
+            while (!done) {
+                int eventType = parser.next();
+                if (eventType == XmlPullParser.START_TAG) {
+                    if (parser.getName().equals("delay"))
+                        di = (DelayInfo)dip.parseExtension(parser);
+                    else if (parser.getName().equals("message"))
+                        packet = PacketParserUtils.parseMessage(parser);
+                    else throw new Exception("Unsupported forwarded packet type: " + parser.getName());
+                }
+                else if (eventType == XmlPullParser.END_TAG && parser.getName().equals(ELEMENT_NAME))
+                    done = true;
+            }
+            if (packet == null)
+                throw new Exception("forwarded extension must contain a packet");
+            return new Forwarded(di, packet);
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/muc/Affiliate.java b/src/org/jivesoftware/smackx/muc/Affiliate.java
new file mode 100644
index 0000000..09a04f6
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/Affiliate.java
@@ -0,0 +1,98 @@
+/**
+ * $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.smackx.muc;
+
+import org.jivesoftware.smackx.packet.MUCAdmin;
+import org.jivesoftware.smackx.packet.MUCOwner;
+
+/**
+ * Represents an affiliation of a user to a given room. The affiliate's information will always have
+ * the bare jid of the real user and its affiliation. If the affiliate is an occupant of the room
+ * then we will also have information about the role and nickname of the user in the room.
+ *
+ * @author Gaston Dombiak
+ */
+public class Affiliate {
+    // Fields that must have a value
+    private String jid;
+    private String affiliation;
+
+    // Fields that may have a value
+    private String role;
+    private String nick;
+
+    Affiliate(MUCOwner.Item item) {
+        super();
+        this.jid = item.getJid();
+        this.affiliation = item.getAffiliation();
+        this.role = item.getRole();
+        this.nick = item.getNick();
+    }
+
+    Affiliate(MUCAdmin.Item item) {
+        super();
+        this.jid = item.getJid();
+        this.affiliation = item.getAffiliation();
+        this.role = item.getRole();
+        this.nick = item.getNick();
+    }
+
+    /**
+     * Returns the bare JID of the affiliated user. This information will always be available.
+     *
+     * @return the bare JID of the affiliated user.
+     */
+    public String getJid() {
+        return jid;
+    }
+
+    /**
+     * Returns the affiliation of the afffiliated user. Possible affiliations are: "owner", "admin",
+     * "member", "outcast". This information will always be available.
+     *
+     * @return the affiliation of the afffiliated user.
+     */
+    public String getAffiliation() {
+        return affiliation;
+    }
+
+    /**
+     * Returns the current role of the affiliated user if the user is currently in the room.
+     * If the user is not present in the room then the answer will be null.
+     *
+     * @return the current role of the affiliated user in the room or null if the user is not in
+     *         the room.
+     */
+    public String getRole() {
+        return role;
+    }
+
+    /**
+     * Returns the current nickname of the affiliated user if the user is currently in the room.
+     * If the user is not present in the room then the answer will be null.
+     *
+     * @return the current nickname of the affiliated user in the room or null if the user is not in
+     *         the room.
+     */
+    public String getNick() {
+        return nick;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/muc/ConnectionDetachedPacketCollector.java b/src/org/jivesoftware/smackx/muc/ConnectionDetachedPacketCollector.java
new file mode 100644
index 0000000..243c298
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/ConnectionDetachedPacketCollector.java
@@ -0,0 +1,123 @@
+/**
+ * $RCSfile$
+ * $Revision: 2779 $
+ * $Date: 2005-09-05 17:00:45 -0300 (Mon, 05 Sep 2005) $
+ *
+ * Copyright 2003-2006 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.smackx.muc;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.packet.Packet;
+
+/**
+ * A variant of the {@link org.jivesoftware.smack.PacketCollector} class
+ * that does not force attachment to a <code>Connection</code>
+ * on creation and no filter is required. Used to collect message
+ * packets targeted to a group chat room.
+ *
+ * @author Larry Kirschner
+ */
+class ConnectionDetachedPacketCollector {
+    /**
+     * Max number of packets that any one collector can hold. After the max is
+     * reached, older packets will be automatically dropped from the queue as
+     * new packets are added.
+     */
+    private int maxPackets = SmackConfiguration.getPacketCollectorSize();
+
+    private ArrayBlockingQueue<Packet> resultQueue;
+
+    /**
+     * Creates a new packet collector. If the packet filter is <tt>null</tt>, then
+     * all packets will match this collector.
+     */
+    public ConnectionDetachedPacketCollector() {
+        this(SmackConfiguration.getPacketCollectorSize());
+    }
+
+    /**
+     * Creates a new packet collector. If the packet filter is <tt>null</tt>, then
+     * all packets will match this collector.
+     */
+    public ConnectionDetachedPacketCollector(int maxSize) {
+        this.resultQueue = new ArrayBlockingQueue<Packet>(maxSize);
+    }
+
+    /**
+     * Polls to see if a packet is currently available and returns it, or
+     * immediately returns <tt>null</tt> if no packets are currently in the
+     * result queue.
+     *
+     * @return the next packet result, or <tt>null</tt> if there are no more
+     *      results.
+     */
+    public Packet pollResult() {
+    	return resultQueue.poll();
+    }
+
+    /**
+     * Returns the next available packet. The method call will block (not return)
+     * until a packet is available.
+     *
+     * @return the next available packet.
+     */
+    public Packet nextResult() {
+        try {
+			return resultQueue.take();
+		}
+		catch (InterruptedException e) {
+			throw new RuntimeException(e);
+		}
+    }
+
+    /**
+     * Returns the next available packet. The method call will block (not return)
+     * until a packet is available or the <tt>timeout</tt> has elapased. If the
+     * timeout elapses without a result, <tt>null</tt> will be returned.
+     *
+     * @param timeout the amount of time to wait for the next packet (in milleseconds).
+     * @return the next available packet.
+     */
+    public Packet nextResult(long timeout) {
+        try {
+        	return resultQueue.poll(timeout, TimeUnit.MILLISECONDS);
+		}
+		catch (InterruptedException e) {
+			throw new RuntimeException(e);
+		}
+    }
+
+    /**
+     * Processes a packet to see if it meets the criteria for this packet collector.
+     * If so, the packet is added to the result queue.
+     *
+     * @param packet the packet to process.
+     */
+    protected void processPacket(Packet packet) {
+        if (packet == null) {
+            return;
+        }
+        
+    	while (!resultQueue.offer(packet)) {
+    		// Since we know the queue is full, this poll should never actually block.
+    		resultQueue.poll();
+    	}
+    }
+}
diff --git a/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java b/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java
new file mode 100644
index 0000000..24570fd
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/DeafOccupantInterceptor.java
@@ -0,0 +1,76 @@
+/**
+ * $RCSfile$
+ * $Revision:  $
+ * $Date$
+ *
+ * Copyright 2003-2006 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.smackx.muc;
+
+import org.jivesoftware.smack.PacketInterceptor;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.packet.Presence;
+
+/**
+ * Packet interceptor that will intercept presence packets sent to the MUC service to indicate
+ * that the user wants to be a deaf occupant. A user can only indicate that he wants to be a
+ * deaf occupant while joining the room. It is not possible to become deaf or stop being deaf
+ * after the user joined the room.<p>
+ *
+ * Deaf occupants will not get messages broadcasted to all room occupants. However, they will
+ * be able to get private messages, presences, IQ packets or room history. To use this
+ * functionality you will need to send the message
+ * {@link MultiUserChat#addPresenceInterceptor(org.jivesoftware.smack.PacketInterceptor)} and
+ * pass this interceptor as the parameter.<p>
+ *
+ * Note that this is a custom extension to the MUC service so it may not work with other servers
+ * than Wildfire.
+ *
+ * @author Gaston Dombiak
+ */
+public class DeafOccupantInterceptor implements PacketInterceptor {
+
+    public void interceptPacket(Packet packet) {
+        Presence presence = (Presence) packet;
+        // Check if user is joining a room
+        if (Presence.Type.available == presence.getType() &&
+                presence.getExtension("x", "http://jabber.org/protocol/muc") != null) {
+            // Add extension that indicates that user wants to be a deaf occupant
+            packet.addExtension(new DeafExtension());
+        }
+    }
+
+    private static class DeafExtension implements PacketExtension {
+
+        public String getElementName() {
+            return "x";
+        }
+
+        public String getNamespace() {
+            return "http://jivesoftware.org/protocol/muc";
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace())
+                    .append("\">");
+            buf.append("<deaf-occupant/>");
+            buf.append("</").append(getElementName()).append(">");
+            return buf.toString();
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java b/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java
new file mode 100644
index 0000000..6eb9efa
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/DefaultParticipantStatusListener.java
@@ -0,0 +1,79 @@
+/**
+ * $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.smackx.muc;
+
+/**
+ * Default implementation of the ParticipantStatusListener interface.<p>
+ *
+ * This class does not provide any behavior by default. It just avoids having
+ * to implement all the inteface methods if the user is only interested in implementing
+ * some of the methods.
+ * 
+ * @author Gaston Dombiak
+ */
+public class DefaultParticipantStatusListener implements ParticipantStatusListener {
+
+    public void joined(String participant) {
+    }
+
+    public void left(String participant) {
+    }
+
+    public void kicked(String participant, String actor, String reason) {
+    }
+
+    public void voiceGranted(String participant) {
+    }
+
+    public void voiceRevoked(String participant) {
+    }
+
+    public void banned(String participant, String actor, String reason) {
+    }
+
+    public void membershipGranted(String participant) {
+    }
+
+    public void membershipRevoked(String participant) {
+    }
+
+    public void moderatorGranted(String participant) {
+    }
+
+    public void moderatorRevoked(String participant) {
+    }
+
+    public void ownershipGranted(String participant) {
+    }
+
+    public void ownershipRevoked(String participant) {
+    }
+
+    public void adminGranted(String participant) {
+    }
+
+    public void adminRevoked(String participant) {
+    }
+
+    public void nicknameChanged(String participant, String newNickname) {
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java b/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java
new file mode 100644
index 0000000..de7cc87
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/DefaultUserStatusListener.java
@@ -0,0 +1,70 @@
+/**
+ * $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.smackx.muc;
+
+/**
+ * Default implementation of the UserStatusListener interface.<p>
+ *
+ * This class does not provide any behavior by default. It just avoids having
+ * to implement all the inteface methods if the user is only interested in implementing
+ * some of the methods.
+ * 
+ * @author Gaston Dombiak
+ */
+public class DefaultUserStatusListener implements UserStatusListener {
+
+    public void kicked(String actor, String reason) {
+    }
+
+    public void voiceGranted() {
+    }
+
+    public void voiceRevoked() {
+    }
+
+    public void banned(String actor, String reason) {
+    }
+
+    public void membershipGranted() {
+    }
+
+    public void membershipRevoked() {
+    }
+
+    public void moderatorGranted() {
+    }
+
+    public void moderatorRevoked() {
+    }
+
+    public void ownershipGranted() {
+    }
+
+    public void ownershipRevoked() {
+    }
+
+    public void adminGranted() {
+    }
+
+    public void adminRevoked() {
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/muc/DiscussionHistory.java b/src/org/jivesoftware/smackx/muc/DiscussionHistory.java
new file mode 100644
index 0000000..036f6cb
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/DiscussionHistory.java
@@ -0,0 +1,173 @@
+/**
+ * $RCSfile$
+/**
+ * $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.smackx.muc;
+
+import java.util.Date;
+
+import org.jivesoftware.smackx.packet.MUCInitialPresence;
+
+/**
+ * The DiscussionHistory class controls the number of characters or messages to receive
+ * when entering a room. The room will decide the amount of history to return if you don't 
+ * specify a DiscussionHistory while joining a room.<p>
+ * 
+ * You can use some or all of these variable to control the amount of history to receive:   
+ * <ul>
+ *  <li>maxchars -> total number of characters to receive in the history.
+ *  <li>maxstanzas -> total number of messages to receive in the history.
+ *  <li>seconds -> only the messages received in the last "X" seconds will be included in the 
+ * history.
+ *  <li>since -> only the messages received since the datetime specified will be included in 
+ * the history.
+ * </ul>
+ * 
+ * Note: Setting maxchars to 0 indicates that the user requests to receive no history.
+ * 
+ * @author Gaston Dombiak
+ */
+public class DiscussionHistory {
+
+    private int maxChars = -1;
+    private int maxStanzas = -1;
+    private int seconds = -1;
+    private Date since;
+
+    /**
+     * Returns the total number of characters to receive in the history.
+     * 
+     * @return total number of characters to receive in the history.
+     */
+    public int getMaxChars() {
+        return maxChars;
+    }
+
+    /**
+     * Returns the total number of messages to receive in the history.
+     * 
+     * @return the total number of messages to receive in the history.
+     */
+    public int getMaxStanzas() {
+        return maxStanzas;
+    }
+
+    /**
+     * Returns the number of seconds to use to filter the messages received during that time. 
+     * In other words, only the messages received in the last "X" seconds will be included in 
+     * the history.
+     * 
+     * @return the number of seconds to use to filter the messages received during that time.
+     */
+    public int getSeconds() {
+        return seconds;
+    }
+
+    /**
+     * Returns the since date to use to filter the messages received during that time. 
+     * In other words, only the messages received since the datetime specified will be 
+     * included in the history.
+     * 
+     * @return the since date to use to filter the messages received during that time.
+     */
+    public Date getSince() {
+        return since;
+    }
+
+    /**
+     * Sets the total number of characters to receive in the history.
+     * 
+     * @param maxChars the total number of characters to receive in the history.
+     */
+    public void setMaxChars(int maxChars) {
+        this.maxChars = maxChars;
+    }
+
+    /**
+     * Sets the total number of messages to receive in the history.
+     * 
+     * @param maxStanzas the total number of messages to receive in the history.
+     */
+    public void setMaxStanzas(int maxStanzas) {
+        this.maxStanzas = maxStanzas;
+    }
+
+    /**
+     * Sets the number of seconds to use to filter the messages received during that time. 
+     * In other words, only the messages received in the last "X" seconds will be included in 
+     * the history.
+     * 
+     * @param seconds the number of seconds to use to filter the messages received during 
+     * that time.
+     */
+    public void setSeconds(int seconds) {
+        this.seconds = seconds;
+    }
+
+    /**
+     * Sets the since date to use to filter the messages received during that time. 
+     * In other words, only the messages received since the datetime specified will be 
+     * included in the history.
+     * 
+     * @param since the since date to use to filter the messages received during that time.
+     */
+    public void setSince(Date since) {
+        this.since = since;
+    }
+
+    /**
+     * Returns true if the history has been configured with some values.
+     * 
+     * @return true if the history has been configured with some values.
+     */
+    private boolean isConfigured() {
+        return maxChars > -1 || maxStanzas > -1 || seconds > -1 || since != null;
+    }
+
+    /**
+     * Returns the History that manages the amount of discussion history provided on entering a 
+     * room.
+     * 
+     * @return the History that manages the amount of discussion history provided on entering a 
+     * room.
+     */
+    MUCInitialPresence.History getMUCHistory() {
+        // Return null if the history was not properly configured  
+        if (!isConfigured()) {
+            return null;
+        }
+        
+        MUCInitialPresence.History mucHistory = new MUCInitialPresence.History();
+        if (maxChars > -1) {
+            mucHistory.setMaxChars(maxChars);
+        }
+        if (maxStanzas > -1) {
+            mucHistory.setMaxStanzas(maxStanzas);
+        }
+        if (seconds > -1) {
+            mucHistory.setSeconds(seconds);
+        }
+        if (since != null) {
+            mucHistory.setSince(since);
+        }
+        return mucHistory;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/muc/HostedRoom.java b/src/org/jivesoftware/smackx/muc/HostedRoom.java
new file mode 100644
index 0000000..7cd580b
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/HostedRoom.java
@@ -0,0 +1,65 @@
+/**
+ * $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.smackx.muc;
+
+import org.jivesoftware.smackx.packet.DiscoverItems;
+
+/**
+ * Hosted rooms by a chat service may be discovered if they are configured to appear in the room
+ * directory . The information that may be discovered is the XMPP address of the room and the room
+ * name. The address of the room may be used for obtaining more detailed information
+ * {@link org.jivesoftware.smackx.muc.MultiUserChat#getRoomInfo(org.jivesoftware.smack.Connection, String)}
+ * or could be used for joining the room
+ * {@link org.jivesoftware.smackx.muc.MultiUserChat#MultiUserChat(org.jivesoftware.smack.Connection, String)}
+ * and {@link org.jivesoftware.smackx.muc.MultiUserChat#join(String)}.
+ *
+ * @author Gaston Dombiak
+ */
+public class HostedRoom {
+
+    private String jid;
+
+    private String name;
+
+    public HostedRoom(DiscoverItems.Item item) {
+        super();
+        jid = item.getEntityID();
+        name = item.getName();
+    }
+
+    /**
+     * Returns the XMPP address of the hosted room by the chat service. This address may be used
+     * when creating a <code>MultiUserChat</code> when joining a room.
+     *
+     * @return the XMPP address of the hosted room by the chat service.
+     */
+    public String getJid() {
+        return jid;
+    }
+
+    /**
+     * Returns the name of the room.
+     *
+     * @return the name of the room.
+     */
+    public String getName() {
+        return name;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/muc/InvitationListener.java b/src/org/jivesoftware/smackx/muc/InvitationListener.java
new file mode 100644
index 0000000..34c915d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/InvitationListener.java
@@ -0,0 +1,49 @@
+/**
+ * $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.smackx.muc;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.Message;
+
+/**
+ * A listener that is fired anytime an invitation to join a MUC room is received.
+ * 
+ * @author Gaston Dombiak
+ */
+public interface InvitationListener {
+
+    /**
+     * Called when the an invitation to join a MUC room is received.<p>
+     * 
+     * If the room is password-protected, the invitee will receive a password to use to join
+     * the room. If the room is members-only, the the invitee may be added to the member list.
+     * 
+     * @param conn the Connection that received the invitation.
+     * @param room the room that invitation refers to.
+     * @param inviter the inviter that sent the invitation. (e.g. crone1@shakespeare.lit).
+     * @param reason the reason why the inviter sent the invitation.
+     * @param password the password to use when joining the room.
+     * @param message the message used by the inviter to send the invitation.
+     */
+    public abstract void invitationReceived(Connection conn, String room, String inviter, String reason,
+                                            String password, Message message);
+
+}
diff --git a/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java b/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java
new file mode 100644
index 0000000..1580c6f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/InvitationRejectionListener.java
@@ -0,0 +1,38 @@
+/**
+ * $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.smackx.muc;
+
+/**
+ * A listener that is fired anytime an invitee declines or rejects an invitation.
+ * 
+ * @author Gaston Dombiak
+ */
+public interface InvitationRejectionListener {
+
+    /**
+     * Called when the invitee declines the invitation.
+     * 
+     * @param invitee the invitee that declined the invitation. (e.g. hecate@shakespeare.lit).
+     * @param reason the reason why the invitee declined the invitation.
+     */
+    public abstract void invitationDeclined(String invitee, String reason);
+    
+}
diff --git a/src/org/jivesoftware/smackx/muc/MultiUserChat.java b/src/org/jivesoftware/smackx/muc/MultiUserChat.java
new file mode 100644
index 0000000..9a46444
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/MultiUserChat.java
@@ -0,0 +1,2743 @@
+/**
+ * $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.smackx.muc;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.jivesoftware.smack.Chat;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.MessageListener;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.PacketInterceptor;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.FromMatchesFilter;
+import org.jivesoftware.smack.filter.MessageTypeFilter;
+import org.jivesoftware.smack.filter.PacketExtensionFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+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.smackx.Form;
+import org.jivesoftware.smackx.NodeInformationProvider;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+import org.jivesoftware.smackx.packet.MUCAdmin;
+import org.jivesoftware.smackx.packet.MUCInitialPresence;
+import org.jivesoftware.smackx.packet.MUCOwner;
+import org.jivesoftware.smackx.packet.MUCUser;
+
+/**
+ * A MultiUserChat is a conversation that takes place among many users in a virtual
+ * room. A room could have many occupants with different affiliation and roles.
+ * Possible affiliatons are "owner", "admin", "member", and "outcast". Possible roles
+ * are "moderator", "participant", and "visitor". Each role and affiliation guarantees
+ * different privileges (e.g. Send messages to all occupants, Kick participants and visitors,
+ * Grant voice, Edit member list, etc.).
+ *
+ * @author Gaston Dombiak, Larry Kirschner
+ */
+public class MultiUserChat {
+
+    private final static String discoNamespace = "http://jabber.org/protocol/muc";
+    private final static String discoNode = "http://jabber.org/protocol/muc#rooms";
+
+    private static Map<Connection, List<String>> joinedRooms =
+            new WeakHashMap<Connection, List<String>>();
+
+    private Connection connection;
+    private String room;
+    private String subject;
+    private String nickname = null;
+    private boolean joined = false;
+    private Map<String, Presence> occupantsMap = new ConcurrentHashMap<String, Presence>();
+
+    private final List<InvitationRejectionListener> invitationRejectionListeners =
+            new ArrayList<InvitationRejectionListener>();
+    private final List<SubjectUpdatedListener> subjectUpdatedListeners =
+            new ArrayList<SubjectUpdatedListener>();
+    private final List<UserStatusListener> userStatusListeners =
+            new ArrayList<UserStatusListener>();
+    private final List<ParticipantStatusListener> participantStatusListeners =
+            new ArrayList<ParticipantStatusListener>();
+
+    private PacketFilter presenceFilter;
+    private List<PacketInterceptor> presenceInterceptors = new ArrayList<PacketInterceptor>();
+    private PacketFilter messageFilter;
+    private RoomListenerMultiplexor roomListenerMultiplexor;
+    private ConnectionDetachedPacketCollector messageCollector;
+    private List<PacketListener> connectionListeners = new ArrayList<PacketListener>();
+
+    static {
+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+            public void connectionCreated(final Connection connection) {
+                // Set on every established connection that this client supports the Multi-User
+                // Chat protocol. This information will be used when another client tries to
+                // discover whether this client supports MUC or not.
+                ServiceDiscoveryManager.getInstanceFor(connection).addFeature(discoNamespace);
+                // Set the NodeInformationProvider that will provide information about the
+                // joined rooms whenever a disco request is received
+                ServiceDiscoveryManager.getInstanceFor(connection).setNodeInformationProvider(
+                    discoNode,
+                    new NodeInformationProvider() {
+                        public List<DiscoverItems.Item> getNodeItems() {
+                            List<DiscoverItems.Item> answer = new ArrayList<DiscoverItems.Item>();
+                            Iterator<String> rooms=MultiUserChat.getJoinedRooms(connection);
+                            while (rooms.hasNext()) {
+                                answer.add(new DiscoverItems.Item(rooms.next()));
+                            }
+                            return answer;
+                        }
+
+                        public List<String> getNodeFeatures() {
+                            return null;
+                        }
+
+                        public List<DiscoverInfo.Identity> getNodeIdentities() {
+                            return null;
+                        }
+
+                        @Override
+                        public List<PacketExtension> getNodePacketExtensions() {
+                            return null;
+                        }
+                    });
+            }
+        });
+    }
+
+    /**
+     * Creates a new multi user chat with the specified connection and room name. Note: no
+     * information is sent to or received from the server until you attempt to
+     * {@link #join(String) join} the chat room. On some server implementations,
+     * the room will not be created until the first person joins it.<p>
+     *
+     * Most XMPP servers use a sub-domain for the chat service (eg chat.example.com
+     * for the XMPP server example.com). You must ensure that the room address you're
+     * trying to connect to includes the proper chat sub-domain.
+     *
+     * @param connection the XMPP connection.
+     * @param room the name of the room in the form "roomName@service", where
+     *      "service" is the hostname at which the multi-user chat
+     *      service is running. Make sure to provide a valid JID.
+     */
+    public MultiUserChat(Connection connection, String room) {
+        this.connection = connection;
+        this.room = room.toLowerCase();
+        init();
+    }
+
+    /**
+     * Returns true if the specified user supports the Multi-User Chat protocol.
+     *
+     * @param connection the connection to use to perform the service discovery.
+     * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
+     * @return a boolean indicating whether the specified user supports the MUC protocol.
+     */
+    public static boolean isServiceEnabled(Connection connection, String user) {
+        try {
+            DiscoverInfo result =
+                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(user);
+            return result.containsFeature(discoNamespace);
+        }
+        catch (XMPPException e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    /**
+     * Returns an Iterator on the rooms where the user has joined using a given connection.
+     * The Iterator will contain Strings where each String represents a room
+     * (e.g. room@muc.jabber.org).
+     *
+     * @param connection the connection used to join the rooms.
+     * @return an Iterator on the rooms where the user has joined using a given connection.
+     */
+    private static Iterator<String> getJoinedRooms(Connection connection) {
+        List<String> rooms = joinedRooms.get(connection);
+        if (rooms != null) {
+            return rooms.iterator();
+        }
+        // Return an iterator on an empty collection (i.e. the user never joined a room)
+        return new ArrayList<String>().iterator();
+    }
+
+    /**
+     * Returns an Iterator on the rooms where the requested user has joined. The Iterator will
+     * contain Strings where each String represents a room (e.g. room@muc.jabber.org).
+     *
+     * @param connection the connection to use to perform the service discovery.
+     * @param user the user to check. A fully qualified xmpp ID, e.g. jdoe@example.com.
+     * @return an Iterator on the rooms where the requested user has joined.
+     */
+    public static Iterator<String> getJoinedRooms(Connection connection, String user) {
+        try {
+            ArrayList<String> answer = new ArrayList<String>();
+            // Send the disco packet to the user
+            DiscoverItems result =
+                ServiceDiscoveryManager.getInstanceFor(connection).discoverItems(user, discoNode);
+            // Collect the entityID for each returned item
+            for (Iterator<DiscoverItems.Item> items=result.getItems(); items.hasNext();) {
+                answer.add(items.next().getEntityID());
+            }
+            return answer.iterator();
+        }
+        catch (XMPPException e) {
+            e.printStackTrace();
+            // Return an iterator on an empty collection
+            return new ArrayList<String>().iterator();
+        }
+    }
+
+    /**
+     * Returns the discovered information of a given room without actually having to join the room.
+     * The server will provide information only for rooms that are public.
+     *
+     * @param connection the XMPP connection to use for discovering information about the room.
+     * @param room the name of the room in the form "roomName@service" of which we want to discover
+     *        its information.
+     * @return the discovered information of a given room without actually having to join the room.
+     * @throws XMPPException if an error occured while trying to discover information of a room.
+     */
+    public static RoomInfo getRoomInfo(Connection connection, String room)
+            throws XMPPException {
+        DiscoverInfo info = ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(room);
+        return new RoomInfo(info);
+    }
+
+    /**
+     * Returns a collection with the XMPP addresses of the Multi-User Chat services.
+     *
+     * @param connection the XMPP connection to use for discovering Multi-User Chat services.
+     * @return a collection with the XMPP addresses of the Multi-User Chat services.
+     * @throws XMPPException if an error occured while trying to discover MUC services.
+     */
+    public static Collection<String> getServiceNames(Connection connection) throws XMPPException {
+        final List<String> answer = new ArrayList<String>();
+        ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
+        DiscoverItems items = discoManager.discoverItems(connection.getServiceName());
+        for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
+            DiscoverItems.Item item = it.next();
+            try {
+                DiscoverInfo info = discoManager.discoverInfo(item.getEntityID());
+                if (info.containsFeature("http://jabber.org/protocol/muc")) {
+                    answer.add(item.getEntityID());
+                }
+            }
+            catch (XMPPException e) {
+                // Trouble finding info in some cases. This is a workaround for
+                // discovering info on remote servers.
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * Returns a collection of HostedRooms where each HostedRoom has the XMPP address of the room
+     * and the room's name. Once discovered the rooms hosted by a chat service it is possible to
+     * discover more detailed room information or join the room.
+     *
+     * @param connection the XMPP connection to use for discovering hosted rooms by the MUC service.
+     * @param serviceName the service that is hosting the rooms to discover.
+     * @return a collection of HostedRooms.
+     * @throws XMPPException if an error occured while trying to discover the information.
+     */
+    public static Collection<HostedRoom> getHostedRooms(Connection connection, String serviceName)
+            throws XMPPException {
+        List<HostedRoom> answer = new ArrayList<HostedRoom>();
+        ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);
+        DiscoverItems items = discoManager.discoverItems(serviceName);
+        for (Iterator<DiscoverItems.Item> it = items.getItems(); it.hasNext();) {
+            answer.add(new HostedRoom(it.next()));
+        }
+        return answer;
+    }
+
+    /**
+     * Returns the name of the room this MultiUserChat object represents.
+     *
+     * @return the multi user chat room name.
+     */
+    public String getRoom() {
+        return room;
+    }
+
+    /**
+     * Creates the room according to some default configuration, assign the requesting user
+     * as the room owner, and add the owner to the room but not allow anyone else to enter
+     * the room (effectively "locking" the room). The requesting user will join the room
+     * under the specified nickname as soon as the room has been created.<p>
+     *
+     * To create an "Instant Room", that means a room with some default configuration that is
+     * available for immediate access, the room's owner should send an empty form after creating
+     * the room. {@link #sendConfigurationForm(Form)}<p>
+     *
+     * To create a "Reserved Room", that means a room manually configured by the room creator
+     * before anyone is allowed to enter, the room's owner should complete and send a form after
+     * creating the room. Once the completed configutation form is sent to the server, the server
+     * will unlock the room. {@link #sendConfigurationForm(Form)}
+     *
+     * @param nickname the nickname to use.
+     * @throws XMPPException if the room couldn't be created for some reason
+     *          (e.g. room already exists; user already joined to an existant room or
+     *          405 error if the user is not allowed to create the room)
+     */
+    public synchronized void create(String nickname) throws XMPPException {
+        if (nickname == null || nickname.equals("")) {
+            throw new IllegalArgumentException("Nickname must not be null or blank.");
+        }
+        // If we've already joined the room, leave it before joining under a new
+        // nickname.
+        if (joined) {
+            throw new IllegalStateException("Creation failed - User already joined the room.");
+        }
+        // We create a room by sending a presence packet to room@service/nick
+        // and signal support for MUC. The owner will be automatically logged into the room.
+        Presence joinPresence = new Presence(Presence.Type.available);
+        joinPresence.setTo(room + "/" + nickname);
+        // Indicate the the client supports MUC
+        joinPresence.addExtension(new MUCInitialPresence());
+        // Invoke presence interceptors so that extra information can be dynamically added
+        for (PacketInterceptor packetInterceptor : presenceInterceptors) {
+            packetInterceptor.interceptPacket(joinPresence);
+        }
+
+        // Wait for a presence packet back from the server.
+        PacketFilter responseFilter =
+            new AndFilter(
+                new FromMatchesFilter(room + "/" + nickname),
+                new PacketTypeFilter(Presence.class));
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send create & join packet.
+        connection.sendPacket(joinPresence);
+        // Wait up to a certain number of seconds for a reply.
+        Presence presence =
+            (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (presence == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (presence.getError() != null) {
+            throw new XMPPException(presence.getError());
+        }
+        // Whether the room existed before or was created, the user has joined the room
+        this.nickname = nickname;
+        joined = true;
+        userHasJoined();
+
+        // Look for confirmation of room creation from the server
+        MUCUser mucUser = getMUCUserExtension(presence);
+        if (mucUser != null && mucUser.getStatus() != null) {
+            if ("201".equals(mucUser.getStatus().getCode())) {
+                // Room was created and the user has joined the room
+                return;
+            }
+        }
+        // We need to leave the room since it seems that the room already existed
+        leave();
+        throw new XMPPException("Creation failed - Missing acknowledge of room creation.");
+    }
+
+    /**
+     * Joins the chat room using the specified nickname. If already joined
+     * using another nickname, this method will first leave the room and then
+     * re-join using the new nickname. The default timeout of Smack for a reply
+     * from the group chat server that the join succeeded will be used. After
+     * joining the room, the room will decide the amount of history to send.
+     *
+     * @param nickname the nickname to use.
+     * @throws XMPPException if an error occurs joining the room. In particular, a
+     *      401 error can occur if no password was provided and one is required; or a
+     *      403 error can occur if the user is banned; or a
+     *      404 error can occur if the room does not exist or is locked; or a
+     *      407 error can occur if user is not on the member list; or a
+     *      409 error can occur if someone is already in the group chat with the same nickname.
+     */
+    public void join(String nickname) throws XMPPException {
+        join(nickname, null, null, SmackConfiguration.getPacketReplyTimeout());
+    }
+
+    /**
+     * Joins the chat room using the specified nickname and password. If already joined
+     * using another nickname, this method will first leave the room and then
+     * re-join using the new nickname. The default timeout of Smack for a reply
+     * from the group chat server that the join succeeded will be used. After
+     * joining the room, the room will decide the amount of history to send.<p>
+     *
+     * A password is required when joining password protected rooms. If the room does
+     * not require a password there is no need to provide one.
+     *
+     * @param nickname the nickname to use.
+     * @param password the password to use.
+     * @throws XMPPException if an error occurs joining the room. In particular, a
+     *      401 error can occur if no password was provided and one is required; or a
+     *      403 error can occur if the user is banned; or a
+     *      404 error can occur if the room does not exist or is locked; or a
+     *      407 error can occur if user is not on the member list; or a
+     *      409 error can occur if someone is already in the group chat with the same nickname.
+     */
+    public void join(String nickname, String password) throws XMPPException {
+        join(nickname, password, null, SmackConfiguration.getPacketReplyTimeout());
+    }
+
+    /**
+     * Joins the chat room using the specified nickname and password. If already joined
+     * using another nickname, this method will first leave the room and then
+     * re-join using the new nickname.<p>
+     *
+     * To control the amount of history to receive while joining a room you will need to provide
+     * a configured DiscussionHistory object.<p>
+     *
+     * A password is required when joining password protected rooms. If the room does
+     * not require a password there is no need to provide one.<p>
+     *
+     * If the room does not already exist when the user seeks to enter it, the server will
+     * decide to create a new room or not.
+     *
+     * @param nickname the nickname to use.
+     * @param password the password to use.
+     * @param history the amount of discussion history to receive while joining a room.
+     * @param timeout the amount of time to wait for a reply from the MUC service(in milleseconds).
+     * @throws XMPPException if an error occurs joining the room. In particular, a
+     *      401 error can occur if no password was provided and one is required; or a
+     *      403 error can occur if the user is banned; or a
+     *      404 error can occur if the room does not exist or is locked; or a
+     *      407 error can occur if user is not on the member list; or a
+     *      409 error can occur if someone is already in the group chat with the same nickname.
+     */
+    public synchronized void join(
+        String nickname,
+        String password,
+        DiscussionHistory history,
+        long timeout)
+        throws XMPPException {
+        if (nickname == null || nickname.equals("")) {
+            throw new IllegalArgumentException("Nickname must not be null or blank.");
+        }
+        // If we've already joined the room, leave it before joining under a new
+        // nickname.
+        if (joined) {
+            leave();
+        }
+        // We join a room by sending a presence packet where the "to"
+        // field is in the form "roomName@service/nickname"
+        Presence joinPresence = new Presence(Presence.Type.available);
+        joinPresence.setTo(room + "/" + nickname);
+
+        // Indicate the the client supports MUC
+        MUCInitialPresence mucInitialPresence = new MUCInitialPresence();
+        if (password != null) {
+            mucInitialPresence.setPassword(password);
+        }
+        if (history != null) {
+            mucInitialPresence.setHistory(history.getMUCHistory());
+        }
+        joinPresence.addExtension(mucInitialPresence);
+        // Invoke presence interceptors so that extra information can be dynamically added
+        for (PacketInterceptor packetInterceptor : presenceInterceptors) {
+            packetInterceptor.interceptPacket(joinPresence);
+        }
+
+        // Wait for a presence packet back from the server.
+        PacketFilter responseFilter =
+                new AndFilter(
+                        new FromMatchesFilter(room + "/" + nickname),
+                        new PacketTypeFilter(Presence.class));
+        PacketCollector response = null;
+        Presence presence;
+        try {
+            response = connection.createPacketCollector(responseFilter);
+            // Send join packet.
+            connection.sendPacket(joinPresence);
+            // Wait up to a certain number of seconds for a reply.
+            presence = (Presence) response.nextResult(timeout);
+        }
+        finally {
+            // Stop queuing results
+            if (response != null) {
+                response.cancel();
+            }
+        }
+
+        if (presence == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (presence.getError() != null) {
+            throw new XMPPException(presence.getError());
+        }
+        this.nickname = nickname;
+        joined = true;
+        userHasJoined();
+    }
+
+    /**
+     * Returns true if currently in the multi user chat (after calling the {@link
+     * #join(String)} method).
+     *
+     * @return true if currently in the multi user chat room.
+     */
+    public boolean isJoined() {
+        return joined;
+    }
+
+    /**
+     * Leave the chat room.
+     */
+    public synchronized void leave() {
+        // If not joined already, do nothing.
+        if (!joined) {
+            return;
+        }
+        // We leave a room by sending a presence packet where the "to"
+        // field is in the form "roomName@service/nickname"
+        Presence leavePresence = new Presence(Presence.Type.unavailable);
+        leavePresence.setTo(room + "/" + nickname);
+        // Invoke presence interceptors so that extra information can be dynamically added
+        for (PacketInterceptor packetInterceptor : presenceInterceptors) {
+            packetInterceptor.interceptPacket(leavePresence);
+        }
+        connection.sendPacket(leavePresence);
+        // Reset occupant information.
+        occupantsMap.clear();
+        nickname = null;
+        joined = false;
+        userHasLeft();
+    }
+
+    /**
+     * Returns the room's configuration form that the room's owner can use or <tt>null</tt> if
+     * no configuration is possible. The configuration form allows to set the room's language,
+     * enable logging, specify room's type, etc..
+     *
+     * @return the Form that contains the fields to complete together with the instrucions or
+     * <tt>null</tt> if no configuration is possible.
+     * @throws XMPPException if an error occurs asking the configuration form for the room.
+     */
+    public Form getConfigurationForm() throws XMPPException {
+        MUCOwner iq = new MUCOwner();
+        iq.setTo(room);
+        iq.setType(IQ.Type.GET);
+
+        // Filter packets looking for an answer from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Request the configuration form to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+        return Form.getFormFrom(answer);
+    }
+
+    /**
+     * Sends the completed configuration form to the server. The room will be configured
+     * with the new settings defined in the form. If the form is empty then the server
+     * will create an instant room (will use default configuration).
+     *
+     * @param form the form with the new settings.
+     * @throws XMPPException if an error occurs setting the new rooms' configuration.
+     */
+    public void sendConfigurationForm(Form form) throws XMPPException {
+        MUCOwner iq = new MUCOwner();
+        iq.setTo(room);
+        iq.setType(IQ.Type.SET);
+        iq.addExtension(form.getDataFormToSend());
+
+        // Filter packets looking for an answer from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the completed configuration form to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+    }
+
+    /**
+     * Returns the room's registration form that an unaffiliated user, can use to become a member
+     * of the room or <tt>null</tt> if no registration is possible. Some rooms may restrict the
+     * privilege to register members and allow only room admins to add new members.<p>
+     *
+     * If the user requesting registration requirements is not allowed to register with the room
+     * (e.g. because that privilege has been restricted), the room will return a "Not Allowed"
+     * error to the user (error code 405).
+     *
+     * @return the registration Form that contains the fields to complete together with the
+     * instrucions or <tt>null</tt> if no registration is possible.
+     * @throws XMPPException if an error occurs asking the registration form for the room or a
+     * 405 error if the user is not allowed to register with the room.
+     */
+    public Form getRegistrationForm() throws XMPPException {
+        Registration reg = new Registration();
+        reg.setType(IQ.Type.GET);
+        reg.setTo(room);
+
+        PacketFilter filter =
+            new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
+        PacketCollector collector = connection.createPacketCollector(filter);
+        connection.sendPacket(reg);
+        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        collector.cancel();
+        if (result == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (result.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(result.getError());
+        }
+        return Form.getFormFrom(result);
+    }
+
+    /**
+     * Sends the completed registration form to the server. After the user successfully submits
+     * the form, the room may queue the request for review by the room admins or may immediately
+     * add the user to the member list by changing the user's affiliation from "none" to "member.<p>
+     *
+     * If the desired room nickname is already reserved for that room, the room will return a
+     * "Conflict" error to the user (error code 409). If the room does not support registration,
+     * it will return a "Service Unavailable" error to the user (error code 503).
+     *
+     * @param form the completed registration form.
+     * @throws XMPPException if an error occurs submitting the registration form. In particular, a
+     *      409 error can occur if the desired room nickname is already reserved for that room;
+     *      or a 503 error can occur if the room does not support registration.
+     */
+    public void sendRegistrationForm(Form form) throws XMPPException {
+        Registration reg = new Registration();
+        reg.setType(IQ.Type.SET);
+        reg.setTo(room);
+        reg.addExtension(form.getDataFormToSend());
+
+        PacketFilter filter =
+            new AndFilter(new PacketIDFilter(reg.getPacketID()), new PacketTypeFilter(IQ.class));
+        PacketCollector collector = connection.createPacketCollector(filter);
+        connection.sendPacket(reg);
+        IQ result = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        collector.cancel();
+        if (result == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (result.getType() == IQ.Type.ERROR) {
+            throw new XMPPException(result.getError());
+        }
+    }
+
+    /**
+     * Sends a request to the server to destroy the room. The sender of the request
+     * should be the room's owner. If the sender of the destroy request is not the room's owner
+     * then the server will answer a "Forbidden" error (403).
+     *
+     * @param reason the reason for the room destruction.
+     * @param alternateJID the JID of an alternate location.
+     * @throws XMPPException if an error occurs while trying to destroy the room.
+     *      An error can occur which will be wrapped by an XMPPException --
+     *      XMPP error code 403. The error code can be used to present more
+     *      appropiate error messages to end-users.
+     */
+    public void destroy(String reason, String alternateJID) throws XMPPException {
+        MUCOwner iq = new MUCOwner();
+        iq.setTo(room);
+        iq.setType(IQ.Type.SET);
+
+        // Create the reason for the room destruction
+        MUCOwner.Destroy destroy = new MUCOwner.Destroy();
+        destroy.setReason(reason);
+        destroy.setJid(alternateJID);
+        iq.setDestroy(destroy);
+
+        // Wait for a presence packet back from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the room destruction request.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+        // Reset occupant information.
+        occupantsMap.clear();
+        nickname = null;
+        joined = false;
+        userHasLeft();
+    }
+
+    /**
+     * Invites another user to the room in which one is an occupant. The invitation
+     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
+     *
+     * If the room is password-protected, the invitee will receive a password to use to join
+     * the room. If the room is members-only, the the invitee may be added to the member list.
+     *
+     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
+     * @param reason the reason why the user is being invited.
+     */
+    public void invite(String user, String reason) {
+        invite(new Message(), user, reason);
+    }
+
+    /**
+     * Invites another user to the room in which one is an occupant using a given Message. The invitation
+     * will be sent to the room which in turn will forward the invitation to the invitee.<p>
+     *
+     * If the room is password-protected, the invitee will receive a password to use to join
+     * the room. If the room is members-only, the the invitee may be added to the member list.
+     *
+     * @param message the message to use for sending the invitation.
+     * @param user the user to invite to the room.(e.g. hecate@shakespeare.lit)
+     * @param reason the reason why the user is being invited.
+     */
+    public void invite(Message message, String user, String reason) {
+        // TODO listen for 404 error code when inviter supplies a non-existent JID
+        message.setTo(room);
+
+        // Create the MUCUser packet that will include the invitation
+        MUCUser mucUser = new MUCUser();
+        MUCUser.Invite invite = new MUCUser.Invite();
+        invite.setTo(user);
+        invite.setReason(reason);
+        mucUser.setInvite(invite);
+        // Add the MUCUser packet that includes the invitation to the message
+        message.addExtension(mucUser);
+
+        connection.sendPacket(message);
+    }
+
+    /**
+     * Informs the sender of an invitation that the invitee declines the invitation. The rejection
+     * will be sent to the room which in turn will forward the rejection to the inviter.
+     *
+     * @param conn the connection to use for sending the rejection.
+     * @param room the room that sent the original invitation.
+     * @param inviter the inviter of the declined invitation.
+     * @param reason the reason why the invitee is declining the invitation.
+     */
+    public static void decline(Connection conn, String room, String inviter, String reason) {
+        Message message = new Message(room);
+
+        // Create the MUCUser packet that will include the rejection
+        MUCUser mucUser = new MUCUser();
+        MUCUser.Decline decline = new MUCUser.Decline();
+        decline.setTo(inviter);
+        decline.setReason(reason);
+        mucUser.setDecline(decline);
+        // Add the MUCUser packet that includes the rejection
+        message.addExtension(mucUser);
+
+        conn.sendPacket(message);
+    }
+
+    /**
+     * Adds a listener to invitation notifications. The listener will be fired anytime
+     * an invitation is received.
+     *
+     * @param conn the connection where the listener will be applied.
+     * @param listener an invitation listener.
+     */
+    public static void addInvitationListener(Connection conn, InvitationListener listener) {
+        InvitationsMonitor.getInvitationsMonitor(conn).addInvitationListener(listener);
+    }
+
+    /**
+     * Removes a listener to invitation notifications. The listener will be fired anytime
+     * an invitation is received.
+     *
+     * @param conn the connection where the listener was applied.
+     * @param listener an invitation listener.
+     */
+    public static void removeInvitationListener(Connection conn, InvitationListener listener) {
+        InvitationsMonitor.getInvitationsMonitor(conn).removeInvitationListener(listener);
+    }
+
+    /**
+     * Adds a listener to invitation rejections notifications. The listener will be fired anytime
+     * an invitation is declined.
+     *
+     * @param listener an invitation rejection listener.
+     */
+    public void addInvitationRejectionListener(InvitationRejectionListener listener) {
+        synchronized (invitationRejectionListeners) {
+            if (!invitationRejectionListeners.contains(listener)) {
+                invitationRejectionListeners.add(listener);
+            }
+        }
+    }
+
+    /**
+     * Removes a listener from invitation rejections notifications. The listener will be fired
+     * anytime an invitation is declined.
+     *
+     * @param listener an invitation rejection listener.
+     */
+    public void removeInvitationRejectionListener(InvitationRejectionListener listener) {
+        synchronized (invitationRejectionListeners) {
+            invitationRejectionListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Fires invitation rejection listeners.
+     *
+     * @param invitee the user being invited.
+     * @param reason the reason for the rejection
+     */
+    private void fireInvitationRejectionListeners(String invitee, String reason) {
+        InvitationRejectionListener[] listeners;
+        synchronized (invitationRejectionListeners) {
+            listeners = new InvitationRejectionListener[invitationRejectionListeners.size()];
+            invitationRejectionListeners.toArray(listeners);
+        }
+        for (InvitationRejectionListener listener : listeners) {
+            listener.invitationDeclined(invitee, reason);
+        }
+    }
+
+    /**
+     * Adds a listener to subject change notifications. The listener will be fired anytime
+     * the room's subject changes.
+     *
+     * @param listener a subject updated listener.
+     */
+    public void addSubjectUpdatedListener(SubjectUpdatedListener listener) {
+        synchronized (subjectUpdatedListeners) {
+            if (!subjectUpdatedListeners.contains(listener)) {
+                subjectUpdatedListeners.add(listener);
+            }
+        }
+    }
+
+    /**
+     * Removes a listener from subject change notifications. The listener will be fired
+     * anytime the room's subject changes.
+     *
+     * @param listener a subject updated listener.
+     */
+    public void removeSubjectUpdatedListener(SubjectUpdatedListener listener) {
+        synchronized (subjectUpdatedListeners) {
+            subjectUpdatedListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Fires subject updated listeners.
+     */
+    private void fireSubjectUpdatedListeners(String subject, String from) {
+        SubjectUpdatedListener[] listeners;
+        synchronized (subjectUpdatedListeners) {
+            listeners = new SubjectUpdatedListener[subjectUpdatedListeners.size()];
+            subjectUpdatedListeners.toArray(listeners);
+        }
+        for (SubjectUpdatedListener listener : listeners) {
+            listener.subjectUpdated(subject, from);
+        }
+    }
+
+    /**
+     * Adds a new {@link PacketInterceptor} that will be invoked every time a new presence
+     * is going to be sent by this MultiUserChat to the server. Packet interceptors may
+     * add new extensions to the presence that is going to be sent to the MUC service.
+     *
+     * @param presenceInterceptor the new packet interceptor that will intercept presence packets.
+     */
+    public void addPresenceInterceptor(PacketInterceptor presenceInterceptor) {
+        presenceInterceptors.add(presenceInterceptor);
+    }
+
+    /**
+     * Removes a {@link PacketInterceptor} that was being invoked every time a new presence
+     * was being sent by this MultiUserChat to the server. Packet interceptors may
+     * add new extensions to the presence that is going to be sent to the MUC service.
+     *
+     * @param presenceInterceptor the packet interceptor to remove.
+     */
+    public void removePresenceInterceptor(PacketInterceptor presenceInterceptor) {
+        presenceInterceptors.remove(presenceInterceptor);
+    }
+
+    /**
+     * Returns the last known room's subject or <tt>null</tt> if the user hasn't joined the room
+     * or the room does not have a subject yet. In case the room has a subject, as soon as the
+     * user joins the room a message with the current room's subject will be received.<p>
+     *
+     * To be notified every time the room's subject change you should add a listener
+     * to this room. {@link #addSubjectUpdatedListener(SubjectUpdatedListener)}<p>
+     *
+     * To change the room's subject use {@link #changeSubject(String)}.
+     *
+     * @return the room's subject or <tt>null</tt> if the user hasn't joined the room or the
+     * room does not have a subject yet.
+     */
+    public String getSubject() {
+        return subject;
+    }
+
+    /**
+     * Returns the reserved room nickname for the user in the room. A user may have a reserved
+     * nickname, for example through explicit room registration or database integration. In such
+     * cases it may be desirable for the user to discover the reserved nickname before attempting
+     * to enter the room.
+     *
+     * @return the reserved room nickname or <tt>null</tt> if none.
+     */
+    public String getReservedNickname() {
+        try {
+            DiscoverInfo result =
+                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(
+                    room,
+                    "x-roomuser-item");
+            // Look for an Identity that holds the reserved nickname and return its name
+            for (Iterator<DiscoverInfo.Identity> identities = result.getIdentities();
+                 identities.hasNext();) {
+                DiscoverInfo.Identity identity = identities.next();
+                return identity.getName();
+            }
+            // If no Identity was found then the user does not have a reserved room nickname
+            return null;
+        }
+        catch (XMPPException e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * Returns the nickname that was used to join the room, or <tt>null</tt> if not
+     * currently joined.
+     *
+     * @return the nickname currently being used.
+     */
+    public String getNickname() {
+        return nickname;
+    }
+
+    /**
+     * Changes the occupant's nickname to a new nickname within the room. Each room occupant
+     * will receive two presence packets. One of type "unavailable" for the old nickname and one
+     * indicating availability for the new nickname. The unavailable presence will contain the new
+     * nickname and an appropriate status code (namely 303) as extended presence information. The
+     * status code 303 indicates that the occupant is changing his/her nickname.
+     *
+     * @param nickname the new nickname within the room.
+     * @throws XMPPException if the new nickname is already in use by another occupant.
+     */
+    public void changeNickname(String nickname) throws XMPPException {
+        if (nickname == null || nickname.equals("")) {
+            throw new IllegalArgumentException("Nickname must not be null or blank.");
+        }
+        // Check that we already have joined the room before attempting to change the
+        // nickname.
+        if (!joined) {
+            throw new IllegalStateException("Must be logged into the room to change nickname.");
+        }
+        // We change the nickname by sending a presence packet where the "to"
+        // field is in the form "roomName@service/nickname"
+        // We don't have to signal the MUC support again
+        Presence joinPresence = new Presence(Presence.Type.available);
+        joinPresence.setTo(room + "/" + nickname);
+        // Invoke presence interceptors so that extra information can be dynamically added
+        for (PacketInterceptor packetInterceptor : presenceInterceptors) {
+            packetInterceptor.interceptPacket(joinPresence);
+        }
+
+        // Wait for a presence packet back from the server.
+        PacketFilter responseFilter =
+            new AndFilter(
+                new FromMatchesFilter(room + "/" + nickname),
+                new PacketTypeFilter(Presence.class));
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send join packet.
+        connection.sendPacket(joinPresence);
+        // Wait up to a certain number of seconds for a reply.
+        Presence presence =
+            (Presence) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (presence == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (presence.getError() != null) {
+            throw new XMPPException(presence.getError());
+        }
+        this.nickname = nickname;
+    }
+
+    /**
+     * Changes the occupant's availability status within the room. The presence type
+     * will remain available but with a new status that describes the presence update and
+     * a new presence mode (e.g. Extended away).
+     *
+     * @param status a text message describing the presence update.
+     * @param mode the mode type for the presence update.
+     */
+    public void changeAvailabilityStatus(String status, Presence.Mode mode) {
+        if (nickname == null || nickname.equals("")) {
+            throw new IllegalArgumentException("Nickname must not be null or blank.");
+        }
+        // Check that we already have joined the room before attempting to change the
+        // availability status.
+        if (!joined) {
+            throw new IllegalStateException(
+                "Must be logged into the room to change the " + "availability status.");
+        }
+        // We change the availability status by sending a presence packet to the room with the
+        // new presence status and mode
+        Presence joinPresence = new Presence(Presence.Type.available);
+        joinPresence.setStatus(status);
+        joinPresence.setMode(mode);
+        joinPresence.setTo(room + "/" + nickname);
+        // Invoke presence interceptors so that extra information can be dynamically added
+        for (PacketInterceptor packetInterceptor : presenceInterceptors) {
+            packetInterceptor.interceptPacket(joinPresence);
+        }
+
+        // Send join packet.
+        connection.sendPacket(joinPresence);
+    }
+
+    /**
+     * Kicks a visitor or participant from the room. The kicked occupant will receive a presence
+     * of type "unavailable" including a status code 307 and optionally along with the reason
+     * (if provided) and the bare JID of the user who initiated the kick. After the occupant
+     * was kicked from the room, the rest of the occupants will receive a presence of type
+     * "unavailable". The presence will include a status code 307 which means that the occupant
+     * was kicked from the room.
+     *
+     * @param nickname the nickname of the participant or visitor to kick from the room
+     * (e.g. "john").
+     * @param reason the reason why the participant or visitor is being kicked from the room.
+     * @throws XMPPException if an error occurs kicking the occupant. In particular, a
+     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
+     *      was intended to be kicked (i.e. Not Allowed error); or a
+     *      403 error can occur if the occupant that intended to kick another occupant does
+     *      not have kicking privileges (i.e. Forbidden error); or a
+     *      400 error can occur if the provided nickname is not present in the room.
+     */
+    public void kickParticipant(String nickname, String reason) throws XMPPException {
+        changeRole(nickname, "none", reason);
+    }
+
+    /**
+     * Grants voice to visitors in the room. In a moderated room, a moderator may want to manage
+     * who does and does not have "voice" in the room. To have voice means that a room occupant
+     * is able to send messages to the room occupants.
+     *
+     * @param nicknames the nicknames of the visitors to grant voice in the room (e.g. "john").
+     * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
+     *      403 error can occur if the occupant that intended to grant voice is not
+     *      a moderator in this room (i.e. Forbidden error); or a
+     *      400 error can occur if the provided nickname is not present in the room.
+     */
+    public void grantVoice(Collection<String> nicknames) throws XMPPException {
+        changeRole(nicknames, "participant");
+    }
+
+    /**
+     * Grants voice to a visitor in the room. In a moderated room, a moderator may want to manage
+     * who does and does not have "voice" in the room. To have voice means that a room occupant
+     * is able to send messages to the room occupants.
+     *
+     * @param nickname the nickname of the visitor to grant voice in the room (e.g. "john").
+     * @throws XMPPException if an error occurs granting voice to a visitor. In particular, a
+     *      403 error can occur if the occupant that intended to grant voice is not
+     *      a moderator in this room (i.e. Forbidden error); or a
+     *      400 error can occur if the provided nickname is not present in the room.
+     */
+    public void grantVoice(String nickname) throws XMPPException {
+        changeRole(nickname, "participant", null);
+    }
+
+    /**
+     * Revokes voice from participants in the room. In a moderated room, a moderator may want to
+     * revoke an occupant's privileges to speak. To have voice means that a room occupant
+     * is able to send messages to the room occupants.
+     *
+     * @param nicknames the nicknames of the participants to revoke voice (e.g. "john").
+     * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
+     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
+     *      was tried to revoke his voice (i.e. Not Allowed error); or a
+     *      400 error can occur if the provided nickname is not present in the room.
+     */
+    public void revokeVoice(Collection<String> nicknames) throws XMPPException {
+        changeRole(nicknames, "visitor");
+    }
+
+    /**
+     * Revokes voice from a participant in the room. In a moderated room, a moderator may want to
+     * revoke an occupant's privileges to speak. To have voice means that a room occupant
+     * is able to send messages to the room occupants.
+     *
+     * @param nickname the nickname of the participant to revoke voice (e.g. "john").
+     * @throws XMPPException if an error occurs revoking voice from a participant. In particular, a
+     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
+     *      was tried to revoke his voice (i.e. Not Allowed error); or a
+     *      400 error can occur if the provided nickname is not present in the room.
+     */
+    public void revokeVoice(String nickname) throws XMPPException {
+        changeRole(nickname, "visitor", null);
+    }
+
+    /**
+     * Bans users from the room. An admin or owner of the room can ban users from a room. This
+     * means that the banned user will no longer be able to join the room unless the ban has been
+     * removed. If the banned user was present in the room then he/she will be removed from the
+     * room and notified that he/she was banned along with the reason (if provided) and the bare
+     * XMPP user ID of the user who initiated the ban.
+     *
+     * @param jids the bare XMPP user IDs of the users to ban.
+     * @throws XMPPException if an error occurs banning a user. In particular, a
+     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
+     *      was tried to be banned (i.e. Not Allowed error).
+     */
+    public void banUsers(Collection<String> jids) throws XMPPException {
+        changeAffiliationByAdmin(jids, "outcast");
+    }
+
+    /**
+     * Bans a user from the room. An admin or owner of the room can ban users from a room. This
+     * means that the banned user will no longer be able to join the room unless the ban has been
+     * removed. If the banned user was present in the room then he/she will be removed from the
+     * room and notified that he/she was banned along with the reason (if provided) and the bare
+     * XMPP user ID of the user who initiated the ban.
+     *
+     * @param jid the bare XMPP user ID of the user to ban (e.g. "user@host.org").
+     * @param reason the optional reason why the user was banned.
+     * @throws XMPPException if an error occurs banning a user. In particular, a
+     *      405 error can occur if a moderator or a user with an affiliation of "owner" or "admin"
+     *      was tried to be banned (i.e. Not Allowed error).
+     */
+    public void banUser(String jid, String reason) throws XMPPException {
+        changeAffiliationByAdmin(jid, "outcast", reason);
+    }
+
+    /**
+     * Grants membership to other users. Only administrators are able to grant membership. A user
+     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
+     * that a user cannot enter without being on the member list).
+     *
+     * @param jids the XMPP user IDs of the users to grant membership.
+     * @throws XMPPException if an error occurs granting membership to a user.
+     */
+    public void grantMembership(Collection<String> jids) throws XMPPException {
+        changeAffiliationByAdmin(jids, "member");
+    }
+
+    /**
+     * Grants membership to a user. Only administrators are able to grant membership. A user
+     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
+     * that a user cannot enter without being on the member list).
+     *
+     * @param jid the XMPP user ID of the user to grant membership (e.g. "user@host.org").
+     * @throws XMPPException if an error occurs granting membership to a user.
+     */
+    public void grantMembership(String jid) throws XMPPException {
+        changeAffiliationByAdmin(jid, "member", null);
+    }
+
+    /**
+     * Revokes users' membership. Only administrators are able to revoke membership. A user
+     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
+     * that a user cannot enter without being on the member list). If the user is in the room and
+     * the room is of type members-only then the user will be removed from the room.
+     *
+     * @param jids the bare XMPP user IDs of the users to revoke membership.
+     * @throws XMPPException if an error occurs revoking membership to a user.
+     */
+    public void revokeMembership(Collection<String> jids) throws XMPPException {
+        changeAffiliationByAdmin(jids, "none");
+    }
+
+    /**
+     * Revokes a user's membership. Only administrators are able to revoke membership. A user
+     * that becomes a room member will be able to enter a room of type Members-Only (i.e. a room
+     * that a user cannot enter without being on the member list). If the user is in the room and
+     * the room is of type members-only then the user will be removed from the room.
+     *
+     * @param jid the bare XMPP user ID of the user to revoke membership (e.g. "user@host.org").
+     * @throws XMPPException if an error occurs revoking membership to a user.
+     */
+    public void revokeMembership(String jid) throws XMPPException {
+        changeAffiliationByAdmin(jid, "none", null);
+    }
+
+    /**
+     * Grants moderator privileges to participants or visitors. Room administrators may grant
+     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
+     * other users, modify room's subject plus all the partcipants privileges.
+     *
+     * @param nicknames the nicknames of the occupants to grant moderator privileges.
+     * @throws XMPPException if an error occurs granting moderator privileges to a user.
+     */
+    public void grantModerator(Collection<String> nicknames) throws XMPPException {
+        changeRole(nicknames, "moderator");
+    }
+
+    /**
+     * Grants moderator privileges to a participant or visitor. Room administrators may grant
+     * moderator privileges. A moderator is allowed to kick users, grant and revoke voice, invite
+     * other users, modify room's subject plus all the partcipants privileges.
+     *
+     * @param nickname the nickname of the occupant to grant moderator privileges.
+     * @throws XMPPException if an error occurs granting moderator privileges to a user.
+     */
+    public void grantModerator(String nickname) throws XMPPException {
+        changeRole(nickname, "moderator", null);
+    }
+
+    /**
+     * Revokes moderator privileges from other users. The occupant that loses moderator
+     * privileges will become a participant. Room administrators may revoke moderator privileges
+     * only to occupants whose affiliation is member or none. This means that an administrator is
+     * not allowed to revoke moderator privileges from other room administrators or owners.
+     *
+     * @param nicknames the nicknames of the occupants to revoke moderator privileges.
+     * @throws XMPPException if an error occurs revoking moderator privileges from a user.
+     */
+    public void revokeModerator(Collection<String> nicknames) throws XMPPException {
+        changeRole(nicknames, "participant");
+    }
+
+    /**
+     * Revokes moderator privileges from another user. The occupant that loses moderator
+     * privileges will become a participant. Room administrators may revoke moderator privileges
+     * only to occupants whose affiliation is member or none. This means that an administrator is
+     * not allowed to revoke moderator privileges from other room administrators or owners.
+     *
+     * @param nickname the nickname of the occupant to revoke moderator privileges.
+     * @throws XMPPException if an error occurs revoking moderator privileges from a user.
+     */
+    public void revokeModerator(String nickname) throws XMPPException {
+        changeRole(nickname, "participant", null);
+    }
+
+    /**
+     * Grants ownership privileges to other users. Room owners may grant ownership privileges.
+     * Some room implementations will not allow to grant ownership privileges to other users.
+     * An owner is allowed to change defining room features as well as perform all administrative
+     * functions.
+     *
+     * @param jids the collection of bare XMPP user IDs of the users to grant ownership.
+     * @throws XMPPException if an error occurs granting ownership privileges to a user.
+     */
+    public void grantOwnership(Collection<String> jids) throws XMPPException {
+        changeAffiliationByAdmin(jids, "owner");
+    }
+
+    /**
+     * Grants ownership privileges to another user. Room owners may grant ownership privileges.
+     * Some room implementations will not allow to grant ownership privileges to other users.
+     * An owner is allowed to change defining room features as well as perform all administrative
+     * functions.
+     *
+     * @param jid the bare XMPP user ID of the user to grant ownership (e.g. "user@host.org").
+     * @throws XMPPException if an error occurs granting ownership privileges to a user.
+     */
+    public void grantOwnership(String jid) throws XMPPException {
+        changeAffiliationByAdmin(jid, "owner", null);
+    }
+
+    /**
+     * Revokes ownership privileges from other users. The occupant that loses ownership
+     * privileges will become an administrator. Room owners may revoke ownership privileges.
+     * Some room implementations will not allow to grant ownership privileges to other users.
+     *
+     * @param jids the bare XMPP user IDs of the users to revoke ownership.
+     * @throws XMPPException if an error occurs revoking ownership privileges from a user.
+     */
+    public void revokeOwnership(Collection<String> jids) throws XMPPException {
+        changeAffiliationByAdmin(jids, "admin");
+    }
+
+    /**
+     * Revokes ownership privileges from another user. The occupant that loses ownership
+     * privileges will become an administrator. Room owners may revoke ownership privileges.
+     * Some room implementations will not allow to grant ownership privileges to other users.
+     *
+     * @param jid the bare XMPP user ID of the user to revoke ownership (e.g. "user@host.org").
+     * @throws XMPPException if an error occurs revoking ownership privileges from a user.
+     */
+    public void revokeOwnership(String jid) throws XMPPException {
+        changeAffiliationByAdmin(jid, "admin", null);
+    }
+
+    /**
+     * Grants administrator privileges to other users. Room owners may grant administrator
+     * privileges to a member or unaffiliated user. An administrator is allowed to perform
+     * administrative functions such as banning users and edit moderator list.
+     *
+     * @param jids the bare XMPP user IDs of the users to grant administrator privileges.
+     * @throws XMPPException if an error occurs granting administrator privileges to a user.
+     */
+    public void grantAdmin(Collection<String> jids) throws XMPPException {
+        changeAffiliationByOwner(jids, "admin");
+    }
+
+    /**
+     * Grants administrator privileges to another user. Room owners may grant administrator
+     * privileges to a member or unaffiliated user. An administrator is allowed to perform
+     * administrative functions such as banning users and edit moderator list.
+     *
+     * @param jid the bare XMPP user ID of the user to grant administrator privileges
+     * (e.g. "user@host.org").
+     * @throws XMPPException if an error occurs granting administrator privileges to a user.
+     */
+    public void grantAdmin(String jid) throws XMPPException {
+        changeAffiliationByOwner(jid, "admin");
+    }
+
+    /**
+     * Revokes administrator privileges from users. The occupant that loses administrator
+     * privileges will become a member. Room owners may revoke administrator privileges from
+     * a member or unaffiliated user.
+     *
+     * @param jids the bare XMPP user IDs of the user to revoke administrator privileges.
+     * @throws XMPPException if an error occurs revoking administrator privileges from a user.
+     */
+    public void revokeAdmin(Collection<String> jids) throws XMPPException {
+        changeAffiliationByOwner(jids, "member");
+    }
+
+    /**
+     * Revokes administrator privileges from a user. The occupant that loses administrator
+     * privileges will become a member. Room owners may revoke administrator privileges from
+     * a member or unaffiliated user.
+     *
+     * @param jid the bare XMPP user ID of the user to revoke administrator privileges
+     * (e.g. "user@host.org").
+     * @throws XMPPException if an error occurs revoking administrator privileges from a user.
+     */
+    public void revokeAdmin(String jid) throws XMPPException {
+        changeAffiliationByOwner(jid, "member");
+    }
+
+    private void changeAffiliationByOwner(String jid, String affiliation) throws XMPPException {
+        MUCOwner iq = new MUCOwner();
+        iq.setTo(room);
+        iq.setType(IQ.Type.SET);
+        // Set the new affiliation.
+        MUCOwner.Item item = new MUCOwner.Item(affiliation);
+        item.setJid(jid);
+        iq.addItem(item);
+
+        // Wait for a response packet back from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the change request to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+    }
+
+    private void changeAffiliationByOwner(Collection<String> jids, String affiliation)
+            throws XMPPException {
+        MUCOwner iq = new MUCOwner();
+        iq.setTo(room);
+        iq.setType(IQ.Type.SET);
+        for (String jid : jids) {
+            // Set the new affiliation.
+            MUCOwner.Item item = new MUCOwner.Item(affiliation);
+            item.setJid(jid);
+            iq.addItem(item);
+        }
+
+        // Wait for a response packet back from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the change request to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+    }
+
+    /**
+     * Tries to change the affiliation with an 'muc#admin' namespace
+     *
+     * @param jid
+     * @param affiliation
+     * @param reason the reason for the affiliation change (optional)
+     * @throws XMPPException
+     */
+    private void changeAffiliationByAdmin(String jid, String affiliation, String reason)
+            throws XMPPException {
+        MUCAdmin iq = new MUCAdmin();
+        iq.setTo(room);
+        iq.setType(IQ.Type.SET);
+        // Set the new affiliation.
+        MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
+        item.setJid(jid);
+        if(reason != null)
+            item.setReason(reason);
+        iq.addItem(item);
+
+        // Wait for a response packet back from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the change request to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+    }
+
+    private void changeAffiliationByAdmin(Collection<String> jids, String affiliation)
+            throws XMPPException {
+        MUCAdmin iq = new MUCAdmin();
+        iq.setTo(room);
+        iq.setType(IQ.Type.SET);
+        for (String jid : jids) {
+            // Set the new affiliation.
+            MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
+            item.setJid(jid);
+            iq.addItem(item);
+        }
+
+        // Wait for a response packet back from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the change request to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+    }
+
+    private void changeRole(String nickname, String role, String reason) throws XMPPException {
+        MUCAdmin iq = new MUCAdmin();
+        iq.setTo(room);
+        iq.setType(IQ.Type.SET);
+        // Set the new role.
+        MUCAdmin.Item item = new MUCAdmin.Item(null, role);
+        item.setNick(nickname);
+        item.setReason(reason);
+        iq.addItem(item);
+
+        // Wait for a response packet back from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the change request to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+    }
+
+    private void changeRole(Collection<String> nicknames, String role) throws XMPPException {
+        MUCAdmin iq = new MUCAdmin();
+        iq.setTo(room);
+        iq.setType(IQ.Type.SET);
+        for (String nickname : nicknames) {
+            // Set the new role.
+            MUCAdmin.Item item = new MUCAdmin.Item(null, role);
+            item.setNick(nickname);
+            iq.addItem(item);
+        }
+
+        // Wait for a response packet back from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the change request to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        IQ answer = (IQ) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+    }
+
+    /**
+     * Returns the number of occupants in the group chat.<p>
+     *
+     * Note: this value will only be accurate after joining the group chat, and
+     * may fluctuate over time. If you query this value directly after joining the
+     * group chat it may not be accurate, as it takes a certain amount of time for
+     * the server to send all presence packets to this client.
+     *
+     * @return the number of occupants in the group chat.
+     */
+    public int getOccupantsCount() {
+        return occupantsMap.size();
+    }
+
+    /**
+     * Returns an Iterator (of Strings) for the list of fully qualified occupants
+     * in the group chat. For example, "conference@chat.jivesoftware.com/SomeUser".
+     * Typically, a client would only display the nickname of the occupant. To
+     * get the nickname from the fully qualified name, use the
+     * {@link org.jivesoftware.smack.util.StringUtils#parseResource(String)} method.
+     * Note: this value will only be accurate after joining the group chat, and may
+     * fluctuate over time.
+     *
+     * @return an Iterator for the occupants in the group chat.
+     */
+    public Iterator<String> getOccupants() {
+        return Collections.unmodifiableList(new ArrayList<String>(occupantsMap.keySet()))
+                .iterator();
+    }
+
+    /**
+     * Returns the presence info for a particular user, or <tt>null</tt> if the user
+     * is not in the room.<p>
+     *
+     * @param user the room occupant to search for his presence. The format of user must
+     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
+     * @return the occupant's current presence, or <tt>null</tt> if the user is unavailable
+     *      or if no presence information is available.
+     */
+    public Presence getOccupantPresence(String user) {
+        return occupantsMap.get(user);
+    }
+
+    /**
+     * Returns the Occupant information for a particular occupant, or <tt>null</tt> if the
+     * user is not in the room. The Occupant object may include information such as full
+     * JID of the user as well as the role and affiliation of the user in the room.<p>
+     *
+     * @param user the room occupant to search for his presence. The format of user must
+     * be: roomName@service/nickname (e.g. darkcave@macbeth.shakespeare.lit/thirdwitch).
+     * @return the Occupant or <tt>null</tt> if the user is unavailable (i.e. not in the room).
+     */
+    public Occupant getOccupant(String user) {
+        Presence presence = occupantsMap.get(user);
+        if (presence != null) {
+            return new Occupant(presence);
+        }
+        return null;
+    }
+
+    /**
+     * Adds a packet listener that will be notified of any new Presence packets
+     * sent to the group chat. Using a listener is a suitable way to know when the list
+     * of occupants should be re-loaded due to any changes.
+     *
+     * @param listener a packet listener that will be notified of any presence packets
+     *      sent to the group chat.
+     */
+    public void addParticipantListener(PacketListener listener) {
+        connection.addPacketListener(listener, presenceFilter);
+        connectionListeners.add(listener);
+    }
+
+    /**
+     * Remoces a packet listener that was being notified of any new Presence packets
+     * sent to the group chat.
+     *
+     * @param listener a packet listener that was being notified of any presence packets
+     *      sent to the group chat.
+     */
+    public void removeParticipantListener(PacketListener listener) {
+        connection.removePacketListener(listener);
+        connectionListeners.remove(listener);
+    }
+
+    /**
+     * Returns a collection of <code>Affiliate</code> with the room owners.
+     *
+     * @return a collection of <code>Affiliate</code> with the room owners.
+     * @throws XMPPException if an error occured while performing the request to the server or you
+     *         don't have enough privileges to get this information.
+     */
+    public Collection<Affiliate> getOwners() throws XMPPException {
+        return getAffiliatesByAdmin("owner");
+    }
+
+    /**
+     * Returns a collection of <code>Affiliate</code> with the room administrators.
+     *
+     * @return a collection of <code>Affiliate</code> with the room administrators.
+     * @throws XMPPException if an error occured while performing the request to the server or you
+     *         don't have enough privileges to get this information.
+     */
+    public Collection<Affiliate> getAdmins() throws XMPPException {
+        return getAffiliatesByOwner("admin");
+    }
+
+    /**
+     * Returns a collection of <code>Affiliate</code> with the room members.
+     *
+     * @return a collection of <code>Affiliate</code> with the room members.
+     * @throws XMPPException if an error occured while performing the request to the server or you
+     *         don't have enough privileges to get this information.
+     */
+    public Collection<Affiliate> getMembers() throws XMPPException {
+        return getAffiliatesByAdmin("member");
+    }
+
+    /**
+     * Returns a collection of <code>Affiliate</code> with the room outcasts.
+     *
+     * @return a collection of <code>Affiliate</code> with the room outcasts.
+     * @throws XMPPException if an error occured while performing the request to the server or you
+     *         don't have enough privileges to get this information.
+     */
+    public Collection<Affiliate> getOutcasts() throws XMPPException {
+        return getAffiliatesByAdmin("outcast");
+    }
+
+    /**
+     * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
+     * sending a request in the owner namespace.
+     *
+     * @param affiliation the affiliation of the users in the room.
+     * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
+     * @throws XMPPException if an error occured while performing the request to the server or you
+     *         don't have enough privileges to get this information.
+     */
+    private Collection<Affiliate> getAffiliatesByOwner(String affiliation) throws XMPPException {
+        MUCOwner iq = new MUCOwner();
+        iq.setTo(room);
+        iq.setType(IQ.Type.GET);
+        // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
+        MUCOwner.Item item = new MUCOwner.Item(affiliation);
+        iq.addItem(item);
+
+        // Wait for a response packet back from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the request to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        MUCOwner answer = (MUCOwner) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+        // Get the list of affiliates from the server's answer
+        List<Affiliate> affiliates = new ArrayList<Affiliate>();
+        for (Iterator<MUCOwner.Item> it = answer.getItems(); it.hasNext();) {
+            affiliates.add(new Affiliate(it.next()));
+        }
+        return affiliates;
+    }
+
+    /**
+     * Returns a collection of <code>Affiliate</code> that have the specified room affiliation
+     * sending a request in the admin namespace.
+     *
+     * @param affiliation the affiliation of the users in the room.
+     * @return a collection of <code>Affiliate</code> that have the specified room affiliation.
+     * @throws XMPPException if an error occured while performing the request to the server or you
+     *         don't have enough privileges to get this information.
+     */
+    private Collection<Affiliate> getAffiliatesByAdmin(String affiliation) throws XMPPException {
+        MUCAdmin iq = new MUCAdmin();
+        iq.setTo(room);
+        iq.setType(IQ.Type.GET);
+        // Set the specified affiliation. This may request the list of owners/admins/members/outcasts.
+        MUCAdmin.Item item = new MUCAdmin.Item(affiliation, null);
+        iq.addItem(item);
+
+        // Wait for a response packet back from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the request to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+        // Get the list of affiliates from the server's answer
+        List<Affiliate> affiliates = new ArrayList<Affiliate>();
+        for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) {
+            affiliates.add(new Affiliate(it.next()));
+        }
+        return affiliates;
+    }
+
+    /**
+     * Returns a collection of <code>Occupant</code> with the room moderators.
+     *
+     * @return a collection of <code>Occupant</code> with the room moderators.
+     * @throws XMPPException if an error occured while performing the request to the server or you
+     *         don't have enough privileges to get this information.
+     */
+    public Collection<Occupant> getModerators() throws XMPPException {
+        return getOccupants("moderator");
+    }
+
+    /**
+     * Returns a collection of <code>Occupant</code> with the room participants.
+     *
+     * @return a collection of <code>Occupant</code> with the room participants.
+     * @throws XMPPException if an error occured while performing the request to the server or you
+     *         don't have enough privileges to get this information.
+     */
+    public Collection<Occupant> getParticipants() throws XMPPException {
+        return getOccupants("participant");
+    }
+
+    /**
+     * Returns a collection of <code>Occupant</code> that have the specified room role.
+     *
+     * @param role the role of the occupant in the room.
+     * @return a collection of <code>Occupant</code> that have the specified room role.
+     * @throws XMPPException if an error occured while performing the request to the server or you
+     *         don't have enough privileges to get this information.
+     */
+    private Collection<Occupant> getOccupants(String role) throws XMPPException {
+        MUCAdmin iq = new MUCAdmin();
+        iq.setTo(room);
+        iq.setType(IQ.Type.GET);
+        // Set the specified role. This may request the list of moderators/participants.
+        MUCAdmin.Item item = new MUCAdmin.Item(null, role);
+        iq.addItem(item);
+
+        // Wait for a response packet back from the server.
+        PacketFilter responseFilter = new PacketIDFilter(iq.getPacketID());
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send the request to the server.
+        connection.sendPacket(iq);
+        // Wait up to a certain number of seconds for a reply.
+        MUCAdmin answer = (MUCAdmin) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+        // Get the list of participants from the server's answer
+        List<Occupant> participants = new ArrayList<Occupant>();
+        for (Iterator<MUCAdmin.Item> it = answer.getItems(); it.hasNext();) {
+            participants.add(new Occupant(it.next()));
+        }
+        return participants;
+    }
+
+    /**
+     * Sends a message to the chat room.
+     *
+     * @param text the text of the message to send.
+     * @throws XMPPException if sending the message fails.
+     */
+    public void sendMessage(String text) throws XMPPException {
+        Message message = new Message(room, Message.Type.groupchat);
+        message.setBody(text);
+        connection.sendPacket(message);
+    }
+
+    /**
+     * Returns a new Chat for sending private messages to a given room occupant.
+     * The Chat's occupant address is the room's JID (i.e. roomName@service/nick). The server
+     * service will change the 'from' address to the sender's room JID and delivering the message
+     * to the intended recipient's full JID.
+     *
+     * @param occupant occupant unique room JID (e.g. 'darkcave@macbeth.shakespeare.lit/Paul').
+     * @param listener the listener is a message listener that will handle messages for the newly
+     * created chat.
+     * @return new Chat for sending private messages to a given room occupant.
+     */
+    public Chat createPrivateChat(String occupant, MessageListener listener) {
+        return connection.getChatManager().createChat(occupant, listener);
+    }
+
+    /**
+     * Creates a new Message to send to the chat room.
+     *
+     * @return a new Message addressed to the chat room.
+     */
+    public Message createMessage() {
+        return new Message(room, Message.Type.groupchat);
+    }
+
+    /**
+     * Sends a Message to the chat room.
+     *
+     * @param message the message.
+     * @throws XMPPException if sending the message fails.
+     */
+    public void sendMessage(Message message) throws XMPPException {
+        connection.sendPacket(message);
+    }
+
+    /**
+    * Polls for and returns the next message, or <tt>null</tt> if there isn't
+    * a message immediately available. This method provides significantly different
+    * functionalty than the {@link #nextMessage()} method since it's non-blocking.
+    * In other words, the method call will always return immediately, whereas the
+    * nextMessage method will return only when a message is available (or after
+    * a specific timeout).
+    *
+    * @return the next message if one is immediately available and
+    *      <tt>null</tt> otherwise.
+    */
+    public Message pollMessage() {
+        return (Message) messageCollector.pollResult();
+    }
+
+    /**
+     * Returns the next available message in the chat. The method call will block
+     * (not return) until a message is available.
+     *
+     * @return the next message.
+     */
+    public Message nextMessage() {
+        return (Message) messageCollector.nextResult();
+    }
+
+    /**
+     * Returns the next available message in the chat. The method call will block
+     * (not return) until a packet is available or the <tt>timeout</tt> has elapased.
+     * If the timeout elapses without a result, <tt>null</tt> will be returned.
+     *
+     * @param timeout the maximum amount of time to wait for the next message.
+     * @return the next message, or <tt>null</tt> if the timeout elapses without a
+     *      message becoming available.
+     */
+    public Message nextMessage(long timeout) {
+        return (Message) messageCollector.nextResult(timeout);
+    }
+
+    /**
+     * Adds a packet listener that will be notified of any new messages in the
+     * group chat. Only "group chat" messages addressed to this group chat will
+     * be delivered to the listener. If you wish to listen for other packets
+     * that may be associated with this group chat, you should register a
+     * PacketListener directly with the Connection with the appropriate
+     * PacketListener.
+     *
+     * @param listener a packet listener.
+     */
+    public void addMessageListener(PacketListener listener) {
+        connection.addPacketListener(listener, messageFilter);
+        connectionListeners.add(listener);
+    }
+
+    /**
+     * Removes a packet listener that was being notified of any new messages in the
+     * multi user chat. Only "group chat" messages addressed to this multi user chat were
+     * being delivered to the listener.
+     *
+     * @param listener a packet listener.
+     */
+    public void removeMessageListener(PacketListener listener) {
+        connection.removePacketListener(listener);
+        connectionListeners.remove(listener);
+    }
+
+    /**
+     * Changes the subject within the room. As a default, only users with a role of "moderator"
+     * are allowed to change the subject in a room. Although some rooms may be configured to
+     * allow a mere participant or even a visitor to change the subject.
+     *
+     * @param subject the new room's subject to set.
+     * @throws XMPPException if someone without appropriate privileges attempts to change the
+     *          room subject will throw an error with code 403 (i.e. Forbidden)
+     */
+    public void changeSubject(final String subject) throws XMPPException {
+        Message message = new Message(room, Message.Type.groupchat);
+        message.setSubject(subject);
+        // Wait for an error or confirmation message back from the server.
+        PacketFilter responseFilter =
+            new AndFilter(
+                new FromMatchesFilter(room),
+                new PacketTypeFilter(Message.class));
+        responseFilter = new AndFilter(responseFilter, new PacketFilter() {
+            public boolean accept(Packet packet) {
+                Message msg = (Message) packet;
+                return subject.equals(msg.getSubject());
+            }
+        });
+        PacketCollector response = connection.createPacketCollector(responseFilter);
+        // Send change subject packet.
+        connection.sendPacket(message);
+        // Wait up to a certain number of seconds for a reply.
+        Message answer =
+            (Message) response.nextResult(SmackConfiguration.getPacketReplyTimeout());
+        // Stop queuing results
+        response.cancel();
+
+        if (answer == null) {
+            throw new XMPPException("No response from server.");
+        }
+        else if (answer.getError() != null) {
+            throw new XMPPException(answer.getError());
+        }
+    }
+
+    /**
+     * Notification message that the user has joined the room.
+     */
+    private synchronized void userHasJoined() {
+        // Update the list of joined rooms through this connection
+        List<String> rooms = joinedRooms.get(connection);
+        if (rooms == null) {
+            rooms = new ArrayList<String>();
+            joinedRooms.put(connection, rooms);
+        }
+        rooms.add(room);
+    }
+
+    /**
+     * Notification message that the user has left the room.
+     */
+    private synchronized void userHasLeft() {
+        // Update the list of joined rooms through this connection
+        List<String> rooms = joinedRooms.get(connection);
+        if (rooms == null) {
+            return;
+        }
+        rooms.remove(room);
+        cleanup();
+    }
+
+    /**
+     * Returns the MUCUser packet extension included in the packet or <tt>null</tt> if none.
+     *
+     * @param packet the packet that may include the MUCUser extension.
+     * @return the MUCUser found in the packet.
+     */
+    private MUCUser getMUCUserExtension(Packet packet) {
+        if (packet != null) {
+            // Get the MUC User extension
+            return (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
+        }
+        return null;
+    }
+
+    /**
+     * Adds a listener that will be notified of changes in your status in the room
+     * such as the user being kicked, banned, or granted admin permissions.
+     *
+     * @param listener a user status listener.
+     */
+    public void addUserStatusListener(UserStatusListener listener) {
+        synchronized (userStatusListeners) {
+            if (!userStatusListeners.contains(listener)) {
+                userStatusListeners.add(listener);
+            }
+        }
+    }
+
+    /**
+     * Removes a listener that was being notified of changes in your status in the room
+     * such as the user being kicked, banned, or granted admin permissions.
+     *
+     * @param listener a user status listener.
+     */
+    public void removeUserStatusListener(UserStatusListener listener) {
+        synchronized (userStatusListeners) {
+            userStatusListeners.remove(listener);
+        }
+    }
+
+    private void fireUserStatusListeners(String methodName, Object[] params) {
+        UserStatusListener[] listeners;
+        synchronized (userStatusListeners) {
+            listeners = new UserStatusListener[userStatusListeners.size()];
+            userStatusListeners.toArray(listeners);
+        }
+        // Get the classes of the method parameters
+        Class<?>[] paramClasses = new Class[params.length];
+        for (int i = 0; i < params.length; i++) {
+            paramClasses[i] = params[i].getClass();
+        }
+        try {
+            // Get the method to execute based on the requested methodName and parameters classes
+            Method method = UserStatusListener.class.getDeclaredMethod(methodName, paramClasses);
+            for (UserStatusListener listener : listeners) {
+                method.invoke(listener, params);
+            }
+        } catch (NoSuchMethodException e) {
+            e.printStackTrace();
+        } catch (InvocationTargetException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * Adds a listener that will be notified of changes in occupants status in the room
+     * such as the user being kicked, banned, or granted admin permissions.
+     *
+     * @param listener a participant status listener.
+     */
+    public void addParticipantStatusListener(ParticipantStatusListener listener) {
+        synchronized (participantStatusListeners) {
+            if (!participantStatusListeners.contains(listener)) {
+                participantStatusListeners.add(listener);
+            }
+        }
+    }
+
+    /**
+     * Removes a listener that was being notified of changes in occupants status in the room
+     * such as the user being kicked, banned, or granted admin permissions.
+     *
+     * @param listener a participant status listener.
+     */
+    public void removeParticipantStatusListener(ParticipantStatusListener listener) {
+        synchronized (participantStatusListeners) {
+            participantStatusListeners.remove(listener);
+        }
+    }
+
+    private void fireParticipantStatusListeners(String methodName, List<String> params) {
+        ParticipantStatusListener[] listeners;
+        synchronized (participantStatusListeners) {
+            listeners = new ParticipantStatusListener[participantStatusListeners.size()];
+            participantStatusListeners.toArray(listeners);
+        }
+        try {
+            // Get the method to execute based on the requested methodName and parameter
+            Class<?>[] classes = new Class[params.size()];
+            for (int i=0;i<params.size(); i++) {
+                classes[i] = String.class;
+            }
+            Method method = ParticipantStatusListener.class.getDeclaredMethod(methodName, classes);
+            for (ParticipantStatusListener listener : listeners) {
+                method.invoke(listener, params.toArray());
+            }
+        } catch (NoSuchMethodException e) {
+            e.printStackTrace();
+        } catch (InvocationTargetException e) {
+            e.printStackTrace();
+        } catch (IllegalAccessException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private void init() {
+        // Create filters
+        messageFilter =
+            new AndFilter(
+                new FromMatchesFilter(room),
+                new MessageTypeFilter(Message.Type.groupchat));
+        messageFilter = new AndFilter(messageFilter, new PacketFilter() {
+            public boolean accept(Packet packet) {
+                Message msg = (Message) packet;
+                return msg.getBody() != null;
+            }
+        });
+        presenceFilter =
+            new AndFilter(new FromMatchesFilter(room), new PacketTypeFilter(Presence.class));
+
+        // Create a collector for incoming messages.
+        messageCollector = new ConnectionDetachedPacketCollector();
+
+        // Create a listener for subject updates.
+        PacketListener subjectListener = new PacketListener() {
+            public void processPacket(Packet packet) {
+                Message msg = (Message) packet;
+                // Update the room subject
+                subject = msg.getSubject();
+                // Fire event for subject updated listeners
+                fireSubjectUpdatedListeners(
+                    msg.getSubject(),
+                    msg.getFrom());
+
+            }
+        };
+
+        // Create a listener for all presence updates.
+        PacketListener presenceListener = new PacketListener() {
+            public void processPacket(Packet packet) {
+                Presence presence = (Presence) packet;
+                String from = presence.getFrom();
+                String myRoomJID = room + "/" + nickname;
+                boolean isUserStatusModification = presence.getFrom().equals(myRoomJID);
+                if (presence.getType() == Presence.Type.available) {
+                    Presence oldPresence = occupantsMap.put(from, presence);
+                    if (oldPresence != null) {
+                        // Get the previous occupant's affiliation & role
+                        MUCUser mucExtension = getMUCUserExtension(oldPresence);
+                        String oldAffiliation = mucExtension.getItem().getAffiliation();
+                        String oldRole = mucExtension.getItem().getRole();
+                        // Get the new occupant's affiliation & role
+                        mucExtension = getMUCUserExtension(presence);
+                        String newAffiliation = mucExtension.getItem().getAffiliation();
+                        String newRole = mucExtension.getItem().getRole();
+                        // Fire role modification events
+                        checkRoleModifications(oldRole, newRole, isUserStatusModification, from);
+                        // Fire affiliation modification events
+                        checkAffiliationModifications(
+                            oldAffiliation,
+                            newAffiliation,
+                            isUserStatusModification,
+                            from);
+                    }
+                    else {
+                        // A new occupant has joined the room
+                        if (!isUserStatusModification) {
+                            List<String> params = new ArrayList<String>();
+                            params.add(from);
+                            fireParticipantStatusListeners("joined", params);
+                        }
+                    }
+                }
+                else if (presence.getType() == Presence.Type.unavailable) {
+                    occupantsMap.remove(from);
+                    MUCUser mucUser = getMUCUserExtension(presence);
+                    if (mucUser != null && mucUser.getStatus() != null) {
+                        // Fire events according to the received presence code
+                        checkPresenceCode(
+                            mucUser.getStatus().getCode(),
+                            presence.getFrom().equals(myRoomJID),
+                            mucUser,
+                            from);
+                    } else {
+                        // An occupant has left the room
+                        if (!isUserStatusModification) {
+                            List<String> params = new ArrayList<String>();
+                            params.add(from);
+                            fireParticipantStatusListeners("left", params);
+                        }
+                    }
+                }
+            }
+        };
+
+        // Listens for all messages that include a MUCUser extension and fire the invitation
+        // rejection listeners if the message includes an invitation rejection.
+        PacketListener declinesListener = new PacketListener() {
+            public void processPacket(Packet packet) {
+                // Get the MUC User extension
+                MUCUser mucUser = getMUCUserExtension(packet);
+                // Check if the MUCUser informs that the invitee has declined the invitation
+                if (mucUser.getDecline() != null &&
+                        ((Message) packet).getType() != Message.Type.error) {
+                    // Fire event for invitation rejection listeners
+                    fireInvitationRejectionListeners(
+                        mucUser.getDecline().getFrom(),
+                        mucUser.getDecline().getReason());
+                }
+            }
+        };
+
+        PacketMultiplexListener packetMultiplexor = new PacketMultiplexListener(
+                messageCollector, presenceListener, subjectListener,
+                declinesListener);
+
+        roomListenerMultiplexor = RoomListenerMultiplexor.getRoomMultiplexor(connection);
+
+        roomListenerMultiplexor.addRoom(room, packetMultiplexor);
+    }
+
+    /**
+     * Fires notification events if the role of a room occupant has changed. If the occupant that
+     * changed his role is your occupant then the <code>UserStatusListeners</code> added to this
+     * <code>MultiUserChat</code> will be fired. On the other hand, if the occupant that changed
+     * his role is not yours then the <code>ParticipantStatusListeners</code> added to this
+     * <code>MultiUserChat</code> will be fired. The following table shows the events that will
+     * be fired depending on the previous and new role of the occupant.
+     *
+     * <pre>
+     * <table border="1">
+     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
+     *
+     * <tr><td>None</td><td>Visitor</td><td>--</td></tr>
+     * <tr><td>Visitor</td><td>Participant</td><td>voiceGranted</td></tr>
+     * <tr><td>Participant</td><td>Moderator</td><td>moderatorGranted</td></tr>
+     *
+     * <tr><td>None</td><td>Participant</td><td>voiceGranted</td></tr>
+     * <tr><td>None</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
+     * <tr><td>Visitor</td><td>Moderator</td><td>voiceGranted + moderatorGranted</td></tr>
+     *
+     * <tr><td>Moderator</td><td>Participant</td><td>moderatorRevoked</td></tr>
+     * <tr><td>Participant</td><td>Visitor</td><td>voiceRevoked</td></tr>
+     * <tr><td>Visitor</td><td>None</td><td>kicked</td></tr>
+     *
+     * <tr><td>Moderator</td><td>Visitor</td><td>voiceRevoked + moderatorRevoked</td></tr>
+     * <tr><td>Moderator</td><td>None</td><td>kicked</td></tr>
+     * <tr><td>Participant</td><td>None</td><td>kicked</td></tr>
+     * </table>
+     * </pre>
+     *
+     * @param oldRole the previous role of the user in the room before receiving the new presence
+     * @param newRole the new role of the user in the room after receiving the new presence
+     * @param isUserModification whether the received presence is about your user in the room or not
+     * @param from the occupant whose role in the room has changed
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    private void checkRoleModifications(
+        String oldRole,
+        String newRole,
+        boolean isUserModification,
+        String from) {
+        // Voice was granted to a visitor
+        if (("visitor".equals(oldRole) || "none".equals(oldRole))
+            && "participant".equals(newRole)) {
+            if (isUserModification) {
+                fireUserStatusListeners("voiceGranted", new Object[] {});
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                fireParticipantStatusListeners("voiceGranted", params);
+            }
+        }
+        // The participant's voice was revoked from the room
+        else if (
+            "participant".equals(oldRole)
+                && ("visitor".equals(newRole) || "none".equals(newRole))) {
+            if (isUserModification) {
+                fireUserStatusListeners("voiceRevoked", new Object[] {});
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                fireParticipantStatusListeners("voiceRevoked", params);
+            }
+        }
+        // Moderator privileges were granted to a participant
+        if (!"moderator".equals(oldRole) && "moderator".equals(newRole)) {
+            if ("visitor".equals(oldRole) || "none".equals(oldRole)) {
+                if (isUserModification) {
+                    fireUserStatusListeners("voiceGranted", new Object[] {});
+                }
+                else {
+                    List<String> params = new ArrayList<String>();
+                    params.add(from);
+                    fireParticipantStatusListeners("voiceGranted", params);
+                }
+            }
+            if (isUserModification) {
+                fireUserStatusListeners("moderatorGranted", new Object[] {});
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                fireParticipantStatusListeners("moderatorGranted", params);
+            }
+        }
+        // Moderator privileges were revoked from a participant
+        else if ("moderator".equals(oldRole) && !"moderator".equals(newRole)) {
+            if ("visitor".equals(newRole) || "none".equals(newRole)) {
+                if (isUserModification) {
+                    fireUserStatusListeners("voiceRevoked", new Object[] {});
+                }
+                else {
+                    List<String> params = new ArrayList<String>();
+                    params.add(from);
+                    fireParticipantStatusListeners("voiceRevoked", params);
+                }
+            }
+            if (isUserModification) {
+                fireUserStatusListeners("moderatorRevoked", new Object[] {});
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                fireParticipantStatusListeners("moderatorRevoked", params);
+            }
+        }
+    }
+
+    /**
+     * Fires notification events if the affiliation of a room occupant has changed. If the
+     * occupant that changed his affiliation is your occupant then the
+     * <code>UserStatusListeners</code> added to this <code>MultiUserChat</code> will be fired.
+     * On the other hand, if the occupant that changed his affiliation is not yours then the
+     * <code>ParticipantStatusListeners</code> added to this <code>MultiUserChat</code> will be
+     * fired. The following table shows the events that will be fired depending on the previous
+     * and new affiliation of the occupant.
+     *
+     * <pre>
+     * <table border="1">
+     * <tr><td><b>Old</b></td><td><b>New</b></td><td><b>Events</b></td></tr>
+     *
+     * <tr><td>None</td><td>Member</td><td>membershipGranted</td></tr>
+     * <tr><td>Member</td><td>Admin</td><td>membershipRevoked + adminGranted</td></tr>
+     * <tr><td>Admin</td><td>Owner</td><td>adminRevoked + ownershipGranted</td></tr>
+     *
+     * <tr><td>None</td><td>Admin</td><td>adminGranted</td></tr>
+     * <tr><td>None</td><td>Owner</td><td>ownershipGranted</td></tr>
+     * <tr><td>Member</td><td>Owner</td><td>membershipRevoked + ownershipGranted</td></tr>
+     *
+     * <tr><td>Owner</td><td>Admin</td><td>ownershipRevoked + adminGranted</td></tr>
+     * <tr><td>Admin</td><td>Member</td><td>adminRevoked + membershipGranted</td></tr>
+     * <tr><td>Member</td><td>None</td><td>membershipRevoked</td></tr>
+     *
+     * <tr><td>Owner</td><td>Member</td><td>ownershipRevoked + membershipGranted</td></tr>
+     * <tr><td>Owner</td><td>None</td><td>ownershipRevoked</td></tr>
+     * <tr><td>Admin</td><td>None</td><td>adminRevoked</td></tr>
+     * <tr><td><i>Anyone</i></td><td>Outcast</td><td>banned</td></tr>
+     * </table>
+     * </pre>
+     *
+     * @param oldAffiliation the previous affiliation of the user in the room before receiving the
+     * new presence
+     * @param newAffiliation the new affiliation of the user in the room after receiving the new
+     * presence
+     * @param isUserModification whether the received presence is about your user in the room or not
+     * @param from the occupant whose role in the room has changed
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    private void checkAffiliationModifications(
+        String oldAffiliation,
+        String newAffiliation,
+        boolean isUserModification,
+        String from) {
+        // First check for revoked affiliation and then for granted affiliations. The idea is to
+        // first fire the "revoke" events and then fire the "grant" events.
+
+        // The user's ownership to the room was revoked
+        if ("owner".equals(oldAffiliation) && !"owner".equals(newAffiliation)) {
+            if (isUserModification) {
+                fireUserStatusListeners("ownershipRevoked", new Object[] {});
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                fireParticipantStatusListeners("ownershipRevoked", params);
+            }
+        }
+        // The user's administrative privileges to the room were revoked
+        else if ("admin".equals(oldAffiliation) && !"admin".equals(newAffiliation)) {
+            if (isUserModification) {
+                fireUserStatusListeners("adminRevoked", new Object[] {});
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                fireParticipantStatusListeners("adminRevoked", params);
+            }
+        }
+        // The user's membership to the room was revoked
+        else if ("member".equals(oldAffiliation) && !"member".equals(newAffiliation)) {
+            if (isUserModification) {
+                fireUserStatusListeners("membershipRevoked", new Object[] {});
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                fireParticipantStatusListeners("membershipRevoked", params);
+            }
+        }
+
+        // The user was granted ownership to the room
+        if (!"owner".equals(oldAffiliation) && "owner".equals(newAffiliation)) {
+            if (isUserModification) {
+                fireUserStatusListeners("ownershipGranted", new Object[] {});
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                fireParticipantStatusListeners("ownershipGranted", params);
+            }
+        }
+        // The user was granted administrative privileges to the room
+        else if (!"admin".equals(oldAffiliation) && "admin".equals(newAffiliation)) {
+            if (isUserModification) {
+                fireUserStatusListeners("adminGranted", new Object[] {});
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                fireParticipantStatusListeners("adminGranted", params);
+            }
+        }
+        // The user was granted membership to the room
+        else if (!"member".equals(oldAffiliation) && "member".equals(newAffiliation)) {
+            if (isUserModification) {
+                fireUserStatusListeners("membershipGranted", new Object[] {});
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                fireParticipantStatusListeners("membershipGranted", params);
+            }
+        }
+    }
+
+    /**
+     * Fires events according to the received presence code.
+     *
+     * @param code
+     * @param isUserModification
+     * @param mucUser
+     * @param from
+     */
+    private void checkPresenceCode(
+        String code,
+        boolean isUserModification,
+        MUCUser mucUser,
+        String from) {
+        // Check if an occupant was kicked from the room
+        if ("307".equals(code)) {
+            // Check if this occupant was kicked
+            if (isUserModification) {
+                joined = false;
+
+                fireUserStatusListeners(
+                    "kicked",
+                    new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
+
+                // Reset occupant information.
+                occupantsMap.clear();
+                nickname = null;
+                userHasLeft();
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                params.add(mucUser.getItem().getActor());
+                params.add(mucUser.getItem().getReason());
+                fireParticipantStatusListeners("kicked", params);
+            }
+        }
+        // A user was banned from the room
+        else if ("301".equals(code)) {
+            // Check if this occupant was banned
+            if (isUserModification) {
+                joined = false;
+
+                fireUserStatusListeners(
+                    "banned",
+                    new Object[] { mucUser.getItem().getActor(), mucUser.getItem().getReason()});
+
+                // Reset occupant information.
+                occupantsMap.clear();
+                nickname = null;
+                userHasLeft();
+            }
+            else {
+                List<String> params = new ArrayList<String>();
+                params.add(from);
+                params.add(mucUser.getItem().getActor());
+                params.add(mucUser.getItem().getReason());
+                fireParticipantStatusListeners("banned", params);
+            }
+        }
+        // A user's membership was revoked from the room
+        else if ("321".equals(code)) {
+            // Check if this occupant's membership was revoked
+            if (isUserModification) {
+                joined = false;
+
+                fireUserStatusListeners("membershipRevoked", new Object[] {});
+
+                // Reset occupant information.
+                occupantsMap.clear();
+                nickname = null;
+                userHasLeft();
+            }
+        }
+        // A occupant has changed his nickname in the room
+        else if ("303".equals(code)) {
+            List<String> params = new ArrayList<String>();
+            params.add(from);
+            params.add(mucUser.getItem().getNick());
+            fireParticipantStatusListeners("nicknameChanged", params);
+        }
+    }
+
+    private void cleanup() {
+        try {
+            if (connection != null) {
+                roomListenerMultiplexor.removeRoom(room);
+                // Remove all the PacketListeners added to the connection by this chat
+                for (PacketListener connectionListener : connectionListeners) {
+                    connection.removePacketListener(connectionListener);
+                }
+            }
+        } catch (Exception e) {
+            // Do nothing
+        }
+    }
+
+    protected void finalize() throws Throwable {
+        cleanup();
+        super.finalize();
+    }
+
+    /**
+     * An InvitationsMonitor monitors a given connection to detect room invitations. Every
+     * time the InvitationsMonitor detects a new invitation it will fire the invitation listeners.
+     *
+     * @author Gaston Dombiak
+     */
+    private static class InvitationsMonitor implements ConnectionListener {
+        // We use a WeakHashMap so that the GC can collect the monitor when the
+        // connection is no longer referenced by any object.
+        // Note that when the InvitationsMonitor is used, i.e. when there are InvitationListeners, it will add a
+        // PacketListener to the Connection and therefore a strong reference from the Connection to the
+        // InvitationsMonior will exists, preventing it from beeing gc'ed. After the last InvitationListener is gone,
+        // the PacketListener will get removed (cancel()) allowing the garbage collection of the InvitationsMonitor
+        // instance.
+        private final static Map<Connection, WeakReference<InvitationsMonitor>> monitors =
+                new WeakHashMap<Connection, WeakReference<InvitationsMonitor>>();
+
+        // We don't use a synchronized List here because it would break the semantic of (add|remove)InvitationListener
+        private final List<InvitationListener> invitationsListeners =
+                new ArrayList<InvitationListener>();
+        private Connection connection;
+        private PacketFilter invitationFilter;
+        private PacketListener invitationPacketListener;
+
+        /**
+         * Returns a new or existing InvitationsMonitor for a given connection.
+         *
+         * @param conn the connection to monitor for room invitations.
+         * @return a new or existing InvitationsMonitor for a given connection.
+         */
+        public static InvitationsMonitor getInvitationsMonitor(Connection conn) {
+            synchronized (monitors) {
+                if (!monitors.containsKey(conn) || monitors.get(conn).get() == null) {
+                    // We need to use a WeakReference because the monitor references the
+                    // connection and this could prevent the GC from collecting the monitor
+                    // when no other object references the monitor
+                    InvitationsMonitor ivm = new InvitationsMonitor(conn);
+                    monitors.put(conn, new WeakReference<InvitationsMonitor>(ivm));
+                    return ivm;
+                }
+                // Return the InvitationsMonitor that monitors the connection
+                return monitors.get(conn).get();
+            }
+        }
+
+        /**
+         * Creates a new InvitationsMonitor that will monitor invitations received
+         * on a given connection.
+         * 
+         * @param connection the connection to monitor for possible room invitations
+         */
+        private InvitationsMonitor(Connection connection) {
+            this.connection = connection;
+        }
+
+        /**
+         * Adds a listener to invitation notifications. The listener will be fired anytime
+         * an invitation is received.<p>
+         *
+         * If this is the first monitor's listener then the monitor will be initialized in
+         * order to start listening to room invitations.
+         *
+         * @param listener an invitation listener.
+         */
+        public void addInvitationListener(InvitationListener listener) {
+            synchronized (invitationsListeners) {
+                // If this is the first monitor's listener then initialize the listeners
+                // on the connection to detect room invitations
+                if (invitationsListeners.size() == 0) {
+                    init();
+                }
+                if (!invitationsListeners.contains(listener)) {
+                    invitationsListeners.add(listener);
+                }
+            }
+        }
+
+        /**
+         * Removes a listener to invitation notifications. The listener will be fired anytime
+         * an invitation is received.<p>
+         *
+         * If there are no more listeners to notifiy for room invitations then the monitor will
+         * be stopped. As soon as a new listener is added to the monitor, the monitor will resume
+         * monitoring the connection for new room invitations.
+         *
+         * @param listener an invitation listener.
+         */
+        public void removeInvitationListener(InvitationListener listener) {
+            synchronized (invitationsListeners) {
+                if (invitationsListeners.contains(listener)) {
+                    invitationsListeners.remove(listener);
+                }
+                // If there are no more listeners to notifiy for room invitations
+                // then proceed to cancel/release this monitor
+                if (invitationsListeners.size() == 0) {
+                    cancel();
+                }
+            }
+        }
+
+        /**
+         * Fires invitation listeners.
+         */
+        private void fireInvitationListeners(String room, String inviter, String reason, String password,
+                                             Message message) {
+            InvitationListener[] listeners;
+            synchronized (invitationsListeners) {
+                listeners = new InvitationListener[invitationsListeners.size()];
+                invitationsListeners.toArray(listeners);
+            }
+            for (InvitationListener listener : listeners) {
+                listener.invitationReceived(connection, room, inviter, reason, password, message);
+            }
+        }
+
+        public void connectionClosed() {
+            cancel();
+        }
+
+        public void connectionClosedOnError(Exception e) {
+            // ignore              
+        }
+
+        public void reconnectingIn(int seconds) {
+            // ignore
+        }
+
+        public void reconnectionSuccessful() {
+            // ignore
+        }
+
+        public void reconnectionFailed(Exception e) {
+            // ignore
+        }
+
+        /**
+         * Initializes the listeners to detect received room invitations and to detect when the
+         * connection gets closed. As soon as a room invitation is received the invitations
+         * listeners will be fired. When the connection gets closed the monitor will remove
+         * his listeners on the connection.
+         */
+        private void init() {
+            // Listens for all messages that include a MUCUser extension and fire the invitation
+            // listeners if the message includes an invitation.
+            invitationFilter =
+                new PacketExtensionFilter("x", "http://jabber.org/protocol/muc#user");
+            invitationPacketListener = new PacketListener() {
+                public void processPacket(Packet packet) {
+                    // Get the MUCUser extension
+                    MUCUser mucUser =
+                        (MUCUser) packet.getExtension("x", "http://jabber.org/protocol/muc#user");
+                    // Check if the MUCUser extension includes an invitation
+                    if (mucUser.getInvite() != null &&
+                            ((Message) packet).getType() != Message.Type.error) {
+                        // Fire event for invitation listeners
+                        fireInvitationListeners(packet.getFrom(), mucUser.getInvite().getFrom(),
+                                mucUser.getInvite().getReason(), mucUser.getPassword(), (Message) packet);
+                    }
+                }
+            };
+            connection.addPacketListener(invitationPacketListener, invitationFilter);
+            // Add a listener to detect when the connection gets closed in order to
+            // cancel/release this monitor 
+            connection.addConnectionListener(this);
+        }
+
+        /**
+         * Cancels all the listeners that this InvitationsMonitor has added to the connection.
+         */
+        private void cancel() {
+            connection.removePacketListener(invitationPacketListener);
+            connection.removeConnectionListener(this);
+        }
+
+    }
+}
diff --git a/src/org/jivesoftware/smackx/muc/Occupant.java b/src/org/jivesoftware/smackx/muc/Occupant.java
new file mode 100644
index 0000000..3b199a5
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/Occupant.java
@@ -0,0 +1,121 @@
+/**
+ * $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.smackx.muc;
+
+import org.jivesoftware.smackx.packet.MUCAdmin;
+import org.jivesoftware.smackx.packet.MUCUser;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * Represents the information about an occupant in a given room. The information will always have
+ * the affiliation and role of the occupant in the room. The full JID and nickname are optional.
+ *
+ * @author Gaston Dombiak
+ */
+public class Occupant {
+    // Fields that must have a value
+    private String affiliation;
+    private String role;
+    // Fields that may have a value
+    private String jid;
+    private String nick;
+
+    Occupant(MUCAdmin.Item item) {
+        super();
+        this.jid = item.getJid();
+        this.affiliation = item.getAffiliation();
+        this.role = item.getRole();
+        this.nick = item.getNick();
+    }
+
+    Occupant(Presence presence) {
+        super();
+        MUCUser mucUser = (MUCUser) presence.getExtension("x",
+                "http://jabber.org/protocol/muc#user");
+        MUCUser.Item item = mucUser.getItem();
+        this.jid = item.getJid();
+        this.affiliation = item.getAffiliation();
+        this.role = item.getRole();
+        // Get the nickname from the FROM attribute of the presence
+        this.nick = StringUtils.parseResource(presence.getFrom());
+    }
+
+    /**
+     * Returns the full JID of the occupant. If this information was extracted from a presence and
+     * the room is semi or full-anonymous then the answer will be null. On the other hand, if this
+     * information was obtained while maintaining the voice list or the moderator list then we will
+     * always have a full JID.
+     *
+     * @return the full JID of the occupant.
+     */
+    public String getJid() {
+        return jid;
+    }
+
+    /**
+     * Returns the affiliation of the occupant. Possible affiliations are: "owner", "admin",
+     * "member", "outcast". This information will always be available.
+     *
+     * @return the affiliation of the occupant.
+     */
+    public String getAffiliation() {
+        return affiliation;
+    }
+
+    /**
+     * Returns the current role of the occupant in the room. This information will always be
+     * available.
+     *
+     * @return the current role of the occupant in the room.
+     */
+    public String getRole() {
+        return role;
+    }
+
+    /**
+     * Returns the current nickname of the occupant in the room. If this information was extracted
+     * from a presence then the answer will be null.
+     *
+     * @return the current nickname of the occupant in the room or null if this information was
+     *         obtained from a presence.
+     */
+    public String getNick() {
+        return nick;
+    }
+
+    public boolean equals(Object obj) {
+        if(!(obj instanceof Occupant)) {
+            return false;
+        }
+        Occupant occupant = (Occupant)obj;
+        return jid.equals(occupant.jid);
+    }
+
+    public int hashCode() {
+        int result;
+        result = affiliation.hashCode();
+        result = 17 * result + role.hashCode();
+        result = 17 * result + jid.hashCode();
+        result = 17 * result + (nick != null ? nick.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/muc/PacketMultiplexListener.java b/src/org/jivesoftware/smackx/muc/PacketMultiplexListener.java
new file mode 100644
index 0000000..c1863c2
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/PacketMultiplexListener.java
@@ -0,0 +1,96 @@
+/**
+ * $RCSfile$
+ * $Revision: 2779 $
+ * $Date: 2005-09-05 17:00:45 -0300 (Mon, 05 Sep 2005) $
+ *
+ * Copyright 2003-2006 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.smackx.muc;
+
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.filter.MessageTypeFilter;
+import org.jivesoftware.smack.filter.PacketExtensionFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+
+/**
+ * The single <code>PacketListener</code> used by each {@link MultiUserChat}
+ * for all basic processing of presence, and message packets targeted to that chat.
+ *
+ * @author Larry Kirschner
+ */
+class PacketMultiplexListener implements PacketListener {
+
+    private static final PacketFilter MESSAGE_FILTER =
+            new MessageTypeFilter(Message.Type.groupchat);
+    private static final PacketFilter PRESENCE_FILTER = new PacketTypeFilter(Presence.class);
+    private static final PacketFilter SUBJECT_FILTER = new PacketFilter() {
+        public boolean accept(Packet packet) {
+            Message msg = (Message) packet;
+            return msg.getSubject() != null;
+        }
+    };
+    private static final PacketFilter DECLINES_FILTER =
+            new PacketExtensionFilter("x",
+                    "http://jabber.org/protocol/muc#user");
+
+    private ConnectionDetachedPacketCollector messageCollector;
+    private PacketListener presenceListener;
+    private PacketListener subjectListener;
+    private PacketListener declinesListener;
+
+    public PacketMultiplexListener(
+            ConnectionDetachedPacketCollector messageCollector,
+            PacketListener presenceListener,
+            PacketListener subjectListener, PacketListener declinesListener) {
+        if (messageCollector == null) {
+            throw new IllegalArgumentException("MessageCollector is null");
+        }
+        if (presenceListener == null) {
+            throw new IllegalArgumentException("Presence listener is null");
+        }
+        if (subjectListener == null) {
+            throw new IllegalArgumentException("Subject listener is null");
+        }
+        if (declinesListener == null) {
+            throw new IllegalArgumentException("Declines listener is null");
+        }
+        this.messageCollector = messageCollector;
+        this.presenceListener = presenceListener;
+        this.subjectListener = subjectListener;
+        this.declinesListener = declinesListener;
+    }
+
+    public void processPacket(Packet p) {
+        if (PRESENCE_FILTER.accept(p)) {
+            presenceListener.processPacket(p);
+        }
+        else if (MESSAGE_FILTER.accept(p)) {
+            messageCollector.processPacket(p);
+
+            if (SUBJECT_FILTER.accept(p)) {
+                subjectListener.processPacket(p);
+            }
+        }
+        else if (DECLINES_FILTER.accept(p)) {
+            declinesListener.processPacket(p);
+        }
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java b/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java
new file mode 100644
index 0000000..c3e248d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/ParticipantStatusListener.java
@@ -0,0 +1,179 @@
+/**
+ * $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.smackx.muc;
+
+/**
+ * A listener that is fired anytime a participant's status in a room is changed, such as the 
+ * user being kicked, banned, or granted admin permissions.
+ * 
+ * @author Gaston Dombiak
+ */
+public interface ParticipantStatusListener {
+
+    /**
+     * Called when a new room occupant has joined the room. Note: Take in consideration that when
+     * you join a room you will receive the list of current occupants in the room. This message will
+     * be sent for each occupant.
+     *
+     * @param participant the participant that has just joined the room
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void joined(String participant);
+
+    /**
+     * Called when a room occupant has left the room on its own. This means that the occupant was
+     * neither kicked nor banned from the room.
+     *
+     * @param participant the participant that has left the room on its own.
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void left(String participant);
+
+    /**
+     * Called when a room participant has been kicked from the room. This means that the kicked 
+     * participant is no longer participating in the room.
+     * 
+     * @param participant the participant that was kicked from the room 
+     * (e.g. room@conference.jabber.org/nick).
+     * @param actor the moderator that kicked the occupant from the room (e.g. user@host.org).
+     * @param reason the reason provided by the actor to kick the occupant from the room.
+     */
+    public abstract void kicked(String participant, String actor, String reason);
+
+    /**
+     * Called when a moderator grants voice to a visitor. This means that the visitor 
+     * can now participate in the moderated room sending messages to all occupants.
+     * 
+     * @param participant the participant that was granted voice in the room 
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void voiceGranted(String participant);
+
+    /**
+     * Called when a moderator revokes voice from a participant. This means that the participant 
+     * in the room was able to speak and now is a visitor that can't send messages to the room 
+     * occupants.
+     * 
+     * @param participant the participant that was revoked voice from the room 
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void voiceRevoked(String participant);
+
+    /**
+     * Called when an administrator or owner banned a participant from the room. This means that 
+     * banned participant will no longer be able to join the room unless the ban has been removed.
+     * 
+     * @param participant the participant that was banned from the room 
+     * (e.g. room@conference.jabber.org/nick).
+     * @param actor the administrator that banned the occupant (e.g. user@host.org).
+     * @param reason the reason provided by the administrator to ban the occupant.
+     */
+    public abstract void banned(String participant, String actor, String reason);
+
+    /**
+     * Called when an administrator grants a user membership to the room. This means that the user 
+     * will be able to join the members-only room.
+     * 
+     * @param participant the participant that was granted membership in the room 
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void membershipGranted(String participant);
+
+    /**
+     * Called when an administrator revokes a user membership to the room. This means that the 
+     * user will not be able to join the members-only room.
+     * 
+     * @param participant the participant that was revoked membership from the room 
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void membershipRevoked(String participant);
+
+    /**
+     * Called when an administrator grants moderator privileges to a user. This means that the user 
+     * will be able to kick users, grant and revoke voice, invite other users, modify room's 
+     * subject plus all the partcipants privileges.
+     * 
+     * @param participant the participant that was granted moderator privileges in the room 
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void moderatorGranted(String participant);
+
+    /**
+     * Called when an administrator revokes moderator privileges from a user. This means that the 
+     * user will no longer be able to kick users, grant and revoke voice, invite other users, 
+     * modify room's subject plus all the partcipants privileges.
+     * 
+     * @param participant the participant that was revoked moderator privileges in the room 
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void moderatorRevoked(String participant);
+
+    /**
+     * Called when an owner grants a user ownership on the room. This means that the user 
+     * will be able to change defining room features as well as perform all administrative 
+     * functions.
+     * 
+     * @param participant the participant that was granted ownership on the room 
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void ownershipGranted(String participant);
+
+    /**
+     * Called when an owner revokes a user ownership on the room. This means that the user 
+     * will no longer be able to change defining room features as well as perform all 
+     * administrative functions.
+     * 
+     * @param participant the participant that was revoked ownership on the room 
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void ownershipRevoked(String participant);
+
+    /**
+     * Called when an owner grants administrator privileges to a user. This means that the user 
+     * will be able to perform administrative functions such as banning users and edit moderator 
+     * list.
+     * 
+     * @param participant the participant that was granted administrator privileges 
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void adminGranted(String participant);
+
+    /**
+     * Called when an owner revokes administrator privileges from a user. This means that the user 
+     * will no longer be able to perform administrative functions such as banning users and edit 
+     * moderator list.
+     * 
+     * @param participant the participant that was revoked administrator privileges 
+     * (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void adminRevoked(String participant);
+
+    /**
+     * Called when a participant changed his/her nickname in the room. The new participant's 
+     * nickname will be informed with the next available presence.
+     * 
+     * @param participant the participant that was revoked administrator privileges
+     * (e.g. room@conference.jabber.org/nick).
+     * @param newNickname the new nickname that the participant decided to use.
+     */
+    public abstract void nicknameChanged(String participant, String newNickname);
+
+}
diff --git a/src/org/jivesoftware/smackx/muc/RoomInfo.java b/src/org/jivesoftware/smackx/muc/RoomInfo.java
new file mode 100644
index 0000000..f97f544
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/RoomInfo.java
@@ -0,0 +1,190 @@
+/**
+ * $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.smackx.muc;
+
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+
+import java.util.Iterator;
+
+/**
+ * Represents the room information that was discovered using Service Discovery. It's possible to
+ * obtain information about a room before joining the room but only for rooms that are public (i.e.
+ * rooms that may be discovered).
+ *
+ * @author Gaston Dombiak
+ */
+public class RoomInfo {
+
+    /**
+     * JID of the room. The node of the JID is commonly used as the ID of the room or name.
+     */
+    private String room;
+    /**
+     * Description of the room.
+     */
+    private String description = "";
+    /**
+     * Last known subject of the room.
+     */
+    private String subject = "";
+    /**
+     * Current number of occupants in the room.
+     */
+    private int occupantsCount = -1;
+    /**
+     * A room is considered members-only if an invitation is required in order to enter the room.
+     * Any user that is not a member of the room won't be able to join the room unless the user
+     * decides to register with the room (thus becoming a member).
+     */
+    private boolean membersOnly;
+    /**
+     * Moderated rooms enable only participants to speak. Users that join the room and aren't
+     * participants can't speak (they are just visitors).
+     */
+    private boolean moderated;
+    /**
+     * Every presence packet can include the JID of every occupant unless the owner deactives this
+     * configuration.
+     */
+    private boolean nonanonymous;
+    /**
+     * Indicates if users must supply a password to join the room.
+     */
+    private boolean passwordProtected;
+    /**
+     * Persistent rooms are saved to the database to make sure that rooms configurations can be
+     * restored in case the server goes down.
+     */
+    private boolean persistent;
+
+    RoomInfo(DiscoverInfo info) {
+        super();
+        this.room = info.getFrom();
+        // Get the information based on the discovered features
+        this.membersOnly = info.containsFeature("muc_membersonly");
+        this.moderated = info.containsFeature("muc_moderated");
+        this.nonanonymous = info.containsFeature("muc_nonanonymous");
+        this.passwordProtected = info.containsFeature("muc_passwordprotected");
+        this.persistent = info.containsFeature("muc_persistent");
+        // Get the information based on the discovered extended information
+        Form form = Form.getFormFrom(info);
+        if (form != null) {
+            FormField descField = form.getField("muc#roominfo_description");
+            this.description = ( descField == null || !(descField.getValues().hasNext()) )? "" : descField.getValues().next();
+
+            FormField subjField = form.getField("muc#roominfo_subject");
+            this.subject = ( subjField == null || !(subjField.getValues().hasNext()) ) ? "" : subjField.getValues().next();
+
+            FormField occCountField = form.getField("muc#roominfo_occupants");
+            this.occupantsCount = occCountField == null ? -1 : Integer.parseInt(occCountField.getValues()
+                    .next());
+        }
+    }
+
+    /**
+     * Returns the JID of the room whose information was discovered.
+     *
+     * @return the JID of the room whose information was discovered.
+     */
+    public String getRoom() {
+        return room;
+    }
+
+    /**
+     * Returns the discovered description of the room.
+     *
+     * @return the discovered description of the room.
+     */
+    public String getDescription() {
+        return description;
+    }
+
+    /**
+     * Returns the discovered subject of the room. The subject may be empty if the room does not
+     * have a subject.
+     *
+     * @return the discovered subject of the room.
+     */
+    public String getSubject() {
+        return subject;
+    }
+
+    /**
+     * Returns the discovered number of occupants that are currently in the room. If this
+     * information was not discovered (i.e. the server didn't send it) then a value of -1 will be
+     * returned.
+     *
+     * @return the number of occupants that are currently in the room or -1 if that information was
+     * not provided by the server.
+     */
+    public int getOccupantsCount() {
+        return occupantsCount;
+    }
+
+    /**
+     * Returns true if the room has restricted the access so that only members may enter the room.
+     *
+     * @return true if the room has restricted the access so that only members may enter the room.
+     */
+    public boolean isMembersOnly() {
+        return membersOnly;
+    }
+
+    /**
+     * Returns true if the room enabled only participants to speak. Occupants with a role of
+     * visitor won't be able to speak in the room.
+     *
+     * @return true if the room enabled only participants to speak.
+     */
+    public boolean isModerated() {
+        return moderated;
+    }
+
+    /**
+     * Returns true if presence packets will include the JID of every occupant.
+     *
+     * @return true if presence packets will include the JID of every occupant.
+     */
+    public boolean isNonanonymous() {
+        return nonanonymous;
+    }
+
+    /**
+     * Returns true if users musy provide a valid password in order to join the room.
+     *
+     * @return true if users musy provide a valid password in order to join the room.
+     */
+    public boolean isPasswordProtected() {
+        return passwordProtected;
+    }
+
+    /**
+     * Returns true if the room will persist after the last occupant have left the room.
+     *
+     * @return true if the room will persist after the last occupant have left the room.
+     */
+    public boolean isPersistent() {
+        return persistent;
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/muc/RoomListenerMultiplexor.java b/src/org/jivesoftware/smackx/muc/RoomListenerMultiplexor.java
new file mode 100644
index 0000000..6f8bf05
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/RoomListenerMultiplexor.java
@@ -0,0 +1,227 @@
+/**
+ * $RCSfile$
+ * $Revision: 2779 $
+ * $Date: 2005-09-05 17:00:45 -0300 (Mon, 05 Sep 2005) $
+ *
+ * Copyright 2003-2006 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.smackx.muc;
+
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.Map;
+import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A <code>RoomListenerMultiplexor</code> multiplexes incoming packets on
+ * a <code>Connection</code> using a single listener/filter pair.
+ * A single <code>RoomListenerMultiplexor</code> is created for each
+ * {@link org.jivesoftware.smack.Connection} that has joined MUC rooms
+ * within its session.
+ *
+ * @author Larry Kirschner
+ */
+class RoomListenerMultiplexor implements ConnectionListener {
+
+    // We use a WeakHashMap so that the GC can collect the monitor when the
+    // connection is no longer referenced by any object.
+    private static final Map<Connection, WeakReference<RoomListenerMultiplexor>> monitors =
+            new WeakHashMap<Connection, WeakReference<RoomListenerMultiplexor>>();
+
+    private Connection connection;
+    private RoomMultiplexFilter filter;
+    private RoomMultiplexListener listener;
+
+    /**
+     * Returns a new or existing RoomListenerMultiplexor for a given connection.
+     *
+     * @param conn the connection to monitor for room invitations.
+     * @return a new or existing RoomListenerMultiplexor for a given connection.
+     */
+    public static RoomListenerMultiplexor getRoomMultiplexor(Connection conn) {
+        synchronized (monitors) {
+            if (!monitors.containsKey(conn) || monitors.get(conn).get() == null) {
+                RoomListenerMultiplexor rm = new RoomListenerMultiplexor(conn, new RoomMultiplexFilter(),
+                        new RoomMultiplexListener());
+
+                rm.init();
+
+                // We need to use a WeakReference because the monitor references the
+                // connection and this could prevent the GC from collecting the monitor
+                // when no other object references the monitor
+                monitors.put(conn, new WeakReference<RoomListenerMultiplexor>(rm));
+            }
+            // Return the InvitationsMonitor that monitors the connection
+            return monitors.get(conn).get();
+        }
+    }
+
+    /**
+     * All access should be through
+     * the static method {@link #getRoomMultiplexor(Connection)}.
+     */
+    private RoomListenerMultiplexor(Connection connection, RoomMultiplexFilter filter,
+            RoomMultiplexListener listener) {
+        if (connection == null) {
+            throw new IllegalArgumentException("Connection is null");
+        }
+        if (filter == null) {
+            throw new IllegalArgumentException("Filter is null");
+        }
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener is null");
+        }
+        this.connection = connection;
+        this.filter = filter;
+        this.listener = listener;
+    }
+
+    public void addRoom(String address, PacketMultiplexListener roomListener) {
+        filter.addRoom(address);
+        listener.addRoom(address, roomListener);
+    }
+
+    public void connectionClosed() {
+        cancel();
+    }
+
+    public void connectionClosedOnError(Exception e) {
+        cancel();
+    }
+
+    public void reconnectingIn(int seconds) {
+        // ignore
+    }
+
+    public void reconnectionSuccessful() {
+        // ignore
+    }
+
+    public void reconnectionFailed(Exception e) {
+        // ignore
+    }
+
+    /**
+     * Initializes the listeners to detect received room invitations and to detect when the
+     * connection gets closed. As soon as a room invitation is received the invitations
+     * listeners will be fired. When the connection gets closed the monitor will remove
+     * his listeners on the connection.
+     */
+    public void init() {
+        connection.addConnectionListener(this);
+        connection.addPacketListener(listener, filter);
+    }
+
+    public void removeRoom(String address) {
+        filter.removeRoom(address);
+        listener.removeRoom(address);
+    }
+
+    /**
+     * Cancels all the listeners that this InvitationsMonitor has added to the connection.
+     */
+    private void cancel() {
+        connection.removeConnectionListener(this);
+        connection.removePacketListener(listener);
+    }
+
+    /**
+     * The single <code>Connection</code>-level <code>PacketFilter</code> used by a {@link RoomListenerMultiplexor}
+     * for all muc chat rooms on an <code>Connection</code>.
+     * Each time a muc chat room is added to/removed from an
+     * <code>Connection</code> the address for that chat room
+     * is added to/removed from that <code>Connection</code>'s
+     * <code>RoomMultiplexFilter</code>.
+     */
+    private static class RoomMultiplexFilter implements PacketFilter {
+
+        private Map<String, String> roomAddressTable = new ConcurrentHashMap<String, String>();
+
+        public boolean accept(Packet p) {
+            String from = p.getFrom();
+            if (from == null) {
+                return false;
+            }
+            return roomAddressTable.containsKey(StringUtils.parseBareAddress(from).toLowerCase());
+        }
+
+        public void addRoom(String address) {
+            if (address == null) {
+                return;
+            }
+            roomAddressTable.put(address.toLowerCase(), address);
+        }
+
+        public void removeRoom(String address) {
+            if (address == null) {
+                return;
+            }
+            roomAddressTable.remove(address.toLowerCase());
+        }
+    }
+
+    /**
+     * The single <code>Connection</code>-level <code>PacketListener</code>
+     * used by a {@link RoomListenerMultiplexor}
+     * for all muc chat rooms on an <code>Connection</code>.
+     * Each time a muc chat room is added to/removed from an
+     * <code>Connection</code> the address and listener for that chat room
+     * are added to/removed from that <code>Connection</code>'s
+     * <code>RoomMultiplexListener</code>.
+     *
+     * @author Larry Kirschner
+     */
+    private static class RoomMultiplexListener implements PacketListener {
+
+        private Map<String, PacketMultiplexListener> roomListenersByAddress =
+                new ConcurrentHashMap<String, PacketMultiplexListener>();
+
+        public void processPacket(Packet p) {
+            String from = p.getFrom();
+            if (from == null) {
+                return;
+            }
+
+            PacketMultiplexListener listener =
+                    roomListenersByAddress.get(StringUtils.parseBareAddress(from).toLowerCase());
+
+            if (listener != null) {
+                listener.processPacket(p);
+            }
+        }
+
+        public void addRoom(String address, PacketMultiplexListener listener) {
+            if (address == null) {
+                return;
+            }
+            roomListenersByAddress.put(address.toLowerCase(), listener);
+        }
+
+        public void removeRoom(String address) {
+            if (address == null) {
+                return;
+            }
+            roomListenersByAddress.remove(address.toLowerCase());
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java b/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java
new file mode 100644
index 0000000..3a2deab
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/SubjectUpdatedListener.java
@@ -0,0 +1,38 @@
+/**
+ * $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.smackx.muc;
+
+/**
+ * A listener that is fired anytime a MUC room changes its subject.
+ * 
+ * @author Gaston Dombiak
+ */
+public interface SubjectUpdatedListener {
+
+    /**
+     * Called when a MUC room has changed its subject.
+     * 
+     * @param subject the new room's subject.
+     * @param from the user that changed the room's subject (e.g. room@conference.jabber.org/nick).
+     */
+    public abstract void subjectUpdated(String subject, String from);
+
+}
diff --git a/src/org/jivesoftware/smackx/muc/UserStatusListener.java b/src/org/jivesoftware/smackx/muc/UserStatusListener.java
new file mode 100644
index 0000000..27f0f58
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/UserStatusListener.java
@@ -0,0 +1,127 @@
+/**
+ * $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.smackx.muc;
+
+/**
+ * A listener that is fired anytime your participant's status in a room is changed, such as the 
+ * user being kicked, banned, or granted admin permissions.
+ * 
+ * @author Gaston Dombiak
+ */
+public interface UserStatusListener {
+
+    /**
+     * Called when a moderator kicked your user from the room. This means that you are no longer
+     * participanting in the room.
+     * 
+     * @param actor the moderator that kicked your user from the room (e.g. user@host.org).
+     * @param reason the reason provided by the actor to kick you from the room.
+     */
+    public abstract void kicked(String actor, String reason);
+
+    /**
+     * Called when a moderator grants voice to your user. This means that you were a visitor in 
+     * the moderated room before and now you can participate in the room by sending messages to 
+     * all occupants.
+     * 
+     */
+    public abstract void voiceGranted();
+
+    /**
+     * Called when a moderator revokes voice from your user. This means that you were a 
+     * participant in the room able to speak and now you are a visitor that can't send 
+     * messages to the room occupants.
+     * 
+     */
+    public abstract void voiceRevoked();
+
+    /**
+     * Called when an administrator or owner banned your user from the room. This means that you 
+     * will no longer be able to join the room unless the ban has been removed.
+     * 
+     * @param actor the administrator that banned your user (e.g. user@host.org).
+     * @param reason the reason provided by the administrator to banned you.
+     */
+    public abstract void banned(String actor, String reason);
+
+    /**
+     * Called when an administrator grants your user membership to the room. This means that you 
+     * will be able to join the members-only room. 
+     * 
+     */
+    public abstract void membershipGranted();
+
+    /**
+     * Called when an administrator revokes your user membership to the room. This means that you 
+     * will not be able to join the members-only room.
+     * 
+     */
+    public abstract void membershipRevoked();
+
+    /**
+     * Called when an administrator grants moderator privileges to your user. This means that you 
+     * will be able to kick users, grant and revoke voice, invite other users, modify room's 
+     * subject plus all the partcipants privileges.
+     * 
+     */
+    public abstract void moderatorGranted();
+
+    /**
+     * Called when an administrator revokes moderator privileges from your user. This means that 
+     * you will no longer be able to kick users, grant and revoke voice, invite other users, 
+     * modify room's subject plus all the partcipants privileges.
+     * 
+     */
+    public abstract void moderatorRevoked();
+
+    /**
+     * Called when an owner grants to your user ownership on the room. This means that you 
+     * will be able to change defining room features as well as perform all administrative 
+     * functions.
+     * 
+     */
+    public abstract void ownershipGranted();
+
+    /**
+     * Called when an owner revokes from your user ownership on the room. This means that you 
+     * will no longer be able to change defining room features as well as perform all 
+     * administrative functions.
+     * 
+     */
+    public abstract void ownershipRevoked();
+
+    /**
+     * Called when an owner grants administrator privileges to your user. This means that you 
+     * will be able to perform administrative functions such as banning users and edit moderator 
+     * list.
+     * 
+     */
+    public abstract void adminGranted();
+
+    /**
+     * Called when an owner revokes administrator privileges from your user. This means that you 
+     * will no longer be able to perform administrative functions such as banning users and edit 
+     * moderator list.
+     * 
+     */
+    public abstract void adminRevoked();
+
+}
diff --git a/src/org/jivesoftware/smackx/muc/package.html b/src/org/jivesoftware/smackx/muc/package.html
new file mode 100644
index 0000000..dcfaeaa
--- /dev/null
+++ b/src/org/jivesoftware/smackx/muc/package.html
@@ -0,0 +1 @@
+<body>Classes and Interfaces that implement Multi-User Chat (MUC).</body>
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/package.html b/src/org/jivesoftware/smackx/package.html
new file mode 100644
index 0000000..d574a2a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/package.html
@@ -0,0 +1 @@
+<body>Smack extensions API.</body>
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/packet/AdHocCommandData.java b/src/org/jivesoftware/smackx/packet/AdHocCommandData.java
new file mode 100755
index 0000000..bceffcd
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/AdHocCommandData.java
@@ -0,0 +1,279 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2005-2008 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.smackx.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.commands.AdHocCommand;

+import org.jivesoftware.smackx.commands.AdHocCommand.Action;

+import org.jivesoftware.smackx.commands.AdHocCommand.SpecificErrorCondition;

+import org.jivesoftware.smackx.commands.AdHocCommandNote;

+

+import java.util.ArrayList;

+import java.util.List;

+

+/**

+ * Represents the state and the request of the execution of an adhoc command.

+ * 

+ * @author Gabriel Guardincerri

+ */

+public class AdHocCommandData extends IQ {

+

+    /* JID of the command host */

+    private String id;

+

+    /* Command name */

+    private String name;

+

+    /* Command identifier */

+    private String node;

+

+    /* Unique ID of the execution */

+    private String sessionID;

+

+    private List<AdHocCommandNote> notes = new ArrayList<AdHocCommandNote>();

+

+    private DataForm form;

+

+    /* Action request to be executed */

+    private AdHocCommand.Action action;

+

+    /* Current execution status */

+    private AdHocCommand.Status status;

+

+    private ArrayList<AdHocCommand.Action> actions = new ArrayList<AdHocCommand.Action>();

+

+    private AdHocCommand.Action executeAction;

+

+    private String lang;

+

+    public AdHocCommandData() {

+    }

+

+    @Override

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<command xmlns=\"http://jabber.org/protocol/commands\"");

+        buf.append(" node=\"").append(node).append("\"");

+        if (sessionID != null) {

+            if (!sessionID.equals("")) {

+                buf.append(" sessionid=\"").append(sessionID).append("\"");

+            }

+        }

+        if (status != null) {

+            buf.append(" status=\"").append(status).append("\"");

+        }

+        if (action != null) {

+            buf.append(" action=\"").append(action).append("\"");

+        }

+

+        if (lang != null) {

+            if (!lang.equals("")) {

+                buf.append(" lang=\"").append(lang).append("\"");

+            }

+        }

+        buf.append(">");

+

+        if (getType() == Type.RESULT) {

+            buf.append("<actions");

+

+            if (executeAction != null) {

+                buf.append(" execute=\"").append(executeAction).append("\"");

+            }

+            if (actions.size() == 0) {

+                buf.append("/>");

+            } else {

+                buf.append(">");

+

+                for (AdHocCommand.Action action : actions) {

+                    buf.append("<").append(action).append("/>");

+                }

+                buf.append("</actions>");

+            }

+        }

+

+        if (form != null) {

+            buf.append(form.toXML());

+        }

+

+        for (AdHocCommandNote note : notes) {

+            buf.append("<note type=\"").append(note.getType().toString()).append("\">");

+            buf.append(note.getValue());

+            buf.append("</note>");

+        }

+

+        // TODO ERRORS

+//        if (getError() != null) {

+//            buf.append(getError().toXML());

+//        }

+

+        buf.append("</command>");

+        return buf.toString();

+    }

+

+    /**

+     * Returns the JID of the command host.

+     *

+     * @return the JID of the command host.

+     */

+    public String getId() {

+        return id;

+    }

+

+    public void setId(String id) {

+        this.id = id;

+    }

+

+    /**

+     * Returns the human name of the command

+     *

+     * @return the name of the command.

+     */

+    public String getName() {

+        return name;

+    }

+

+    public void setName(String name) {

+        this.name = name;

+    }

+

+    /**

+     * Returns the identifier of the command

+     *

+     * @return the node.

+     */

+    public String getNode() {

+        return node;

+    }

+

+    public void setNode(String node) {

+        this.node = node;

+    }

+

+    /**

+     * Returns the list of notes that the command has.

+     *

+     * @return the notes.

+     */

+    public List<AdHocCommandNote> getNotes() {

+        return notes;

+    }

+

+    public void addNote(AdHocCommandNote note) {

+        this.notes.add(note);

+    }

+

+    public void remveNote(AdHocCommandNote note) {

+        this.notes.remove(note);

+    }

+

+    /**

+     * Returns the form of the command.

+     *

+     * @return the data form associated with the command.

+     */

+    public DataForm getForm() {

+        return form;

+    }

+

+    public void setForm(DataForm form) {

+        this.form = form;

+    }

+

+    /**

+     * Returns the action to execute. The action is set only on a request.

+     *

+     * @return the action to execute.

+     */

+    public AdHocCommand.Action getAction() {

+        return action;

+    }

+

+    public void setAction(AdHocCommand.Action action) {

+        this.action = action;

+    }

+

+    /**

+     * Returns the status of the execution.

+     *

+     * @return the status.

+     */

+    public AdHocCommand.Status getStatus() {

+        return status;

+    }

+

+    public void setStatus(AdHocCommand.Status status) {

+        this.status = status;

+    }

+

+    public List<Action> getActions() {

+        return actions;

+    }

+

+    public void addAction(Action action) {

+        actions.add(action);

+    }

+

+    public void setExecuteAction(Action executeAction) {

+        this.executeAction = executeAction;

+    }

+

+    public Action getExecuteAction() {

+        return executeAction;

+    }

+

+    public void setSessionID(String sessionID) {

+        this.sessionID = sessionID;

+    }

+

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    public static class SpecificError implements PacketExtension {

+

+        public static final String namespace = "http://jabber.org/protocol/commands";

+        

+        public SpecificErrorCondition condition;

+        

+        public SpecificError(SpecificErrorCondition condition) {

+            this.condition = condition;

+        }

+        

+        public String getElementName() {

+            return condition.toString();

+        }

+        public String getNamespace() {

+            return namespace;

+        }

+

+        public SpecificErrorCondition getCondition() {

+            return condition;

+        }

+        

+        public String toXML() {

+            StringBuilder buf = new StringBuilder();

+            buf.append("<").append(getElementName());

+            buf.append(" xmlns=\"").append(getNamespace()).append("\"/>");

+            return buf.toString();

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/packet/AttentionExtension.java b/src/org/jivesoftware/smackx/packet/AttentionExtension.java
new file mode 100644
index 0000000..d86fa41
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/AttentionExtension.java
@@ -0,0 +1,100 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2010 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.smackx.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * A PacketExtension that implements XEP-0224: Attention

+ * 

+ * This extension is expected to be added to message stanzas of type 'headline.'

+ * Please refer to the XEP for more implementation guidelines.

+ * 

+ * @author Guus der Kinderen, guus.der.kinderen@gmail.com

+ * @see <a

+ *      href="http://xmpp.org/extensions/xep-0224.html">XEP-0224:&nbsp;Attention</a>

+ */

+public class AttentionExtension implements PacketExtension {

+

+    /**

+     * The XML element name of an 'attention' extension.

+     */

+    public static final String ELEMENT_NAME = "attention";

+

+    /**

+     * The namespace that qualifies the XML element of an 'attention' extension.

+     */

+    public static final String NAMESPACE = "urn:xmpp:attention:0";

+

+    /*

+     * (non-Javadoc)

+     * 

+     * @see org.jivesoftware.smack.packet.PacketExtension#getElementName()

+     */

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    /*

+     * (non-Javadoc)

+     * 

+     * @see org.jivesoftware.smack.packet.PacketExtension#getNamespace()

+     */

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    /*

+     * (non-Javadoc)

+     * 

+     * @see org.jivesoftware.smack.packet.PacketExtension#toXML()

+     */

+    public String toXML() {

+        final StringBuilder sb = new StringBuilder();

+        sb.append("<").append(getElementName()).append(" xmlns=\"").append(

+                getNamespace()).append("\"/>");

+        return sb.toString();

+    }

+

+    /**

+     * A {@link PacketExtensionProvider} for the {@link AttentionExtension}. As

+     * Attention elements have no state/information other than the element name

+     * and namespace, this implementation simply returns new instances of

+     * {@link AttentionExtension}.

+     * 

+     * @author Guus der Kinderen, guus.der.kinderen@gmail.com

+s     */

+    public static class Provider implements PacketExtensionProvider {

+

+        /*

+         * (non-Javadoc)

+         * 

+         * @see

+         * org.jivesoftware.smack.provider.PacketExtensionProvider#parseExtension

+         * (org.xmlpull.v1.XmlPullParser)

+         */

+        public PacketExtension parseExtension(XmlPullParser arg0)

+                throws Exception {

+            return new AttentionExtension();

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/packet/ChatStateExtension.java b/src/org/jivesoftware/smackx/packet/ChatStateExtension.java
new file mode 100644
index 0000000..ecc6acc
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/ChatStateExtension.java
@@ -0,0 +1,73 @@
+/**

+ * $RCSfile$

+ * $Revision: 2407 $

+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $

+ *

+ * 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.smackx.packet;

+

+import org.jivesoftware.smackx.ChatState;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Represents a chat state which is an extension to message packets which is used to indicate

+ * the current status of a chat participant.

+ *

+ * @author Alexander Wenckus

+ * @see org.jivesoftware.smackx.ChatState

+ */

+public class ChatStateExtension implements PacketExtension {

+

+    private ChatState state;

+

+    /**

+     * Default constructor. The argument provided is the state that the extension will represent.

+     *

+     * @param state the state that the extension represents.

+     */

+    public ChatStateExtension(ChatState state) {

+        this.state = state;

+    }

+

+    public String getElementName() {

+        return state.name();

+    }

+

+    public String getNamespace() {

+        return "http://jabber.org/protocol/chatstates";

+    }

+

+    public String toXML() {

+        return "<" + getElementName() + " xmlns=\"" + getNamespace() + "\" />";

+    }

+

+    public static class Provider implements PacketExtensionProvider {

+

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            ChatState state;

+            try {

+                state = ChatState.valueOf(parser.getName());

+            }

+            catch (Exception ex) {

+                state = ChatState.active;

+            }

+            return new ChatStateExtension(state);

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/packet/DataForm.java b/src/org/jivesoftware/smackx/packet/DataForm.java
new file mode 100644
index 0000000..4d12892
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/DataForm.java
@@ -0,0 +1,312 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Represents a form that could be use for gathering data as well as for reporting data
+ * returned from a search.
+ *
+ * @author Gaston Dombiak
+ */
+public class DataForm implements PacketExtension {
+
+    private String type;
+    private String title;
+    private List<String> instructions = new ArrayList<String>();
+    private ReportedData reportedData;
+    private final List<Item> items = new ArrayList<Item>();
+    private final List<FormField> fields = new ArrayList<FormField>();
+    
+    public DataForm(String type) {
+        this.type = type;
+    }
+    
+    /**
+     * Returns the meaning of the data within the context. The data could be part of a form
+     * to fill out, a form submission or data results.<p>
+     * 
+     * Possible form types are:
+     * <ul>
+     *  <li>form -> This packet contains a form to fill out. Display it to the user (if your 
+     * program can).</li>
+     *  <li>submit -> The form is filled out, and this is the data that is being returned from 
+     * the form.</li>
+     *  <li>cancel -> The form was cancelled. Tell the asker that piece of information.</li>
+     *  <li>result -> Data results being returned from a search, or some other query.</li>
+     * </ul>
+     * 
+     * @return the form's type.
+     */
+    public String getType() {
+        return type; 
+    }
+    
+    /**
+     * Returns the description of the data. It is similar to the title on a web page or an X 
+     * window.  You can put a <title/> on either a form to fill out, or a set of data results.
+     * 
+     * @return description of the data.
+     */
+    public String getTitle() {
+        return title;
+    }
+
+    /**
+     * Returns an Iterator for the list of instructions that explain how to fill out the form and 
+     * what the form is about. The dataform could include multiple instructions since each 
+     * instruction could not contain newlines characters. Join the instructions together in order 
+     * to show them to the user.    
+     * 
+     * @return an Iterator for the list of instructions that explain how to fill out the form.
+     */
+    public Iterator<String> getInstructions() {
+        synchronized (instructions) {
+            return Collections.unmodifiableList(new ArrayList<String>(instructions)).iterator();
+        }
+    }
+
+    /**
+     * Returns the fields that will be returned from a search.
+     * 
+     * @return fields that will be returned from a search.
+     */
+    public ReportedData getReportedData() {
+        return reportedData;
+    }
+
+    /**
+     * Returns an Iterator for the items returned from a search.
+     *
+     * @return an Iterator for the items returned from a search.
+     */
+    public Iterator<Item> getItems() {
+        synchronized (items) {
+            return Collections.unmodifiableList(new ArrayList<Item>(items)).iterator();
+        }
+    }
+
+    /**
+     * Returns an Iterator for the fields that are part of the form.
+     *
+     * @return an Iterator for the fields that are part of the form.
+     */
+    public Iterator<FormField> getFields() {
+        synchronized (fields) {
+            return Collections.unmodifiableList(new ArrayList<FormField>(fields)).iterator();
+        }
+    }
+
+    public String getElementName() {
+        return Form.ELEMENT;
+    }
+
+    public String getNamespace() {
+        return Form.NAMESPACE;
+    }
+
+    /**
+     * Sets the description of the data. It is similar to the title on a web page or an X window.
+     * You can put a <title/> on either a form to fill out, or a set of data results.
+     * 
+     * @param title description of the data.
+     */
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    /**
+     * Sets the list of instructions that explain how to fill out the form and what the form is 
+     * about. The dataform could include multiple instructions since each instruction could not 
+     * contain newlines characters. 
+     * 
+     * @param instructions list of instructions that explain how to fill out the form.
+     */
+    public void setInstructions(List<String> instructions) {
+        this.instructions = instructions;
+    }
+
+    /**
+     * Sets the fields that will be returned from a search.
+     * 
+     * @param reportedData the fields that will be returned from a search.
+     */
+    public void setReportedData(ReportedData reportedData) {
+        this.reportedData = reportedData;
+    }
+
+    /**
+     * Adds a new field as part of the form.
+     * 
+     * @param field the field to add to the form.
+     */
+    public void addField(FormField field) {
+        synchronized (fields) {
+            fields.add(field);
+        }
+    }
+    
+    /**
+     * Adds a new instruction to the list of instructions that explain how to fill out the form 
+     * and what the form is about. The dataform could include multiple instructions since each 
+     * instruction could not contain newlines characters. 
+     * 
+     * @param instruction the new instruction that explain how to fill out the form.
+     */
+    public void addInstruction(String instruction) {
+        synchronized (instructions) {
+            instructions.add(instruction);
+        }
+    }
+
+    /**
+     * Adds a new item returned from a search.
+     * 
+     * @param item the item returned from a search.
+     */
+    public void addItem(Item item) {
+        synchronized (items) {
+            items.add(item);
+        }
+    }
+
+    /**
+     * Returns true if this DataForm has at least one FORM_TYPE field which is
+     * hidden. This method is used for sanity checks.
+     *
+     * @return
+     */
+    public boolean hasHiddenFormTypeField() {
+        boolean found = false;
+        for (FormField f : fields) {
+            if (f.getVariable().equals("FORM_TYPE") && f.getType() != null && f.getType().equals("hidden"))
+                found = true;
+        }
+        return found;
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+            "\" type=\"" + getType() +"\">");
+        if (getTitle() != null) {
+            buf.append("<title>").append(getTitle()).append("</title>");
+        }
+        for (Iterator<String> it=getInstructions(); it.hasNext();) {
+            buf.append("<instructions>").append(it.next()).append("</instructions>");
+        }
+        // Append the list of fields returned from a search
+        if (getReportedData() != null) {
+            buf.append(getReportedData().toXML());
+        }
+        // Loop through all the items returned from a search and append them to the string buffer
+        for (Iterator<Item> i = getItems(); i.hasNext();) {
+            Item item = i.next();
+            buf.append(item.toXML());
+        }
+        // Loop through all the form fields and append them to the string buffer
+        for (Iterator<FormField> i = getFields(); i.hasNext();) {
+            FormField field = i.next();
+            buf.append(field.toXML());
+        }
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+    /**
+     * 
+     * Represents the fields that will be returned from a search. This information is useful when 
+     * you try to use the jabber:iq:search namespace to return dynamic form information.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class ReportedData {
+        private List<FormField> fields = new ArrayList<FormField>();
+        
+        public ReportedData(List<FormField> fields) {
+            this.fields = fields;
+        }
+        
+        /**
+         * Returns the fields returned from a search.
+         * 
+         * @return the fields returned from a search.
+         */
+        public Iterator<FormField> getFields() {
+            return Collections.unmodifiableList(new ArrayList<FormField>(fields)).iterator();
+        }
+        
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<reported>");
+            // Loop through all the form items and append them to the string buffer
+            for (Iterator<FormField> i = getFields(); i.hasNext();) {
+                FormField field = i.next();
+                buf.append(field.toXML());
+            }
+            buf.append("</reported>");
+            return buf.toString();
+        }
+    }
+    
+    /**
+     * 
+     * Represents items of reported data.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Item {
+        private List<FormField> fields = new ArrayList<FormField>();
+        
+        public Item(List<FormField> fields) {
+            this.fields = fields;
+        }
+        
+        /**
+         * Returns the fields that define the data that goes with the item.
+         * 
+         * @return the fields that define the data that goes with the item.
+         */
+        public Iterator<FormField> getFields() {
+            return Collections.unmodifiableList(new ArrayList<FormField>(fields)).iterator();
+        }
+        
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<item>");
+            // Loop through all the form items and append them to the string buffer
+            for (Iterator<FormField> i = getFields(); i.hasNext();) {
+                FormField field = i.next();
+                buf.append(field.toXML());
+            }
+            buf.append("</item>");
+            return buf.toString();
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java b/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java
new file mode 100644
index 0000000..b58fc6c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/DefaultPrivateData.java
@@ -0,0 +1,137 @@
+/**
+ * $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.smackx.packet;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Default implementation of the PrivateData interface. Unless a PrivateDataProvider
+ * is registered with the PrivateDataManager class, instances of this class will be
+ * returned when getting private data.<p>
+ *
+ * This class provides a very simple representation of an XML sub-document. Each element
+ * is a key in a Map with its CDATA being the value. For example, given the following
+ * XML sub-document:
+ *
+ * <pre>
+ * &lt;foo xmlns="http://bar.com"&gt;
+ *     &lt;color&gt;blue&lt;/color&gt;
+ *     &lt;food&gt;pizza&lt;/food&gt;
+ * &lt;/foo&gt;</pre>
+ *
+ * In this case, getValue("color") would return "blue", and getValue("food") would
+ * return "pizza". This parsing mechanism mechanism is very simplistic and will not work
+ * as desired in all cases (for example, if some of the elements have attributes. In those
+ * cases, a custom {@link org.jivesoftware.smackx.provider.PrivateDataProvider} should be used.
+ *
+ * @author Matt Tucker
+ */
+public class DefaultPrivateData implements PrivateData {
+
+    private String elementName;
+    private String namespace;
+    private Map<String, String> map;
+
+    /**
+     * Creates a new generic private data object.
+     *
+     * @param elementName the name of the element of the XML sub-document.
+     * @param namespace the namespace of the element.
+     */
+    public DefaultPrivateData(String elementName, String namespace) {
+        this.elementName = elementName;
+        this.namespace = namespace;
+    }
+
+     /**
+     * Returns the XML element name of the private data sub-packet root element.
+     *
+     * @return the XML element name of the packet extension.
+     */
+    public String getElementName() {
+        return elementName;
+    }
+
+    /**
+     * Returns the XML namespace of the private data sub-packet root element.
+     *
+     * @return the XML namespace of the packet extension.
+     */
+    public String getNamespace() {
+        return namespace;
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(elementName).append(" xmlns=\"").append(namespace).append("\">");
+        for (Iterator<String> i=getNames(); i.hasNext(); ) {
+            String name = i.next();
+            String value = getValue(name);
+            buf.append("<").append(name).append(">");
+            buf.append(value);
+            buf.append("</").append(name).append(">");
+        }
+        buf.append("</").append(elementName).append(">");
+        return buf.toString();
+    }
+
+    /**
+     * Returns an Iterator for the names that can be used to get
+     * values of the private data.
+     *
+     * @return an Iterator for the names.
+     */
+    public synchronized Iterator<String> getNames() {
+        if (map == null) {
+            return Collections.<String>emptyList().iterator();
+        }
+        return Collections.unmodifiableSet(map.keySet()).iterator();
+    }
+
+    /**
+     * Returns a value given a name.
+     *
+     * @param name the name.
+     * @return the value.
+     */
+    public synchronized String getValue(String name) {
+        if (map == null) {
+            return null;
+        }
+        return (String)map.get(name);
+    }
+
+    /**
+     * Sets a value given the name.
+     *
+     * @param name the name.
+     * @param value the value.
+     */
+    public synchronized void setValue(String name, String value) {
+        if (map == null) {
+            map = new HashMap<String,String>();
+        }
+        map.put(name, value);
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/packet/DelayInfo.java b/src/org/jivesoftware/smackx/packet/DelayInfo.java
new file mode 100644
index 0000000..f404971
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/DelayInfo.java
@@ -0,0 +1,105 @@
+/**
+ * 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.smackx.packet;
+
+import java.util.Date;
+
+import org.jivesoftware.smack.util.StringUtils;

+
+/**
+ * A decorator for the {@link DelayInformation} class to transparently support
+ * both the new <b>Delay Delivery</b> specification <a href="http://xmpp.org/extensions/xep-0203.html">XEP-0203</a> and 
+ * the old one <a href="http://xmpp.org/extensions/xep-0091.html">XEP-0091</a>.
+ * 
+ * Existing code can be backward compatible. 
+ * 
+ * @author Robin Collier
+ */
+public class DelayInfo extends DelayInformation
+{
+    
+	DelayInformation wrappedInfo;
+
+        /**
+         * Creates a new instance with given delay information. 
+         * @param delay the delay information
+         */
+	public DelayInfo(DelayInformation delay)
+	{
+		super(delay.getStamp());
+		wrappedInfo = delay;
+	}
+	
+	@Override
+	public String getFrom()
+	{
+		return wrappedInfo.getFrom();
+	}
+
+	@Override
+	public String getReason()
+	{
+		return wrappedInfo.getReason();
+	}
+
+	@Override
+	public Date getStamp()
+	{
+		return wrappedInfo.getStamp();
+	}
+
+	@Override
+	public void setFrom(String from)
+	{
+		wrappedInfo.setFrom(from);
+	}
+
+	@Override
+	public void setReason(String reason)
+	{
+		wrappedInfo.setReason(reason);
+	}
+
+	@Override
+	public String getElementName()
+	{
+		return "delay";
+	}
+
+	@Override
+	public String getNamespace()
+	{
+		return "urn:xmpp:delay";
+	}
+
+	@Override
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+                "\"");
+        buf.append(" stamp=\"");
+        buf.append(StringUtils.formatXEP0082Date(getStamp()));

+        buf.append("\"");
+        if (getFrom() != null && getFrom().length() > 0) {
+            buf.append(" from=\"").append(getFrom()).append("\"");
+        }
+        buf.append(">");
+        if (getReason() != null && getReason().length() > 0) {
+            buf.append(getReason());
+        }
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+	
+}
diff --git a/src/org/jivesoftware/smackx/packet/DelayInformation.java b/src/org/jivesoftware/smackx/packet/DelayInformation.java
new file mode 100644
index 0000000..b9ab485
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/DelayInformation.java
@@ -0,0 +1,149 @@
+/**
+ * $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.smackx.packet;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents timestamp information about data stored for later delivery. A DelayInformation will 
+ * always includes the timestamp when the packet was originally sent and may include more 
+ * information such as the JID of the entity that originally sent the packet as well as the reason
+ * for the delay.<p>
+ * 
+ * For more information see <a href="http://xmpp.org/extensions/xep-0091.html">XEP-0091</a>
+ * and <a href="http://xmpp.org/extensions/xep-0203.html">XEP-0203</a>.
+ * 
+ * @author Gaston Dombiak
+ */
+public class DelayInformation implements PacketExtension {
+
+    /**
+     * Date format according to the obsolete XEP-0091 specification.
+     * XEP-0091 recommends to use this old format for date-time instead of
+     * the one specified in XEP-0082.
+     * <p>
+     * Date formats are not synchronized. Since multiple threads access the format concurrently,
+     * it must be synchronized externally. 
+     */
+    public static final DateFormat XEP_0091_UTC_FORMAT = new SimpleDateFormat(
+            "yyyyMMdd'T'HH:mm:ss");
+    static {
+        XEP_0091_UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
+    }
+
+    private Date stamp;
+    private String from;
+    private String reason;
+
+    /**
+     * Creates a new instance with the specified timestamp. 
+     * @param stamp the timestamp
+     */
+    public DelayInformation(Date stamp) {
+        super();
+        this.stamp = stamp;
+    }
+
+    /**
+     * Returns the JID of the entity that originally sent the packet or that delayed the 
+     * delivery of the packet or <tt>null</tt> if this information is not available.
+     * 
+     * @return the JID of the entity that originally sent the packet or that delayed the 
+     *         delivery of the packet.
+     */
+    public String getFrom() {
+        return from;
+    }
+
+    /**
+     * Sets the JID of the entity that originally sent the packet or that delayed the 
+     * delivery of the packet or <tt>null</tt> if this information is not available.
+     * 
+     * @param from the JID of the entity that originally sent the packet.
+     */
+    public void setFrom(String from) {
+        this.from = from;
+    }
+
+    /**
+     * Returns the timestamp when the packet was originally sent. The returned Date is 
+     * be understood as UTC.
+     * 
+     * @return the timestamp when the packet was originally sent.
+     */
+    public Date getStamp() {
+        return stamp;
+    }
+
+    /**
+     * Returns a natural-language description of the reason for the delay or <tt>null</tt> if 
+     * this information is not available.
+     * 
+     * @return a natural-language description of the reason for the delay or <tt>null</tt>.
+     */
+    public String getReason() {
+        return reason;
+    }
+
+    /**
+     * Sets a natural-language description of the reason for the delay or <tt>null</tt> if 
+     * this information is not available.
+     * 
+     * @param reason a natural-language description of the reason for the delay or <tt>null</tt>.
+     */
+    public void setReason(String reason) {
+        this.reason = reason;
+    }
+
+    public String getElementName() {
+        return "x";
+    }
+
+    public String getNamespace() {
+        return "jabber:x:delay";
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+                "\"");
+        buf.append(" stamp=\"");
+        synchronized (XEP_0091_UTC_FORMAT) {
+            buf.append(XEP_0091_UTC_FORMAT.format(stamp));
+        }
+        buf.append("\"");
+        if (from != null && from.length() > 0) {
+            buf.append(" from=\"").append(from).append("\"");
+        }
+        buf.append(">");
+        if (reason != null && reason.length() > 0) {
+            buf.append(reason);
+        }
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/packet/DiscoverInfo.java b/src/org/jivesoftware/smackx/packet/DiscoverInfo.java
new file mode 100644
index 0000000..ba873a9
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/DiscoverInfo.java
@@ -0,0 +1,507 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * A DiscoverInfo IQ packet, which is used by XMPP clients to request and receive information 
+ * to/from other XMPP entities.<p> 
+ * 
+ * The received information may contain one or more identities of the requested XMPP entity, and 
+ * a list of supported features by the requested XMPP entity.
+ *
+ * @author Gaston Dombiak
+ */
+public class DiscoverInfo extends IQ {
+
+    public static final String NAMESPACE = "http://jabber.org/protocol/disco#info";
+
+    private final List<Feature> features = new CopyOnWriteArrayList<Feature>();
+    private final List<Identity> identities = new CopyOnWriteArrayList<Identity>();
+    private String node;
+
+    public DiscoverInfo() {
+        super();
+    }
+
+    /**
+     * Copy constructor
+     * 
+     * @param d
+     */
+    public DiscoverInfo(DiscoverInfo d) {
+        super(d);
+
+        // Set node
+        setNode(d.getNode());
+
+        // Copy features
+        synchronized (d.features) {
+            for (Feature f : d.features) {
+                addFeature(f);
+            }
+        }
+
+        // Copy identities
+        synchronized (d.identities) {
+            for (Identity i : d.identities) {
+                addIdentity(i);
+            }
+        }
+    }
+
+    /**
+     * Adds a new feature to the discovered information.
+     *
+     * @param feature the discovered feature
+     */
+    public void addFeature(String feature) {
+        addFeature(new Feature(feature));
+    }
+
+    /**
+     * Adds a collection of features to the packet. Does noting if featuresToAdd is null.
+     *
+     * @param featuresToAdd
+     */
+    public void addFeatures(Collection<String> featuresToAdd) {
+        if (featuresToAdd == null) return;
+        for (String feature : featuresToAdd) {
+            addFeature(feature);
+        }
+    }
+
+    private void addFeature(Feature feature) {
+        synchronized (features) {
+            features.add(feature);
+        }
+    }
+
+    /**
+     * Returns the discovered features of an XMPP entity.
+     *
+     * @return an Iterator on the discovered features of an XMPP entity
+     */
+    public Iterator<Feature> getFeatures() {
+        synchronized (features) {
+            return Collections.unmodifiableList(features).iterator();
+        }
+    }
+
+    /**
+     * Adds a new identity of the requested entity to the discovered information.
+     * 
+     * @param identity the discovered entity's identity
+     */
+    public void addIdentity(Identity identity) {
+        synchronized (identities) {
+            identities.add(identity);
+        }
+    }
+
+    /**
+     * Adds identities to the DiscoverInfo stanza
+     * 
+     * @param identitiesToAdd
+     */
+    public void addIdentities(Collection<Identity> identitiesToAdd) {
+        if (identitiesToAdd == null) return;
+        synchronized (identities) {
+            identities.addAll(identitiesToAdd);
+        }
+    }
+
+    /**
+     * Returns the discovered identities of an XMPP entity.
+     * 
+     * @return an Iterator on the discoveted identities 
+     */
+    public Iterator<Identity> getIdentities() {
+        synchronized (identities) {
+            return Collections.unmodifiableList(identities).iterator();
+        }
+    }
+
+    /**
+     * Returns the node attribute that supplements the 'jid' attribute. A node is merely 
+     * something that is associated with a JID and for which the JID can provide information.<p> 
+     * 
+     * Node attributes SHOULD be used only when trying to provide or query information which 
+     * is not directly addressable.
+     *
+     * @return the node attribute that supplements the 'jid' attribute
+     */
+    public String getNode() {
+        return node;
+    }
+
+    /**
+     * Sets the node attribute that supplements the 'jid' attribute. A node is merely 
+     * something that is associated with a JID and for which the JID can provide information.<p> 
+     * 
+     * Node attributes SHOULD be used only when trying to provide or query information which 
+     * is not directly addressable.
+     * 
+     * @param node the node attribute that supplements the 'jid' attribute
+     */
+    public void setNode(String node) {
+        this.node = node;
+    }
+
+    /**
+     * Returns true if the specified feature is part of the discovered information.
+     * 
+     * @param feature the feature to check
+     * @return true if the requestes feature has been discovered
+     */
+    public boolean containsFeature(String feature) {
+        for (Iterator<Feature> it = getFeatures(); it.hasNext();) {
+            if (feature.equals(it.next().getVar()))
+                return true;
+        }
+        return false;
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"" + NAMESPACE + "\"");
+        if (getNode() != null) {
+            buf.append(" node=\"");
+            buf.append(StringUtils.escapeForXML(getNode()));
+            buf.append("\"");
+        }
+        buf.append(">");
+        synchronized (identities) {
+            for (Identity identity : identities) {
+                buf.append(identity.toXML());
+            }
+        }
+        synchronized (features) {
+            for (Feature feature : features) {
+                buf.append(feature.toXML());
+            }
+        }
+        // Add packet extensions, if any are defined.
+        buf.append(getExtensionsXML());
+        buf.append("</query>");
+        return buf.toString();
+    }
+
+    /**
+     * Test if a DiscoverInfo response contains duplicate identities.
+     * 
+     * @return true if duplicate identities where found, otherwise false
+     */
+    public boolean containsDuplicateIdentities() {
+        List<Identity> checkedIdentities = new LinkedList<Identity>();
+        for (Identity i : identities) {
+            for (Identity i2 : checkedIdentities) {
+                if (i.equals(i2))
+                    return true;
+            }
+            checkedIdentities.add(i);
+        }
+        return false;
+    }
+
+    /**
+     * Test if a DiscoverInfo response contains duplicate features.
+     * 
+     * @return true if duplicate identities where found, otherwise false
+     */
+    public boolean containsDuplicateFeatures() {
+        List<Feature> checkedFeatures = new LinkedList<Feature>();
+        for (Feature f : features) {
+            for (Feature f2 : checkedFeatures) {
+                if (f.equals(f2))
+                    return true;
+            }
+            checkedFeatures.add(f);
+        }
+        return false;
+    }
+
+    /**
+     * Represents the identity of a given XMPP entity. An entity may have many identities but all
+     * the identities SHOULD have the same name.<p>
+     * 
+     * Refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a>
+     * in order to get the official registry of values for the <i>category</i> and <i>type</i> 
+     * attributes.
+     * 
+     */
+    public static class Identity implements Comparable<Identity> {
+
+        private String category;
+        private String name;
+        private String type;
+        private String lang; // 'xml:lang;
+
+        /**
+         * Creates a new identity for an XMPP entity.
+         * 
+         * @param category the entity's category.
+         * @param name the entity's name.
+         * @deprecated As per the spec, the type field is mandatory and the 3 argument constructor should be used instead.
+         */
+        public Identity(String category, String name) {
+            this.category = category;
+            this.name = name;
+        }
+        
+        /**
+         * Creates a new identity for an XMPP entity.
+         * 'category' and 'type' are required by 
+         * <a href="http://xmpp.org/extensions/xep-0030.html#schemas">XEP-30 XML Schemas</a>
+         * 
+         * @param category the entity's category (required as per XEP-30).
+         * @param name the entity's name.
+         * @param type the entity's type (required as per XEP-30).
+         */
+        public Identity(String category, String name, String type) {
+            if ((category == null) || (type == null))
+                throw new IllegalArgumentException("category and type cannot be null");
+            
+            this.category = category;
+            this.name = name;
+            this.type = type;
+        }
+
+        /**
+         * Returns the entity's category. To get the official registry of values for the 
+         * 'category' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> 
+         *
+         * @return the entity's category.
+         */
+        public String getCategory() {
+            return category;
+        }
+
+        /**
+         * Returns the identity's name.
+         *
+         * @return the identity's name.
+         */
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * Returns the entity's type. To get the official registry of values for the 
+         * 'type' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> 
+         *
+         * @return the entity's type.
+         */
+        public String getType() {
+            return type;
+        }
+
+        /**
+         * Sets the entity's type. To get the official registry of values for the 
+         * 'type' attribute refer to <a href="http://www.jabber.org/registrar/disco-categories.html">Jabber::Registrar</a> 
+         *
+         * @param type the identity's type.
+         * @deprecated As per the spec, this field is mandatory and the 3 argument constructor should be used instead.
+         */
+        public void setType(String type) {
+            this.type = type;
+        }
+
+        /**
+         * Sets the natural language (xml:lang) for this identity (optional)
+         * 
+         * @param lang the xml:lang of this Identity
+         */
+        public void setLanguage(String lang) {
+            this.lang = lang;
+        }
+
+        /**
+         * Returns the identities natural language if one is set
+         * 
+         * @return the value of xml:lang of this Identity
+         */
+        public String getLanguage() {
+            return lang;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<identity");
+            // Check if this packet has 'lang' set and maybe append it to the resulting string
+            if (lang != null)
+                buf.append(" xml:lang=\"").append(StringUtils.escapeForXML(lang)).append("\"");
+            // Category must always be set
+            buf.append(" category=\"").append(StringUtils.escapeForXML(category)).append("\"");
+            // Name must always be set
+            buf.append(" name=\"").append(StringUtils.escapeForXML(name)).append("\"");
+            // Check if this packet has 'type' set and maybe append it to the resulting string
+            if (type != null) {
+                buf.append(" type=\"").append(StringUtils.escapeForXML(type)).append("\"");
+            }
+            buf.append("/>");
+            return buf.toString();
+        }
+
+        /** 
+         * Check equality for Identity  for category, type, lang and name
+         * in that order as defined by
+         * <a href="http://xmpp.org/extensions/xep-0115.html#ver-proc">XEP-0015 5.4 Processing Method (Step 3.3)</a>
+         *  
+         */
+        public boolean equals(Object obj) {
+            if (obj == null)
+                return false;
+            if (obj == this)
+                return true;
+            if (obj.getClass() != getClass())
+                return false;
+
+            DiscoverInfo.Identity other = (DiscoverInfo.Identity) obj;
+            if (!this.category.equals(other.category))
+                return false;
+
+            String otherLang = other.lang == null ? "" : other.lang;
+            String thisLang = lang == null ? "" : lang;
+            if (!otherLang.equals(thisLang))
+                return false;
+            
+            // This safeguard can be removed once the deprecated constructor is removed.
+            String otherType = other.type == null ? "" : other.type;
+            String thisType = type == null ? "" : type;
+            if (!otherType.equals(thisType))
+                return false;
+
+            String otherName = other.name == null ? "" : other.name;
+            String thisName = name == null ? "" : other.name;
+            if (!thisName.equals(otherName))
+                return false;
+
+            return true;
+        }
+        
+        @Override
+        public int hashCode() {
+            int result = 1;
+            result = 37 * result + category.hashCode();
+            result = 37 * result + (lang == null ? 0 : lang.hashCode());
+            result = 37 * result + (type == null ? 0 : type.hashCode());
+            result = 37 * result + (name == null ? 0 : name.hashCode());
+            return result;
+        }
+
+        /**
+         * Compares this identity with another one. The comparison order is:
+         * Category, Type, Lang. If all three are identical the other Identity is considered equal.
+         * Name is not used for comparision, as defined by XEP-0115
+         * 
+         * @param obj
+         * @return
+         */
+        public int compareTo(DiscoverInfo.Identity other) {
+            String otherLang = other.lang == null ? "" : other.lang;
+            String thisLang = lang == null ? "" : lang;
+            
+            // This can be removed once the deprecated constructor is removed.
+            String otherType = other.type == null ? "" : other.type;
+            String thisType = type == null ? "" : type;
+
+            if (category.equals(other.category)) {
+                if (thisType.equals(otherType)) {
+                    if (thisLang.equals(otherLang)) {
+                        // Don't compare on name, XEP-30 says that name SHOULD
+                        // be equals for all identities of an entity
+                        return 0;
+                    } else {
+                        return thisLang.compareTo(otherLang);
+                    }
+                } else {
+                    return thisType.compareTo(otherType);
+                }
+            } else {
+                return category.compareTo(other.category);
+            }
+        }
+    }
+
+    /**
+     * Represents the features offered by the item. This information helps requestors determine 
+     * what actions are possible with regard to this item (registration, search, join, etc.) 
+     * as well as specific feature types of interest, if any (e.g., for the purpose of feature 
+     * negotiation).
+     */
+    public static class Feature {
+
+        private String variable;
+
+        /**
+         * Creates a new feature offered by an XMPP entity or item.
+         * 
+         * @param variable the feature's variable.
+         */
+        public Feature(String variable) {
+            if (variable == null)
+                throw new IllegalArgumentException("variable cannot be null");
+            this.variable = variable;
+        }
+
+        /**
+         * Returns the feature's variable.
+         *
+         * @return the feature's variable.
+         */
+        public String getVar() {
+            return variable;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<feature var=\"").append(StringUtils.escapeForXML(variable)).append("\"/>");
+            return buf.toString();
+        }
+
+        public boolean equals(Object obj) {
+            if (obj == null)
+                return false;
+            if (obj == this)
+                return true;
+            if (obj.getClass() != getClass())
+                return false;
+
+            DiscoverInfo.Feature other = (DiscoverInfo.Feature) obj;
+            return variable.equals(other.variable);
+        }
+
+        @Override
+        public int hashCode() {
+            return 37 * variable.hashCode();
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/packet/DiscoverItems.java b/src/org/jivesoftware/smackx/packet/DiscoverItems.java
new file mode 100644
index 0000000..f6a0941
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/DiscoverItems.java
@@ -0,0 +1,253 @@
+/**
+ * $RCSfile$
+ * $Revision: 7071 $
+ * $Date: 2007-02-12 08:59:05 +0800 (Mon, 12 Feb 2007) $
+ *
+ * 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * A DiscoverItems IQ packet, which is used by XMPP clients to request and receive items 
+ * associated with XMPP entities.<p>
+ * 
+ * The items could also be queried in order to discover if they contain items inside. Some items 
+ * may be addressable by its JID and others may require to be addressed by a JID and a node name.
+ *
+ * @author Gaston Dombiak
+ */
+public class DiscoverItems extends IQ {
+
+    public static final String NAMESPACE = "http://jabber.org/protocol/disco#items";
+
+    private final List<Item> items = new CopyOnWriteArrayList<Item>();
+    private String node;
+
+    /**
+     * Adds a new item to the discovered information.
+     * 
+     * @param item the discovered entity's item
+     */
+    public void addItem(Item item) {
+        synchronized (items) {
+            items.add(item);
+        }
+    }
+
+    /**
+     * Adds a collection of items to the discovered information. Does nothing if itemsToAdd is null
+     *
+     * @param itemsToAdd
+     */
+    public void addItems(Collection<Item> itemsToAdd) {
+        if (itemsToAdd == null) return;
+        for (Item i : itemsToAdd) {
+            addItem(i);
+        }
+    }
+
+    /**
+     * Returns the discovered items of the queried XMPP entity. 
+     *
+     * @return an Iterator on the discovered entity's items
+     */
+    public Iterator<DiscoverItems.Item> getItems() {
+        synchronized (items) {
+            return Collections.unmodifiableList(items).iterator();
+        }
+    }
+
+    /**
+     * Returns the node attribute that supplements the 'jid' attribute. A node is merely 
+     * something that is associated with a JID and for which the JID can provide information.<p> 
+     * 
+     * Node attributes SHOULD be used only when trying to provide or query information which 
+     * is not directly addressable.
+     *
+     * @return the node attribute that supplements the 'jid' attribute
+     */
+    public String getNode() {
+        return node;
+    }
+
+    /**
+     * Sets the node attribute that supplements the 'jid' attribute. A node is merely 
+     * something that is associated with a JID and for which the JID can provide information.<p> 
+     * 
+     * Node attributes SHOULD be used only when trying to provide or query information which 
+     * is not directly addressable.
+     * 
+     * @param node the node attribute that supplements the 'jid' attribute
+     */
+    public void setNode(String node) {
+        this.node = node;
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"" + NAMESPACE + "\"");
+        if (getNode() != null) {
+            buf.append(" node=\"");
+            buf.append(StringUtils.escapeForXML(getNode()));
+            buf.append("\"");
+        }
+        buf.append(">");
+        synchronized (items) {
+            for (Item item : items) {
+                buf.append(item.toXML());
+            }
+        }
+        buf.append("</query>");
+        return buf.toString();
+    }
+
+    /**
+     * An item is associated with an XMPP Entity, usually thought of a children of the parent 
+     * entity and normally are addressable as a JID.<p> 
+     * 
+     * An item associated with an entity may not be addressable as a JID. In order to handle 
+     * such items, Service Discovery uses an optional 'node' attribute that supplements the 
+     * 'jid' attribute.
+     */
+    public static class Item {
+
+        /**
+         * Request to create or update the item.
+         */
+        public static final String UPDATE_ACTION = "update";
+
+        /**
+         * Request to remove the item.
+         */
+        public static final String REMOVE_ACTION = "remove";
+
+        private String entityID;
+        private String name;
+        private String node;
+        private String action;
+
+        /**
+         * Create a new Item associated with a given entity.
+         * 
+         * @param entityID the id of the entity that contains the item
+         */
+        public Item(String entityID) {
+            this.entityID = entityID;
+        }
+
+        /**
+         * Returns the entity's ID.
+         *
+         * @return the entity's ID.
+         */
+        public String getEntityID() {
+            return entityID;
+        }
+
+        /**
+         * Returns the entity's name.
+         *
+         * @return the entity's name.
+         */
+        public String getName() {
+            return name;
+        }
+
+        /**
+         * Sets the entity's name.
+         *
+         * @param name the entity's name.
+         */
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        /**
+         * Returns the node attribute that supplements the 'jid' attribute. A node is merely 
+         * something that is associated with a JID and for which the JID can provide information.<p> 
+         * 
+         * Node attributes SHOULD be used only when trying to provide or query information which 
+         * is not directly addressable.
+         *
+         * @return the node attribute that supplements the 'jid' attribute
+         */
+        public String getNode() {
+            return node;
+        }
+
+        /**
+         * Sets the node attribute that supplements the 'jid' attribute. A node is merely 
+         * something that is associated with a JID and for which the JID can provide information.<p> 
+         * 
+         * Node attributes SHOULD be used only when trying to provide or query information which 
+         * is not directly addressable.
+         * 
+         * @param node the node attribute that supplements the 'jid' attribute
+         */
+        public void setNode(String node) {
+            this.node = node;
+        }
+
+        /**
+         * Returns the action that specifies the action being taken for this item. Possible action 
+         * values are: "update" and "remove". Update should either create a new entry if the node 
+         * and jid combination does not already exist, or simply update an existing entry. If 
+         * "remove" is used as the action, the item should be removed from persistent storage.
+         *  
+         * @return the action being taken for this item
+         */
+        public String getAction() {
+            return action;
+        }
+
+        /**
+         * Sets the action that specifies the action being taken for this item. Possible action 
+         * values are: "update" and "remove". Update should either create a new entry if the node 
+         * and jid combination does not already exist, or simply update an existing entry. If 
+         * "remove" is used as the action, the item should be removed from persistent storage.
+         * 
+         * @param action the action being taken for this item
+         */
+        public void setAction(String action) {
+            this.action = action;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<item jid=\"").append(entityID).append("\"");
+            if (name != null) {
+                buf.append(" name=\"").append(StringUtils.escapeForXML(name)).append("\"");
+            }
+            if (node != null) {
+                buf.append(" node=\"").append(StringUtils.escapeForXML(node)).append("\"");
+            }
+            if (action != null) {
+                buf.append(" action=\"").append(StringUtils.escapeForXML(action)).append("\"");
+            }
+            buf.append("/>");
+            return buf.toString();
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/packet/Header.java b/src/org/jivesoftware/smackx/packet/Header.java
new file mode 100644
index 0000000..3fa8386
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/Header.java
@@ -0,0 +1,59 @@
+/*

+ * 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.smackx.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+

+/**

+ * Represents a <b>Header</b> entry as specified by the <a href="http://xmpp.org/extensions/xep-031.html">Stanza Headers and Internet Metadata (SHIM)</a>

+

+ * @author Robin Collier

+ */

+public class Header implements PacketExtension

+{

+	private String name;

+	private String value;

+	

+	public Header(String name, String value)

+	{

+		this.name = name;

+		this.value = value;

+	}

+	

+	public String getName()

+	{

+		return name;

+	}

+

+	public String getValue()

+	{

+		return value;

+	}

+

+	public String getElementName()

+	{

+		return "header";

+	}

+

+	public String getNamespace()

+	{

+		return HeadersExtension.NAMESPACE;

+	}

+

+	public String toXML()

+	{

+		return "<header name='" + name + "'>" + value + "</header>";

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/packet/HeadersExtension.java b/src/org/jivesoftware/smackx/packet/HeadersExtension.java
new file mode 100644
index 0000000..78564db
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/HeadersExtension.java
@@ -0,0 +1,69 @@
+/**

+ * 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.smackx.packet;

+

+import java.util.Collection;

+import java.util.Collections;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+

+/**

+ * Extension representing a list of headers as specified in <a href="http://xmpp.org/extensions/xep-0131">Stanza Headers and Internet Metadata (SHIM)</a>

+ * 

+ * @see Header

+ * 

+ * @author Robin Collier

+ */

+public class HeadersExtension implements PacketExtension

+{

+	public static final String NAMESPACE = "http://jabber.org/protocol/shim";

+	

+	private Collection<Header> headers = Collections.EMPTY_LIST;

+	

+	public HeadersExtension(Collection<Header> headerList)

+	{

+		if (headerList != null)

+			headers = headerList;

+	}

+	

+	public Collection<Header> getHeaders()

+	{

+		return headers;

+	}

+

+	public String getElementName()

+	{

+		return "headers";

+	}

+

+	public String getNamespace()

+	{

+		return NAMESPACE;

+	}

+

+	public String toXML()

+	{

+		StringBuilder builder = new StringBuilder("<" + getElementName() + " xmlns='" + getNamespace() + "'>");

+		

+		for (Header header : headers)

+		{

+			builder.append(header.toXML());

+		}

+		builder.append("</" + getElementName() + '>');

+

+		return builder.toString();

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/packet/LastActivity.java b/src/org/jivesoftware/smackx/packet/LastActivity.java
new file mode 100644
index 0000000..6f4f15a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/LastActivity.java
@@ -0,0 +1,164 @@
+/**
+ * $RCSfile$
+ * $Revision: 2407 $
+ * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
+ *
+ * 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.smackx.packet;
+
+import java.io.IOException;
+
+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.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.StringUtils;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+/**
+ * A last activity IQ for retrieving information about the last activity associated with a Jabber ID.
+ * LastActivity (XEP-0012) allows for retrieval of how long a particular user has been idle and the
+ * message the specified when doing so. Use {@link org.jivesoftware.smackx.LastActivityManager}
+ * to get the last activity of a user.
+ *
+ * @author Derek DeMoro
+ */
+public class LastActivity extends IQ {
+
+    public static final String NAMESPACE = "jabber:iq:last";
+
+    public long lastActivity = -1;
+    public String message;
+
+    public LastActivity() {
+        setType(IQ.Type.GET);
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"" + NAMESPACE + "\"");
+        if (lastActivity != -1) {
+            buf.append(" seconds=\"").append(lastActivity).append("\"");
+        }
+        buf.append("></query>");
+        return buf.toString();
+    }
+
+
+    public void setLastActivity(long lastActivity) {
+        this.lastActivity = lastActivity;
+    }
+
+
+    private void setMessage(String message) {
+        this.message = message;
+    }
+
+    /**
+     * Returns number of seconds that have passed since the user last logged out.
+     * If the user is offline, 0 will be returned.
+     *
+     * @return the number of seconds that have passed since the user last logged out.
+     */
+    public long getIdleTime() {
+        return lastActivity;
+    }
+
+
+    /**
+     * Returns the status message of the last unavailable presence received from the user.
+     *
+     * @return the status message of the last unavailable presence received from the user
+     */
+    public String getStatusMessage() {
+        return message;
+    }
+
+
+    /**
+     * The IQ Provider for LastActivity.
+     *
+     * @author Derek DeMoro
+     */
+    public static class Provider implements IQProvider {
+
+        public Provider() {
+            super();
+        }
+
+        public IQ parseIQ(XmlPullParser parser) throws XMPPException, XmlPullParserException {
+            if (parser.getEventType() != XmlPullParser.START_TAG) {
+                throw new XMPPException("Parser not in proper position, or bad XML.");
+            }
+
+            LastActivity lastActivity = new LastActivity();
+            String seconds = parser.getAttributeValue("", "seconds");
+            String message = null;
+            try {
+                message = parser.nextText();
+            } catch (IOException e1) {
+                // Ignore
+            }
+            if (seconds != null) {
+                try {
+                    lastActivity.setLastActivity(Long.parseLong(seconds));
+                } catch (NumberFormatException e) {
+                    // Ignore
+                }
+            }
+
+            if (message != null) {
+                lastActivity.setMessage(message);
+            }
+            return lastActivity;
+        }
+    }
+
+    /**
+     * Retrieve the last activity of a particular jid.
+     * @param con the current Connection.
+     * @param jid the JID of the user.
+     * @return the LastActivity packet of the jid.
+     * @throws XMPPException thrown if a server error has occured.
+     * @deprecated This method only retreives the lapsed time since the last logout of a particular jid. 
+     * Replaced by {@link  org.jivesoftware.smackx.LastActivityManager#getLastActivity(Connection, String)  getLastActivity}
+     */
+    public static LastActivity getLastActivity(Connection con, String jid) throws XMPPException {
+        LastActivity activity = new LastActivity();
+        jid = StringUtils.parseBareAddress(jid);
+        activity.setTo(jid);
+
+        PacketCollector collector = con.createPacketCollector(new PacketIDFilter(activity.getPacketID()));
+        con.sendPacket(activity);
+
+        LastActivity response = (LastActivity) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+        // Cancel the collector.
+        collector.cancel();
+        if (response == null) {
+            throw new XMPPException("No response from server on status set.");
+        }
+        if (response.getError() != null) {
+            throw new XMPPException(response.getError());
+        }
+        return response;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/packet/MUCAdmin.java b/src/org/jivesoftware/smackx/packet/MUCAdmin.java
new file mode 100644
index 0000000..75d17e0
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/MUCAdmin.java
@@ -0,0 +1,237 @@
+/**
+ * $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.smackx.packet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * IQ packet that serves for kicking users, granting and revoking voice, banning users, 
+ * modifying the ban list, granting and revoking membership and granting and revoking 
+ * moderator privileges. All these operations are scoped by the 
+ * 'http://jabber.org/protocol/muc#admin' namespace.
+ * 
+ * @author Gaston Dombiak
+ */
+public class MUCAdmin extends IQ {
+
+    private List<Item> items = new ArrayList<Item>();
+
+    /**
+     * Returns an Iterator for item childs that holds information about roles, affiliation, 
+     * jids and nicks.
+     * 
+     * @return an Iterator for item childs that holds information about roles, affiliation,
+     *          jids and nicks.
+     */
+    public Iterator<Item> getItems() {
+        synchronized (items) {
+            return Collections.unmodifiableList(new ArrayList<Item>(items)).iterator();
+        }
+    }
+
+    /**
+     * Adds an item child that holds information about roles, affiliation, jids and nicks.
+     * 
+     * @param item the item child that holds information about roles, affiliation, jids and nicks.
+     */
+    public void addItem(Item item) {
+        synchronized (items) {
+            items.add(item);
+        }
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"http://jabber.org/protocol/muc#admin\">");
+        synchronized (items) {
+            for (int i = 0; i < items.size(); i++) {
+                Item item = items.get(i);
+                buf.append(item.toXML());
+            }
+        }
+        // Add packet extensions, if any are defined.
+        buf.append(getExtensionsXML());
+        buf.append("</query>");
+        return buf.toString();
+    }
+
+    /**
+     * Item child that holds information about roles, affiliation, jids and nicks.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Item {
+        private String actor;
+        private String reason;
+        private String affiliation;
+        private String jid;
+        private String nick;
+        private String role;
+        
+        /**
+         * Creates a new item child. 
+         * 
+         * @param affiliation the actor's affiliation to the room
+         * @param role the privilege level of an occupant within a room.
+         */
+        public Item(String affiliation, String role) {
+            this.affiliation = affiliation;
+            this.role = role;
+        }
+        
+        /**
+         * Returns the actor (JID of an occupant in the room) that was kicked or banned.
+         * 
+         * @return the JID of an occupant in the room that was kicked or banned.
+         */
+        public String getActor() {
+            return actor;
+        }
+
+        /**
+         * Returns the reason for the item child. The reason is optional and could be used to
+         * explain the reason why a user (occupant) was kicked or banned.
+         *  
+         * @return the reason for the item child.
+         */
+        public String getReason() {
+            return reason;
+        }
+
+        /**
+         * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent 
+         * association or connection with a room. The possible affiliations are "owner", "admin", 
+         * "member", and "outcast" (naturally it is also possible to have no affiliation). An 
+         * affiliation lasts across a user's visits to a room.
+         * 
+         * @return the actor's affiliation to the room
+         */
+        public String getAffiliation() {
+            return affiliation;
+        }
+
+        /**
+         * Returns the <room@service/nick> by which an occupant is identified within the context 
+         * of a room. If the room is non-anonymous, the JID will be included in the item. 
+         * 
+         * @return the room JID by which an occupant is identified within the room.
+         */
+        public String getJid() {
+            return jid;
+        }
+
+        /**
+         * Returns the new nickname of an occupant that is changing his/her nickname. The new 
+         * nickname is sent as part of the unavailable presence.  
+         * 
+         * @return the new nickname of an occupant that is changing his/her nickname.
+         */
+        public String getNick() {
+            return nick;
+        }
+
+        /**
+         * Returns the temporary position or privilege level of an occupant within a room. The 
+         * possible roles are "moderator", "participant", and "visitor" (it is also possible to 
+         * have no defined role). A role lasts only for the duration of an occupant's visit to 
+         * a room. 
+         * 
+         * @return the privilege level of an occupant within a room.
+         */
+        public String getRole() {
+            return role;
+        }
+
+        /**
+         * Sets the actor (JID of an occupant in the room) that was kicked or banned.
+         * 
+         * @param actor the actor (JID of an occupant in the room) that was kicked or banned.
+         */
+        public void setActor(String actor) {
+            this.actor = actor;
+        }
+
+        /**
+         * Sets the reason for the item child. The reason is optional and could be used to
+         * explain the reason why a user (occupant) was kicked or banned.
+         * 
+         * @param reason the reason why a user (occupant) was kicked or banned.
+         */
+        public void setReason(String reason) {
+            this.reason = reason;
+        }
+
+        /**
+         * Sets the <room@service/nick> by which an occupant is identified within the context 
+         * of a room. If the room is non-anonymous, the JID will be included in the item.
+         *  
+         * @param jid the JID by which an occupant is identified within a room.
+         */
+        public void setJid(String jid) {
+            this.jid = jid;
+        }
+
+        /**
+         * Sets the new nickname of an occupant that is changing his/her nickname. The new 
+         * nickname is sent as part of the unavailable presence.
+         *   
+         * @param nick the new nickname of an occupant that is changing his/her nickname.
+         */
+        public void setNick(String nick) {
+            this.nick = nick;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<item");
+            if (getAffiliation() != null) {
+                buf.append(" affiliation=\"").append(getAffiliation()).append("\"");
+            }
+            if (getJid() != null) {
+                buf.append(" jid=\"").append(getJid()).append("\"");
+            }
+            if (getNick() != null) {
+                buf.append(" nick=\"").append(getNick()).append("\"");
+            }
+            if (getRole() != null) {
+                buf.append(" role=\"").append(getRole()).append("\"");
+            }
+            if (getReason() == null && getActor() == null) {
+                buf.append("/>");
+            }
+            else {
+                buf.append(">");
+                if (getReason() != null) {
+                    buf.append("<reason>").append(getReason()).append("</reason>");
+                }
+                if (getActor() != null) {
+                    buf.append("<actor jid=\"").append(getActor()).append("\"/>");
+                }
+                buf.append("</item>");
+            }
+            return buf.toString();
+        }
+    };
+}
diff --git a/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java b/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java
new file mode 100644
index 0000000..d3d2796
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/MUCInitialPresence.java
@@ -0,0 +1,223 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * Represents extended presence information whose sole purpose is to signal the ability of 
+ * the occupant to speak the MUC protocol when joining a room. If the room requires a password 
+ * then the MUCInitialPresence should include one.<p>
+ * 
+ * The amount of discussion history provided on entering a room (perhaps because the 
+ * user is on a low-bandwidth connection or is using a small-footprint client) could be managed by
+ * setting a configured History instance to the MUCInitialPresence instance. 
+ * @see MUCInitialPresence#setHistory(MUCInitialPresence.History).
+ *
+ * @author Gaston Dombiak
+ */
+public class MUCInitialPresence implements PacketExtension {
+
+    private String password;
+    private History history; 
+
+    public String getElementName() {
+        return "x";
+    }
+
+    public String getNamespace() {
+        return "http://jabber.org/protocol/muc";
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+            "\">");
+        if (getPassword() != null) {
+            buf.append("<password>").append(getPassword()).append("</password>");
+        }
+        if (getHistory() != null) {
+            buf.append(getHistory().toXML());
+        }
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+    /**
+     * Returns the history that manages the amount of discussion history provided on 
+     * entering a room.
+     * 
+     * @return the history that manages the amount of discussion history provided on 
+     * entering a room.
+     */
+    public History getHistory() {
+        return history;
+    }
+
+    /**
+     * Returns the password to use when the room requires a password.
+     * 
+     * @return the password to use when the room requires a password.
+     */
+    public String getPassword() {
+        return password;
+    }
+
+    /**
+     * Sets the History that manages the amount of discussion history provided on 
+     * entering a room.
+     * 
+     * @param history that manages the amount of discussion history provided on 
+     * entering a room.
+     */
+    public void setHistory(History history) {
+        this.history = history;
+    }
+
+    /**
+     * Sets the password to use when the room requires a password.
+     * 
+     * @param password the password to use when the room requires a password.
+     */
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    /**
+     * The History class controls the number of characters or messages to receive
+     * when entering a room.
+     * 
+     * @author Gaston Dombiak
+     */
+    public static class History {
+
+        private int maxChars = -1;
+        private int maxStanzas = -1; 
+        private int seconds = -1; 
+        private Date since; 
+
+        /**
+         * Returns the total number of characters to receive in the history.
+         * 
+         * @return total number of characters to receive in the history.
+         */
+        public int getMaxChars() {
+            return maxChars;
+        }
+
+        /**
+         * Returns the total number of messages to receive in the history.
+         * 
+         * @return the total number of messages to receive in the history.
+         */
+        public int getMaxStanzas() {
+            return maxStanzas;
+        }
+
+        /**
+         * Returns the number of seconds to use to filter the messages received during that time. 
+         * In other words, only the messages received in the last "X" seconds will be included in 
+         * the history.
+         * 
+         * @return the number of seconds to use to filter the messages received during that time.
+         */
+        public int getSeconds() {
+            return seconds;
+        }
+
+        /**
+         * Returns the since date to use to filter the messages received during that time. 
+         * In other words, only the messages received since the datetime specified will be 
+         * included in the history.
+         * 
+         * @return the since date to use to filter the messages received during that time.
+         */
+        public Date getSince() {
+            return since;
+        }
+
+        /**
+         * Sets the total number of characters to receive in the history.
+         * 
+         * @param maxChars the total number of characters to receive in the history.
+         */
+        public void setMaxChars(int maxChars) {
+            this.maxChars = maxChars;
+        }
+
+        /**
+         * Sets the total number of messages to receive in the history.
+         * 
+         * @param maxStanzas the total number of messages to receive in the history.
+         */
+        public void setMaxStanzas(int maxStanzas) {
+            this.maxStanzas = maxStanzas;
+        }
+
+        /**
+         * Sets the number of seconds to use to filter the messages received during that time. 
+         * In other words, only the messages received in the last "X" seconds will be included in 
+         * the history.
+         * 
+         * @param seconds the number of seconds to use to filter the messages received during 
+         * that time.
+         */
+        public void setSeconds(int seconds) {
+            this.seconds = seconds;
+        }
+
+        /**
+         * Sets the since date to use to filter the messages received during that time. 
+         * In other words, only the messages received since the datetime specified will be 
+         * included in the history.
+         * 
+         * @param since the since date to use to filter the messages received during that time.
+         */
+        public void setSince(Date since) {
+            this.since = since;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<history");
+            if (getMaxChars() != -1) {
+                buf.append(" maxchars=\"").append(getMaxChars()).append("\"");
+            }
+            if (getMaxStanzas() != -1) {
+                buf.append(" maxstanzas=\"").append(getMaxStanzas()).append("\"");
+            }
+            if (getSeconds() != -1) {
+                buf.append(" seconds=\"").append(getSeconds()).append("\"");
+            }
+            if (getSince() != null) {
+                SimpleDateFormat utcFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
+                utcFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
+                buf.append(" since=\"").append(utcFormat.format(getSince())).append("\"");
+            }
+            buf.append("/>");
+            return buf.toString();
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/packet/MUCOwner.java b/src/org/jivesoftware/smackx/packet/MUCOwner.java
new file mode 100644
index 0000000..e33806e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/MUCOwner.java
@@ -0,0 +1,342 @@
+/**
+ * $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.smackx.packet;
+import org.jivesoftware.smack.packet.IQ;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * IQ packet that serves for granting and revoking ownership privileges, granting 
+ * and revoking administrative privileges and destroying a room. All these operations 
+ * are scoped by the 'http://jabber.org/protocol/muc#owner' namespace.
+ * 
+ * @author Gaston Dombiak
+ */
+public class MUCOwner extends IQ {
+
+    private List<Item> items = new ArrayList<Item>();
+    private Destroy destroy;
+
+    /**
+     * Returns an Iterator for item childs that holds information about affiliation, 
+     * jids and nicks.
+     * 
+     * @return an Iterator for item childs that holds information about affiliation,
+     *          jids and nicks.
+     */
+    public Iterator<Item> getItems() {
+        synchronized (items) {
+            return Collections.unmodifiableList(new ArrayList<Item>(items)).iterator();
+        }
+    }
+
+    /**
+     * Returns a request to the server to destroy a room. The sender of the request
+     * should be the room's owner. If the sender of the destroy request is not the room's owner
+     * then the server will answer a "Forbidden" error.
+     * 
+     * @return a request to the server to destroy a room.
+     */
+    public Destroy getDestroy() {
+        return destroy;
+    }
+
+    /**
+     * Sets a request to the server to destroy a room. The sender of the request
+     * should be the room's owner. If the sender of the destroy request is not the room's owner
+     * then the server will answer a "Forbidden" error.
+     * 
+     * @param destroy the request to the server to destroy a room.
+     */
+    public void setDestroy(Destroy destroy) {
+        this.destroy = destroy;
+    }
+
+    /**
+     * Adds an item child that holds information about affiliation, jids and nicks.
+     * 
+     * @param item the item child that holds information about affiliation, jids and nicks.
+     */
+    public void addItem(Item item) {
+        synchronized (items) {
+            items.add(item);
+        }
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"http://jabber.org/protocol/muc#owner\">");
+        synchronized (items) {
+            for (int i = 0; i < items.size(); i++) {
+                Item item = (Item) items.get(i);
+                buf.append(item.toXML());
+            }
+        }
+        if (getDestroy() != null) {
+            buf.append(getDestroy().toXML());
+        }
+        // Add packet extensions, if any are defined.
+        buf.append(getExtensionsXML());
+        buf.append("</query>");
+        return buf.toString();
+    }
+
+    /**
+     * Item child that holds information about affiliation, jids and nicks.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Item {
+        
+        private String actor;
+        private String reason;
+        private String affiliation;
+        private String jid;
+        private String nick;
+        private String role;
+
+        /**
+         * Creates a new item child. 
+         * 
+         * @param affiliation the actor's affiliation to the room
+         */
+        public Item(String affiliation) {
+            this.affiliation = affiliation;
+        }
+        
+        /**
+         * Returns the actor (JID of an occupant in the room) that was kicked or banned.
+         * 
+         * @return the JID of an occupant in the room that was kicked or banned.
+         */
+        public String getActor() {
+            return actor;
+        }
+
+        /**
+         * Returns the reason for the item child. The reason is optional and could be used to
+         * explain the reason why a user (occupant) was kicked or banned.
+         *  
+         * @return the reason for the item child.
+         */
+        public String getReason() {
+            return reason;
+        }
+
+        /**
+         * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent 
+         * association or connection with a room. The possible affiliations are "owner", "admin", 
+         * "member", and "outcast" (naturally it is also possible to have no affiliation). An 
+         * affiliation lasts across a user's visits to a room.
+         * 
+         * @return the actor's affiliation to the room
+         */
+        public String getAffiliation() {
+            return affiliation;
+        }
+
+        /**
+         * Returns the <room@service/nick> by which an occupant is identified within the context 
+         * of a room. If the room is non-anonymous, the JID will be included in the item. 
+         * 
+         * @return the room JID by which an occupant is identified within the room.
+         */
+        public String getJid() {
+            return jid;
+        }
+
+        /**
+         * Returns the new nickname of an occupant that is changing his/her nickname. The new 
+         * nickname is sent as part of the unavailable presence.  
+         * 
+         * @return the new nickname of an occupant that is changing his/her nickname.
+         */
+        public String getNick() {
+            return nick;
+        }
+
+        /**
+         * Returns the temporary position or privilege level of an occupant within a room. The
+         * possible roles are "moderator", "participant", and "visitor" (it is also possible to
+         * have no defined role). A role lasts only for the duration of an occupant's visit to
+         * a room.
+         *
+         * @return the privilege level of an occupant within a room.
+         */
+        public String getRole() {
+            return role;
+        }
+
+        /**
+         * Sets the actor (JID of an occupant in the room) that was kicked or banned.
+         * 
+         * @param actor the actor (JID of an occupant in the room) that was kicked or banned.
+         */
+        public void setActor(String actor) {
+            this.actor = actor;
+        }
+
+        /**
+         * Sets the reason for the item child. The reason is optional and could be used to
+         * explain the reason why a user (occupant) was kicked or banned.
+         * 
+         * @param reason the reason why a user (occupant) was kicked or banned.
+         */
+        public void setReason(String reason) {
+            this.reason = reason;
+        }
+
+        /**
+         * Sets the <room@service/nick> by which an occupant is identified within the context 
+         * of a room. If the room is non-anonymous, the JID will be included in the item.
+         *  
+         * @param jid the JID by which an occupant is identified within a room.
+         */
+        public void setJid(String jid) {
+            this.jid = jid;
+        }
+
+        /**
+         * Sets the new nickname of an occupant that is changing his/her nickname. The new 
+         * nickname is sent as part of the unavailable presence.
+         *   
+         * @param nick the new nickname of an occupant that is changing his/her nickname.
+         */
+        public void setNick(String nick) {
+            this.nick = nick;
+        }
+
+        /**
+         * Sets the temporary position or privilege level of an occupant within a room. The
+         * possible roles are "moderator", "participant", and "visitor" (it is also possible to
+         * have no defined role). A role lasts only for the duration of an occupant's visit to
+         * a room.
+         *
+         * @param role the new privilege level of an occupant within a room.
+         */
+        public void setRole(String role) {
+            this.role = role;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<item");
+            if (getAffiliation() != null) {
+                buf.append(" affiliation=\"").append(getAffiliation()).append("\"");
+            }
+            if (getJid() != null) {
+                buf.append(" jid=\"").append(getJid()).append("\"");
+            }
+            if (getNick() != null) {
+                buf.append(" nick=\"").append(getNick()).append("\"");
+            }
+            if (getRole() != null) {
+                buf.append(" role=\"").append(getRole()).append("\"");
+            }
+            if (getReason() == null && getActor() == null) {
+                buf.append("/>");
+            }
+            else {
+                buf.append(">");
+                if (getReason() != null) {
+                    buf.append("<reason>").append(getReason()).append("</reason>");
+                }
+                if (getActor() != null) {
+                    buf.append("<actor jid=\"").append(getActor()).append("\"/>");
+                }
+                buf.append("</item>");
+            }
+            return buf.toString();
+        }
+    };
+
+    /**
+     * Represents a request to the server to destroy a room. The sender of the request
+     * should be the room's owner. If the sender of the destroy request is not the room's owner
+     * then the server will answer a "Forbidden" error.
+     * 
+     * @author Gaston Dombiak
+     */
+    public static class Destroy {
+        private String reason;
+        private String jid;
+        
+        
+        /**
+         * Returns the JID of an alternate location since the current room is being destroyed.
+         * 
+         * @return the JID of an alternate location.
+         */
+        public String getJid() {
+            return jid;
+        }
+
+        /**
+         * Returns the reason for the room destruction.
+         * 
+         * @return the reason for the room destruction.
+         */
+        public String getReason() {
+            return reason;
+        }
+
+        /**
+         * Sets the JID of an alternate location since the current room is being destroyed.
+         * 
+         * @param jid the JID of an alternate location.
+         */
+        public void setJid(String jid) {
+            this.jid = jid;
+        }
+
+        /**
+         * Sets the reason for the room destruction.
+         * 
+         * @param reason the reason for the room destruction.
+         */
+        public void setReason(String reason) {
+            this.reason = reason;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<destroy");
+            if (getJid() != null) {
+                buf.append(" jid=\"").append(getJid()).append("\"");
+            }
+            if (getReason() == null) {
+                buf.append("/>");
+            }
+            else {
+                buf.append(">");
+                if (getReason() != null) {
+                    buf.append("<reason>").append(getReason()).append("</reason>");
+                }
+                buf.append("</destroy>");
+            }
+            return buf.toString();
+        }
+
+    }
+}
diff --git a/src/org/jivesoftware/smackx/packet/MUCUser.java b/src/org/jivesoftware/smackx/packet/MUCUser.java
new file mode 100644
index 0000000..bfcd67c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/MUCUser.java
@@ -0,0 +1,627 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents extended presence information about roles, affiliations, full JIDs,
+ * or status codes scoped by the 'http://jabber.org/protocol/muc#user' namespace.
+ *
+ * @author Gaston Dombiak
+ */
+public class MUCUser implements PacketExtension {
+
+    private Invite invite;
+    private Decline decline;
+    private Item item;
+    private String password;
+    private Status status;
+    private Destroy destroy;
+
+    public String getElementName() {
+        return "x";
+    }
+
+    public String getNamespace() {
+        return "http://jabber.org/protocol/muc#user";
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+            "\">");
+        if (getInvite() != null) {
+            buf.append(getInvite().toXML());
+        }
+        if (getDecline() != null) {
+            buf.append(getDecline().toXML());
+        }
+        if (getItem() != null) {
+            buf.append(getItem().toXML());
+        }
+        if (getPassword() != null) {
+            buf.append("<password>").append(getPassword()).append("</password>");
+        }
+        if (getStatus() != null) {
+            buf.append(getStatus().toXML());
+        }
+        if (getDestroy() != null) {
+            buf.append(getDestroy().toXML());
+        }
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+    /**
+     * Returns the invitation for another user to a room. The sender of the invitation
+     * must be an occupant of the room. The invitation will be sent to the room which in turn
+     * will forward the invitation to the invitee.
+     *
+     * @return an invitation for another user to a room.
+     */
+    public Invite getInvite() {
+        return invite;
+    }
+
+    /**
+     * Returns the rejection to an invitation from another user to a room. The rejection will be
+     * sent to the room which in turn will forward the refusal to the inviter.
+     *
+     * @return a rejection to an invitation from another user to a room.
+     */
+    public Decline getDecline() {
+        return decline;
+    }
+
+    /**
+     * Returns the item child that holds information about roles, affiliation, jids and nicks.
+     *
+     * @return an item child that holds information about roles, affiliation, jids and nicks.
+     */
+    public Item getItem() {
+        return item;
+    }
+
+    /**
+     * Returns the password to use to enter Password-Protected Room. A Password-Protected Room is
+     * a room that a user cannot enter without first providing the correct password.
+     *
+     * @return the password to use to enter Password-Protected Room.
+     */
+    public String getPassword() {
+        return password;
+    }
+
+    /**
+     * Returns the status which holds a code that assists in presenting notification messages.
+     *
+     * @return the status which holds a code that assists in presenting notification messages.
+     */
+    public Status getStatus() {
+        return status;
+    }
+
+    /**
+     * Returns the notification that the room has been destroyed. After a room has been destroyed,
+     * the room occupants will receive a Presence packet of type 'unavailable' with the reason for
+     * the room destruction if provided by the room owner.
+     *
+     * @return a notification that the room has been destroyed.
+     */
+    public Destroy getDestroy() {
+        return destroy;
+    }
+
+    /**
+     * Sets the invitation for another user to a room. The sender of the invitation
+     * must be an occupant of the room. The invitation will be sent to the room which in turn
+     * will forward the invitation to the invitee.
+     *
+     * @param invite the invitation for another user to a room.
+     */
+    public void setInvite(Invite invite) {
+        this.invite = invite;
+    }
+
+    /**
+     * Sets the rejection to an invitation from another user to a room. The rejection will be
+     * sent to the room which in turn will forward the refusal to the inviter.
+     *
+     * @param decline the rejection to an invitation from another user to a room.
+     */
+    public void setDecline(Decline decline) {
+        this.decline = decline;
+    }
+
+    /**
+     * Sets the item child that holds information about roles, affiliation, jids and nicks.
+     *
+     * @param item the item child that holds information about roles, affiliation, jids and nicks.
+     */
+    public void setItem(Item item) {
+        this.item = item;
+    }
+
+    /**
+     * Sets the password to use to enter Password-Protected Room. A Password-Protected Room is
+     * a room that a user cannot enter without first providing the correct password.
+     *
+     * @param string the password to use to enter Password-Protected Room.
+     */
+    public void setPassword(String string) {
+        password = string;
+    }
+
+    /**
+     * Sets the status which holds a code that assists in presenting notification messages.
+     *
+     * @param status the status which holds a code that assists in presenting notification
+     * messages.
+     */
+    public void setStatus(Status status) {
+        this.status = status;
+    }
+
+    /**
+     * Sets the notification that the room has been destroyed. After a room has been destroyed,
+     * the room occupants will receive a Presence packet of type 'unavailable' with the reason for
+     * the room destruction if provided by the room owner.
+     *
+     * @param destroy the notification that the room has been destroyed.
+     */
+    public void setDestroy(Destroy destroy) {
+        this.destroy = destroy;
+    }
+
+    /**
+     * Represents an invitation for another user to a room. The sender of the invitation
+     * must be an occupant of the room. The invitation will be sent to the room which in turn
+     * will forward the invitation to the invitee.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Invite {
+        private String reason;
+        private String from;
+        private String to;
+
+        /**
+         * Returns the bare JID of the inviter or, optionally, the room JID. (e.g.
+         * 'crone1@shakespeare.lit/desktop').
+         *
+         * @return the room's occupant that sent the invitation.
+         */
+        public String getFrom() {
+            return from;
+        }
+
+        /**
+         * Returns the message explaining the invitation.
+         *
+         * @return the message explaining the invitation.
+         */
+        public String getReason() {
+            return reason;
+        }
+
+        /**
+         * Returns the bare JID of the invitee. (e.g. 'hecate@shakespeare.lit')
+         *
+         * @return the bare JID of the invitee.
+         */
+        public String getTo() {
+            return to;
+        }
+
+        /**
+         * Sets the bare JID of the inviter or, optionally, the room JID. (e.g.
+         * 'crone1@shakespeare.lit/desktop')
+         *
+         * @param from the bare JID of the inviter or, optionally, the room JID.
+         */
+        public void setFrom(String from) {
+            this.from = from;
+        }
+
+        /**
+         * Sets the message explaining the invitation.
+         *
+         * @param reason the message explaining the invitation.
+         */
+        public void setReason(String reason) {
+            this.reason = reason;
+        }
+
+        /**
+         * Sets the bare JID of the invitee. (e.g. 'hecate@shakespeare.lit')
+         *
+         * @param to the bare JID of the invitee.
+         */
+        public void setTo(String to) {
+            this.to = to;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<invite ");
+            if (getTo() != null) {
+                buf.append(" to=\"").append(getTo()).append("\"");
+            }
+            if (getFrom() != null) {
+                buf.append(" from=\"").append(getFrom()).append("\"");
+            }
+            buf.append(">");
+            if (getReason() != null) {
+                buf.append("<reason>").append(getReason()).append("</reason>");
+            }
+            buf.append("</invite>");
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Represents a rejection to an invitation from another user to a room. The rejection will be
+     * sent to the room which in turn will forward the refusal to the inviter.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Decline {
+        private String reason;
+        private String from;
+        private String to;
+
+        /**
+         * Returns the bare JID of the invitee that rejected the invitation. (e.g.
+         * 'crone1@shakespeare.lit/desktop').
+         *
+         * @return the bare JID of the invitee that rejected the invitation.
+         */
+        public String getFrom() {
+            return from;
+        }
+
+        /**
+         * Returns the message explaining why the invitation was rejected.
+         *
+         * @return the message explaining the reason for the rejection.
+         */
+        public String getReason() {
+            return reason;
+        }
+
+        /**
+         * Returns the bare JID of the inviter. (e.g. 'hecate@shakespeare.lit')
+         *
+         * @return the bare JID of the inviter.
+         */
+        public String getTo() {
+            return to;
+        }
+
+        /**
+         * Sets the bare JID of the invitee that rejected the invitation. (e.g.
+         * 'crone1@shakespeare.lit/desktop').
+         *
+         * @param from the bare JID of the invitee that rejected the invitation.
+         */
+        public void setFrom(String from) {
+            this.from = from;
+        }
+
+        /**
+         * Sets the message explaining why the invitation was rejected.
+         *
+         * @param reason the message explaining the reason for the rejection.
+         */
+        public void setReason(String reason) {
+            this.reason = reason;
+        }
+
+        /**
+         * Sets the bare JID of the inviter. (e.g. 'hecate@shakespeare.lit')
+         *
+         * @param to the bare JID of the inviter.
+         */
+        public void setTo(String to) {
+            this.to = to;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<decline ");
+            if (getTo() != null) {
+                buf.append(" to=\"").append(getTo()).append("\"");
+            }
+            if (getFrom() != null) {
+                buf.append(" from=\"").append(getFrom()).append("\"");
+            }
+            buf.append(">");
+            if (getReason() != null) {
+                buf.append("<reason>").append(getReason()).append("</reason>");
+            }
+            buf.append("</decline>");
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Item child that holds information about roles, affiliation, jids and nicks.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Item {
+        private String actor;
+        private String reason;
+        private String affiliation;
+        private String jid;
+        private String nick;
+        private String role;
+
+        /**
+         * Creates a new item child.
+         *
+         * @param affiliation the actor's affiliation to the room
+         * @param role the privilege level of an occupant within a room.
+         */
+        public Item(String affiliation, String role) {
+            this.affiliation = affiliation;
+            this.role = role;
+        }
+
+        /**
+         * Returns the actor (JID of an occupant in the room) that was kicked or banned.
+         *
+         * @return the JID of an occupant in the room that was kicked or banned.
+         */
+        public String getActor() {
+            return actor == null ? "" : actor;
+        }
+
+        /**
+         * Returns the reason for the item child. The reason is optional and could be used to
+         * explain the reason why a user (occupant) was kicked or banned.
+         *
+         * @return the reason for the item child.
+         */
+        public String getReason() {
+            return reason == null ? "" : reason;
+        }
+
+        /**
+         * Returns the occupant's affiliation to the room. The affiliation is a semi-permanent
+         * association or connection with a room. The possible affiliations are "owner", "admin",
+         * "member", and "outcast" (naturally it is also possible to have no affiliation). An
+         * affiliation lasts across a user's visits to a room.
+         *
+         * @return the actor's affiliation to the room
+         */
+        public String getAffiliation() {
+            return affiliation;
+        }
+
+        /**
+         * Returns the <room@service/nick> by which an occupant is identified within the context
+         * of a room. If the room is non-anonymous, the JID will be included in the item.
+         *
+         * @return the room JID by which an occupant is identified within the room.
+         */
+        public String getJid() {
+            return jid;
+        }
+
+        /**
+         * Returns the new nickname of an occupant that is changing his/her nickname. The new
+         * nickname is sent as part of the unavailable presence.
+         *
+         * @return the new nickname of an occupant that is changing his/her nickname.
+         */
+        public String getNick() {
+            return nick;
+        }
+
+        /**
+         * Returns the temporary position or privilege level of an occupant within a room. The
+         * possible roles are "moderator", "participant", and "visitor" (it is also possible to
+         * have no defined role). A role lasts only for the duration of an occupant's visit to
+         * a room.
+         *
+         * @return the privilege level of an occupant within a room.
+         */
+        public String getRole() {
+            return role;
+        }
+
+        /**
+         * Sets the actor (JID of an occupant in the room) that was kicked or banned.
+         *
+         * @param actor the actor (JID of an occupant in the room) that was kicked or banned.
+         */
+        public void setActor(String actor) {
+            this.actor = actor;
+        }
+
+        /**
+         * Sets the reason for the item child. The reason is optional and could be used to
+         * explain the reason why a user (occupant) was kicked or banned.
+         *
+         * @param reason the reason why a user (occupant) was kicked or banned.
+         */
+        public void setReason(String reason) {
+            this.reason = reason;
+        }
+
+        /**
+         * Sets the <room@service/nick> by which an occupant is identified within the context
+         * of a room. If the room is non-anonymous, the JID will be included in the item.
+         *
+         * @param jid the JID by which an occupant is identified within a room.
+         */
+        public void setJid(String jid) {
+            this.jid = jid;
+        }
+
+        /**
+         * Sets the new nickname of an occupant that is changing his/her nickname. The new
+         * nickname is sent as part of the unavailable presence.
+         *
+         * @param nick the new nickname of an occupant that is changing his/her nickname.
+         */
+        public void setNick(String nick) {
+            this.nick = nick;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<item");
+            if (getAffiliation() != null) {
+                buf.append(" affiliation=\"").append(getAffiliation()).append("\"");
+            }
+            if (getJid() != null) {
+                buf.append(" jid=\"").append(getJid()).append("\"");
+            }
+            if (getNick() != null) {
+                buf.append(" nick=\"").append(getNick()).append("\"");
+            }
+            if (getRole() != null) {
+                buf.append(" role=\"").append(getRole()).append("\"");
+            }
+            if (getReason() == null && getActor() == null) {
+                buf.append("/>");
+            }
+            else {
+                buf.append(">");
+                if (getReason() != null) {
+                    buf.append("<reason>").append(getReason()).append("</reason>");
+                }
+                if (getActor() != null) {
+                    buf.append("<actor jid=\"").append(getActor()).append("\"/>");
+                }
+                buf.append("</item>");
+            }
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Status code assists in presenting notification messages. The following link provides the
+     * list of existing error codes (@link http://www.jabber.org/jeps/jep-0045.html#errorstatus).
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Status {
+        private String code;
+
+        /**
+         * Creates a new instance of Status with the specified code.
+         *
+         * @param code the code that uniquely identifies the reason of the error.
+         */
+        public Status(String code) {
+            this.code = code;
+        }
+
+        /**
+         * Returns the code that uniquely identifies the reason of the error. The code
+         * assists in presenting notification messages.
+         *
+         * @return the code that uniquely identifies the reason of the error.
+         */
+        public String getCode() {
+            return code;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<status code=\"").append(getCode()).append("\"/>");
+            return buf.toString();
+        }
+    }
+
+    /**
+     * Represents a notification that the room has been destroyed. After a room has been destroyed,
+     * the room occupants will receive a Presence packet of type 'unavailable' with the reason for
+     * the room destruction if provided by the room owner.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Destroy {
+        private String reason;
+        private String jid;
+
+
+        /**
+         * Returns the JID of an alternate location since the current room is being destroyed.
+         *
+         * @return the JID of an alternate location.
+         */
+        public String getJid() {
+            return jid;
+        }
+
+        /**
+         * Returns the reason for the room destruction.
+         *
+         * @return the reason for the room destruction.
+         */
+        public String getReason() {
+            return reason;
+        }
+
+        /**
+         * Sets the JID of an alternate location since the current room is being destroyed.
+         *
+         * @param jid the JID of an alternate location.
+         */
+        public void setJid(String jid) {
+            this.jid = jid;
+        }
+
+        /**
+         * Sets the reason for the room destruction.
+         *
+         * @param reason the reason for the room destruction.
+         */
+        public void setReason(String reason) {
+            this.reason = reason;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<destroy");
+            if (getJid() != null) {
+                buf.append(" jid=\"").append(getJid()).append("\"");
+            }
+            if (getReason() == null) {
+                buf.append("/>");
+            }
+            else {
+                buf.append(">");
+                if (getReason() != null) {
+                    buf.append("<reason>").append(getReason()).append("</reason>");
+                }
+                buf.append("</destroy>");
+            }
+            return buf.toString();
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/packet/MessageEvent.java b/src/org/jivesoftware/smackx/packet/MessageEvent.java
new file mode 100644
index 0000000..5e146dc
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/MessageEvent.java
@@ -0,0 +1,335 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * Represents message events relating to the delivery, display, composition and cancellation of 
+ * messages.<p>
+ * 
+ * There are four message events currently defined in this namespace:
+ * <ol>
+ * <li>Offline<br>
+ * Indicates that the message has been stored offline by the intended recipient's server. This 
+ * event is triggered only if the intended recipient's server supports offline storage, has that 
+ * support enabled, and the recipient is offline when the server receives the message for delivery.</li>
+ * 
+ * <li>Delivered<br>
+ * Indicates that the message has been delivered to the recipient. This signifies that the message
+ * has reached the recipient's XMPP client, but does not necessarily mean that the message has 
+ * been displayed. This event is to be raised by the XMPP client.</li>
+ * 
+ * <li>Displayed<br>
+ * Once the message has been received by the recipient's XMPP client, it may be displayed to the
+ * user. This event indicates that the message has been displayed, and is to be raised by the 
+ * XMPP client. Even if a message is displayed multiple times, this event should be raised only 
+ * once.</li>
+ * 
+ * <li>Composing<br>
+ * In threaded chat conversations, this indicates that the recipient is composing a reply to a 
+ * message. The event is to be raised by the recipient's XMPP client. A XMPP client is allowed
+ * to raise this event multiple times in response to the same request, providing the original 
+ * event is cancelled first.</li>
+ * </ol>
+ *
+ * @author Gaston Dombiak
+ */
+public class MessageEvent implements PacketExtension {
+
+    public static final String OFFLINE = "offline";
+    public static final String COMPOSING = "composing";
+    public static final String DISPLAYED = "displayed";
+    public static final String DELIVERED = "delivered";
+    public static final String CANCELLED = "cancelled";
+
+    private boolean offline = false;
+    private boolean delivered = false;
+    private boolean displayed = false;
+    private boolean composing = false;
+    private boolean cancelled = true;
+
+    private String packetID = null;
+
+    /**
+    * Returns the XML element name of the extension sub-packet root element.
+    * Always returns "x"
+    *
+    * @return the XML element name of the packet extension.
+    */
+    public String getElementName() {
+        return "x";
+    }
+
+    /** 
+     * Returns the XML namespace of the extension sub-packet root element.
+     * According the specification the namespace is always "jabber:x:event"
+     *
+     * @return the XML namespace of the packet extension.
+     */
+    public String getNamespace() {
+        return "jabber:x:event";
+    }
+
+    /**
+     * When the message is a request returns if the sender of the message requests to be notified
+     * when the receiver is composing a reply.
+     * When the message is a notification returns if the receiver of the message is composing a 
+     * reply.
+     * 
+     * @return true if the sender is requesting to be notified when composing or when notifying
+     * that the receiver of the message is composing a reply
+     */
+    public boolean isComposing() {
+        return composing;
+    }
+
+    /**
+     * When the message is a request returns if the sender of the message requests to be notified
+     * when the message is delivered.
+     * When the message is a notification returns if the message was delivered or not.
+     * 
+     * @return true if the sender is requesting to be notified when delivered or when notifying 
+     * that the message was delivered 
+     */
+    public boolean isDelivered() {
+        return delivered;
+    }
+
+    /**
+     * When the message is a request returns if the sender of the message requests to be notified
+     * when the message is displayed.
+     * When the message is a notification returns if the message was displayed or not.
+     * 
+     * @return true if the sender is requesting to be notified when displayed or when notifying 
+     * that the message was displayed
+     */
+    public boolean isDisplayed() {
+        return displayed;
+    }
+
+    /**
+     * When the message is a request returns if the sender of the message requests to be notified
+     * when the receiver of the message is offline.
+     * When the message is a notification returns if the receiver of the message was offline.
+     * 
+     * @return true if the sender is requesting to be notified when offline or when notifying 
+     * that the receiver of the message is offline
+     */
+    public boolean isOffline() {
+        return offline;
+    }
+
+    /**
+     * When the message is a notification returns if the receiver of the message cancelled 
+     * composing a reply.
+     * 
+     * @return true if the receiver of the message cancelled composing a reply
+     */
+    public boolean isCancelled() {
+        return cancelled;
+    }
+
+    /**
+     * Returns the unique ID of the message that requested to be notified of the event.
+     * The packet id is not used when the message is a request for notifications
+     *
+     * @return the message id that requested to be notified of the event.
+     */
+    public String getPacketID() {
+        return packetID;
+    }
+
+    /**
+     * Returns the types of events. The type of event could be:
+     * "offline", "composing","delivered","displayed", "offline"
+     *
+     * @return an iterator over all the types of events of the MessageEvent.
+     */
+    public Iterator<String> getEventTypes() {
+        ArrayList<String> allEvents = new ArrayList<String>();
+        if (isDelivered()) {
+            allEvents.add(MessageEvent.DELIVERED);
+        }
+        if (!isMessageEventRequest() && isCancelled()) {
+            allEvents.add(MessageEvent.CANCELLED);
+        }
+        if (isComposing()) {
+            allEvents.add(MessageEvent.COMPOSING);
+        }
+        if (isDisplayed()) {
+            allEvents.add(MessageEvent.DISPLAYED);
+        }
+        if (isOffline()) {
+            allEvents.add(MessageEvent.OFFLINE);
+        }
+        return allEvents.iterator();
+    }
+
+    /**
+     * When the message is a request sets if the sender of the message requests to be notified
+     * when the receiver is composing a reply.
+     * When the message is a notification sets if the receiver of the message is composing a 
+     * reply.
+     * 
+     * @param composing sets if the sender is requesting to be notified when composing or when 
+     * notifying that the receiver of the message is composing a reply
+     */
+    public void setComposing(boolean composing) {
+        this.composing = composing;
+        setCancelled(false);
+    }
+
+    /**
+     * When the message is a request sets if the sender of the message requests to be notified
+     * when the message is delivered.
+     * When the message is a notification sets if the message was delivered or not.
+     * 
+     * @param delivered sets if the sender is requesting to be notified when delivered or when 
+     * notifying that the message was delivered 
+     */
+    public void setDelivered(boolean delivered) {
+        this.delivered = delivered;
+        setCancelled(false);
+    }
+
+    /**
+     * When the message is a request sets if the sender of the message requests to be notified
+     * when the message is displayed.
+     * When the message is a notification sets if the message was displayed or not.
+     * 
+     * @param displayed sets if the sender is requesting to be notified when displayed or when 
+     * notifying that the message was displayed
+     */
+    public void setDisplayed(boolean displayed) {
+        this.displayed = displayed;
+        setCancelled(false);
+    }
+
+    /**
+     * When the message is a request sets if the sender of the message requests to be notified
+     * when the receiver of the message is offline.
+     * When the message is a notification sets if the receiver of the message was offline.
+     * 
+     * @param offline sets if the sender is requesting to be notified when offline or when 
+     * notifying that the receiver of the message is offline
+     */
+    public void setOffline(boolean offline) {
+        this.offline = offline;
+        setCancelled(false);
+    }
+
+    /**
+     * When the message is a notification sets if the receiver of the message cancelled 
+     * composing a reply.
+     * The Cancelled event is never requested explicitly. It is requested implicitly when
+     * requesting to be notified of the Composing event.
+     * 
+     * @param cancelled sets if the receiver of the message cancelled composing a reply
+     */
+    public void setCancelled(boolean cancelled) {
+        this.cancelled = cancelled;
+    }
+
+    /**
+     * Sets the unique ID of the message that requested to be notified of the event.
+     * The packet id is not used when the message is a request for notifications
+     *
+     * @param packetID the message id that requested to be notified of the event.
+     */
+    public void setPacketID(String packetID) {
+        this.packetID = packetID;
+    }
+
+    /**
+     * Returns true if this MessageEvent is a request for notifications.
+     * Returns false if this MessageEvent is a notification of an event.
+     *
+    * @return true if this message is a request for notifications.
+     */
+    public boolean isMessageEventRequest() {
+        return this.packetID == null;
+    }
+
+    /**
+     * Returns the XML representation of a Message Event according the specification.
+     * 
+     * Usually the XML representation will be inside of a Message XML representation like
+     * in the following examples:<p>
+     * 
+     * Request to be notified when displayed:
+     * <pre>
+     * &lt;message
+     *    to='romeo@montague.net/orchard'
+     *    from='juliet@capulet.com/balcony'
+     *    id='message22'&gt;
+     * &lt;x xmlns='jabber:x:event'&gt;
+     *   &lt;displayed/&gt;
+     * &lt;/x&gt;
+     * &lt;/message&gt;
+     * </pre>
+     * 
+     * Notification of displayed:
+     * <pre>
+     * &lt;message
+     *    from='romeo@montague.net/orchard'
+     *    to='juliet@capulet.com/balcony'&gt;
+     * &lt;x xmlns='jabber:x:event'&gt;
+     *   &lt;displayed/&gt;
+     *   &lt;id&gt;message22&lt;/id&gt;
+     * &lt;/x&gt;
+     * &lt;/message&gt;
+     * </pre>
+     * 
+     */
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+            "\">");
+        // Note: Cancellation events don't specify any tag. They just send the packetID
+
+        // Add the offline tag if the sender requests to be notified of offline events or if 
+        // the target is offline
+        if (isOffline())
+            buf.append("<").append(MessageEvent.OFFLINE).append("/>");
+        // Add the delivered tag if the sender requests to be notified when the message is 
+        // delivered or if the target notifies that the message has been delivered
+        if (isDelivered())
+            buf.append("<").append(MessageEvent.DELIVERED).append("/>");
+        // Add the displayed tag if the sender requests to be notified when the message is 
+        // displayed or if the target notifies that the message has been displayed
+        if (isDisplayed())
+            buf.append("<").append(MessageEvent.DISPLAYED).append("/>");
+        // Add the composing tag if the sender requests to be notified when the target is 
+        // composing a reply or if the target notifies that he/she is composing a reply
+        if (isComposing())
+            buf.append("<").append(MessageEvent.COMPOSING).append("/>");
+        // Add the id tag only if the MessageEvent is a notification message (not a request)
+        if (getPacketID() != null)
+            buf.append("<id>").append(getPacketID()).append("</id>");
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/packet/MultipleAddresses.java b/src/org/jivesoftware/smackx/packet/MultipleAddresses.java
new file mode 100644
index 0000000..522250a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/MultipleAddresses.java
@@ -0,0 +1,205 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Packet extension that contains the list of addresses that a packet should be sent or was sent.
+ *
+ * @author Gaston Dombiak
+ */
+public class MultipleAddresses implements PacketExtension {
+
+    public static final String BCC = "bcc";
+    public static final String CC = "cc";
+    public static final String NO_REPLY = "noreply";
+    public static final String REPLY_ROOM = "replyroom";
+    public static final String REPLY_TO = "replyto";
+    public static final String TO = "to";
+
+
+    private List<Address> addresses = new ArrayList<Address>();
+
+    /**
+     * Adds a new address to which the packet is going to be sent or was sent.
+     *
+     * @param type on of the static type (BCC, CC, NO_REPLY, REPLY_ROOM, etc.)
+     * @param jid the JID address of the recipient.
+     * @param node used to specify a sub-addressable unit at a particular JID, corresponding to
+     *             a Service Discovery node.
+     * @param desc used to specify human-readable information for this address.
+     * @param delivered true when the packet was already delivered to this address.
+     * @param uri used to specify an external system address, such as a sip:, sips:, or im: URI.
+     */
+    public void addAddress(String type, String jid, String node, String desc, boolean delivered,
+            String uri) {
+        // Create a new address with the specificed configuration
+        Address address = new Address(type);
+        address.setJid(jid);
+        address.setNode(node);
+        address.setDescription(desc);
+        address.setDelivered(delivered);
+        address.setUri(uri);
+        // Add the new address to the list of multiple recipients
+        addresses.add(address);
+    }
+
+    /**
+     * Indicate that the packet being sent should not be replied.
+     */
+    public void setNoReply() {
+        // Create a new address with the specificed configuration
+        Address address = new Address(NO_REPLY);
+        // Add the new address to the list of multiple recipients
+        addresses.add(address);
+    }
+
+    /**
+     * Returns the list of addresses that matches the specified type. Examples of address
+     * type are: TO, CC, BCC, etc..
+     *
+     * @param type Examples of address type are: TO, CC, BCC, etc.
+     * @return the list of addresses that matches the specified type.
+     */
+    public List<Address> getAddressesOfType(String type) {
+        List<Address> answer = new ArrayList<Address>(addresses.size());
+        for (Iterator<Address> it = addresses.iterator(); it.hasNext();) {
+            Address address = (Address) it.next();
+            if (address.getType().equals(type)) {
+                answer.add(address);
+            }
+        }
+
+        return answer;
+    }
+
+    public String getElementName() {
+        return "addresses";
+    }
+
+    public String getNamespace() {
+        return "http://jabber.org/protocol/address";
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName());
+        buf.append(" xmlns=\"").append(getNamespace()).append("\">");
+        // Loop through all the addresses and append them to the string buffer
+        for (Iterator<Address> i = addresses.iterator(); i.hasNext();) {
+            Address address = (Address) i.next();
+            buf.append(address.toXML());
+        }
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+    public static class Address {
+
+        private String type;
+        private String jid;
+        private String node;
+        private String description;
+        private boolean delivered;
+        private String uri;
+
+        private Address(String type) {
+            this.type = type;
+        }
+
+        public String getType() {
+            return type;
+        }
+
+        public String getJid() {
+            return jid;
+        }
+
+        private void setJid(String jid) {
+            this.jid = jid;
+        }
+
+        public String getNode() {
+            return node;
+        }
+
+        private void setNode(String node) {
+            this.node = node;
+        }
+
+        public String getDescription() {
+            return description;
+        }
+
+        private void setDescription(String description) {
+            this.description = description;
+        }
+
+        public boolean isDelivered() {
+            return delivered;
+        }
+
+        private void setDelivered(boolean delivered) {
+            this.delivered = delivered;
+        }
+
+        public String getUri() {
+            return uri;
+        }
+
+        private void setUri(String uri) {
+            this.uri = uri;
+        }
+
+        private String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<address type=\"");
+            // Append the address type (e.g. TO/CC/BCC)
+            buf.append(type).append("\"");
+            if (jid != null) {
+                buf.append(" jid=\"");
+                buf.append(jid).append("\"");
+            }
+            if (node != null) {
+                buf.append(" node=\"");
+                buf.append(node).append("\"");
+            }
+            if (description != null && description.trim().length() > 0) {
+                buf.append(" desc=\"");
+                buf.append(description).append("\"");
+            }
+            if (delivered) {
+                buf.append(" delivered=\"true\"");
+            }
+            if (uri != null) {
+                buf.append(" uri=\"");
+                buf.append(uri).append("\"");
+            }
+            buf.append("/>");
+            return buf.toString();
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/packet/Nick.java b/src/org/jivesoftware/smackx/packet/Nick.java
new file mode 100644
index 0000000..9a64eaa
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/Nick.java
@@ -0,0 +1,112 @@
+/**

+ * $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.smackx.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * A minimalistic implementation of a {@link PacketExtension} for nicknames.

+ * 

+ * @author Guus der Kinderen, guus.der.kinderen@gmail.com

+ * @see <a href="http://xmpp.org/extensions/xep-0172.html">XEP-0172: User Nickname</a>

+ */

+public class Nick implements PacketExtension {

+

+	public static final String NAMESPACE = "http://jabber.org/protocol/nick";

+

+	public static final String ELEMENT_NAME = "nick";

+

+	private String name = null;

+

+	public Nick(String name) {

+		this.name = name;

+	}

+

+	/**

+	 * The value of this nickname

+	 * 

+	 * @return the nickname

+	 */

+	public String getName() {

+		return name;

+	}

+

+	/**

+	 * Sets the value of this nickname

+	 * 

+	 * @param name

+	 *            the name to set

+	 */

+	public void setName(String name) {

+		this.name = name;

+	}

+

+	/*

+	 * (non-Javadoc)

+	 * 

+	 * @see org.jivesoftware.smack.packet.PacketExtension#getElementName()

+	 */

+	public String getElementName() {

+		return ELEMENT_NAME;

+	}

+

+	/*

+	 * (non-Javadoc)

+	 * 

+	 * @see org.jivesoftware.smack.packet.PacketExtension#getNamespace()

+	 */

+	public String getNamespace() {

+		return NAMESPACE;

+	}

+

+	/*

+	 * (non-Javadoc)

+	 * 

+	 * @see org.jivesoftware.smack.packet.PacketExtension#toXML()

+	 */

+	public String toXML() {

+		final StringBuilder buf = new StringBuilder();

+

+		buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(

+				NAMESPACE).append("\">");

+		buf.append(getName());

+		buf.append("</").append(ELEMENT_NAME).append('>');

+

+		return buf.toString();

+	}

+

+	public static class Provider implements PacketExtensionProvider {

+

+		public PacketExtension parseExtension(XmlPullParser parser)

+				throws Exception {

+			

+            parser.next();

+			final String name = parser.getText();

+

+			// Advance to end of extension.

+			while(parser.getEventType() != XmlPullParser.END_TAG) {

+				parser.next();

+			}

+

+			return new Nick(name);

+		}

+	}

+}

diff --git a/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java b/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java
new file mode 100644
index 0000000..5f9954d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/OfflineMessageInfo.java
@@ -0,0 +1,128 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * OfflineMessageInfo is an extension included in the retrieved offline messages requested by
+ * the {@link org.jivesoftware.smackx.OfflineMessageManager}. This extension includes a stamp
+ * that uniquely identifies the offline message. This stamp may be used for deleting the offline
+ * message. The stamp may be of the form UTC timestamps but it is not required to have that format.
+ *
+ * @author Gaston Dombiak
+ */
+public class OfflineMessageInfo implements PacketExtension {
+
+    private String node = null;
+
+    /**
+    * Returns the XML element name of the extension sub-packet root element.
+    * Always returns "offline"
+    *
+    * @return the XML element name of the packet extension.
+    */
+    public String getElementName() {
+        return "offline";
+    }
+
+    /**
+     * Returns the XML namespace of the extension sub-packet root element.
+     * According the specification the namespace is always "http://jabber.org/protocol/offline"
+     *
+     * @return the XML namespace of the packet extension.
+     */
+    public String getNamespace() {
+        return "http://jabber.org/protocol/offline";
+    }
+
+    /**
+     * Returns the stamp that uniquely identifies the offline message. This stamp may
+     * be used for deleting the offline message. The stamp may be of the form UTC timestamps
+     * but it is not required to have that format.
+     *
+     * @return the stamp that uniquely identifies the offline message.
+     */
+    public String getNode() {
+        return node;
+    }
+
+    /**
+     * Sets the stamp that uniquely identifies the offline message. This stamp may
+     * be used for deleting the offline message. The stamp may be of the form UTC timestamps
+     * but it is not required to have that format.
+     *
+     * @param node the stamp that uniquely identifies the offline message.
+     */
+    public void setNode(String node) {
+        this.node = node;
+    }
+
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+            "\">");
+        if (getNode() != null)
+            buf.append("<item node=\"").append(getNode()).append("\"/>");
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+    public static class Provider implements PacketExtensionProvider {
+
+        /**
+         * Creates a new Provider.
+         * ProviderManager requires that every PacketExtensionProvider has a public,
+         * no-argument constructor
+         */
+        public Provider() {
+        }
+
+        /**
+         * Parses a OfflineMessageInfo packet (extension sub-packet).
+         *
+         * @param parser the XML parser, positioned at the starting element of the extension.
+         * @return a PacketExtension.
+         * @throws Exception if a parsing error occurs.
+         */
+        public PacketExtension parseExtension(XmlPullParser parser)
+            throws Exception {
+            OfflineMessageInfo info = new OfflineMessageInfo();
+            boolean done = false;
+            while (!done) {
+                int eventType = parser.next();
+                if (eventType == XmlPullParser.START_TAG) {
+                    if (parser.getName().equals("item"))
+                        info.setNode(parser.getAttributeValue("", "node"));
+                } else if (eventType == XmlPullParser.END_TAG) {
+                    if (parser.getName().equals("offline")) {
+                        done = true;
+                    }
+                }
+            }
+
+            return info;
+        }
+
+    }
+}
diff --git a/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java b/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java
new file mode 100644
index 0000000..1d9d096
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/OfflineMessageRequest.java
@@ -0,0 +1,237 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Represents a request to get some or all the offline messages of a user. This class can also
+ * be used for deleting some or all the offline messages of a user.
+ *
+ * @author Gaston Dombiak
+ */
+public class OfflineMessageRequest extends IQ {
+
+    private List<Item> items = new ArrayList<Item>();
+    private boolean purge = false;
+    private boolean fetch = false;
+
+    /**
+     * Returns an Iterator for item childs that holds information about offline messages to
+     * view or delete.
+     *
+     * @return an Iterator for item childs that holds information about offline messages to
+     *         view or delete.
+     */
+    public Iterator<Item> getItems() {
+        synchronized (items) {
+            return Collections.unmodifiableList(new ArrayList<Item>(items)).iterator();
+        }
+    }
+
+    /**
+     * Adds an item child that holds information about offline messages to view or delete.
+     *
+     * @param item the item child that holds information about offline messages to view or delete.
+     */
+    public void addItem(Item item) {
+        synchronized (items) {
+            items.add(item);
+        }
+    }
+
+    /**
+     * Returns true if all the offline messages of the user should be deleted.
+     *
+     * @return true if all the offline messages of the user should be deleted.
+     */
+    public boolean isPurge() {
+        return purge;
+    }
+
+    /**
+     * Sets if all the offline messages of the user should be deleted.
+     *
+     * @param purge true if all the offline messages of the user should be deleted.
+     */
+    public void setPurge(boolean purge) {
+        this.purge = purge;
+    }
+
+    /**
+     * Returns true if all the offline messages of the user should be retrieved.
+     *
+     * @return true if all the offline messages of the user should be retrieved.
+     */
+    public boolean isFetch() {
+        return fetch;
+    }
+
+    /**
+     * Sets if all the offline messages of the user should be retrieved.
+     *
+     * @param fetch true if all the offline messages of the user should be retrieved.
+     */
+    public void setFetch(boolean fetch) {
+        this.fetch = fetch;
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<offline xmlns=\"http://jabber.org/protocol/offline\">");
+        synchronized (items) {
+            for (int i = 0; i < items.size(); i++) {
+                Item item = items.get(i);
+                buf.append(item.toXML());
+            }
+        }
+        if (purge) {
+            buf.append("<purge/>");
+        }
+        if (fetch) {
+            buf.append("<fetch/>");
+        }
+        // Add packet extensions, if any are defined.
+        buf.append(getExtensionsXML());
+        buf.append("</offline>");
+        return buf.toString();
+    }
+
+    /**
+     * Item child that holds information about offline messages to view or delete.
+     *
+     * @author Gaston Dombiak
+     */
+    public static class Item {
+        private String action;
+        private String jid;
+        private String node;
+
+        /**
+         * Creates a new item child.
+         *
+         * @param node the actor's affiliation to the room
+         */
+        public Item(String node) {
+            this.node = node;
+        }
+
+        public String getNode() {
+            return node;
+        }
+
+        /**
+         * Returns "view" or "remove" that indicate if the server should return the specified
+         * offline message or delete it.
+         *
+         * @return "view" or "remove" that indicate if the server should return the specified
+         *         offline message or delete it.
+         */
+        public String getAction() {
+            return action;
+        }
+
+        /**
+         * Sets if the server should return the specified offline message or delete it. Possible
+         * values are "view" or "remove".
+         *
+         * @param action if the server should return the specified offline message or delete it.
+         */
+        public void setAction(String action) {
+            this.action = action;
+        }
+
+        public String getJid() {
+            return jid;
+        }
+
+        public void setJid(String jid) {
+            this.jid = jid;
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf.append("<item");
+            if (getAction() != null) {
+                buf.append(" action=\"").append(getAction()).append("\"");
+            }
+            if (getJid() != null) {
+                buf.append(" jid=\"").append(getJid()).append("\"");
+            }
+            if (getNode() != null) {
+                buf.append(" node=\"").append(getNode()).append("\"");
+            }
+            buf.append("/>");
+            return buf.toString();
+        }
+    }
+
+    public static class Provider implements IQProvider {
+
+        public IQ parseIQ(XmlPullParser parser) throws Exception {
+            OfflineMessageRequest request = new OfflineMessageRequest();
+            boolean done = false;
+            while (!done) {
+                int eventType = parser.next();
+                if (eventType == XmlPullParser.START_TAG) {
+                    if (parser.getName().equals("item")) {
+                        request.addItem(parseItem(parser));
+                    }
+                    else if (parser.getName().equals("purge")) {
+                        request.setPurge(true);
+                    }
+                    else if (parser.getName().equals("fetch")) {
+                        request.setFetch(true);
+                    }
+                } else if (eventType == XmlPullParser.END_TAG) {
+                    if (parser.getName().equals("offline")) {
+                        done = true;
+                    }
+                }
+            }
+
+            return request;
+        }
+
+        private Item parseItem(XmlPullParser parser) throws Exception {
+            boolean done = false;
+            Item item = new Item(parser.getAttributeValue("", "node"));
+            item.setAction(parser.getAttributeValue("", "action"));
+            item.setJid(parser.getAttributeValue("", "jid"));
+            while (!done) {
+                int eventType = parser.next();
+                if (eventType == XmlPullParser.END_TAG) {
+                    if (parser.getName().equals("item")) {
+                        done = true;
+                    }
+                }
+            }
+            return item;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/packet/PEPEvent.java b/src/org/jivesoftware/smackx/packet/PEPEvent.java
new file mode 100644
index 0000000..48f1de2
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/PEPEvent.java
@@ -0,0 +1,105 @@
+/**
+ * $RCSfile: PEPEvent.java,v $
+ * $Revision: 1.1 $
+ * $Date: 2007/11/03 00:14:32 $
+ *
+ * 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents XMPP Personal Event Protocol packets.<p>
+ * 
+ * The 'http://jabber.org/protocol/pubsub#event' namespace  is used to publish personal events items from one client 
+ * to subscribed clients (See XEP-163).
+ *
+ * @author Jeff Williams
+ */
+public class PEPEvent implements PacketExtension {
+
+    PEPItem item;
+
+    /**
+     * Creates a new empty roster exchange package.
+     *
+     */
+    public PEPEvent() {
+        super();
+    }
+
+    /**
+     * Creates a new empty roster exchange package.
+     *
+     */
+    public PEPEvent(PEPItem item) {
+        super();
+
+        this.item = item;
+    }
+    
+    public void addPEPItem(PEPItem item) {
+        this.item = item;
+    }
+
+    /**
+    * Returns the XML element name of the extension sub-packet root element.
+    * Always returns "x"
+    *
+    * @return the XML element name of the packet extension.
+    */
+    public String getElementName() {
+        return "event";
+    }
+
+    /** 
+     * Returns the XML namespace of the extension sub-packet root element.
+     * According the specification the namespace is always "jabber:x:roster"
+     * (which is not to be confused with the 'jabber:iq:roster' namespace
+     *
+     * @return the XML namespace of the packet extension.
+     */
+    public String getNamespace() {
+        return "http://jabber.org/protocol/pubsub";
+    }
+
+    /**
+     * Returns the XML representation of a Personal Event Publish according the specification.
+     * 
+     * Usually the XML representation will be inside of a Message XML representation like
+     * in the following example:
+     * <pre>
+     * &lt;message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"&gt;
+     *     &lt;subject&gt;Any subject you want&lt;/subject&gt;
+     *     &lt;body&gt;This message contains roster items.&lt;/body&gt;
+     *     &lt;x xmlns="jabber:x:roster"&gt;
+     *         &lt;item jid="gato1@gato.home"/&gt;
+     *         &lt;item jid="gato2@gato.home"/&gt;
+     *     &lt;/x&gt;
+     * &lt;/message&gt;
+     * </pre>
+     * 
+     */
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\">");
+        buf.append(item.toXML());
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/packet/PEPItem.java b/src/org/jivesoftware/smackx/packet/PEPItem.java
new file mode 100644
index 0000000..c3ff6f4
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/PEPItem.java
@@ -0,0 +1,92 @@
+/**
+ * $RCSfile: PEPItem.java,v $
+ * $Revision: 1.2 $
+ * $Date: 2007/11/06 02:05:09 $
+ *
+ * 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+/**
+ * Represents XMPP Personal Event Protocol packets.<p>
+ * 
+ * The 'http://jabber.org/protocol/pubsub#event' namespace  is used to publish personal events items from one client 
+ * to subscribed clients (See XEP-163).
+ *
+ * @author Jeff Williams
+ */
+public abstract class PEPItem implements PacketExtension {
+    
+    String id;
+    abstract String getNode();
+    abstract String getItemDetailsXML();
+    
+    /**
+    * Creates a new PEPItem.
+    *
+    */
+    public PEPItem(String id) {
+        super();
+        this.id = id;
+    }
+    
+     /**
+    * Returns the XML element name of the extension sub-packet root element.
+    * Always returns "x"
+    *
+    * @return the XML element name of the packet extension.
+    */
+    public String getElementName() {
+        return "item";
+    }
+
+    /** 
+     * Returns the XML namespace of the extension sub-packet root element.
+     *
+     * @return the XML namespace of the packet extension.
+     */
+    public String getNamespace() {
+        return "http://jabber.org/protocol/pubsub";
+    }
+
+    /**
+     * Returns the XML representation of a Personal Event Publish according the specification.
+     * 
+     * Usually the XML representation will be inside of a Message XML representation like
+     * in the following example:
+     * <pre>
+     * &lt;message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"&gt;
+     *     &lt;subject&gt;Any subject you want&lt;/subject&gt;
+     *     &lt;body&gt;This message contains roster items.&lt;/body&gt;
+     *     &lt;x xmlns="jabber:x:roster"&gt;
+     *         &lt;item jid="gato1@gato.home"/&gt;
+     *         &lt;item jid="gato2@gato.home"/&gt;
+     *     &lt;/x&gt;
+     * &lt;/message&gt;
+     * </pre>
+     * 
+     */
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" id=\"").append(id).append("\">");
+        buf.append(getItemDetailsXML());
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/packet/PEPPubSub.java b/src/org/jivesoftware/smackx/packet/PEPPubSub.java
new file mode 100644
index 0000000..420ce61
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/PEPPubSub.java
@@ -0,0 +1,95 @@
+/**
+ * $RCSfile: PEPPubSub.java,v $
+ * $Revision: 1.2 $
+ * $Date: 2007/11/03 04:46:52 $
+ *
+ * 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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * Represents XMPP PEP/XEP-163 pubsub packets.<p>
+ * 
+ * The 'http://jabber.org/protocol/pubsub' namespace  is used to publish personal events items from one client 
+ * to subscribed clients (See XEP-163).
+ *
+ * @author Jeff Williams
+ */
+public class PEPPubSub extends IQ {
+    
+    PEPItem item;
+
+    /**
+    * Creates a new PubSub.
+    *
+    */
+    public PEPPubSub(PEPItem item) {
+        super();
+        
+        this.item = item;
+    }
+
+    /**
+    * Returns the XML element name of the extension sub-packet root element.
+    * Always returns "x"
+    *
+    * @return the XML element name of the packet extension.
+    */
+    public String getElementName() {
+        return "pubsub";
+    }
+
+    /** 
+     * Returns the XML namespace of the extension sub-packet root element.
+     * According the specification the namespace is always "jabber:x:roster"
+     * (which is not to be confused with the 'jabber:iq:roster' namespace
+     *
+     * @return the XML namespace of the packet extension.
+     */
+    public String getNamespace() {
+        return "http://jabber.org/protocol/pubsub";
+    }
+
+    /**
+     * Returns the XML representation of a Personal Event Publish according the specification.
+     * 
+     * Usually the XML representation will be inside of a Message XML representation like
+     * in the following example:
+     * <pre>
+     * &lt;message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"&gt;
+     *     &lt;subject&gt;Any subject you want&lt;/subject&gt;
+     *     &lt;body&gt;This message contains roster items.&lt;/body&gt;
+     *     &lt;x xmlns="jabber:x:roster"&gt;
+     *         &lt;item jid="gato1@gato.home"/&gt;
+     *         &lt;item jid="gato2@gato.home"/&gt;
+     *     &lt;/x&gt;
+     * &lt;/message&gt;
+     * </pre>
+     * 
+     */
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\">");
+        buf.append("<publish node=\"").append(item.getNode()).append("\">");
+        buf.append(item.toXML());
+        buf.append("</publish>");
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/packet/PrivateData.java b/src/org/jivesoftware/smackx/packet/PrivateData.java
new file mode 100644
index 0000000..3ddb7d5
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/PrivateData.java
@@ -0,0 +1,52 @@
+/**
+ * $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.smackx.packet;
+
+/**
+ * Interface to represent private data. Each private data chunk is an XML sub-document
+ * with a root element name and namespace.
+ *
+ * @see org.jivesoftware.smackx.PrivateDataManager
+ * @author Matt Tucker
+ */
+public interface PrivateData {
+
+    /**
+     * Returns the root element name.
+     *
+     * @return the element name.
+     */
+    public String getElementName();
+
+    /**
+     * Returns the root element XML namespace.
+     *
+     * @return the namespace.
+     */
+    public String getNamespace();
+
+    /**
+     * Returns the XML reppresentation of the PrivateData.
+     *
+     * @return the private data as XML.
+     */
+    public String toXML();
+}
diff --git a/src/org/jivesoftware/smackx/packet/RosterExchange.java b/src/org/jivesoftware/smackx/packet/RosterExchange.java
new file mode 100644
index 0000000..ad59146
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/RosterExchange.java
@@ -0,0 +1,181 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.Roster;
+import org.jivesoftware.smack.RosterEntry;
+import org.jivesoftware.smack.RosterGroup;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.RemoteRosterEntry;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Represents XMPP Roster Item Exchange packets.<p>
+ * 
+ * The 'jabber:x:roster' namespace (which is not to be confused with the 'jabber:iq:roster' 
+ * namespace) is used to send roster items from one client to another. A roster item is sent by 
+ * adding to the &lt;message/&gt; element an &lt;x/&gt; child scoped by the 'jabber:x:roster' namespace. This 
+ * &lt;x/&gt; element may contain one or more &lt;item/&gt; children (one for each roster item to be sent).<p>
+ * 
+ * Each &lt;item/&gt; element may possess the following attributes:<p>
+ * 
+ * &lt;jid/&gt; -- The id of the contact being sent. This attribute is required.<br>
+ * &lt;name/&gt; -- A natural-language nickname for the contact. This attribute is optional.<p>
+ * 
+ * Each &lt;item/&gt; element may also contain one or more &lt;group/&gt; children specifying the 
+ * natural-language name of a user-specified group, for the purpose of categorizing this contact 
+ * into one or more roster groups.
+ *
+ * @author Gaston Dombiak
+ */
+public class RosterExchange implements PacketExtension {
+
+    private List<RemoteRosterEntry> remoteRosterEntries = new ArrayList<RemoteRosterEntry>();
+
+    /**
+     * Creates a new empty roster exchange package.
+     *
+     */
+    public RosterExchange() {
+        super();
+    }
+
+    /**
+     * Creates a new roster exchange package with the entries specified in roster.
+     *
+     * @param roster the roster to send to other XMPP entity.
+     */
+    public RosterExchange(Roster roster) {
+        // Add all the roster entries to the new RosterExchange 
+        for (RosterEntry rosterEntry : roster.getEntries()) {
+            this.addRosterEntry(rosterEntry);
+        }
+    }
+
+    /**
+     * Adds a roster entry to the packet.
+     *
+     * @param rosterEntry a roster entry to add.
+     */
+    public void addRosterEntry(RosterEntry rosterEntry) {
+		// Obtain a String[] from the roster entry groups name 
+		List<String> groupNamesList = new ArrayList<String>();
+		String[] groupNames;
+		for (RosterGroup group : rosterEntry.getGroups()) {
+			groupNamesList.add(group.getName());
+		}
+		groupNames = groupNamesList.toArray(new String[groupNamesList.size()]);
+
+        // Create a new Entry based on the rosterEntry and add it to the packet
+        RemoteRosterEntry remoteRosterEntry = new RemoteRosterEntry(rosterEntry.getUser(),
+                rosterEntry.getName(), groupNames);
+		
+        addRosterEntry(remoteRosterEntry);
+    }
+
+    /**
+     * Adds a remote roster entry to the packet.
+     *
+     * @param remoteRosterEntry a remote roster entry to add.
+     */
+    public void addRosterEntry(RemoteRosterEntry remoteRosterEntry) {
+        synchronized (remoteRosterEntries) {
+            remoteRosterEntries.add(remoteRosterEntry);
+        }
+    }
+    
+    /**
+    * Returns the XML element name of the extension sub-packet root element.
+    * Always returns "x"
+    *
+    * @return the XML element name of the packet extension.
+    */
+    public String getElementName() {
+        return "x";
+    }
+
+    /** 
+     * Returns the XML namespace of the extension sub-packet root element.
+     * According the specification the namespace is always "jabber:x:roster"
+     * (which is not to be confused with the 'jabber:iq:roster' namespace
+     *
+     * @return the XML namespace of the packet extension.
+     */
+    public String getNamespace() {
+        return "jabber:x:roster";
+    }
+
+    /**
+     * Returns an Iterator for the roster entries in the packet.
+     *
+     * @return an Iterator for the roster entries in the packet.
+     */
+    public Iterator<RemoteRosterEntry> getRosterEntries() {
+        synchronized (remoteRosterEntries) {
+            List<RemoteRosterEntry> entries = Collections.unmodifiableList(new ArrayList<RemoteRosterEntry>(remoteRosterEntries));
+            return entries.iterator();
+        }
+    }
+
+    /**
+     * Returns a count of the entries in the roster exchange.
+     *
+     * @return the number of entries in the roster exchange.
+     */
+    public int getEntryCount() {
+        return remoteRosterEntries.size();
+    }
+
+    /**
+     * Returns the XML representation of a Roster Item Exchange according the specification.
+     * 
+     * Usually the XML representation will be inside of a Message XML representation like
+     * in the following example:
+     * <pre>
+     * &lt;message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"&gt;
+     *     &lt;subject&gt;Any subject you want&lt;/subject&gt;
+     *     &lt;body&gt;This message contains roster items.&lt;/body&gt;
+     *     &lt;x xmlns="jabber:x:roster"&gt;
+     *         &lt;item jid="gato1@gato.home"/&gt;
+     *         &lt;item jid="gato2@gato.home"/&gt;
+     *     &lt;/x&gt;
+     * &lt;/message&gt;
+     * </pre>
+     * 
+     */
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+            "\">");
+        // Loop through all roster entries and append them to the string buffer
+        for (Iterator<RemoteRosterEntry> i = getRosterEntries(); i.hasNext();) {
+            RemoteRosterEntry remoteRosterEntry = i.next();
+            buf.append(remoteRosterEntry.toXML());
+        }
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java b/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java
new file mode 100644
index 0000000..59bd98e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/SharedGroupsInfo.java
@@ -0,0 +1,92 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 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.smackx.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.util.ArrayList;

+import java.util.Iterator;

+import java.util.List;

+

+/**

+ * IQ packet used for discovering the user's shared groups and for getting the answer back

+ * from the server.<p>

+ *

+ * Important note: This functionality is not part of the XMPP spec and it will only work

+ * with Wildfire.

+ *

+ * @author Gaston Dombiak

+ */

+public class SharedGroupsInfo extends IQ {

+

+    private List<String> groups = new ArrayList<String>();

+

+    /**

+     * Returns a collection with the shared group names returned from the server.

+     *

+     * @return collection with the shared group names returned from the server.

+     */

+    public List<String> getGroups() {

+        return groups;

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<sharedgroup xmlns=\"http://www.jivesoftware.org/protocol/sharedgroup\">");

+        for (Iterator<String> it=groups.iterator(); it.hasNext();) {

+            buf.append("<group>").append(it.next()).append("</group>");

+        }

+        buf.append("</sharedgroup>");

+        return buf.toString();

+    }

+

+    /**

+     * Internal Search service Provider.

+     */

+    public static class Provider implements IQProvider {

+

+        /**

+         * Provider Constructor.

+         */

+        public Provider() {

+            super();

+        }

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            SharedGroupsInfo groupsInfo = new SharedGroupsInfo();

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if (eventType == XmlPullParser.START_TAG && parser.getName().equals("group")) {

+                    groupsInfo.getGroups().add(parser.nextText());

+                }

+                else if (eventType == XmlPullParser.END_TAG) {

+                    if (parser.getName().equals("sharedgroup")) {

+                        done = true;

+                    }

+                }

+            }

+            return groupsInfo;

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/packet/StreamInitiation.java b/src/org/jivesoftware/smackx/packet/StreamInitiation.java
new file mode 100644
index 0000000..511a02c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/StreamInitiation.java
@@ -0,0 +1,419 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.packet;

+

+import java.util.Date;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.util.StringUtils;

+

+/**

+ * The process by which two entities initiate a stream.

+ *

+ * @author Alexander Wenckus

+ */

+public class StreamInitiation extends IQ {

+

+    private String id;

+

+    private String mimeType;

+

+    private File file;

+

+    private Feature featureNegotiation;

+

+    /**

+     * The "id" attribute is an opaque identifier. This attribute MUST be

+     * present on type='set', and MUST be a valid string. This SHOULD NOT be

+     * sent back on type='result', since the <iq/> "id" attribute provides the

+     * only context needed. This value is generated by the Sender, and the same

+     * value MUST be used throughout a session when talking to the Receiver.

+     *

+     * @param id The "id" attribute.

+     */

+    public void setSesssionID(final String id) {

+        this.id = id;

+    }

+

+    /**

+     * Uniquely identifies a stream initiation to the recipient.

+     *

+     * @return The "id" attribute.

+     * @see #setSesssionID(String)

+     */

+    public String getSessionID() {

+        return id;

+    }

+

+    /**

+     * The "mime-type" attribute identifies the MIME-type for the data across

+     * the stream. This attribute MUST be a valid MIME-type as registered with

+     * the Internet Assigned Numbers Authority (IANA) [3] (specifically, as

+     * listed at <http://www.iana.org/assignments/media-types>). During

+     * negotiation, this attribute SHOULD be present, and is otherwise not

+     * required. If not included during negotiation, its value is assumed to be

+     * "binary/octect-stream".

+     *

+     * @param mimeType The valid mime-type.

+     */

+    public void setMimeType(final String mimeType) {

+        this.mimeType = mimeType;

+    }

+

+    /**

+     * Identifies the type of file that is desired to be transfered.

+     *

+     * @return The mime-type.

+     * @see #setMimeType(String)

+     */

+    public String getMimeType() {

+        return mimeType;

+    }

+

+    /**

+     * Sets the file which contains the information pertaining to the file to be

+     * transfered.

+     *

+     * @param file The file identified by the stream initiator to be sent.

+     */

+    public void setFile(final File file) {

+        this.file = file;

+    }

+

+    /**

+     * Returns the file containing the information about the request.

+     *

+     * @return Returns the file containing the information about the request.

+     */

+    public File getFile() {

+        return file;

+    }

+

+    /**

+     * Sets the data form which contains the valid methods of stream neotiation

+     * and transfer.

+     *

+     * @param form The dataform containing the methods.

+     */

+    public void setFeatureNegotiationForm(final DataForm form) {

+        this.featureNegotiation = new Feature(form);

+    }

+

+    /**

+     * Returns the data form which contains the valid methods of stream

+     * neotiation and transfer.

+     *

+     * @return Returns the data form which contains the valid methods of stream

+     *         neotiation and transfer.

+     */

+    public DataForm getFeatureNegotiationForm() {

+        return featureNegotiation.getData();

+    }

+

+    /*

+      * (non-Javadoc)

+      *

+      * @see org.jivesoftware.smack.packet.IQ#getChildElementXML()

+      */

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        if (this.getType().equals(IQ.Type.SET)) {

+            buf.append("<si xmlns=\"http://jabber.org/protocol/si\" ");

+            if (getSessionID() != null) {

+                buf.append("id=\"").append(getSessionID()).append("\" ");

+            }

+            if (getMimeType() != null) {

+                buf.append("mime-type=\"").append(getMimeType()).append("\" ");

+            }

+            buf

+                    .append("profile=\"http://jabber.org/protocol/si/profile/file-transfer\">");

+

+            // Add the file section if there is one.

+            String fileXML = file.toXML();

+            if (fileXML != null) {

+                buf.append(fileXML);

+            }

+        }

+        else if (this.getType().equals(IQ.Type.RESULT)) {

+            buf.append("<si xmlns=\"http://jabber.org/protocol/si\">");

+        }

+        else {

+            throw new IllegalArgumentException("IQ Type not understood");

+        }

+        if (featureNegotiation != null) {

+            buf.append(featureNegotiation.toXML());

+        }

+        buf.append("</si>");

+        return buf.toString();

+    }

+

+    /**

+     * <ul>

+     * <li>size: The size, in bytes, of the data to be sent.</li>

+     * <li>name: The name of the file that the Sender wishes to send.</li>

+     * <li>date: The last modification time of the file. This is specified

+     * using the DateTime profile as described in Jabber Date and Time Profiles.</li>

+     * <li>hash: The MD5 sum of the file contents.</li>

+     * </ul>

+     * <p/>

+     * <p/>

+     * &lt;desc&gt; is used to provide a sender-generated description of the

+     * file so the receiver can better understand what is being sent. It MUST

+     * NOT be sent in the result.

+     * <p/>

+     * <p/>

+     * When &lt;range&gt; is sent in the offer, it should have no attributes.

+     * This signifies that the sender can do ranged transfers. When a Stream

+     * Initiation result is sent with the <range> element, it uses these

+     * attributes:

+     * <p/>

+     * <ul>

+     * <li>offset: Specifies the position, in bytes, to start transferring the

+     * file data from. This defaults to zero (0) if not specified.</li>

+     * <li>length - Specifies the number of bytes to retrieve starting at

+     * offset. This defaults to the length of the file from offset to the end.</li>

+     * </ul>

+     * <p/>

+     * <p/>

+     * Both attributes are OPTIONAL on the &lt;range&gt; element. Sending no

+     * attributes is synonymous with not sending the &lt;range&gt; element. When

+     * no &lt;range&gt; element is sent in the Stream Initiation result, the

+     * Sender MUST send the complete file starting at offset 0. More generally,

+     * data is sent over the stream byte for byte starting at the offset

+     * position for the length specified.

+     *

+     * @author Alexander Wenckus

+     */

+    public static class File implements PacketExtension {

+

+        private final String name;

+

+        private final long size;

+

+        private String hash;

+

+        private Date date;

+

+        private String desc;

+

+        private boolean isRanged;

+

+        /**

+         * Constructor providing the name of the file and its size.

+         *

+         * @param name The name of the file.

+         * @param size The size of the file in bytes.

+         */

+        public File(final String name, final long size) {

+            if (name == null) {

+                throw new NullPointerException("name cannot be null");

+            }

+

+            this.name = name;

+            this.size = size;

+        }

+

+        /**

+         * Returns the file's name.

+         *

+         * @return Returns the file's name.

+         */

+        public String getName() {

+            return name;

+        }

+

+        /**

+         * Returns the file's size.

+         *

+         * @return Returns the file's size.

+         */

+        public long getSize() {

+            return size;

+        }

+

+        /**

+         * Sets the MD5 sum of the file's contents

+         *

+         * @param hash The MD5 sum of the file's contents.

+         */

+        public void setHash(final String hash) {

+            this.hash = hash;

+        }

+

+        /**

+         * Returns the MD5 sum of the file's contents

+         *

+         * @return Returns the MD5 sum of the file's contents

+         */

+        public String getHash() {

+            return hash;

+        }

+

+        /**

+         * Sets the date that the file was last modified.

+         *

+         * @param date The date that the file was last modified.

+         */

+        public void setDate(Date date) {

+            this.date = date;

+        }

+

+        /**

+         * Returns the date that the file was last modified.

+         *

+         * @return Returns the date that the file was last modified.

+         */

+        public Date getDate() {

+            return date;

+        }

+

+        /**

+         * Sets the description of the file.

+         *

+         * @param desc The description of the file so that the file reciever can

+         *             know what file it is.

+         */

+        public void setDesc(final String desc) {

+            this.desc = desc;

+        }

+

+        /**

+         * Returns the description of the file.

+         *

+         * @return Returns the description of the file.

+         */

+        public String getDesc() {

+            return desc;

+        }

+

+        /**

+         * True if a range can be provided and false if it cannot.

+         *

+         * @param isRanged True if a range can be provided and false if it cannot.

+         */

+        public void setRanged(final boolean isRanged) {

+            this.isRanged = isRanged;

+        }

+

+        /**

+         * Returns whether or not the initiator can support a range for the file

+         * tranfer.

+         *

+         * @return Returns whether or not the initiator can support a range for

+         *         the file tranfer.

+         */

+        public boolean isRanged() {

+            return isRanged;

+        }

+

+        public String getElementName() {

+            return "file";

+        }

+

+        public String getNamespace() {

+            return "http://jabber.org/protocol/si/profile/file-transfer";

+        }

+

+        public String toXML() {

+            StringBuilder buffer = new StringBuilder();

+

+            buffer.append("<").append(getElementName()).append(" xmlns=\"")

+                    .append(getNamespace()).append("\" ");

+

+            if (getName() != null) {

+                buffer.append("name=\"").append(StringUtils.escapeForXML(getName())).append("\" ");

+            }

+

+            if (getSize() > 0) {

+                buffer.append("size=\"").append(getSize()).append("\" ");

+            }

+

+            if (getDate() != null) {

+                buffer.append("date=\"").append(StringUtils.formatXEP0082Date(date)).append("\" ");

+            }
+
+            if (getHash() != null) {
+                buffer.append("hash=\"").append(getHash()).append("\" ");
+            }
+
+            if ((desc != null && desc.length() > 0) || isRanged) {
+                buffer.append(">");
+                if (getDesc() != null && desc.length() > 0) {
+                    buffer.append("<desc>").append(StringUtils.escapeForXML(getDesc())).append("</desc>");
+                }
+                if (isRanged()) {
+                    buffer.append("<range/>");
+                }
+                buffer.append("</").append(getElementName()).append(">");
+            }
+            else {
+                buffer.append("/>");
+            }
+            return buffer.toString();
+        }
+    }
+
+    /**
+     * The feature negotiation portion of the StreamInitiation packet.
+     *
+     * @author Alexander Wenckus
+     *
+     */
+    public class Feature implements PacketExtension {
+
+        private final DataForm data;
+
+        /**
+         * The dataform can be provided as part of the constructor.
+         *
+         * @param data The dataform.
+         */
+        public Feature(final DataForm data) {
+            this.data = data;
+        }
+
+        /**
+         * Returns the dataform associated with the feature negotiation.
+         *
+         * @return Returns the dataform associated with the feature negotiation.
+         */
+        public DataForm getData() {
+            return data;
+        }
+
+        public String getNamespace() {
+            return "http://jabber.org/protocol/feature-neg";
+        }
+
+        public String getElementName() {
+            return "feature";
+        }
+
+        public String toXML() {
+            StringBuilder buf = new StringBuilder();
+            buf
+                    .append("<feature xmlns=\"http://jabber.org/protocol/feature-neg\">");
+			buf.append(data.toXML());
+			buf.append("</feature>");
+			return buf.toString();
+		}
+	}
+}
diff --git a/src/org/jivesoftware/smackx/packet/Time.java b/src/org/jivesoftware/smackx/packet/Time.java
new file mode 100644
index 0000000..5294e77
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/Time.java
@@ -0,0 +1,198 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * A Time IQ packet, which is used by XMPP clients to exchange their respective local
+ * times. Clients that wish to fully support the entitity time protocol should register
+ * a PacketListener for incoming time requests that then respond with the local time.
+ * This class can be used to request the time from other clients, such as in the
+ * following code snippet:
+ *
+ * <pre>
+ * // Request the time from a remote user.
+ * Time timeRequest = new Time();
+ * timeRequest.setType(IQ.Type.GET);
+ * timeRequest.setTo(someUser@example.com/resource);
+ *
+ * // Create a packet collector to listen for a response.
+ * PacketCollector collector = con.createPacketCollector(
+ *                new PacketIDFilter(timeRequest.getPacketID()));
+ *
+ * con.sendPacket(timeRequest);
+ *
+ * // Wait up to 5 seconds for a result.
+ * IQ result = (IQ)collector.nextResult(5000);
+ * if (result != null && result.getType() == IQ.Type.RESULT) {
+ *     Time timeResult = (Time)result;
+ *     // Do something with result...
+ * }</pre><p>
+ *
+ * Warning: this is an non-standard protocol documented by
+ * <a href="http://www.xmpp.org/extensions/xep-0090.html">XEP-0090</a>. Because this is a
+ * non-standard protocol, it is subject to change.
+ *
+ * @author Matt Tucker
+ */
+public class Time extends IQ {
+
+    private static SimpleDateFormat utcFormat = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");
+    private static DateFormat displayFormat = DateFormat.getDateTimeInstance();
+
+    private String utc = null;
+    private String tz = null;
+    private String display = null;
+
+    /**
+     * Creates a new Time instance with empty values for all fields.
+     */
+    public Time() {
+        
+    }
+
+    /**
+     * Creates a new Time instance using the specified calendar instance as
+     * the time value to send.
+     *
+     * @param cal the time value.
+     */
+    public Time(Calendar cal) {
+        TimeZone timeZone = cal.getTimeZone();
+        tz = cal.getTimeZone().getID();
+        display = displayFormat.format(cal.getTime());
+        // Convert local time to the UTC time.
+        utc = utcFormat.format(new Date(
+                cal.getTimeInMillis() - timeZone.getOffset(cal.getTimeInMillis())));
+    }
+
+    /**
+     * Returns the local time or <tt>null</tt> if the time hasn't been set.
+     *
+     * @return the lcocal time.
+     */
+    public Date getTime() {
+        if (utc == null) {
+            return null;
+        }
+        Date date = null;
+        try {
+            Calendar cal = Calendar.getInstance();
+            // Convert the UTC time to local time.
+            cal.setTime(new Date(utcFormat.parse(utc).getTime() +
+                    cal.getTimeZone().getOffset(cal.getTimeInMillis())));
+            date = cal.getTime();
+        }
+        catch (Exception e) {
+            e.printStackTrace();
+        }
+        return date;
+    }
+
+    /**
+     * Sets the time using the local time.
+     *
+     * @param time the current local time.
+     */
+    public void setTime(Date time) {
+        // Convert local time to UTC time.
+        utc = utcFormat.format(new Date(
+                time.getTime() - TimeZone.getDefault().getOffset(time.getTime())));
+    }
+
+    /**
+     * Returns the time as a UTC formatted String using the format CCYYMMDDThh:mm:ss.
+     *
+     * @return the time as a UTC formatted String.
+     */
+    public String getUtc() {
+        return utc;
+    }
+
+    /**
+     * Sets the time using UTC formatted String in the format CCYYMMDDThh:mm:ss.
+     *
+     * @param utc the time using a formatted String.
+     */
+    public void setUtc(String utc) {
+        this.utc = utc;
+
+    }
+
+    /**
+     * Returns the time zone.
+     *
+     * @return the time zone.
+     */
+    public String getTz() {
+        return tz;
+    }
+
+    /**
+     * Sets the time zone.
+     *
+     * @param tz the time zone.
+     */
+    public void setTz(String tz) {
+        this.tz = tz;
+    }
+
+    /**
+     * Returns the local (non-utc) time in human-friendly format.
+     *
+     * @return the local time in human-friendly format.
+     */
+    public String getDisplay() {
+        return display;
+    }
+
+    /**
+     * Sets the local time in human-friendly format.
+     *
+     * @param display the local time in human-friendly format.
+     */
+    public void setDisplay(String display) {
+        this.display = display;
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"jabber:iq:time\">");
+        if (utc != null) {
+            buf.append("<utc>").append(utc).append("</utc>");
+        }
+        if (tz != null) {
+            buf.append("<tz>").append(tz).append("</tz>");
+        }
+        if (display != null) {
+            buf.append("<display>").append(display).append("</display>");
+        }
+        buf.append("</query>");
+        return buf.toString();
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/packet/VCard.java b/src/org/jivesoftware/smackx/packet/VCard.java
new file mode 100644
index 0000000..9766db8
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/VCard.java
@@ -0,0 +1,883 @@
+/**
+ * $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.smackx.packet;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.net.URL;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+
+/**
+ * A VCard class for use with the
+ * <a href="http://www.jivesoftware.org/smack/" target="_blank">SMACK jabber library</a>.<p>
+ * <p/>
+ * You should refer to the
+ * <a href="http://www.jabber.org/jeps/jep-0054.html" target="_blank">JEP-54 documentation</a>.<p>
+ * <p/>
+ * Please note that this class is incomplete but it does provide the most commonly found
+ * information in vCards. Also remember that VCard transfer is not a standard, and the protocol
+ * may change or be replaced.<p>
+ * <p/>
+ * <b>Usage:</b>
+ * <pre>
+ * <p/>
+ * // To save VCard:
+ * <p/>
+ * VCard vCard = new VCard();
+ * vCard.setFirstName("kir");
+ * vCard.setLastName("max");
+ * vCard.setEmailHome("foo@fee.bar");
+ * vCard.setJabberId("jabber@id.org");
+ * vCard.setOrganization("Jetbrains, s.r.o");
+ * vCard.setNickName("KIR");
+ * <p/>
+ * vCard.setField("TITLE", "Mr");
+ * vCard.setAddressFieldHome("STREET", "Some street");
+ * vCard.setAddressFieldWork("CTRY", "US");
+ * vCard.setPhoneWork("FAX", "3443233");
+ * <p/>
+ * vCard.save(connection);
+ * <p/>
+ * // To load VCard:
+ * <p/>
+ * VCard vCard = new VCard();
+ * vCard.load(conn); // load own VCard
+ * vCard.load(conn, "joe@foo.bar"); // load someone's VCard
+ * </pre>
+ *
+ * @author Kirill Maximov (kir@maxkir.com)
+ */
+public class VCard extends IQ {
+
+    /**
+     * Phone types:
+     * VOICE?, FAX?, PAGER?, MSG?, CELL?, VIDEO?, BBS?, MODEM?, ISDN?, PCS?, PREF?
+     */
+    private Map<String, String> homePhones = new HashMap<String, String>();
+    private Map<String, String> workPhones = new HashMap<String, String>();
+
+
+    /**
+     * Address types:
+     * POSTAL?, PARCEL?, (DOM | INTL)?, PREF?, POBOX?, EXTADR?, STREET?, LOCALITY?,
+     * REGION?, PCODE?, CTRY?
+     */
+    private Map<String, String> homeAddr = new HashMap<String, String>();
+    private Map<String, String> workAddr = new HashMap<String, String>();
+
+    private String firstName;
+    private String lastName;
+    private String middleName;
+
+    private String emailHome;
+    private String emailWork;
+
+    private String organization;
+    private String organizationUnit;
+
+    private String photoMimeType;
+    private String photoBinval;
+
+    /**
+     * Such as DESC ROLE GEO etc.. see JEP-0054
+     */
+    private Map<String, String> otherSimpleFields = new HashMap<String, String>();
+
+    // fields that, as they are should not be escaped before forwarding to the server
+    private Map<String, String> otherUnescapableFields = new HashMap<String, String>();
+
+    public VCard() {
+    }
+
+    /**
+     * Set generic VCard field.
+     *
+     * @param field value of field. Possible values: NICKNAME, PHOTO, BDAY, JABBERID, MAILER, TZ,
+     *              GEO, TITLE, ROLE, LOGO, NOTE, PRODID, REV, SORT-STRING, SOUND, UID, URL, DESC.
+     */
+    public String getField(String field) {
+        return otherSimpleFields.get(field);
+    }
+
+    /**
+     * Set generic VCard field.
+     *
+     * @param value value of field
+     * @param field field to set. See {@link #getField(String)}
+     * @see #getField(String)
+     */
+    public void setField(String field, String value) {
+        setField(field, value, false);
+    }
+
+    /**
+     * Set generic, unescapable VCard field. If unescabale is set to true, XML maybe a part of the
+     * value.
+     *
+     * @param value         value of field
+     * @param field         field to set. See {@link #getField(String)}
+     * @param isUnescapable True if the value should not be escaped, and false if it should.
+     */
+    public void setField(String field, String value, boolean isUnescapable) {
+        if (!isUnescapable) {
+            otherSimpleFields.put(field, value);
+        }
+        else {
+            otherUnescapableFields.put(field, value);
+        }
+    }
+
+    public String getFirstName() {
+        return firstName;
+    }
+
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+        // Update FN field
+        updateFN();
+    }
+
+    public String getLastName() {
+        return lastName;
+    }
+
+    public void setLastName(String lastName) {
+        this.lastName = lastName;
+        // Update FN field
+        updateFN();
+    }
+
+    public String getMiddleName() {
+        return middleName;
+    }
+
+    public void setMiddleName(String middleName) {
+        this.middleName = middleName;
+        // Update FN field
+        updateFN();
+    }
+
+    public String getNickName() {
+        return otherSimpleFields.get("NICKNAME");
+    }
+
+    public void setNickName(String nickName) {
+        otherSimpleFields.put("NICKNAME", nickName);
+    }
+
+    public String getEmailHome() {
+        return emailHome;
+    }
+
+    public void setEmailHome(String email) {
+        this.emailHome = email;
+    }
+
+    public String getEmailWork() {
+        return emailWork;
+    }
+
+    public void setEmailWork(String emailWork) {
+        this.emailWork = emailWork;
+    }
+
+    public String getJabberId() {
+        return otherSimpleFields.get("JABBERID");
+    }
+
+    public void setJabberId(String jabberId) {
+        otherSimpleFields.put("JABBERID", jabberId);
+    }
+
+    public String getOrganization() {
+        return organization;
+    }
+
+    public void setOrganization(String organization) {
+        this.organization = organization;
+    }
+
+    public String getOrganizationUnit() {
+        return organizationUnit;
+    }
+
+    public void setOrganizationUnit(String organizationUnit) {
+        this.organizationUnit = organizationUnit;
+    }
+
+    /**
+     * Get home address field
+     *
+     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
+     *                  LOCALITY, REGION, PCODE, CTRY
+     */
+    public String getAddressFieldHome(String addrField) {
+        return homeAddr.get(addrField);
+    }
+
+    /**
+     * Set home address field
+     *
+     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
+     *                  LOCALITY, REGION, PCODE, CTRY
+     */
+    public void setAddressFieldHome(String addrField, String value) {
+        homeAddr.put(addrField, value);
+    }
+
+    /**
+     * Get work address field
+     *
+     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
+     *                  LOCALITY, REGION, PCODE, CTRY
+     */
+    public String getAddressFieldWork(String addrField) {
+        return workAddr.get(addrField);
+    }
+
+    /**
+     * Set work address field
+     *
+     * @param addrField one of POSTAL, PARCEL, (DOM | INTL), PREF, POBOX, EXTADR, STREET,
+     *                  LOCALITY, REGION, PCODE, CTRY
+     */
+    public void setAddressFieldWork(String addrField, String value) {
+        workAddr.put(addrField, value);
+    }
+
+
+    /**
+     * Set home phone number
+     *
+     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
+     * @param phoneNum  phone number
+     */
+    public void setPhoneHome(String phoneType, String phoneNum) {
+        homePhones.put(phoneType, phoneNum);
+    }
+
+    /**
+     * Get home phone number
+     *
+     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
+     */
+    public String getPhoneHome(String phoneType) {
+        return homePhones.get(phoneType);
+    }
+
+    /**
+     * Set work phone number
+     *
+     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
+     * @param phoneNum  phone number
+     */
+    public void setPhoneWork(String phoneType, String phoneNum) {
+        workPhones.put(phoneType, phoneNum);
+    }
+
+    /**
+     * Get work phone number
+     *
+     * @param phoneType one of VOICE, FAX, PAGER, MSG, CELL, VIDEO, BBS, MODEM, ISDN, PCS, PREF
+     */
+    public String getPhoneWork(String phoneType) {
+        return workPhones.get(phoneType);
+    }
+
+    /**
+     * Set the avatar for the VCard by specifying the url to the image.
+     *
+     * @param avatarURL the url to the image(png,jpeg,gif,bmp)
+     */
+    public void setAvatar(URL avatarURL) {
+        byte[] bytes = new byte[0];
+        try {
+            bytes = getBytes(avatarURL);
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        setAvatar(bytes);
+    }
+
+    /**
+     * Removes the avatar from the vCard
+     *
+     *  This is done by setting the PHOTO value to the empty string as defined in XEP-0153
+     */
+    public void removeAvatar() {
+        // Remove avatar (if any)
+        photoBinval = null;
+        photoMimeType = null;
+    }
+
+    /**
+     * Specify the bytes of the JPEG for the avatar to use.
+     * If bytes is null, then the avatar will be removed.
+     * 'image/jpeg' will be used as MIME type.
+     *
+     * @param bytes the bytes of the avatar, or null to remove the avatar data
+     */
+    public void setAvatar(byte[] bytes) {
+        setAvatar(bytes, "image/jpeg");
+    }
+
+    /**
+     * Specify the bytes for the avatar to use as well as the mime type.
+     *
+     * @param bytes the bytes of the avatar.
+     * @param mimeType the mime type of the avatar.
+     */
+    public void setAvatar(byte[] bytes, String mimeType) {
+        // If bytes is null, remove the avatar
+        if (bytes == null) {
+            removeAvatar();
+            return;
+        }
+
+        // Otherwise, add to mappings.
+        String encodedImage = StringUtils.encodeBase64(bytes);
+
+        setAvatar(encodedImage, mimeType);
+    }
+
+    /**
+     * Specify the Avatar used for this vCard.
+     *
+     * @param encodedImage the Base64 encoded image as String
+     * @param mimeType the MIME type of the image
+     */
+    public void setAvatar(String encodedImage, String mimeType) {
+        photoBinval = encodedImage;
+        photoMimeType = mimeType;
+    }
+
+    /**
+     * Return the byte representation of the avatar(if one exists), otherwise returns null if
+     * no avatar could be found.
+     * <b>Example 1</b>
+     * <pre>
+     * // Load Avatar from VCard
+     * byte[] avatarBytes = vCard.getAvatar();
+     * <p/>
+     * // To create an ImageIcon for Swing applications
+     * ImageIcon icon = new ImageIcon(avatar);
+     * <p/>
+     * // To create just an image object from the bytes
+     * ByteArrayInputStream bais = new ByteArrayInputStream(avatar);
+     * try {
+     *   Image image = ImageIO.read(bais);
+     *  }
+     *  catch (IOException e) {
+     *    e.printStackTrace();
+     * }
+     * </pre>
+     *
+     * @return byte representation of avatar.
+     */
+    public byte[] getAvatar() {
+        if (photoBinval == null) {
+            return null;
+        }
+        return StringUtils.decodeBase64(photoBinval);
+    }
+
+    /**
+     * Returns the MIME Type of the avatar or null if none is set
+     *
+     * @return the MIME Type of the avatar or null
+     */
+    public String getAvatarMimeType() {
+        return photoMimeType;
+    }
+
+    /**
+     * Common code for getting the bytes of a url.
+     *
+     * @param url the url to read.
+     */
+    public static byte[] getBytes(URL url) throws IOException {
+        final String path = url.getPath();
+        final File file = new File(path);
+        if (file.exists()) {
+            return getFileBytes(file);
+        }
+
+        return null;
+    }
+
+    private static byte[] getFileBytes(File file) throws IOException {
+        BufferedInputStream bis = null;
+        try {
+            bis = new BufferedInputStream(new FileInputStream(file));
+            int bytes = (int) file.length();
+            byte[] buffer = new byte[bytes];
+            int readBytes = bis.read(buffer);
+            if (readBytes != buffer.length) {
+                throw new IOException("Entire file not read");
+            }
+            return buffer;
+        }
+        finally {
+            if (bis != null) {
+                bis.close();
+            }
+        }
+    }
+
+    /**
+     * Returns the SHA-1 Hash of the Avatar image.
+     *
+     * @return the SHA-1 Hash of the Avatar image.
+     */
+    public String getAvatarHash() {
+        byte[] bytes = getAvatar();
+        if (bytes == null) {
+            return null;
+        }
+
+        MessageDigest digest;
+        try {
+            digest = MessageDigest.getInstance("SHA-1");
+        }
+        catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+            return null;
+        }
+
+        digest.update(bytes);
+        return StringUtils.encodeHex(digest.digest());
+    }
+
+    private void updateFN() {
+        StringBuilder sb = new StringBuilder();
+        if (firstName != null) {
+            sb.append(StringUtils.escapeForXML(firstName)).append(' ');
+        }
+        if (middleName != null) {
+            sb.append(StringUtils.escapeForXML(middleName)).append(' ');
+        }
+        if (lastName != null) {
+            sb.append(StringUtils.escapeForXML(lastName));
+        }
+        setField("FN", sb.toString());
+    }
+
+    /**
+     * Save this vCard for the user connected by 'connection'. Connection should be authenticated
+     * and not anonymous.<p>
+     * <p/>
+     * NOTE: the method is asynchronous and does not wait for the returned value.
+     *
+     * @param connection the Connection to use.
+     * @throws XMPPException thrown if there was an issue setting the VCard in the server.
+     */
+    public void save(Connection connection) throws XMPPException {
+        checkAuthenticated(connection, true);
+
+        setType(IQ.Type.SET);
+        setFrom(connection.getUser());
+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(getPacketID()));
+        connection.sendPacket(this);
+
+        Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+        // Cancel the collector.
+        collector.cancel();
+        if (response == null) {
+            throw new XMPPException("No response from server on status set.");
+        }
+        if (response.getError() != null) {
+            throw new XMPPException(response.getError());
+        }
+    }
+
+    /**
+     * Load VCard information for a connected user. Connection should be authenticated
+     * and not anonymous.
+     */
+    public void load(Connection connection) throws XMPPException {
+        checkAuthenticated(connection, true);
+
+        setFrom(connection.getUser());
+        doLoad(connection, connection.getUser());
+    }
+
+    /**
+     * Load VCard information for a given user. Connection should be authenticated and not anonymous.
+     */
+    public void load(Connection connection, String user) throws XMPPException {
+        checkAuthenticated(connection, false);
+
+        setTo(user);
+        doLoad(connection, user);
+    }
+
+    private void doLoad(Connection connection, String user) throws XMPPException {
+        setType(Type.GET);
+        PacketCollector collector = connection.createPacketCollector(
+                new PacketIDFilter(getPacketID()));
+        connection.sendPacket(this);
+
+        VCard result = null;
+        try {
+            result = (VCard) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+            if (result == null) {
+                String errorMessage = "Timeout getting VCard information";
+                throw new XMPPException(errorMessage, new XMPPError(
+                        XMPPError.Condition.request_timeout, errorMessage));
+            }
+            if (result.getError() != null) {
+                throw new XMPPException(result.getError());
+            }
+        }
+        catch (ClassCastException e) {
+            System.out.println("No VCard for " + user);
+        }
+
+        copyFieldsFrom(result);
+    }
+
+    public String getChildElementXML() {
+        StringBuilder sb = new StringBuilder();
+        new VCardWriter(sb).write();
+        return sb.toString();
+    }
+
+    private void copyFieldsFrom(VCard from) {
+        Field[] fields = VCard.class.getDeclaredFields();
+        for (Field field : fields) {
+            if (field.getDeclaringClass() == VCard.class &&
+                    !Modifier.isFinal(field.getModifiers())) {
+                try {
+                    field.setAccessible(true);
+                    field.set(this, field.get(from));
+                }
+                catch (IllegalAccessException e) {
+                    throw new RuntimeException("This cannot happen:" + field, e);
+                }
+            }
+        }
+    }
+
+    private void checkAuthenticated(Connection connection, boolean checkForAnonymous) {
+        if (connection == null) {
+            throw new IllegalArgumentException("No connection was provided");
+        }
+        if (!connection.isAuthenticated()) {
+            throw new IllegalArgumentException("Connection is not authenticated");
+        }
+        if (checkForAnonymous && connection.isAnonymous()) {
+            throw new IllegalArgumentException("Connection cannot be anonymous");
+        }
+    }
+
+    private boolean hasContent() {
+        //noinspection OverlyComplexBooleanExpression
+        return hasNameField()
+                || hasOrganizationFields()
+                || emailHome != null
+                || emailWork != null
+                || otherSimpleFields.size() > 0
+                || otherUnescapableFields.size() > 0
+                || homeAddr.size() > 0
+                || homePhones.size() > 0
+                || workAddr.size() > 0
+                || workPhones.size() > 0
+                || photoBinval != null
+                ;
+    }
+
+    private boolean hasNameField() {
+        return firstName != null || lastName != null || middleName != null;
+    }
+
+    private boolean hasOrganizationFields() {
+        return organization != null || organizationUnit != null;
+    }
+
+    // Used in tests:
+
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        final VCard vCard = (VCard) o;
+
+        if (emailHome != null ? !emailHome.equals(vCard.emailHome) : vCard.emailHome != null) {
+            return false;
+        }
+        if (emailWork != null ? !emailWork.equals(vCard.emailWork) : vCard.emailWork != null) {
+            return false;
+        }
+        if (firstName != null ? !firstName.equals(vCard.firstName) : vCard.firstName != null) {
+            return false;
+        }
+        if (!homeAddr.equals(vCard.homeAddr)) {
+            return false;
+        }
+        if (!homePhones.equals(vCard.homePhones)) {
+            return false;
+        }
+        if (lastName != null ? !lastName.equals(vCard.lastName) : vCard.lastName != null) {
+            return false;
+        }
+        if (middleName != null ? !middleName.equals(vCard.middleName) : vCard.middleName != null) {
+            return false;
+        }
+        if (organization != null ?
+                !organization.equals(vCard.organization) : vCard.organization != null) {
+            return false;
+        }
+        if (organizationUnit != null ?
+                !organizationUnit.equals(vCard.organizationUnit) : vCard.organizationUnit != null) {
+            return false;
+        }
+        if (!otherSimpleFields.equals(vCard.otherSimpleFields)) {
+            return false;
+        }
+        if (!workAddr.equals(vCard.workAddr)) {
+            return false;
+        }
+        if (photoBinval != null ? !photoBinval.equals(vCard.photoBinval) : vCard.photoBinval != null) {
+            return false;
+        }
+
+        return workPhones.equals(vCard.workPhones);
+    }
+
+    public int hashCode() {
+        int result;
+        result = homePhones.hashCode();
+        result = 29 * result + workPhones.hashCode();
+        result = 29 * result + homeAddr.hashCode();
+        result = 29 * result + workAddr.hashCode();
+        result = 29 * result + (firstName != null ? firstName.hashCode() : 0);
+        result = 29 * result + (lastName != null ? lastName.hashCode() : 0);
+        result = 29 * result + (middleName != null ? middleName.hashCode() : 0);
+        result = 29 * result + (emailHome != null ? emailHome.hashCode() : 0);
+        result = 29 * result + (emailWork != null ? emailWork.hashCode() : 0);
+        result = 29 * result + (organization != null ? organization.hashCode() : 0);
+        result = 29 * result + (organizationUnit != null ? organizationUnit.hashCode() : 0);
+        result = 29 * result + otherSimpleFields.hashCode();
+        result = 29 * result + (photoBinval != null ? photoBinval.hashCode() : 0);
+        return result;
+    }
+
+    public String toString() {
+        return getChildElementXML();
+    }
+
+    //==============================================================
+
+    private class VCardWriter {
+
+        private final StringBuilder sb;
+
+        VCardWriter(StringBuilder sb) {
+            this.sb = sb;
+        }
+
+        public void write() {
+            appendTag("vCard", "xmlns", "vcard-temp", hasContent(), new ContentBuilder() {
+                public void addTagContent() {
+                    buildActualContent();
+                }
+            });
+        }
+
+        private void buildActualContent() {
+            if (hasNameField()) {
+                appendN();
+            }
+
+            appendOrganization();
+            appendGenericFields();
+            appendPhoto();
+
+            appendEmail(emailWork, "WORK");
+            appendEmail(emailHome, "HOME");
+
+            appendPhones(workPhones, "WORK");
+            appendPhones(homePhones, "HOME");
+
+            appendAddress(workAddr, "WORK");
+            appendAddress(homeAddr, "HOME");
+        }
+
+        private void appendPhoto() {
+            if (photoBinval == null)
+                return;
+
+            appendTag("PHOTO", true, new ContentBuilder() {
+                public void addTagContent() {
+                    appendTag("BINVAL", photoBinval); // No need to escape photoBinval, as it's already Base64 encoded
+                    appendTag("TYPE", StringUtils.escapeForXML(photoMimeType));
+                }
+            });
+        }
+        private void appendEmail(final String email, final String type) {
+            if (email != null) {
+                appendTag("EMAIL", true, new ContentBuilder() {
+                    public void addTagContent() {
+                        appendEmptyTag(type);
+                        appendEmptyTag("INTERNET");
+                        appendEmptyTag("PREF");
+                        appendTag("USERID", StringUtils.escapeForXML(email));
+                    }
+                });
+            }
+        }
+
+        private void appendPhones(Map<String, String> phones, final String code) {
+            Iterator<Map.Entry<String, String>> it = phones.entrySet().iterator();
+            while (it.hasNext()) {
+                final Map.Entry<String,String> entry = it.next();
+                appendTag("TEL", true, new ContentBuilder() {
+                    public void addTagContent() {
+                        appendEmptyTag(entry.getKey());
+                        appendEmptyTag(code);
+                        appendTag("NUMBER", StringUtils.escapeForXML(entry.getValue()));
+                    }
+                });
+            }
+        }
+
+        private void appendAddress(final Map<String, String> addr, final String code) {
+            if (addr.size() > 0) {
+                appendTag("ADR", true, new ContentBuilder() {
+                    public void addTagContent() {
+                        appendEmptyTag(code);
+
+                        Iterator<Map.Entry<String, String>> it = addr.entrySet().iterator();
+                        while (it.hasNext()) {
+                            final Entry<String, String> entry = it.next();
+                            appendTag(entry.getKey(), StringUtils.escapeForXML(entry.getValue()));
+                        }
+                    }
+                });
+            }
+        }
+
+        private void appendEmptyTag(Object tag) {
+            sb.append('<').append(tag).append("/>");
+        }
+
+        private void appendGenericFields() {
+            Iterator<Map.Entry<String, String>> it = otherSimpleFields.entrySet().iterator();
+            while (it.hasNext()) {
+                Map.Entry<String, String> entry = it.next();
+                appendTag(entry.getKey().toString(),
+                        StringUtils.escapeForXML(entry.getValue()));
+            }
+
+            it = otherUnescapableFields.entrySet().iterator();
+            while (it.hasNext()) {
+                Map.Entry<String, String> entry = it.next();
+                appendTag(entry.getKey().toString(),entry.getValue());
+            }
+        }
+
+        private void appendOrganization() {
+            if (hasOrganizationFields()) {
+                appendTag("ORG", true, new ContentBuilder() {
+                    public void addTagContent() {
+                        appendTag("ORGNAME", StringUtils.escapeForXML(organization));
+                        appendTag("ORGUNIT", StringUtils.escapeForXML(organizationUnit));
+                    }
+                });
+            }
+        }
+
+        private void appendN() {
+            appendTag("N", true, new ContentBuilder() {
+                public void addTagContent() {
+                    appendTag("FAMILY", StringUtils.escapeForXML(lastName));
+                    appendTag("GIVEN", StringUtils.escapeForXML(firstName));
+                    appendTag("MIDDLE", StringUtils.escapeForXML(middleName));
+                }
+            });
+        }
+
+        private void appendTag(String tag, String attr, String attrValue, boolean hasContent,
+                ContentBuilder builder) {
+            sb.append('<').append(tag);
+            if (attr != null) {
+                sb.append(' ').append(attr).append('=').append('\'').append(attrValue).append('\'');
+            }
+
+            if (hasContent) {
+                sb.append('>');
+                builder.addTagContent();
+                sb.append("</").append(tag).append(">\n");
+            }
+            else {
+                sb.append("/>\n");
+            }
+        }
+
+        private void appendTag(String tag, boolean hasContent, ContentBuilder builder) {
+            appendTag(tag, null, null, hasContent, builder);
+        }
+
+        private void appendTag(String tag, final String tagText) {
+            if (tagText == null) return;
+            final ContentBuilder contentBuilder = new ContentBuilder() {
+                public void addTagContent() {
+                    sb.append(tagText.trim());
+                }
+            };
+            appendTag(tag, true, contentBuilder);
+        }
+
+    }
+
+    //==============================================================
+
+    private interface ContentBuilder {
+
+        void addTagContent();
+    }
+
+    //==============================================================
+}
+
diff --git a/src/org/jivesoftware/smackx/packet/Version.java b/src/org/jivesoftware/smackx/packet/Version.java
new file mode 100644
index 0000000..41ee419
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/Version.java
@@ -0,0 +1,132 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+
+/**
+ * A Version IQ packet, which is used by XMPP clients to discover version information
+ * about the software running at another entity's JID.<p>
+ *
+ * An example to discover the version of the server:
+ * <pre>
+ * // Request the version from the server.
+ * Version versionRequest = new Version();
+ * timeRequest.setType(IQ.Type.GET);
+ * timeRequest.setTo("example.com");
+ *
+ * // Create a packet collector to listen for a response.
+ * PacketCollector collector = con.createPacketCollector(
+ *                new PacketIDFilter(versionRequest.getPacketID()));
+ *
+ * con.sendPacket(versionRequest);
+ *
+ * // Wait up to 5 seconds for a result.
+ * IQ result = (IQ)collector.nextResult(5000);
+ * if (result != null && result.getType() == IQ.Type.RESULT) {
+ *     Version versionResult = (Version)result;
+ *     // Do something with result...
+ * }</pre><p>
+ *
+ * @author Gaston Dombiak
+ */
+public class Version extends IQ {
+
+    private String name;
+    private String version;
+    private String os;
+
+    /**
+     * Returns the natural-language name of the software. This property will always be
+     * present in a result.
+     *
+     * @return the natural-language name of the software.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Sets the natural-language name of the software. This message should only be
+     * invoked when parsing the XML and setting the property to a Version instance.
+     *
+     * @param name the natural-language name of the software.
+     */
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Returns the specific version of the software. This property will always be
+     * present in a result.
+     *
+     * @return the specific version of the software.
+     */
+    public String getVersion() {
+        return version;
+    }
+
+    /**
+     * Sets the specific version of the software. This message should only be
+     * invoked when parsing the XML and setting the property to a Version instance.
+     *
+     * @param version the specific version of the software.
+     */
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    /**
+     * Returns the operating system of the queried entity. This property will always be
+     * present in a result.
+     *
+     * @return the operating system of the queried entity.
+     */
+    public String getOs() {
+        return os;
+    }
+
+    /**
+     * Sets the operating system of the queried entity. This message should only be
+     * invoked when parsing the XML and setting the property to a Version instance.
+     *
+     * @param os operating system of the queried entity.
+     */
+    public void setOs(String os) {
+        this.os = os;
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"jabber:iq:version\">");
+        if (name != null) {
+            buf.append("<name>").append(name).append("</name>");
+        }
+        if (version != null) {
+            buf.append("<version>").append(version).append("</version>");
+        }
+        if (os != null) {
+            buf.append("<os>").append(os).append("</os>");
+        }
+        buf.append("</query>");
+        return buf.toString();
+    }
+}
diff --git a/src/org/jivesoftware/smackx/packet/XHTMLExtension.java b/src/org/jivesoftware/smackx/packet/XHTMLExtension.java
new file mode 100644
index 0000000..ba5e676
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/XHTMLExtension.java
@@ -0,0 +1,126 @@
+/**
+ * $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.smackx.packet;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * An XHTML sub-packet, which is used by XMPP clients to exchange formatted text. The XHTML 
+ * extension is only a subset of XHTML 1.0.<p>
+ * 
+ * The following link summarizes the requirements of XHTML IM:
+ * <a href="http://www.jabber.org/jeps/jep-0071.html#sect-id2598018">Valid tags</a>.<p>
+ * 
+ * Warning: this is an non-standard protocol documented by
+ * <a href="http://www.jabber.org/jeps/jep-0071.html">JEP-71</a>. Because this is a
+ * non-standard protocol, it is subject to change.
+ *
+ * @author Gaston Dombiak
+ */
+public class XHTMLExtension implements PacketExtension {
+
+    private List<String> bodies = new ArrayList<String>();
+
+    /**
+    * Returns the XML element name of the extension sub-packet root element.
+    * Always returns "html"
+    *
+    * @return the XML element name of the packet extension.
+    */
+    public String getElementName() {
+        return "html";
+    }
+
+    /** 
+     * Returns the XML namespace of the extension sub-packet root element.
+     * According the specification the namespace is always "http://jabber.org/protocol/xhtml-im"
+     *
+     * @return the XML namespace of the packet extension.
+     */
+    public String getNamespace() {
+        return "http://jabber.org/protocol/xhtml-im";
+    }
+
+    /**
+     * Returns the XML representation of a XHTML extension according the specification.
+     * 
+     * Usually the XML representation will be inside of a Message XML representation like
+     * in the following example:
+     * <pre>
+     * &lt;message id="MlIpV-4" to="gato1@gato.home" from="gato3@gato.home/Smack"&gt;
+     *     &lt;subject&gt;Any subject you want&lt;/subject&gt;
+     *     &lt;body&gt;This message contains something interesting.&lt;/body&gt;
+     *     &lt;html xmlns="http://jabber.org/protocol/xhtml-im"&gt;
+     *         &lt;body&gt;&lt;p style='font-size:large'&gt;This message contains something &lt;em&gt;interesting&lt;/em&gt;.&lt;/p&gt;&lt;/body&gt;
+     *     &lt;/html&gt;
+     * &lt;/message&gt;
+     * </pre>
+     * 
+     */
+    public String toXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append(
+            "\">");
+        // Loop through all the bodies and append them to the string buffer
+        for (Iterator<String> i = getBodies(); i.hasNext();) {
+            buf.append(i.next());
+        }
+        buf.append("</").append(getElementName()).append(">");
+        return buf.toString();
+    }
+
+    /**
+     * Returns an Iterator for the bodies in the packet.
+     *
+     * @return an Iterator for the bodies in the packet.
+     */
+    public Iterator<String> getBodies() {
+        synchronized (bodies) {
+            return Collections.unmodifiableList(new ArrayList<String>(bodies)).iterator();
+        }
+    }
+
+    /**
+     * Adds a body to the packet.
+     *
+     * @param body the body to add.
+     */
+    public void addBody(String body) {
+        synchronized (bodies) {
+            bodies.add(body);
+        }
+    }
+
+    /**
+     * Returns a count of the bodies in the XHTML packet.
+     *
+     * @return the number of bodies in the XHTML packet.
+     */
+    public int getBodiesCount() {
+        return bodies.size();
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/packet/package.html b/src/org/jivesoftware/smackx/packet/package.html
new file mode 100644
index 0000000..490d1d7
--- /dev/null
+++ b/src/org/jivesoftware/smackx/packet/package.html
@@ -0,0 +1 @@
+<body>XML packets that are part of the XMPP extension protocols.</body>
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/ping/PingFailedListener.java b/src/org/jivesoftware/smackx/ping/PingFailedListener.java
new file mode 100644
index 0000000..4cda33b
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ping/PingFailedListener.java
@@ -0,0 +1,21 @@
+/**
+ * Copyright 2012 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.smackx.ping;
+
+public interface PingFailedListener {
+    void pingFailed();
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/ping/PingManager.java b/src/org/jivesoftware/smackx/ping/PingManager.java
new file mode 100644
index 0000000..6b4b48c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ping/PingManager.java
@@ -0,0 +1,343 @@
+/**
+ * Copyright 2012-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.smackx.ping;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.ping.packet.Ping;
+import org.jivesoftware.smackx.ping.packet.Pong;
+
+/**
+ * Implements the XMPP Ping as defined by XEP-0199. This protocol offers an
+ * alternative to the traditional 'white space ping' approach of determining the
+ * availability of an entity. The XMPP Ping protocol allows ping messages to be
+ * send in a more XML-friendly approach, which can be used over more than one
+ * hop in the communication path.
+ * 
+ * @author Florian Schmaus
+ * @see <a href="http://www.xmpp.org/extensions/xep-0199.html">XEP-0199:XMPP
+ *      Ping</a>
+ */
+public class PingManager {
+    
+    public static final String NAMESPACE = "urn:xmpp:ping";
+    public static final String ELEMENT = "ping";
+    
+
+    private static Map<Connection, PingManager> instances =
+            Collections.synchronizedMap(new WeakHashMap<Connection, PingManager>());
+    
+    static {
+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+            public void connectionCreated(Connection connection) {
+                new PingManager(connection);
+            }
+        });
+    }
+    
+    private ScheduledExecutorService periodicPingExecutorService;
+    private Connection connection;
+    private int pingInterval = SmackConfiguration.getDefaultPingInterval();
+    private Set<PingFailedListener> pingFailedListeners = Collections
+            .synchronizedSet(new HashSet<PingFailedListener>());
+    private ScheduledFuture<?> periodicPingTask;
+    protected volatile long lastSuccessfulPingByTask = -1;
+
+    
+    // Ping Flood protection
+    private long pingMinDelta = 100;
+    private long lastPingStamp = 0; // timestamp of the last received ping
+    
+    // Timestamp of the last pong received, either from the server or another entity
+    // Note, no need to synchronize this value, it will only increase over time
+    private long lastSuccessfulManualPing = -1;
+    
+    private PingManager(Connection connection) {
+        ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
+        sdm.addFeature(NAMESPACE);
+        this.connection = connection;
+        init();
+    }
+    
+    private void init() {
+        periodicPingExecutorService = new ScheduledThreadPoolExecutor(1);
+        PacketFilter pingPacketFilter = new PacketTypeFilter(Ping.class);
+        connection.addPacketListener(new PacketListener() {
+            /**
+             * Sends a Pong for every Ping
+             */
+            public void processPacket(Packet packet) {
+                if (pingMinDelta > 0) {
+                    // Ping flood protection enabled
+                    long currentMillies = System.currentTimeMillis();
+                    long delta = currentMillies - lastPingStamp;
+                    lastPingStamp = currentMillies;
+                    if (delta < pingMinDelta) {
+                        return;
+                    }
+                }
+                Pong pong = new Pong((Ping)packet);
+                connection.sendPacket(pong);
+            }
+        }
+        , pingPacketFilter);
+        connection.addConnectionListener(new ConnectionListener() {
+
+            @Override
+            public void connectionClosed() {
+                maybeStopPingServerTask();
+            }
+
+            @Override
+            public void connectionClosedOnError(Exception arg0) {
+                maybeStopPingServerTask();
+            }
+
+            @Override
+            public void reconnectionSuccessful() {
+                maybeSchedulePingServerTask();
+            }
+
+            @Override
+            public void reconnectingIn(int seconds) {
+            }
+
+            @Override
+            public void reconnectionFailed(Exception e) {
+            }
+        });
+        instances.put(connection, this);
+        maybeSchedulePingServerTask();
+    }
+
+    public static PingManager getInstanceFor(Connection connection) {
+        PingManager pingManager = instances.get(connection);
+        
+        if (pingManager == null) {
+            pingManager = new PingManager(connection);
+        }
+
+        return pingManager;
+    }
+
+    public void setPingIntervall(int pingIntervall) {
+        this.pingInterval = pingIntervall;
+    }
+
+    public int getPingIntervall() {
+        return pingInterval;
+    }
+    
+    public void registerPingFailedListener(PingFailedListener listener) {
+        pingFailedListeners.add(listener);
+    }
+    
+    public void unregisterPingFailedListener(PingFailedListener listener) {
+        pingFailedListeners.remove(listener);
+    }
+    
+    public void disablePingFloodProtection() {
+        setPingMinimumInterval(-1);
+    }
+    
+    public void setPingMinimumInterval(long ms) {
+        this.pingMinDelta = ms;
+    }
+    
+    public long getPingMinimumInterval() {
+        return this.pingMinDelta;
+    }
+    
+    /**
+     * Pings the given jid and returns the IQ response which is either of 
+     * IQ.Type.ERROR or IQ.Type.RESULT. If we are not connected or if there was
+     * no reply, null is returned.
+     * 
+     * You should use isPingSupported(jid) to determine if XMPP Ping is 
+     * supported by the user.
+     * 
+     * @param jid
+     * @param pingTimeout
+     * @return
+     */
+    public IQ ping(String jid, long pingTimeout) {
+        // Make sure we actually connected to the server
+        if (!connection.isAuthenticated())
+            return null;
+        
+        Ping ping = new Ping(connection.getUser(), jid);
+        
+        PacketCollector collector =
+                connection.createPacketCollector(new PacketIDFilter(ping.getPacketID()));
+        
+        connection.sendPacket(ping);
+        
+        IQ result = (IQ) collector.nextResult(pingTimeout);
+        
+        collector.cancel();
+        return result;
+    }
+    
+    /**
+     * Pings the given jid and returns the IQ response with the default
+     * packet reply timeout
+     * 
+     * @param jid
+     * @return
+     */
+    public IQ ping(String jid) {
+        return ping(jid, SmackConfiguration.getPacketReplyTimeout());
+    }
+    
+    /**
+     * Pings the given Entity.
+     * 
+     * Note that XEP-199 shows that if we receive a error response
+     * service-unavailable there is no way to determine if the response was send
+     * by the entity (e.g. a user JID) or from a server in between. This is
+     * intended behavior to avoid presence leaks.
+     * 
+     * Always use isPingSupported(jid) to determine if XMPP Ping is supported
+     * by the entity.
+     * 
+     * @param jid
+     * @return True if a pong was received, otherwise false
+     */
+    public boolean pingEntity(String jid, long pingTimeout) {
+        IQ result = ping(jid, pingTimeout);
+
+        if (result == null || result.getType() == IQ.Type.ERROR) {
+            return false;
+        }
+        pongReceived();
+        return true;
+    }
+    
+    public boolean pingEntity(String jid) {
+        return pingEntity(jid, SmackConfiguration.getPacketReplyTimeout());
+    }
+    
+    /**
+     * Pings the user's server. Will notify the registered 
+     * pingFailedListeners in case of error.
+     * 
+     * If we receive as response, we can be sure that it came from the server.
+     * 
+     * @return true if successful, otherwise false
+     */
+    public boolean pingMyServer(long pingTimeout) {
+        IQ result = ping(connection.getServiceName(), pingTimeout);
+
+        if (result == null) {
+            for (PingFailedListener l : pingFailedListeners) {
+                l.pingFailed();
+            }
+            return false;
+        }
+        // Maybe not really a pong, but an answer is an answer
+        pongReceived();
+        return true;
+    }
+    
+    /**
+     * Pings the user's server with the PacketReplyTimeout as defined
+     * in SmackConfiguration.
+     * 
+     * @return true if successful, otherwise false
+     */
+    public boolean pingMyServer() {
+        return pingMyServer(SmackConfiguration.getPacketReplyTimeout());
+    }
+    
+    /**
+     * Returns true if XMPP Ping is supported by a given JID
+     * 
+     * @param jid
+     * @return
+     */
+    public boolean isPingSupported(String jid) {
+        try {
+            DiscoverInfo result =
+                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid);
+            return result.containsFeature(NAMESPACE);
+        }
+        catch (XMPPException e) {
+            return false;
+        }
+    }
+    
+    /**
+     * Returns the time of the last successful Ping Pong with the 
+     * users server. If there was no successful Ping (e.g. because this
+     * feature is disabled) -1 will be returned.
+     *  
+     * @return
+     */
+    public long getLastSuccessfulPing() {
+        return Math.max(lastSuccessfulPingByTask, lastSuccessfulManualPing);
+    }
+    
+    protected Set<PingFailedListener> getPingFailedListeners() {
+        return pingFailedListeners;
+    }
+
+    /**
+     * Cancels any existing periodic ping task if there is one and schedules a new ping task if pingInterval is greater
+     * then zero.
+     * 
+     */
+    protected synchronized void maybeSchedulePingServerTask() {
+        maybeStopPingServerTask();
+        if (pingInterval > 0) {
+            periodicPingTask = periodicPingExecutorService.schedule(new ServerPingTask(connection), pingInterval,
+                    TimeUnit.SECONDS);
+        }
+    }
+
+    private void maybeStopPingServerTask() {
+        if (periodicPingTask != null) {
+            periodicPingTask.cancel(true);
+            periodicPingTask = null;
+        }
+    }
+
+    private void pongReceived() {
+        lastSuccessfulManualPing = System.currentTimeMillis();
+    }
+}
diff --git a/src/org/jivesoftware/smackx/ping/ServerPingTask.java b/src/org/jivesoftware/smackx/ping/ServerPingTask.java
new file mode 100644
index 0000000..0901b8f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ping/ServerPingTask.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright 2012-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.smackx.ping;
+
+import java.lang.ref.WeakReference;
+import java.util.Set;
+
+import org.jivesoftware.smack.Connection;
+
+class ServerPingTask implements Runnable {
+
+    // This has to be a weak reference because IIRC all threads are roots
+    // for objects and we have a new thread here that should hold a strong
+    // reference to connection so that it can be GCed.
+    private WeakReference<Connection> weakConnection;
+
+    private int delta = 1000; // 1 seconds
+    private int tries = 3; // 3 tries
+
+    protected ServerPingTask(Connection connection) {
+        this.weakConnection = new WeakReference<Connection>(connection);
+    }
+
+    public void run() {
+        Connection connection = weakConnection.get();
+        if (connection == null) {
+            // connection has been collected by GC
+            // which means we can stop the thread by breaking the loop
+            return;
+        }
+        if (connection.isAuthenticated()) {
+            PingManager pingManager = PingManager.getInstanceFor(connection);
+            boolean res = false;
+
+            for (int i = 0; i < tries; i++) {
+                if (i != 0) {
+                    try {
+                        Thread.sleep(delta);
+                    } catch (InterruptedException e) {
+                        // We received an interrupt
+                        // This only happens if we should stop pinging
+                        return;
+                    }
+                }
+                res = pingManager.pingMyServer();
+                // stop when we receive a pong back
+                if (res) {
+                    pingManager.lastSuccessfulPingByTask = System.currentTimeMillis();
+                    break;
+                }
+            }
+            if (!res) {
+                Set<PingFailedListener> pingFailedListeners = pingManager.getPingFailedListeners();
+                for (PingFailedListener l : pingFailedListeners) {
+                    l.pingFailed();
+                }
+            } else {
+                // Ping was successful, wind-up the periodic task again
+                pingManager.maybeSchedulePingServerTask();
+            }
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/ping/packet/Ping.java b/src/org/jivesoftware/smackx/ping/packet/Ping.java
new file mode 100644
index 0000000..fc5bbdf
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ping/packet/Ping.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright 2012 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.smackx.ping.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.ping.PingManager;
+
+public class Ping extends IQ {
+    
+    public Ping() {
+    }
+    
+    public Ping(String from, String to) {
+        setTo(to);
+        setFrom(from);
+        setType(IQ.Type.GET);
+        setPacketID(getPacketID());
+    }
+    
+    public String getChildElementXML() {
+        return "<" + PingManager.ELEMENT + " xmlns=\'" + PingManager.NAMESPACE + "\' />";
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/ping/packet/Pong.java b/src/org/jivesoftware/smackx/ping/packet/Pong.java
new file mode 100644
index 0000000..9300db0
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ping/packet/Pong.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright 2012 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.smackx.ping.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+
+public class Pong extends IQ {
+    
+    /**
+     * Composes a Pong packet from a received ping packet. This basically swaps
+     * the 'from' and 'to' attributes. And sets the IQ type to result.
+     * 
+     * @param ping
+     */
+    public Pong(Ping ping) {
+        setType(IQ.Type.RESULT);
+        setFrom(ping.getTo());
+        setTo(ping.getFrom());
+        setPacketID(ping.getPacketID());
+    }
+    
+    /*
+     * Returns the child element of the Pong reply, which is non-existent. This
+     * is why we return 'null' here. See e.g. Example 11 from
+     * http://xmpp.org/extensions/xep-0199.html#e2e
+     */
+    public String getChildElementXML() {
+        return null;
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/ping/provider/PingProvider.java b/src/org/jivesoftware/smackx/ping/provider/PingProvider.java
new file mode 100644
index 0000000..ebe7669
--- /dev/null
+++ b/src/org/jivesoftware/smackx/ping/provider/PingProvider.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright 2012 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.smackx.ping.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.ping.packet.Ping;
+import org.xmlpull.v1.XmlPullParser;
+
+public class PingProvider implements IQProvider {
+    
+    public IQ parseIQ(XmlPullParser parser) throws Exception {
+        // No need to use the ping constructor with arguments. IQ will already
+        // have filled out all relevant fields ('from', 'to', 'id').
+        return new Ping();
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/provider/AdHocCommandDataProvider.java b/src/org/jivesoftware/smackx/provider/AdHocCommandDataProvider.java
new file mode 100755
index 0000000..63d24ec
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/AdHocCommandDataProvider.java
@@ -0,0 +1,155 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2005-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.smackx.provider;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.packet.XMPPError;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.jivesoftware.smack.util.PacketParserUtils;

+import org.jivesoftware.smackx.commands.AdHocCommand;

+import org.jivesoftware.smackx.commands.AdHocCommand.Action;

+import org.jivesoftware.smackx.commands.AdHocCommandNote;

+import org.jivesoftware.smackx.packet.AdHocCommandData;

+import org.jivesoftware.smackx.packet.DataForm;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * The AdHocCommandDataProvider parses AdHocCommandData packets.

+ * 

+ * @author Gabriel Guardincerri

+ */

+public class AdHocCommandDataProvider implements IQProvider {

+

+    public IQ parseIQ(XmlPullParser parser) throws Exception {

+        boolean done = false;

+        AdHocCommandData adHocCommandData = new AdHocCommandData();

+        DataFormProvider dataFormProvider = new DataFormProvider();

+

+        int eventType;

+        String elementName;

+        String namespace;

+        adHocCommandData.setSessionID(parser.getAttributeValue("", "sessionid"));

+        adHocCommandData.setNode(parser.getAttributeValue("", "node"));

+

+        // Status

+        String status = parser.getAttributeValue("", "status");

+        if (AdHocCommand.Status.executing.toString().equalsIgnoreCase(status)) {

+            adHocCommandData.setStatus(AdHocCommand.Status.executing);

+        }

+        else if (AdHocCommand.Status.completed.toString().equalsIgnoreCase(status)) {

+            adHocCommandData.setStatus(AdHocCommand.Status.completed);

+        }

+        else if (AdHocCommand.Status.canceled.toString().equalsIgnoreCase(status)) {

+            adHocCommandData.setStatus(AdHocCommand.Status.canceled);

+        }

+

+        // Action

+        String action = parser.getAttributeValue("", "action");

+        if (action != null) {

+            Action realAction = AdHocCommand.Action.valueOf(action);

+            if (realAction == null || realAction.equals(Action.unknown)) {

+                adHocCommandData.setAction(Action.unknown);

+            }

+            else {

+                adHocCommandData.setAction(realAction);

+            }

+        }

+        while (!done) {

+            eventType = parser.next();

+            elementName = parser.getName();

+            namespace = parser.getNamespace();

+            if (eventType == XmlPullParser.START_TAG) {

+                if (parser.getName().equals("actions")) {

+                    String execute = parser.getAttributeValue("", "execute");

+                    if (execute != null) {

+                        adHocCommandData.setExecuteAction(AdHocCommand.Action.valueOf(execute));

+                    }

+                }

+                else if (parser.getName().equals("next")) {

+                    adHocCommandData.addAction(AdHocCommand.Action.next);

+                }

+                else if (parser.getName().equals("complete")) {

+                    adHocCommandData.addAction(AdHocCommand.Action.complete);

+                }

+                else if (parser.getName().equals("prev")) {

+                    adHocCommandData.addAction(AdHocCommand.Action.prev);

+                }

+                else if (elementName.equals("x") && namespace.equals("jabber:x:data")) {

+                    adHocCommandData.setForm((DataForm) dataFormProvider.parseExtension(parser));

+                }

+                else if (parser.getName().equals("note")) {

+                    AdHocCommandNote.Type type = AdHocCommandNote.Type.valueOf(

+                            parser.getAttributeValue("", "type"));

+                    String value = parser.nextText();

+                    adHocCommandData.addNote(new AdHocCommandNote(type, value));

+                }

+                else if (parser.getName().equals("error")) {

+                    XMPPError error = PacketParserUtils.parseError(parser);

+                    adHocCommandData.setError(error);

+                }

+            }

+            else if (eventType == XmlPullParser.END_TAG) {

+                if (parser.getName().equals("command")) {

+                    done = true;

+                }

+            }

+        }

+        return adHocCommandData;

+    }

+

+    public static class BadActionError implements PacketExtensionProvider {

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badAction);

+        }

+    }

+

+    public static class MalformedActionError implements PacketExtensionProvider {

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.malformedAction);

+        }

+    }

+

+    public static class BadLocaleError implements PacketExtensionProvider {

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badLocale);

+        }

+    }

+

+    public static class BadPayloadError implements PacketExtensionProvider {

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badPayload);

+        }

+    }

+

+    public static class BadSessionIDError implements PacketExtensionProvider {

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.badSessionid);

+        }

+    }

+

+    public static class SessionExpiredError implements PacketExtensionProvider {

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            return new AdHocCommandData.SpecificError(AdHocCommand.SpecificErrorCondition.sessionExpired);

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/provider/CapsExtensionProvider.java b/src/org/jivesoftware/smackx/provider/CapsExtensionProvider.java
new file mode 100644
index 0000000..5a7cd2f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/CapsExtensionProvider.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2009 Jonas Ådahl.
+ * Copyright 2011-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.smackx.provider;
+
+import java.io.IOException;
+
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.entitycaps.packet.CapsExtension;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public class CapsExtensionProvider implements PacketExtensionProvider {
+    private static final int MAX_DEPTH = 10;
+
+    public PacketExtension parseExtension(XmlPullParser parser) throws XmlPullParserException, IOException,
+            XMPPException {
+        String hash = null;
+        String version = null;
+        String node = null;
+        int depth = 0;
+        while (true) {
+            if (parser.getEventType() == XmlPullParser.START_TAG && parser.getName().equalsIgnoreCase("c")) {
+                hash = parser.getAttributeValue(null, "hash");
+                version = parser.getAttributeValue(null, "ver");
+                node = parser.getAttributeValue(null, "node");
+            }
+
+            if (parser.getEventType() == XmlPullParser.END_TAG && parser.getName().equalsIgnoreCase("c")) {
+                break;
+            } else {
+                parser.next();
+            }
+
+            if (depth < MAX_DEPTH) {
+                depth++;
+            } else {
+                throw new XMPPException("Malformed caps element");
+            }
+        }
+
+        if (hash != null && version != null && node != null) {
+            return new CapsExtension(node, version, hash);
+        } else {
+            throw new XMPPException("Caps elment with missing attributes");
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/provider/DataFormProvider.java b/src/org/jivesoftware/smackx/provider/DataFormProvider.java
new file mode 100644
index 0000000..c13f234
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/DataFormProvider.java
@@ -0,0 +1,160 @@
+/**
+ * $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.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The DataFormProvider parses DataForm packets.
+ * 
+ * @author Gaston Dombiak
+ */
+public class DataFormProvider implements PacketExtensionProvider {
+
+    /**
+     * Creates a new DataFormProvider.
+     * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor
+     */
+    public DataFormProvider() {
+    }
+
+    public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        StringBuilder buffer = null;
+        DataForm dataForm = new DataForm(parser.getAttributeValue("", "type"));
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("instructions")) { 
+                    dataForm.addInstruction(parser.nextText());
+                }
+                else if (parser.getName().equals("title")) {                    
+                    dataForm.setTitle(parser.nextText());
+                }
+                else if (parser.getName().equals("field")) {                    
+                    dataForm.addField(parseField(parser));
+                }
+                else if (parser.getName().equals("item")) {                    
+                    dataForm.addItem(parseItem(parser));
+                }
+                else if (parser.getName().equals("reported")) {                    
+                    dataForm.setReportedData(parseReported(parser));
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals(dataForm.getElementName())) {
+                    done = true;
+                }
+            }
+        }
+        return dataForm;
+    }
+
+    private FormField parseField(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        FormField formField = new FormField(parser.getAttributeValue("", "var"));
+        formField.setLabel(parser.getAttributeValue("", "label"));
+        formField.setType(parser.getAttributeValue("", "type"));
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("desc")) { 
+                    formField.setDescription(parser.nextText());
+                }
+                else if (parser.getName().equals("value")) {                    
+                    formField.addValue(parser.nextText());
+                }
+                else if (parser.getName().equals("required")) {                    
+                    formField.setRequired(true);
+                }
+                else if (parser.getName().equals("option")) {                    
+                    formField.addOption(parseOption(parser));
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("field")) {
+                    done = true;
+                }
+            }
+        }
+        return formField;
+    }
+
+    private DataForm.Item parseItem(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        List<FormField> fields = new ArrayList<FormField>();
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("field")) { 
+                    fields.add(parseField(parser));
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("item")) {
+                    done = true;
+                }
+            }
+        }
+        return new DataForm.Item(fields);
+    }
+
+    private DataForm.ReportedData parseReported(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        List<FormField> fields = new ArrayList<FormField>();
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("field")) { 
+                    fields.add(parseField(parser));
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("reported")) {
+                    done = true;
+                }
+            }
+        }
+        return new DataForm.ReportedData(fields);
+    }
+
+    private FormField.Option parseOption(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        FormField.Option option = null;
+        String label = parser.getAttributeValue("", "label");
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("value")) {
+                    option = new FormField.Option(label, parser.nextText());                     
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("option")) {
+                    done = true;
+                }
+            }
+        }
+        return option;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/provider/DelayInfoProvider.java b/src/org/jivesoftware/smackx/provider/DelayInfoProvider.java
new file mode 100644
index 0000000..6fa52b7
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/DelayInfoProvider.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.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smackx.packet.DelayInfo;
+import org.jivesoftware.smackx.packet.DelayInformation;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * This provider simply creates a {@link DelayInfo} decorator for the {@link DelayInformation} that
+ * is returned by the superclass.  This allows the new code using
+ * <a href="http://xmpp.org/extensions/xep-0203.html">Delay Information XEP-0203</a> to be
+ * backward compatible with <a href="http://xmpp.org/extensions/xep-0091.html">XEP-0091</a>.  
+ * 
+ * <p>This provider must be registered in the <b>smack.properties</b> file for the element 
+ * <b>delay</b> with namespace <b>urn:xmpp:delay</b></p>
+ *  
+ * @author Robin Collier
+ */
+public class DelayInfoProvider extends DelayInformationProvider
+{
+
+	@Override
+	public PacketExtension parseExtension(XmlPullParser parser) throws Exception
+	{
+		return new DelayInfo((DelayInformation)super.parseExtension(parser));
+	}
+
+}
diff --git a/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java b/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java
new file mode 100644
index 0000000..e5fe010
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/DelayInformationProvider.java
@@ -0,0 +1,74 @@
+/**
+ * $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.smackx.provider;
+
+import java.text.ParseException;
+import java.util.Date;
+
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.packet.DelayInformation;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The DelayInformationProvider parses DelayInformation packets.
+ * 
+ * @author Gaston Dombiak
+ * @author Henning Staib
+ */
+public class DelayInformationProvider implements PacketExtensionProvider {
+    
+    public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+        String stampString = (parser.getAttributeValue("", "stamp"));
+        Date stamp = null;
+        
+        try {
+            stamp = StringUtils.parseDate(stampString);
+        }
+        catch (ParseException parseExc) {
+            /*
+             * if date could not be parsed but XML is valid, don't shutdown
+             * connection by throwing an exception instead set timestamp to epoch 
+             * so that it is obviously wrong. 
+             */
+            if (stamp == null) {
+                stamp = new Date(0);
+            }
+        }
+        
+        
+        DelayInformation delayInformation = new DelayInformation(stamp);
+        delayInformation.setFrom(parser.getAttributeValue("", "from"));
+        String reason = parser.nextText();
+
+        /*
+         * parser.nextText() returns empty string if there is no reason.
+         * DelayInformation API specifies that null should be returned in that
+         * case.
+         */
+        reason = "".equals(reason) ? null : reason;
+        delayInformation.setReason(reason);
+        
+        return delayInformation;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java b/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java
new file mode 100644
index 0000000..6ad6fef
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/DiscoverInfoProvider.java
@@ -0,0 +1,86 @@
+/**
+ * $RCSfile$
+ * $Revision: 7071 $
+ * $Date: 2007-02-12 08:59:05 +0800 (Mon, 12 Feb 2007) $
+ *
+ * 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+* The DiscoverInfoProvider parses Service Discovery information packets.
+*
+* @author Gaston Dombiak
+*/
+public class DiscoverInfoProvider implements IQProvider {
+
+    public IQ parseIQ(XmlPullParser parser) throws Exception {
+        DiscoverInfo discoverInfo = new DiscoverInfo();
+        boolean done = false;
+        DiscoverInfo.Feature feature = null;
+        DiscoverInfo.Identity identity = null;
+        String category = "";
+        String name = "";
+        String type = "";
+        String variable = "";
+        String lang = "";
+        discoverInfo.setNode(parser.getAttributeValue("", "node"));
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("identity")) {
+                    // Initialize the variables from the parsed XML
+                    category = parser.getAttributeValue("", "category");
+                    name = parser.getAttributeValue("", "name");
+                    type = parser.getAttributeValue("", "type");
+                    lang = parser.getAttributeValue(parser.getNamespace("xml"), "lang");
+                }
+                else if (parser.getName().equals("feature")) {
+                    // Initialize the variables from the parsed XML
+                    variable = parser.getAttributeValue("", "var");
+                }
+                // Otherwise, it must be a packet extension.
+                else {
+                    discoverInfo.addExtension(PacketParserUtils.parsePacketExtension(parser
+                            .getName(), parser.getNamespace(), parser));
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("identity")) {
+                    // Create a new identity and add it to the discovered info.
+                    identity = new DiscoverInfo.Identity(category, name, type);
+                    if (lang != null)
+                        identity.setLanguage(lang);
+                    discoverInfo.addIdentity(identity);
+                }
+                if (parser.getName().equals("feature")) {
+                    // Create a new feature and add it to the discovered info.
+                    discoverInfo.addFeature(variable);
+                }
+                if (parser.getName().equals("query")) {
+                    done = true;
+                }
+            }
+        }
+
+        return discoverInfo;
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java b/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java
new file mode 100644
index 0000000..fcbe25f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/DiscoverItemsProvider.java
@@ -0,0 +1,69 @@
+/**
+ * $RCSfile$
+ * $Revision: 7071 $
+ * $Date: 2007-02-12 08:59:05 +0800 (Mon, 12 Feb 2007) $
+ *
+ * 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.packet.*;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+* The DiscoverInfoProvider parses Service Discovery items packets.
+*
+* @author Gaston Dombiak
+*/
+public class DiscoverItemsProvider implements IQProvider {
+
+    public IQ parseIQ(XmlPullParser parser) throws Exception {
+        DiscoverItems discoverItems = new DiscoverItems();
+        boolean done = false;
+        DiscoverItems.Item item;
+        String jid = "";
+        String name = "";
+        String action = "";
+        String node = "";
+        discoverItems.setNode(parser.getAttributeValue("", "node"));
+        while (!done) {
+            int eventType = parser.next();
+
+            if (eventType == XmlPullParser.START_TAG && "item".equals(parser.getName())) {
+                // Initialize the variables from the parsed XML
+                jid = parser.getAttributeValue("", "jid");
+                name = parser.getAttributeValue("", "name");
+                node = parser.getAttributeValue("", "node");
+                action = parser.getAttributeValue("", "action");
+            }
+            else if (eventType == XmlPullParser.END_TAG && "item".equals(parser.getName())) {
+                // Create a new Item and add it to DiscoverItems.
+                item = new DiscoverItems.Item(jid);
+                item.setName(name);
+                item.setNode(node);
+                item.setAction(action);
+                discoverItems.addItem(item);
+            }
+            else if (eventType == XmlPullParser.END_TAG && "query".equals(parser.getName())) {
+                done = true;
+            }
+        }
+
+        return discoverItems;
+    }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/provider/EmbeddedExtensionProvider.java b/src/org/jivesoftware/smackx/provider/EmbeddedExtensionProvider.java
new file mode 100644
index 0000000..3d5ceb4
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/EmbeddedExtensionProvider.java
@@ -0,0 +1,111 @@
+/**

+ * 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.smackx.provider;

+

+import java.util.ArrayList;

+import java.util.HashMap;

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.jivesoftware.smack.util.PacketParserUtils;

+import org.jivesoftware.smackx.pubsub.provider.ItemProvider;

+import org.jivesoftware.smackx.pubsub.provider.ItemsProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * 

+ * This class simplifies parsing of embedded elements by using the 

+ * <a href="http://en.wikipedia.org/wiki/Template_method_pattern">Template Method Pattern</a>.  

+ * After extracting the current element attributes and content of any child elements, the template method 

+ * ({@link #createReturnExtension(String, String, Map, List)} is called.  Subclasses

+ * then override this method to create the specific return type.

+ * 

+ * <p>To use this class, you simply register your subclasses as extension providers in the 

+ * <b>smack.properties</b> file.  Then they will be automatically picked up and used to parse

+ * any child elements.  

+ * 

+ * <pre>

+ * For example, given the following message

+ * 

+ * &lt;message from='pubsub.shakespeare.lit' to='francisco@denmark.lit' id='foo&gt;

+ *    &lt;event xmlns='http://jabber.org/protocol/pubsub#event&gt;

+ *       &lt;items node='princely_musings'&gt;

+ *          &lt;item id='asdjkwei3i34234n356'&gt;

+ *             &lt;entry xmlns='http://www.w3.org/2005/Atom'&gt;

+ *                &lt;title&gt;Soliloquy&lt;/title&gt;

+ *                &lt;link rel='alternative' type='text/html'/&gt;

+ *                &lt;id>tag:denmark.lit,2003:entry-32397&lt;/id&gt;

+ *             &lt;/entry&gt;

+ *          &lt;/item&gt;

+ *       &lt;/items&gt;

+ *    &lt;/event&gt;

+ * &lt;/message&gt;

+ * 

+ * I would have a classes

+ * {@link ItemsProvider} extends {@link EmbeddedExtensionProvider}

+ * {@link ItemProvider} extends {@link EmbeddedExtensionProvider}

+ * and

+ * AtomProvider extends {@link PacketExtensionProvider}

+ * 

+ * These classes are then registered in the meta-inf/smack.providers file

+ * as follows.

+ * 

+ *   &lt;extensionProvider&gt;

+ *      &lt;elementName&gt;items&lt;/elementName&gt;

+ *      &lt;namespace&gt;http://jabber.org/protocol/pubsub#event&lt;/namespace&gt;

+ *      &lt;className&gt;org.jivesoftware.smackx.provider.ItemsEventProvider&lt;/className&gt;

+ *   &lt;/extensionProvider&gt;

+ *   &lt;extensionProvider&gt;

+ *       &lt;elementName&gt;item&lt;/elementName&gt;

+ *       &lt;namespace&gt;http://jabber.org/protocol/pubsub#event&lt;/namespace&gt;

+ *       &lt;className&gt;org.jivesoftware.smackx.provider.ItemProvider&lt;/className&gt;

+ *   &lt;/extensionProvider&gt;

+ * 

+ * </pre>

+ * 

+ * @author Robin Collier

+ * 

+ * @deprecated This has been moved to {@link org.jivesoftware.smack.provider.EmbeddedExtensionProvider}

+ */

+abstract public class EmbeddedExtensionProvider implements PacketExtensionProvider

+{

+

+	final public PacketExtension parseExtension(XmlPullParser parser) throws Exception

+	{

+        String namespace = parser.getNamespace();

+        String name = parser.getName();

+        Map<String, String> attMap = new HashMap<String, String>();

+        

+        for(int i=0; i<parser.getAttributeCount(); i++)

+        {

+        	attMap.put(parser.getAttributeName(i), parser.getAttributeValue(i));

+        }

+        List<PacketExtension> extensions = new ArrayList<PacketExtension>();

+        

+        do

+        {

+            int tag = parser.next();

+

+            if (tag == XmlPullParser.START_TAG) 

+            	extensions.add(PacketParserUtils.parsePacketExtension(parser.getName(), parser.getNamespace(), parser));

+        } while (!name.equals(parser.getName()));

+

+		return createReturnExtension(name, namespace, attMap, extensions);

+	}

+

+	abstract protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content);

+}

diff --git a/src/org/jivesoftware/smackx/provider/HeaderProvider.java b/src/org/jivesoftware/smackx/provider/HeaderProvider.java
new file mode 100644
index 0000000..7344880
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/HeaderProvider.java
@@ -0,0 +1,44 @@
+/**

+ * 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.smackx.provider;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.jivesoftware.smackx.packet.Header;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Parses the header element as defined in <a href="http://xmpp.org/extensions/xep-0131">Stanza Headers and Internet Metadata (SHIM)</a>.

+ * 

+ * @author Robin Collier

+ */

+public class HeaderProvider implements PacketExtensionProvider

+{

+	public PacketExtension parseExtension(XmlPullParser parser) throws Exception

+	{

+		String name = parser.getAttributeValue(null, "name");

+		String value = null;

+		

+		parser.next();

+		

+		if (parser.getEventType() == XmlPullParser.TEXT)

+			value = parser.getText();

+		

+		while(parser.getEventType() != XmlPullParser.END_TAG)

+			parser.next();

+		

+		return new Header(name, value);

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/provider/HeadersProvider.java b/src/org/jivesoftware/smackx/provider/HeadersProvider.java
new file mode 100644
index 0000000..056dd58
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/HeadersProvider.java
@@ -0,0 +1,37 @@
+/**

+ * 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.smackx.provider;

+

+import java.util.Collection;

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.packet.Header;

+import org.jivesoftware.smackx.packet.HeadersExtension;

+

+/**

+ * Parses the headers element as defined in <a href="http://xmpp.org/extensions/xep-0131">Stanza Headers and Internet Metadata (SHIM)</a>.

+ * 

+ * @author Robin Collier

+ */

+public class HeadersProvider extends EmbeddedExtensionProvider

+{

+	@Override

+	protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)

+	{

+		return new HeadersExtension((Collection<Header>)content);

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java b/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java
new file mode 100644
index 0000000..1072232
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/MUCAdminProvider.java
@@ -0,0 +1,81 @@
+/**
+ * $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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smackx.packet.MUCAdmin;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The MUCAdminProvider parses MUCAdmin packets. (@see MUCAdmin)
+ * 
+ * @author Gaston Dombiak
+ */
+public class MUCAdminProvider implements IQProvider {
+
+    public IQ parseIQ(XmlPullParser parser) throws Exception {
+        MUCAdmin mucAdmin = new MUCAdmin();
+        boolean done = false;
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("item")) {
+                    mucAdmin.addItem(parseItem(parser));
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("query")) {
+                    done = true;
+                }
+            }
+        }
+
+        return mucAdmin;
+    }
+
+    private MUCAdmin.Item parseItem(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        MUCAdmin.Item item =
+            new MUCAdmin.Item(
+                parser.getAttributeValue("", "affiliation"),
+                parser.getAttributeValue("", "role"));
+        item.setNick(parser.getAttributeValue("", "nick"));
+        item.setJid(parser.getAttributeValue("", "jid"));
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("actor")) {
+                    item.setActor(parser.getAttributeValue("", "jid"));
+                }
+                if (parser.getName().equals("reason")) {
+                    item.setReason(parser.nextText());
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("item")) {
+                    done = true;
+                }
+            }
+        }
+        return item;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java b/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java
new file mode 100644
index 0000000..ff3094e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/MUCOwnerProvider.java
@@ -0,0 +1,108 @@
+/**
+ * $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.smackx.provider;
+
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.provider.*;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.packet.MUCOwner;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The MUCOwnerProvider parses MUCOwner packets. (@see MUCOwner)
+ * 
+ * @author Gaston Dombiak
+ */
+public class MUCOwnerProvider implements IQProvider {
+
+    public IQ parseIQ(XmlPullParser parser) throws Exception {
+        MUCOwner mucOwner = new MUCOwner();
+        boolean done = false;
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("item")) {
+                    mucOwner.addItem(parseItem(parser));
+                }
+                else if (parser.getName().equals("destroy")) {
+                    mucOwner.setDestroy(parseDestroy(parser));
+                }
+                // Otherwise, it must be a packet extension.
+                else {
+                    mucOwner.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(),
+                            parser.getNamespace(), parser));
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("query")) {
+                    done = true;
+                }
+            }
+        }
+
+        return mucOwner;
+    }
+
+    private MUCOwner.Item parseItem(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        MUCOwner.Item item = new MUCOwner.Item(parser.getAttributeValue("", "affiliation"));
+        item.setNick(parser.getAttributeValue("", "nick"));
+        item.setRole(parser.getAttributeValue("", "role"));
+        item.setJid(parser.getAttributeValue("", "jid"));
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("actor")) {
+                    item.setActor(parser.getAttributeValue("", "jid"));
+                }
+                if (parser.getName().equals("reason")) {
+                    item.setReason(parser.nextText());
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("item")) {
+                    done = true;
+                }
+            }
+        }
+        return item;
+    }
+
+    private MUCOwner.Destroy parseDestroy(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        MUCOwner.Destroy destroy = new MUCOwner.Destroy();
+        destroy.setJid(parser.getAttributeValue("", "jid"));
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("reason")) {
+                    destroy.setReason(parser.nextText());
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("destroy")) {
+                    done = true;
+                }
+            }
+        }
+        return destroy;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/provider/MUCUserProvider.java b/src/org/jivesoftware/smackx/provider/MUCUserProvider.java
new file mode 100644
index 0000000..5a98af6
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/MUCUserProvider.java
@@ -0,0 +1,174 @@
+/**
+ * $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.smackx.provider;
+
+import org.jivesoftware.smack.packet.*;
+import org.jivesoftware.smack.provider.*;
+import org.jivesoftware.smackx.packet.*;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The MUCUserProvider parses packets with extended presence information about 
+ * roles and affiliations.
+ *
+ * @author Gaston Dombiak
+ */
+public class MUCUserProvider implements PacketExtensionProvider {
+
+    /**
+     * Creates a new MUCUserProvider.
+     * ProviderManager requires that every PacketExtensionProvider has a public, no-argument 
+     * constructor
+     */
+    public MUCUserProvider() {
+    }
+
+    /**
+     * Parses a MUCUser packet (extension sub-packet).
+     *
+     * @param parser the XML parser, positioned at the starting element of the extension.
+     * @return a PacketExtension.
+     * @throws Exception if a parsing error occurs.
+     */
+    public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+        MUCUser mucUser = new MUCUser();
+        boolean done = false;
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("invite")) {
+                    mucUser.setInvite(parseInvite(parser));
+                }
+                if (parser.getName().equals("item")) {
+                    mucUser.setItem(parseItem(parser));
+                }
+                if (parser.getName().equals("password")) {
+                    mucUser.setPassword(parser.nextText());
+                }
+                if (parser.getName().equals("status")) {
+                    mucUser.setStatus(new MUCUser.Status(parser.getAttributeValue("", "code")));
+                }
+                if (parser.getName().equals("decline")) {
+                    mucUser.setDecline(parseDecline(parser));
+                }
+                if (parser.getName().equals("destroy")) {
+                    mucUser.setDestroy(parseDestroy(parser));
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("x")) {
+                    done = true;
+                }
+            }
+        }
+
+        return mucUser;
+    }
+
+    private MUCUser.Item parseItem(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        MUCUser.Item item =
+            new MUCUser.Item(
+                parser.getAttributeValue("", "affiliation"),
+                parser.getAttributeValue("", "role"));
+        item.setNick(parser.getAttributeValue("", "nick"));
+        item.setJid(parser.getAttributeValue("", "jid"));
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("actor")) {
+                    item.setActor(parser.getAttributeValue("", "jid"));
+                }
+                if (parser.getName().equals("reason")) {
+                    item.setReason(parser.nextText());
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("item")) {
+                    done = true;
+                }
+            }
+        }
+        return item;
+    }
+
+    private MUCUser.Invite parseInvite(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        MUCUser.Invite invite = new MUCUser.Invite();
+        invite.setFrom(parser.getAttributeValue("", "from"));
+        invite.setTo(parser.getAttributeValue("", "to"));
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("reason")) {
+                    invite.setReason(parser.nextText());
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("invite")) {
+                    done = true;
+                }
+            }
+        }
+        return invite;
+    }
+
+    private MUCUser.Decline parseDecline(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        MUCUser.Decline decline = new MUCUser.Decline();
+        decline.setFrom(parser.getAttributeValue("", "from"));
+        decline.setTo(parser.getAttributeValue("", "to"));
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("reason")) {
+                    decline.setReason(parser.nextText());
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("decline")) {
+                    done = true;
+                }
+            }
+        }
+        return decline;
+    }
+
+    private MUCUser.Destroy parseDestroy(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        MUCUser.Destroy destroy = new MUCUser.Destroy();
+        destroy.setJid(parser.getAttributeValue("", "jid"));
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("reason")) {
+                    destroy.setReason(parser.nextText());
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("destroy")) {
+                    done = true;
+                }
+            }
+        }
+        return destroy;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/provider/MessageEventProvider.java b/src/org/jivesoftware/smackx/provider/MessageEventProvider.java
new file mode 100644
index 0000000..b631546
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/MessageEventProvider.java
@@ -0,0 +1,77 @@
+/**
+ * $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.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.packet.MessageEvent;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * The MessageEventProvider parses Message Event packets.
+*
+ * @author Gaston Dombiak
+ */
+public class MessageEventProvider implements PacketExtensionProvider {
+
+    /**
+     * Creates a new MessageEventProvider.
+     * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor
+     */
+    public MessageEventProvider() {
+    }
+
+    /**
+     * Parses a MessageEvent packet (extension sub-packet).
+     *
+     * @param parser the XML parser, positioned at the starting element of the extension.
+     * @return a PacketExtension.
+     * @throws Exception if a parsing error occurs.
+     */
+    public PacketExtension parseExtension(XmlPullParser parser)
+        throws Exception {
+        MessageEvent messageEvent = new MessageEvent();
+        boolean done = false;
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("id"))
+                    messageEvent.setPacketID(parser.nextText());
+                if (parser.getName().equals(MessageEvent.COMPOSING))
+                    messageEvent.setComposing(true);
+                if (parser.getName().equals(MessageEvent.DELIVERED))
+                    messageEvent.setDelivered(true);
+                if (parser.getName().equals(MessageEvent.DISPLAYED))
+                    messageEvent.setDisplayed(true);
+                if (parser.getName().equals(MessageEvent.OFFLINE))
+                    messageEvent.setOffline(true);
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("x")) {
+                    done = true;
+                }
+            }
+        }
+
+        return messageEvent;
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java b/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java
new file mode 100644
index 0000000..4c3e356
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/MultipleAddressesProvider.java
@@ -0,0 +1,67 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2006 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.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.packet.MultipleAddresses;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The MultipleAddressesProvider parses {@link MultipleAddresses} packets.
+ *
+ * @author Gaston Dombiak
+ */
+public class MultipleAddressesProvider implements PacketExtensionProvider {
+
+    /**
+     * Creates a new MultipleAddressesProvider.
+     * ProviderManager requires that every PacketExtensionProvider has a public, no-argument
+     * constructor.
+     */
+    public MultipleAddressesProvider() {
+    }
+
+    public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+        boolean done = false;
+        MultipleAddresses multipleAddresses = new MultipleAddresses();
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("address")) {
+                    String type = parser.getAttributeValue("", "type");
+                    String jid = parser.getAttributeValue("", "jid");
+                    String node = parser.getAttributeValue("", "node");
+                    String desc = parser.getAttributeValue("", "desc");
+                    boolean delivered = "true".equals(parser.getAttributeValue("", "delivered"));
+                    String uri = parser.getAttributeValue("", "uri");
+                    // Add the parsed address
+                    multipleAddresses.addAddress(type, jid, node, desc, delivered, uri);
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals(multipleAddresses.getElementName())) {
+                    done = true;
+                }
+            }
+        }
+        return multipleAddresses;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/provider/PEPProvider.java b/src/org/jivesoftware/smackx/provider/PEPProvider.java
new file mode 100644
index 0000000..f33dcde
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/PEPProvider.java
@@ -0,0 +1,93 @@
+/**
+ * $RCSfile: PEPProvider.java,v $
+ * $Revision: 1.2 $
+ * $Date: 2007/11/06 02:05:09 $
+ *
+ * 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.smackx.provider;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * The PEPProvider parses incoming PEPEvent packets.
+ * (XEP-163 has a weird asymmetric deal: outbound PEP are <iq> + <pubsub> and inbound are <message> + <event>.
+ * The provider only deals with inbound, and so it only deals with <message>.
+ * 
+ * Anyhoo...
+ * 
+ * The way this works is that PEPxxx classes are generic <pubsub> and <message> providers, and anyone who
+ * wants to publish/receive PEPs, such as <tune>, <geoloc>, etc., simply need to extend PEPItem and register (here)
+ * a PacketExtensionProvider that knows how to parse that PEPItem extension.
+ *
+ * @author Jeff Williams
+ */
+public class PEPProvider implements PacketExtensionProvider {
+
+    Map<String, PacketExtensionProvider> nodeParsers = new HashMap<String, PacketExtensionProvider>();
+    PacketExtension pepItem;
+    
+    /**
+     * Creates a new PEPProvider.
+     * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor
+     */
+    public PEPProvider() {
+    }
+
+    public void registerPEPParserExtension(String node, PacketExtensionProvider pepItemParser) {
+        nodeParsers.put(node, pepItemParser);
+    }
+
+    /**
+     * Parses a PEPEvent packet and extracts a PEPItem from it.
+     * (There is only one per <event>.)
+     *
+     * @param parser the XML parser, positioned at the starting element of the extension.
+     * @return a PacketExtension.
+     * @throws Exception if a parsing error occurs.
+     */
+    public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+
+        boolean done = false;
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("event")) {
+                } else if (parser.getName().equals("items")) {
+                    // Figure out the node for this event.
+                    String node = parser.getAttributeValue("", "node");
+                    // Get the parser for this kind of node, and if found then parse the node.
+                    PacketExtensionProvider nodeParser = nodeParsers.get(node);
+                    if (nodeParser != null) {
+                        pepItem = nodeParser.parseExtension(parser);
+                    }
+                 }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("event")) {
+                    done = true;
+                }
+            }
+        }
+
+        return pepItem;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java b/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java
new file mode 100644
index 0000000..b781a5a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/PrivateDataProvider.java
@@ -0,0 +1,46 @@
+/**
+ * $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.smackx.provider;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.jivesoftware.smackx.packet.PrivateData;
+
+/**
+ * An interface for parsing custom private data. Each PrivateDataProvider must
+ * be registered with the PrivateDataManager class for it to be used. Every implementation
+ * of this interface <b>must</b> have a public, no-argument constructor.
+ *
+ * @author Matt Tucker
+ */
+public interface PrivateDataProvider {
+
+    /**
+     * Parse the private data sub-document and create a PrivateData instance. At the
+     * beginning of the method call, the xml parser will be positioned at the opening
+     * tag of the private data child element. At the end of the method call, the parser
+     * <b>must</b> be positioned on the closing tag of the child element.
+     *
+     * @param parser an XML parser.
+     * @return a new PrivateData instance.
+     * @throws Exception if an error occurs parsing the XML.
+     */
+    public PrivateData parsePrivateData(XmlPullParser parser) throws Exception;
+}
diff --git a/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java b/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java
new file mode 100644
index 0000000..76e09be
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/RosterExchangeProvider.java
@@ -0,0 +1,90 @@
+/**
+ * $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.smackx.provider;
+
+import java.util.ArrayList;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smackx.*;
+import org.jivesoftware.smackx.packet.*;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ *
+ * The RosterExchangeProvider parses RosterExchange packets.
+ *
+ * @author Gaston Dombiak
+ */
+public class RosterExchangeProvider implements PacketExtensionProvider {
+
+    /**
+     * Creates a new RosterExchangeProvider.
+     * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor
+     */
+    public RosterExchangeProvider() {
+    }
+
+    /**
+     * Parses a RosterExchange packet (extension sub-packet).
+     *
+     * @param parser the XML parser, positioned at the starting element of the extension.
+     * @return a PacketExtension.
+     * @throws Exception if a parsing error occurs.
+     */
+    public PacketExtension parseExtension(XmlPullParser parser) throws Exception {
+
+        RosterExchange rosterExchange = new RosterExchange();
+        boolean done = false;
+        RemoteRosterEntry remoteRosterEntry = null;
+		String jid = "";
+		String name = "";
+		ArrayList<String> groupsName = new ArrayList<String>();
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("item")) {
+                	// Reset this variable since they are optional for each item
+					groupsName = new ArrayList<String>();
+					// Initialize the variables from the parsed XML
+                    jid = parser.getAttributeValue("", "jid");
+                    name = parser.getAttributeValue("", "name");
+                }
+                if (parser.getName().equals("group")) {
+					groupsName.add(parser.nextText());
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("item")) {
+					// Create packet.
+					remoteRosterEntry = new RemoteRosterEntry(jid, name, (String[]) groupsName.toArray(new String[groupsName.size()]));
+                    rosterExchange.addRosterEntry(remoteRosterEntry);
+                }
+                if (parser.getName().equals("x")) {
+                    done = true;
+                }
+            }
+        }
+
+        return rosterExchange;
+
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java b/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java
new file mode 100644
index 0000000..8101e4c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/StreamInitiationProvider.java
@@ -0,0 +1,124 @@
+/**

+ * $RCSfile$

+ * $Revision$

+ * $Date$

+ *

+ * Copyright 2003-2006 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.smackx.provider;

+

+import java.text.ParseException;

+import java.util.Date;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smack.util.StringUtils;

+import org.jivesoftware.smackx.packet.DataForm;
+import org.jivesoftware.smackx.packet.StreamInitiation;
+import org.jivesoftware.smackx.packet.StreamInitiation.File;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The StreamInitiationProvider parses StreamInitiation packets.
+ * 
+ * @author Alexander Wenckus
+ * 
+ */
+public class StreamInitiationProvider implements IQProvider {
+
+	public IQ parseIQ(final XmlPullParser parser) throws Exception {
+		boolean done = false;
+
+		// si
+		String id = parser.getAttributeValue("", "id");
+		String mimeType = parser.getAttributeValue("", "mime-type");
+
+		StreamInitiation initiation = new StreamInitiation();
+
+		// file
+		String name = null;
+		String size = null;
+		String hash = null;
+		String date = null;
+		String desc = null;
+		boolean isRanged = false;
+
+		// feature
+		DataForm form = null;
+		DataFormProvider dataFormProvider = new DataFormProvider();
+
+		int eventType;
+		String elementName;
+		String namespace;
+		while (!done) {
+			eventType = parser.next();
+			elementName = parser.getName();
+			namespace = parser.getNamespace();
+			if (eventType == XmlPullParser.START_TAG) {
+				if (elementName.equals("file")) {
+					name = parser.getAttributeValue("", "name");
+					size = parser.getAttributeValue("", "size");
+					hash = parser.getAttributeValue("", "hash");
+					date = parser.getAttributeValue("", "date");
+				} else if (elementName.equals("desc")) {
+					desc = parser.nextText();
+				} else if (elementName.equals("range")) {
+					isRanged = true;
+				} else if (elementName.equals("x")
+						&& namespace.equals("jabber:x:data")) {
+					form = (DataForm) dataFormProvider.parseExtension(parser);
+				}
+			} else if (eventType == XmlPullParser.END_TAG) {
+				if (elementName.equals("si")) {
+					done = true;
+				} else if (elementName.equals("file")) {
+                    long fileSize = 0;
+                    if(size != null && size.trim().length() !=0){
+                        try {
+                            fileSize = Long.parseLong(size);
+                        }
+                        catch (NumberFormatException e) {
+                            e.printStackTrace();
+                        }
+                    }
+                    
+                    Date fileDate = new Date();
+                    if (date != null) {
+                        try {
+                            fileDate = StringUtils.parseXEP0082Date(date);

+                        } catch (ParseException e) {
+                            // couldn't parse date, use current date-time
+                        }
+                    }
+                    
+                    File file = new File(name, fileSize);
+					file.setHash(hash);
+					file.setDate(fileDate);
+					file.setDesc(desc);
+					file.setRanged(isRanged);
+					initiation.setFile(file);
+				}
+			}
+		}
+
+		initiation.setSesssionID(id);
+		initiation.setMimeType(mimeType);
+
+		initiation.setFeatureNegotiationForm(form);
+
+		return initiation;
+	}
+
+}
diff --git a/src/org/jivesoftware/smackx/provider/VCardProvider.java b/src/org/jivesoftware/smackx/provider/VCardProvider.java
new file mode 100644
index 0000000..8fa0421
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/VCardProvider.java
@@ -0,0 +1,293 @@
+/**
+ * $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.smackx.provider;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.packet.VCard;
+import org.w3c.dom.*;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * vCard provider.
+ *
+ * @author Gaston Dombiak
+ * @author Derek DeMoro
+ */
+public class VCardProvider implements IQProvider {
+
+    private static final String PREFERRED_ENCODING = "UTF-8";
+
+    public IQ parseIQ(XmlPullParser parser) throws Exception {
+        final StringBuilder sb = new StringBuilder();
+        try {
+            int event = parser.getEventType();
+            // get the content
+            while (true) {
+                switch (event) {
+                    case XmlPullParser.TEXT:
+                        // We must re-escape the xml so that the DOM won't throw an exception
+                        sb.append(StringUtils.escapeForXML(parser.getText()));
+                        break;
+                    case XmlPullParser.START_TAG:
+                        sb.append('<').append(parser.getName()).append('>');
+                        break;
+                    case XmlPullParser.END_TAG:
+                        sb.append("</").append(parser.getName()).append('>');
+                        break;
+                    default:
+                }
+
+                if (event == XmlPullParser.END_TAG && "vCard".equals(parser.getName())) break;
+
+                event = parser.next();
+            }
+        }
+        catch (XmlPullParserException e) {
+            e.printStackTrace();
+        }
+        catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        String xmlText = sb.toString();
+        return createVCardFromXML(xmlText);
+    }
+
+    /**
+     * Builds a users vCard from xml file.
+     *
+     * @param xml the xml representing a users vCard.
+     * @return the VCard.
+     * @throws Exception if an exception occurs.
+     */
+    public static VCard createVCardFromXML(String xml) throws Exception {
+        VCard vCard = new VCard();
+
+        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
+        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
+        Document document = documentBuilder.parse(
+                new ByteArrayInputStream(xml.getBytes(PREFERRED_ENCODING)));
+
+        new VCardReader(vCard, document).initializeFields();
+        return vCard;
+    }
+
+    private static class VCardReader {
+
+        private final VCard vCard;
+        private final Document document;
+
+        VCardReader(VCard vCard, Document document) {
+            this.vCard = vCard;
+            this.document = document;
+        }
+
+        public void initializeFields() {
+            vCard.setFirstName(getTagContents("GIVEN"));
+            vCard.setLastName(getTagContents("FAMILY"));
+            vCard.setMiddleName(getTagContents("MIDDLE"));
+            setupPhoto();
+
+            setupEmails();
+
+            vCard.setOrganization(getTagContents("ORGNAME"));
+            vCard.setOrganizationUnit(getTagContents("ORGUNIT"));
+
+            setupSimpleFields();
+
+            setupPhones();
+            setupAddresses();
+        }
+
+        private void setupPhoto() {
+            String binval = null;
+            String mimetype = null;
+
+            NodeList photo = document.getElementsByTagName("PHOTO");
+            if (photo.getLength() != 1)
+                return;
+
+            Node photoNode = photo.item(0);
+            NodeList childNodes = photoNode.getChildNodes();
+
+            int childNodeCount = childNodes.getLength();
+            List<Node> nodes = new ArrayList<Node>(childNodeCount);
+            for (int i = 0; i < childNodeCount; i++)
+                nodes.add(childNodes.item(i));
+
+            String name = null;
+            String value = null;
+            for (Node n : nodes) {
+                name = n.getNodeName();
+                value = n.getTextContent();
+                if (name.equals("BINVAL")) {
+                    binval = value;
+                }
+                else if (name.equals("TYPE")) {
+                    mimetype = value;
+                }
+            }
+
+            if (binval == null || mimetype == null)
+                return;
+
+            vCard.setAvatar(binval, mimetype);
+        }
+
+        private void setupEmails() {
+            NodeList nodes = document.getElementsByTagName("USERID");
+            if (nodes == null) return;
+            for (int i = 0; i < nodes.getLength(); i++) {
+                Element element = (Element) nodes.item(i);
+                if ("WORK".equals(element.getParentNode().getFirstChild().getNodeName())) {
+                    vCard.setEmailWork(getTextContent(element));
+                }
+                else {
+                    vCard.setEmailHome(getTextContent(element));
+                }
+            }
+        }
+
+        private void setupPhones() {
+            NodeList allPhones = document.getElementsByTagName("TEL");
+            if (allPhones == null) return;
+            for (int i = 0; i < allPhones.getLength(); i++) {
+                NodeList nodes = allPhones.item(i).getChildNodes();
+                String type = null;
+                String code = null;
+                String value = null;
+                for (int j = 0; j < nodes.getLength(); j++) {
+                    Node node = nodes.item(j);
+                    if (node.getNodeType() != Node.ELEMENT_NODE) continue;
+                    String nodeName = node.getNodeName();
+                    if ("NUMBER".equals(nodeName)) {
+                        value = getTextContent(node);
+                    }
+                    else if (isWorkHome(nodeName)) {
+                        type = nodeName;
+                    }
+                    else {
+                        code = nodeName;
+                    }
+                }
+                if (code == null || value == null) continue;
+                if ("HOME".equals(type)) {
+                    vCard.setPhoneHome(code, value);
+                }
+                else { // By default, setup work phone
+                    vCard.setPhoneWork(code, value);
+                }
+            }
+        }
+
+        private boolean isWorkHome(String nodeName) {
+            return "HOME".equals(nodeName) || "WORK".equals(nodeName);
+        }
+
+        private void setupAddresses() {
+            NodeList allAddresses = document.getElementsByTagName("ADR");
+            if (allAddresses == null) return;
+            for (int i = 0; i < allAddresses.getLength(); i++) {
+                Element addressNode = (Element) allAddresses.item(i);
+
+                String type = null;
+                List<String> code = new ArrayList<String>();
+                List<String> value = new ArrayList<String>();
+                NodeList childNodes = addressNode.getChildNodes();
+                for (int j = 0; j < childNodes.getLength(); j++) {
+                    Node node = childNodes.item(j);
+                    if (node.getNodeType() != Node.ELEMENT_NODE) continue;
+                    String nodeName = node.getNodeName();
+                    if (isWorkHome(nodeName)) {
+                        type = nodeName;
+                    }
+                    else {
+                        code.add(nodeName);
+                        value.add(getTextContent(node));
+                    }
+                }
+                for (int j = 0; j < value.size(); j++) {
+                    if ("HOME".equals(type)) {
+                        vCard.setAddressFieldHome((String) code.get(j), (String) value.get(j));
+                    }
+                    else { // By default, setup work address
+                        vCard.setAddressFieldWork((String) code.get(j), (String) value.get(j));
+                    }
+                }
+            }
+        }
+
+        private String getTagContents(String tag) {
+            NodeList nodes = document.getElementsByTagName(tag);
+            if (nodes != null && nodes.getLength() == 1) {
+                return getTextContent(nodes.item(0));
+            }
+            return null;
+        }
+
+        private void setupSimpleFields() {
+            NodeList childNodes = document.getDocumentElement().getChildNodes();
+            for (int i = 0; i < childNodes.getLength(); i++) {
+                Node node = childNodes.item(i);
+                if (node instanceof Element) {
+                    Element element = (Element) node;
+
+                    String field = element.getNodeName();
+                    if (element.getChildNodes().getLength() == 0) {
+                        vCard.setField(field, "");
+                    }
+                    else if (element.getChildNodes().getLength() == 1 &&
+                            element.getChildNodes().item(0) instanceof Text) {
+                        vCard.setField(field, getTextContent(element));
+                    }
+                }
+            }
+        }
+
+        private String getTextContent(Node node) {
+            StringBuilder result = new StringBuilder();
+            appendText(result, node);
+            return result.toString();
+        }
+
+        private void appendText(StringBuilder result, Node node) {
+            NodeList childNodes = node.getChildNodes();
+            for (int i = 0; i < childNodes.getLength(); i++) {
+                Node nd = childNodes.item(i);
+                String nodeValue = nd.getNodeValue();
+                if (nodeValue != null) {
+                    result.append(nodeValue);
+                }
+                appendText(result, nd);
+            }
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java b/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java
new file mode 100644
index 0000000..50f437f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/XHTMLExtensionProvider.java
@@ -0,0 +1,94 @@
+/**
+ * $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.smackx.provider;
+
+import org.jivesoftware.smack.packet.PacketExtension;
+import org.jivesoftware.smack.provider.PacketExtensionProvider;
+import org.jivesoftware.smack.util.StringUtils;
+import org.jivesoftware.smackx.packet.XHTMLExtension;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * The XHTMLExtensionProvider parses XHTML packets.
+ *
+ * @author Gaston Dombiak
+ */
+public class XHTMLExtensionProvider implements PacketExtensionProvider {
+
+    /**
+     * Creates a new XHTMLExtensionProvider.
+     * ProviderManager requires that every PacketExtensionProvider has a public, no-argument constructor
+     */
+    public XHTMLExtensionProvider() {
+    }
+
+    /**
+     * Parses a XHTMLExtension packet (extension sub-packet).
+     *
+     * @param parser the XML parser, positioned at the starting element of the extension.
+     * @return a PacketExtension.
+     * @throws Exception if a parsing error occurs.
+     */
+    public PacketExtension parseExtension(XmlPullParser parser)
+        throws Exception {
+        XHTMLExtension xhtmlExtension = new XHTMLExtension();
+        boolean done = false;
+        StringBuilder buffer = new StringBuilder();
+        int startDepth = parser.getDepth();
+        int depth = parser.getDepth();
+        String lastTag = "";
+        while (!done) {
+            int eventType = parser.next();
+            if (eventType == XmlPullParser.START_TAG) {
+                if (parser.getName().equals("body")) {
+                    buffer = new StringBuilder();
+                    depth = parser.getDepth();
+                }
+                lastTag = parser.getText();
+                buffer.append(parser.getText());
+            } else if (eventType == XmlPullParser.TEXT) {
+                if (buffer != null) {
+                    // We need to return valid XML so any inner text needs to be re-escaped
+                    buffer.append(StringUtils.escapeForXML(parser.getText()));
+                }
+            } else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("body") && parser.getDepth() <= depth) {
+                    buffer.append(parser.getText());
+                    xhtmlExtension.addBody(buffer.toString());
+                }
+                else if (parser.getName().equals(xhtmlExtension.getElementName())
+                        && parser.getDepth() <= startDepth) {
+                    done = true;
+                }
+                else {
+                    // This is a check for tags that are both a start and end tag like <br/>
+                    // So that they aren't doubled
+                    if(lastTag == null || !lastTag.equals(parser.getText())) {
+                        buffer.append(parser.getText());
+                    }
+                }
+            }
+        }
+
+        return xhtmlExtension;
+    }
+
+}
diff --git a/src/org/jivesoftware/smackx/provider/package.html b/src/org/jivesoftware/smackx/provider/package.html
new file mode 100644
index 0000000..962ba63
--- /dev/null
+++ b/src/org/jivesoftware/smackx/provider/package.html
@@ -0,0 +1 @@
+<body>Provides pluggable parsing logic for Smack extensions.</body>
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/pubsub/AccessModel.java b/src/org/jivesoftware/smackx/pubsub/AccessModel.java
new file mode 100644
index 0000000..c1fa546
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/AccessModel.java
@@ -0,0 +1,38 @@
+/**

+ * 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.smackx.pubsub;

+

+/**

+ * This enumeration represents the access models for the pubsub node

+ * as defined in the pubsub specification section <a href="http://xmpp.org/extensions/xep-0060.html#registrar-formtypes-config">16.4.3</a>

+ * 

+ * @author Robin Collier

+ */

+public enum AccessModel

+{

+	/** Anyone may subscribe and retrieve items	 */

+	open,

+

+	/** Subscription request must be approved and only subscribers may retrieve items */

+	authorize,

+	

+	/** Anyone with a presence subscription of both or from may subscribe and retrieve items */

+	presence,

+	

+	/** Anyone in the specified roster group(s) may subscribe and retrieve items */

+	roster,

+	

+	/** Only those on a whitelist may subscribe and retrieve items */

+	whitelist;

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/Affiliation.java b/src/org/jivesoftware/smackx/pubsub/Affiliation.java
new file mode 100644
index 0000000..d55534d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/Affiliation.java
@@ -0,0 +1,111 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.packet.PacketExtension;

+

+/**

+ * Represents a affiliation between a user and a node, where the {@link #type} defines 

+ * the type of affiliation.

+ * 

+ * Affiliations are retrieved from the {@link PubSubManager#getAffiliations()} method, which 

+ * gets affiliations for the calling user, based on the identity that is associated with 

+ * the {@link Connection}.

+ * 

+ * @author Robin Collier

+ */

+public class Affiliation implements PacketExtension

+{

+	protected String jid;

+	protected String node;

+	protected Type type;

+	

+	public enum Type

+	{

+		member, none, outcast, owner, publisher

+	}

+

+	/**

+	 * Constructs an affiliation.

+	 * 

+	 * @param jid The JID with affiliation.

+	 * @param affiliation The type of affiliation.

+	 */

+	public Affiliation(String jid, Type affiliation)

+	{

+		this(jid, null, affiliation);

+	}

+	

+	/**

+	 * Constructs an affiliation.

+	 * 

+	 * @param jid The JID with affiliation.

+	 * @param node The node with affiliation.

+	 * @param affiliation The type of affiliation.

+	 */

+	public Affiliation(String jid, String node, Type affiliation)

+	{

+		this.jid = jid;

+		this.node = node;

+		type = affiliation;

+	}

+	

+	public String getJid()

+	{

+		return jid;

+	}

+	

+	public String getNode()

+	{

+		return node;

+	}

+	

+	public Type getType()

+	{

+		return type;

+	}

+	

+	public String getElementName()

+	{

+		return "affiliation";

+	}

+

+	public String getNamespace()

+	{

+		return null;

+	}

+

+	public String toXML()

+	{

+		StringBuilder builder = new StringBuilder("<");

+		builder.append(getElementName());

+		if (node != null)

+			appendAttribute(builder, "node", node);

+		appendAttribute(builder, "jid", jid);

+		appendAttribute(builder, "affiliation", type.toString());

+		

+		builder.append("/>");

+		return builder.toString();

+	}

+

+	private void appendAttribute(StringBuilder builder, String att, String value)

+	{

+		builder.append(" ");

+		builder.append(att);

+		builder.append("='");

+		builder.append(value);

+		builder.append("'");

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/AffiliationsExtension.java b/src/org/jivesoftware/smackx/pubsub/AffiliationsExtension.java
new file mode 100644
index 0000000..563147e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/AffiliationsExtension.java
@@ -0,0 +1,91 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.Collections;

+import java.util.List;

+

+/**

+ * Represents the <b>affiliations</b> element of the reply to a request for affiliations.

+ * It is defined in the specification in section <a href="http://xmpp.org/extensions/xep-0060.html#entity-affiliations">5.7 Retrieve Affiliations</a>.

+ * 

+ * @author Robin Collier

+ */

+public class AffiliationsExtension extends NodeExtension

+{

+	protected List<Affiliation> items = Collections.EMPTY_LIST;

+	

+	public AffiliationsExtension()

+	{

+		super(PubSubElementType.AFFILIATIONS);

+	}

+	

+	public AffiliationsExtension(List<Affiliation> affiliationList)

+	{

+		super(PubSubElementType.AFFILIATIONS);

+

+		if (affiliationList != null)

+			items = affiliationList;

+	}

+

+	/**

+	 * Affiliations for the specified node.

+	 * 

+	 * @param nodeId

+	 * @param subList

+	 */

+	public AffiliationsExtension(String nodeId, List<Affiliation> affiliationList)

+	{

+		super(PubSubElementType.AFFILIATIONS, nodeId);

+

+		if (affiliationList != null)

+			items = affiliationList;

+	}

+

+	public List<Affiliation> getAffiliations()

+	{

+		return items;

+	}

+

+	@Override

+	public String toXML()

+	{

+		if ((items == null) || (items.size() == 0))

+		{

+			return super.toXML();

+		}

+		else

+		{

+			StringBuilder builder = new StringBuilder("<");

+			builder.append(getElementName());

+			if (getNode() != null)

+			{

+				builder.append(" node='");

+				builder.append(getNode());

+				builder.append("'");

+			}

+			builder.append(">");

+			

+			for (Affiliation item : items)

+			{

+				builder.append(item.toXML());

+			}

+			

+			builder.append("</");

+			builder.append(getElementName());

+			builder.append(">");

+			return builder.toString();

+		}

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/ChildrenAssociationPolicy.java b/src/org/jivesoftware/smackx/pubsub/ChildrenAssociationPolicy.java
new file mode 100644
index 0000000..933a39e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/ChildrenAssociationPolicy.java
@@ -0,0 +1,32 @@
+/**

+ * 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.smackx.pubsub;

+

+/**

+ * This enumeration represents the children association policy for associating leaf nodes

+ * with collection nodes as defined in the pubsub specification section <a href="http://xmpp.org/extensions/xep-0060.html#registrar-formtypes-config">16.4.3</a>

+ * 

+ * @author Robin Collier

+ */

+public enum ChildrenAssociationPolicy

+{

+	/** Anyone may associate leaf nodes with the collection	 */

+	all,

+	

+	/** Only collection node owners may associate leaf nodes with the collection. */

+	owners,

+	

+	/** Only those on a whitelist may associate leaf nodes with the collection.	 */

+	whitelist;

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/CollectionNode.java b/src/org/jivesoftware/smackx/pubsub/CollectionNode.java
new file mode 100644
index 0000000..dcd1cc4
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/CollectionNode.java
@@ -0,0 +1,31 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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.smackx.pubsub;

+

+import org.jivesoftware.smack.Connection;

+

+public class CollectionNode extends Node

+{

+	CollectionNode(Connection connection, String nodeId)

+	{

+		super(connection, nodeId);

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/ConfigurationEvent.java b/src/org/jivesoftware/smackx/pubsub/ConfigurationEvent.java
new file mode 100644
index 0000000..67b8304
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/ConfigurationEvent.java
@@ -0,0 +1,56 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.Arrays;

+import java.util.Collections;

+import java.util.List;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+

+/**

+ * Represents the <b>configuration</b> element of a pubsub message event which 

+ * associates a configuration form to the node which was configured.  The form 

+ * contains the current node configuration.

+ *  

+ * @author Robin Collier

+ */

+public class ConfigurationEvent extends NodeExtension implements EmbeddedPacketExtension

+{

+	private ConfigureForm form;

+	

+	public ConfigurationEvent(String nodeId)

+	{

+		super(PubSubElementType.CONFIGURATION, nodeId);

+	}

+	

+	public ConfigurationEvent(String nodeId, ConfigureForm configForm)

+	{

+		super(PubSubElementType.CONFIGURATION, nodeId);

+		form = configForm;

+	}

+	

+	public ConfigureForm getConfiguration()

+	{

+		return form;

+	}

+

+	public List<PacketExtension> getExtensions()

+	{

+		if (getConfiguration() == null)

+			return Collections.EMPTY_LIST;

+		else

+			return Arrays.asList(((PacketExtension)getConfiguration().getDataFormToSend()));

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/ConfigureForm.java b/src/org/jivesoftware/smackx/pubsub/ConfigureForm.java
new file mode 100644
index 0000000..f6fe140
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/ConfigureForm.java
@@ -0,0 +1,709 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.ArrayList;

+import java.util.Iterator;

+import java.util.List;

+

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.FormField;

+import org.jivesoftware.smackx.packet.DataForm;

+

+/**

+ * A decorator for a {@link Form} to easily enable reading and updating

+ * of node configuration.  All operations read or update the underlying {@link DataForm}.

+ * 

+ * <p>Unlike the {@link Form}.setAnswer(XXX)} methods, which throw an exception if the field does not

+ * exist, all <b>ConfigureForm.setXXX</b> methods will create the field in the wrapped form

+ * if it does not already exist. 

+ * 

+ * @author Robin Collier

+ */

+public class ConfigureForm extends Form

+{

+	/**

+	 * Create a decorator from an existing {@link DataForm} that has been

+	 * retrieved from parsing a node configuration request.

+	 * 

+	 * @param configDataForm

+	 */

+	public ConfigureForm(DataForm configDataForm)

+	{

+		super(configDataForm);

+	}

+	

+	/**

+	 * Create a decorator from an existing {@link Form} for node configuration.

+	 * Typically, this can be used to create a decorator for an answer form

+	 * by using the result of {@link #createAnswerForm()} as the input parameter.

+	 * 

+	 * @param nodeConfigForm

+	 */

+	public ConfigureForm(Form nodeConfigForm)

+	{

+		super(nodeConfigForm.getDataFormToSend());

+	}

+	

+	/**

+	 * Create a new form for configuring a node.  This would typically only be used 

+	 * when creating and configuring a node at the same time via {@link PubSubManager#createNode(String, Form)}, since 

+	 * configuration of an existing node is typically accomplished by calling {@link LeafNode#getNodeConfiguration()} and

+	 * using the resulting form to create a answer form.  See {@link #ConfigureForm(Form)}.

+	 * @param formType

+	 */

+	public ConfigureForm(FormType formType)

+	{

+		super(formType.toString());

+	}

+	

+	/**

+	 * Get the currently configured {@link AccessModel}, null if it is not set.

+	 * 

+	 * @return The current {@link AccessModel}

+	 */

+	public AccessModel getAccessModel()

+	{

+		String value = getFieldValue(ConfigureNodeFields.access_model);

+		

+		if (value == null)

+			return null;

+		else

+			return AccessModel.valueOf(value);

+	}

+	

+	/**

+	 * Sets the value of access model.

+	 * 

+	 * @param accessModel

+	 */

+	public void setAccessModel(AccessModel accessModel)

+	{

+		addField(ConfigureNodeFields.access_model, FormField.TYPE_LIST_SINGLE);

+		setAnswer(ConfigureNodeFields.access_model.getFieldName(), getListSingle(accessModel.toString()));

+	}

+

+	/**

+	 * Returns the URL of an XSL transformation which can be applied to payloads in order to 

+	 * generate an appropriate message body element.

+	 * 

+	 * @return URL to an XSL

+	 */

+	public String getBodyXSLT()

+	{

+		return getFieldValue(ConfigureNodeFields.body_xslt);

+	}

+

+	/**

+	 * Set the URL of an XSL transformation which can be applied to payloads in order to 

+	 * generate an appropriate message body element.

+	 * 

+	 * @param bodyXslt The URL of an XSL

+	 */

+	public void setBodyXSLT(String bodyXslt)

+	{

+		addField(ConfigureNodeFields.body_xslt, FormField.TYPE_TEXT_SINGLE);

+		setAnswer(ConfigureNodeFields.body_xslt.getFieldName(), bodyXslt);

+	}

+	

+	/**

+	 * The id's of the child nodes associated with a collection node (both leaf and collection).

+	 * 

+	 * @return Iterator over the list of child nodes.

+	 */

+	public Iterator<String> getChildren()

+	{

+		return getFieldValues(ConfigureNodeFields.children);

+	}

+	

+	/**

+	 * Set the list of child node ids that are associated with a collection node.

+	 * 

+	 * @param children

+	 */

+	public void setChildren(List<String> children)

+	{

+		addField(ConfigureNodeFields.children, FormField.TYPE_TEXT_MULTI);

+		setAnswer(ConfigureNodeFields.children.getFieldName(), children);

+	}

+	

+	/**

+	 * Returns the policy that determines who may associate children with the node.

+	 *  

+	 * @return The current policy

+	 */

+	public ChildrenAssociationPolicy getChildrenAssociationPolicy()

+	{

+		String value = getFieldValue(ConfigureNodeFields.children_association_policy);

+		

+		if (value == null)

+			return null;

+		else

+			return ChildrenAssociationPolicy.valueOf(value);

+	}

+	

+	/**

+	 * Sets the policy that determines who may associate children with the node.

+	 * 

+	 * @param policy The policy being set

+	 */

+	public void setChildrenAssociationPolicy(ChildrenAssociationPolicy policy)

+	{

+		addField(ConfigureNodeFields.children_association_policy, FormField.TYPE_LIST_SINGLE);

+        List<String> values = new ArrayList<String>(1);

+        values.add(policy.toString());

+        setAnswer(ConfigureNodeFields.children_association_policy.getFieldName(), values);

+	}

+	

+	/**

+	 * Iterator of JID's that are on the whitelist that determines who can associate child nodes 

+	 * with the collection node.  This is only relevant if {@link #getChildrenAssociationPolicy()} is set to

+	 * {@link ChildrenAssociationPolicy#whitelist}.

+	 * 

+	 * @return Iterator over whitelist

+	 */

+	public Iterator<String> getChildrenAssociationWhitelist()

+	{

+		return getFieldValues(ConfigureNodeFields.children_association_whitelist);

+	}

+	

+	/**

+	 * Set the JID's in the whitelist of users that can associate child nodes with the collection 

+	 * node.  This is only relevant if {@link #getChildrenAssociationPolicy()} is set to

+	 * {@link ChildrenAssociationPolicy#whitelist}.

+	 * 

+	 * @param whitelist The list of JID's

+	 */

+	public void setChildrenAssociationWhitelist(List<String> whitelist)

+	{

+		addField(ConfigureNodeFields.children_association_whitelist, FormField.TYPE_JID_MULTI);

+		setAnswer(ConfigureNodeFields.children_association_whitelist.getFieldName(), whitelist);

+	}

+

+	/**

+	 * Gets the maximum number of child nodes that can be associated with the collection node.

+	 * 

+	 * @return The maximum number of child nodes

+	 */

+	public int getChildrenMax()

+	{

+		return Integer.parseInt(getFieldValue(ConfigureNodeFields.children_max));

+	}

+

+	/**

+	 * Set the maximum number of child nodes that can be associated with a collection node.

+	 * 

+	 * @param max The maximum number of child nodes.

+	 */

+	public void setChildrenMax(int max)

+	{

+		addField(ConfigureNodeFields.children_max, FormField.TYPE_TEXT_SINGLE);

+		setAnswer(ConfigureNodeFields.children_max.getFieldName(), max);

+	}

+

+	/**

+	 * Gets the collection node which the node is affiliated with.

+	 * 

+	 * @return The collection node id

+	 */

+	public String getCollection()

+	{

+		return getFieldValue(ConfigureNodeFields.collection);

+	}

+

+	/**

+	 * Sets the collection node which the node is affiliated with.

+	 * 

+	 * @param collection The node id of the collection node

+	 */

+	public void setCollection(String collection)

+	{

+		addField(ConfigureNodeFields.collection, FormField.TYPE_TEXT_SINGLE);

+		setAnswer(ConfigureNodeFields.collection.getFieldName(), collection);

+	}

+

+	/**

+	 * Gets the URL of an XSL transformation which can be applied to the payload

+	 * format in order to generate a valid Data Forms result that the client could

+	 * display using a generic Data Forms rendering engine.

+	 * 

+	 * @return The URL of an XSL transformation

+	 */

+	public String getDataformXSLT()

+	{

+		return getFieldValue(ConfigureNodeFields.dataform_xslt);

+	}

+

+	/**

+	 * Sets the URL of an XSL transformation which can be applied to the payload

+	 * format in order to generate a valid Data Forms result that the client could

+	 * display using a generic Data Forms rendering engine.

+	 * 

+	 * @param url The URL of an XSL transformation

+	 */

+	public void setDataformXSLT(String url)

+	{

+		addField(ConfigureNodeFields.dataform_xslt, FormField.TYPE_TEXT_SINGLE);

+		setAnswer(ConfigureNodeFields.dataform_xslt.getFieldName(), url);

+	}

+

+	/**

+	 * Does the node deliver payloads with event notifications.

+	 * 

+	 * @return true if it does, false otherwise

+	 */

+	public boolean isDeliverPayloads()

+	{

+		return parseBoolean(getFieldValue(ConfigureNodeFields.deliver_payloads));

+	}

+	

+	/**

+	 * Sets whether the node will deliver payloads with event notifications.

+	 * 

+	 * @param deliver true if the payload will be delivered, false otherwise

+	 */

+	public void setDeliverPayloads(boolean deliver)

+	{

+		addField(ConfigureNodeFields.deliver_payloads, FormField.TYPE_BOOLEAN);

+		setAnswer(ConfigureNodeFields.deliver_payloads.getFieldName(), deliver);

+	}

+

+	/**

+	 * Determines who should get replies to items

+	 * 

+	 * @return Who should get the reply

+	 */

+	public ItemReply getItemReply()

+	{

+		String value = getFieldValue(ConfigureNodeFields.itemreply);

+		

+		if (value == null)

+			return null;

+		else

+			return ItemReply.valueOf(value);

+	}

+

+	/**

+	 * Sets who should get the replies to items

+	 * 

+	 * @param reply Defines who should get the reply

+	 */

+	public void setItemReply(ItemReply reply)

+	{

+		addField(ConfigureNodeFields.itemreply, FormField.TYPE_LIST_SINGLE);

+		setAnswer(ConfigureNodeFields.itemreply.getFieldName(), getListSingle(reply.toString()));

+	}

+

+	/**

+	 * Gets the maximum number of items to persisted to this node if {@link #isPersistItems()} is

+	 * true.

+	 * 

+	 * @return The maximum number of items to persist

+	 */

+	public int getMaxItems()

+	{

+		return Integer.parseInt(getFieldValue(ConfigureNodeFields.max_items));

+	}

+

+	/**

+	 * Set the maximum number of items to persisted to this node if {@link #isPersistItems()} is

+	 * true.

+	 * 

+	 * @param max The maximum number of items to persist

+	 */

+	public void setMaxItems(int max)

+	{

+		addField(ConfigureNodeFields.max_items, FormField.TYPE_TEXT_SINGLE);

+		setAnswer(ConfigureNodeFields.max_items.getFieldName(), max);

+	}

+	

+	/**

+	 * Gets the maximum payload size in bytes.

+	 * 

+	 * @return The maximum payload size

+	 */

+	public int getMaxPayloadSize()

+	{

+		return Integer.parseInt(getFieldValue(ConfigureNodeFields.max_payload_size));

+	}

+

+	/**

+	 * Sets the maximum payload size in bytes

+	 * 

+	 * @param max The maximum payload size

+	 */

+	public void setMaxPayloadSize(int max)

+	{

+		addField(ConfigureNodeFields.max_payload_size, FormField.TYPE_TEXT_SINGLE);

+		setAnswer(ConfigureNodeFields.max_payload_size.getFieldName(), max);

+	}

+	

+	/**

+	 * Gets the node type

+	 * 

+	 * @return The node type

+	 */

+	public NodeType getNodeType()

+	{

+		String value = getFieldValue(ConfigureNodeFields.node_type);

+		

+		if (value == null)

+			return null;

+		else

+			return NodeType.valueOf(value);

+	}

+	

+	/**

+	 * Sets the node type

+	 * 

+	 * @param type The node type

+	 */

+	public void setNodeType(NodeType type)

+	{

+		addField(ConfigureNodeFields.node_type, FormField.TYPE_LIST_SINGLE);

+		setAnswer(ConfigureNodeFields.node_type.getFieldName(), getListSingle(type.toString()));

+	}

+

+	/**

+	 * Determines if subscribers should be notified when the configuration changes.

+	 * 

+	 * @return true if they should be notified, false otherwise

+	 */

+	public boolean isNotifyConfig()

+	{

+		return parseBoolean(getFieldValue(ConfigureNodeFields.notify_config));

+	}

+	

+	/**

+	 * Sets whether subscribers should be notified when the configuration changes.

+	 * 

+	 * @param notify true if subscribers should be notified, false otherwise

+	 */

+	public void setNotifyConfig(boolean notify)

+	{

+		addField(ConfigureNodeFields.notify_config, FormField.TYPE_BOOLEAN);

+		setAnswer(ConfigureNodeFields.notify_config.getFieldName(), notify);

+	}

+

+	/**

+	 * Determines whether subscribers should be notified when the node is deleted.

+	 * 

+	 * @return true if subscribers should be notified, false otherwise

+	 */

+	public boolean isNotifyDelete()

+	{

+		return parseBoolean(getFieldValue(ConfigureNodeFields.notify_delete));

+	}

+	

+	/**

+	 * Sets whether subscribers should be notified when the node is deleted.

+	 * 

+	 * @param notify true if subscribers should be notified, false otherwise

+	 */

+	public void setNotifyDelete(boolean notify) 

+	{

+		addField(ConfigureNodeFields.notify_delete, FormField.TYPE_BOOLEAN);

+		setAnswer(ConfigureNodeFields.notify_delete.getFieldName(), notify);

+	}

+

+	/**

+	 * Determines whether subscribers should be notified when items are deleted 

+	 * from the node.

+	 * 

+	 * @return true if subscribers should be notified, false otherwise

+	 */

+	public boolean isNotifyRetract()

+	{

+		return parseBoolean(getFieldValue(ConfigureNodeFields.notify_retract));

+	}

+	

+	/**

+	 * Sets whether subscribers should be notified when items are deleted 

+	 * from the node.

+	 * 

+	 * @param notify true if subscribers should be notified, false otherwise

+	 */

+	public void setNotifyRetract(boolean notify) 

+	{

+		addField(ConfigureNodeFields.notify_retract, FormField.TYPE_BOOLEAN);

+		setAnswer(ConfigureNodeFields.notify_retract.getFieldName(), notify);

+	}

+	

+	/**

+	 * Determines whether items should be persisted in the node.

+	 * 

+	 * @return true if items are persisted

+	 */

+	public boolean isPersistItems()

+	{

+		return parseBoolean(getFieldValue(ConfigureNodeFields.persist_items));

+	}

+	

+	/**

+	 * Sets whether items should be persisted in the node.

+	 * 

+	 * @param persist true if items should be persisted, false otherwise

+	 */

+	public void setPersistentItems(boolean persist) 

+	{

+		addField(ConfigureNodeFields.persist_items, FormField.TYPE_BOOLEAN);

+		setAnswer(ConfigureNodeFields.persist_items.getFieldName(), persist);

+	}

+

+	/**

+	 * Determines whether to deliver notifications to available users only.

+	 * 

+	 * @return true if users must be available

+	 */

+	public boolean isPresenceBasedDelivery()

+	{

+		return parseBoolean(getFieldValue(ConfigureNodeFields.presence_based_delivery));

+	}

+	

+	/**

+	 * Sets whether to deliver notifications to available users only.

+	 * 

+	 * @param presenceBased true if user must be available, false otherwise

+	 */

+	public void setPresenceBasedDelivery(boolean presenceBased) 

+	{

+		addField(ConfigureNodeFields.presence_based_delivery, FormField.TYPE_BOOLEAN);

+		setAnswer(ConfigureNodeFields.presence_based_delivery.getFieldName(), presenceBased);

+	}

+

+	/**

+	 * Gets the publishing model for the node, which determines who may publish to it.

+	 * 

+	 * @return The publishing model

+	 */

+	public PublishModel getPublishModel()

+	{

+		String value = getFieldValue(ConfigureNodeFields.publish_model);

+		

+		if (value == null)

+			return null;

+		else

+			return PublishModel.valueOf(value);

+	}

+

+	/**

+	 * Sets the publishing model for the node, which determines who may publish to it.

+	 * 

+	 * @param publish The enum representing the possible options for the publishing model

+	 */

+	public void setPublishModel(PublishModel publish) 

+	{

+		addField(ConfigureNodeFields.publish_model, FormField.TYPE_LIST_SINGLE);

+		setAnswer(ConfigureNodeFields.publish_model.getFieldName(), getListSingle(publish.toString()));

+	}

+	

+	/**

+	 * Iterator over the multi user chat rooms that are specified as reply rooms.

+	 * 

+	 * @return The reply room JID's

+	 */

+	public Iterator<String> getReplyRoom()

+	{

+		return getFieldValues(ConfigureNodeFields.replyroom);

+	}

+	

+	/**

+	 * Sets the multi user chat rooms that are specified as reply rooms.

+	 * 

+	 * @param replyRooms The multi user chat room to use as reply rooms

+	 */

+	public void setReplyRoom(List<String> replyRooms) 

+	{

+		addField(ConfigureNodeFields.replyroom, FormField.TYPE_LIST_MULTI);

+		setAnswer(ConfigureNodeFields.replyroom.getFieldName(), replyRooms);

+	}

+	

+	/**

+	 * Gets the specific JID's for reply to.

+	 *  

+	 * @return The JID's

+	 */

+	public Iterator<String> getReplyTo()

+	{

+		return getFieldValues(ConfigureNodeFields.replyto);

+	}

+	

+	/**

+	 * Sets the specific JID's for reply to.

+	 * 

+	 * @param replyTos The JID's to reply to

+	 */

+	public void setReplyTo(List<String> replyTos)

+	{

+		addField(ConfigureNodeFields.replyto, FormField.TYPE_LIST_MULTI);

+		setAnswer(ConfigureNodeFields.replyto.getFieldName(), replyTos);

+	}

+	

+	/**

+	 * Gets the roster groups that are allowed to subscribe and retrieve items.

+	 *  

+	 * @return The roster groups

+	 */

+	public Iterator<String> getRosterGroupsAllowed()

+	{

+		return getFieldValues(ConfigureNodeFields.roster_groups_allowed);

+	}

+	

+	/**

+	 * Sets the roster groups that are allowed to subscribe and retrieve items.

+	 * 

+	 * @param groups The roster groups

+	 */

+	public void setRosterGroupsAllowed(List<String> groups)

+	{

+		addField(ConfigureNodeFields.roster_groups_allowed, FormField.TYPE_LIST_MULTI);

+		setAnswer(ConfigureNodeFields.roster_groups_allowed.getFieldName(), groups);

+	}

+	

+	/**

+	 * Determines if subscriptions are allowed.

+	 * 

+	 * @return true if subscriptions are allowed, false otherwise

+	 */

+	public boolean isSubscibe()

+	{

+		return parseBoolean(getFieldValue(ConfigureNodeFields.subscribe));

+	}

+

+	/**

+	 * Sets whether subscriptions are allowed.

+	 * 

+	 * @param subscribe true if they are, false otherwise

+	 */

+	public void setSubscribe(boolean subscribe)

+	{

+		addField(ConfigureNodeFields.subscribe, FormField.TYPE_BOOLEAN);

+		setAnswer(ConfigureNodeFields.subscribe.getFieldName(), subscribe);

+	}

+	

+	/**

+	 * Gets the human readable node title.

+	 * 

+	 * @return The node title

+	 */

+	public String getTitle()

+	{

+		return getFieldValue(ConfigureNodeFields.title);

+	}

+

+	/**

+	 * Sets a human readable title for the node.

+	 * 

+	 * @param title The node title

+	 */

+	public void setTitle(String title) 

+	{

+		addField(ConfigureNodeFields.title, FormField.TYPE_TEXT_SINGLE);

+		setAnswer(ConfigureNodeFields.title.getFieldName(), title);

+	}

+	

+	/**

+	 * The type of node data, usually specified by the namespace of the payload (if any).

+	 * 

+	 * @return The type of node data

+	 */

+	public String getDataType()

+	{

+		return getFieldValue(ConfigureNodeFields.type);

+	}

+

+	/**

+	 * Sets the type of node data, usually specified by the namespace of the payload (if any).

+	 * 

+	 * @param type The type of node data

+	 */

+	public void setDataType(String type) 

+	{

+		addField(ConfigureNodeFields.type, FormField.TYPE_TEXT_SINGLE);

+		setAnswer(ConfigureNodeFields.type.getFieldName(), type);

+	}

+	

+	@Override

+	public String toString()

+	{

+		StringBuilder result = new StringBuilder(getClass().getName() + " Content [");

+		

+		Iterator<FormField> fields = getFields();

+		

+		while (fields.hasNext())

+		{

+			FormField formField = fields.next();

+			result.append('(');

+			result.append(formField.getVariable());

+			result.append(':');

+			

+			Iterator<String> values = formField.getValues();

+			StringBuilder valuesBuilder = new StringBuilder();

+				

+			while (values.hasNext())

+			{

+				if (valuesBuilder.length() > 0)

+					result.append(',');

+				String value = (String)values.next();

+				valuesBuilder.append(value);

+			}

+			

+			if (valuesBuilder.length() == 0)

+				valuesBuilder.append("NOT SET");

+			result.append(valuesBuilder);

+			result.append(')');

+		}

+		result.append(']');

+		return result.toString();

+	}

+

+	static private boolean parseBoolean(String fieldValue)

+	{

+		return ("1".equals(fieldValue) || "true".equals(fieldValue));

+	}

+

+	private String getFieldValue(ConfigureNodeFields field)

+	{

+		FormField formField = getField(field.getFieldName());

+		

+		return (formField.getValues().hasNext()) ? formField.getValues().next() : null;

+	}

+

+	private Iterator<String> getFieldValues(ConfigureNodeFields field)

+	{

+		FormField formField = getField(field.getFieldName());

+		

+		return formField.getValues();

+	}

+

+	private void addField(ConfigureNodeFields nodeField, String type)

+	{

+		String fieldName = nodeField.getFieldName();

+		

+		if (getField(fieldName) == null)

+		{

+			FormField field = new FormField(fieldName);

+			field.setType(type);

+			addField(field);

+		}

+	}

+

+	private List<String> getListSingle(String value)

+	{

+		List<String> list = new ArrayList<String>(1);

+		list.add(value);

+		return list;

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/ConfigureNodeFields.java b/src/org/jivesoftware/smackx/pubsub/ConfigureNodeFields.java
new file mode 100644
index 0000000..3912483
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/ConfigureNodeFields.java
@@ -0,0 +1,218 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.net.URL;

+

+import org.jivesoftware.smackx.Form;

+

+/**

+ * This enumeration represents all the fields of a node configuration form.  This enumeration

+ * is not required when using the {@link ConfigureForm} to configure nodes, but may be helpful

+ * for generic UI's using only a {@link Form} for configuration.

+ * 

+ * @author Robin Collier

+ */

+public enum ConfigureNodeFields

+{

+	/**

+	 * Determines who may subscribe and retrieve items

+	 * 

+	 * <p><b>Value: {@link AccessModel}</b></p>

+	 */

+	access_model,

+

+	/**

+	 * The URL of an XSL transformation which can be applied to 

+	 * payloads in order to generate an appropriate message

+	 * body element

+	 * 

+	 * <p><b>Value: {@link URL}</b></p>

+	 */

+	body_xslt,

+	

+	/**

+	 * The collection with which a node is affiliated

+	 * 

+	 * <p><b>Value: String</b></p>

+	 */

+	collection,

+

+	/**

+	 * The URL of an XSL transformation which can be applied to 

+	 * payload format in order to generate a valid Data Forms result 

+	 * that the client could display using a generic Data Forms 

+	 * rendering engine body element.

+	 * 

+	 * <p><b>Value: {@link URL}</b></p>

+	 */

+	dataform_xslt,

+

+	/**

+	 * Whether to deliver payloads with event notifications

+	 *

+	 * <p><b>Value: boolean</b></p>

+	 */

+	deliver_payloads,

+	

+	/**

+	 * Whether owners or publisher should receive replies to items

+	 *

+	 * <p><b>Value: {@link ItemReply}</b></p>

+	 */

+	itemreply,

+	

+	/**

+	 * Who may associate leaf nodes with a collection

+	 * 

+	 * <p><b>Value: {@link ChildrenAssociationPolicy}</b></p>

+	 */

+	children_association_policy,

+	

+	/**

+	 * The list of JIDs that may associate leaf nodes with a 

+	 * collection

+	 * 

+	 * <p><b>Value: List of JIDs as Strings</b></p>

+	 */

+	children_association_whitelist,

+	

+	/**

+	 * The child nodes (leaf or collection) associated with a collection

+	 * 

+	 * <p><b>Value: List of Strings</b></p>

+	 */

+	children,

+	

+	/**

+	 * The maximum number of child nodes that can be associated with a 

+	 * collection

+	 * 

+	 * <p><b>Value: int</b></p>

+	 */

+	children_max,

+	

+	/**

+	 * The maximum number of items to persist

+	 * 

+	 * <p><b>Value: int</b></p>

+	 */

+	max_items,

+	

+	/**

+	 * The maximum payload size in bytes

+	 * 

+	 * <p><b>Value: int</b></p>

+	 */

+	max_payload_size,

+	

+	/**

+	 * Whether the node is a leaf (default) or collection

+	 * 

+	 * <p><b>Value: {@link NodeType}</b></p>

+	 */

+	node_type,

+	

+	/**

+	 * Whether to notify subscribers when the node configuration changes

+	 * 

+	 * <p><b>Value: boolean</b></p>

+	 */

+	notify_config,

+	

+	/**

+	 * Whether to notify subscribers when the node is deleted

+	 * 

+	 * <p><b>Value: boolean</b></p>

+	 */

+	notify_delete,

+

+	/**

+	 * Whether to notify subscribers when items are removed from the node

+	 * 

+	 * <p><b>Value: boolean</b></p>

+	 */

+	notify_retract,

+	

+	/**

+	 * Whether to persist items to storage.  This is required to have multiple 

+	 * items in the node. 

+	 * 

+	 * <p><b>Value: boolean</b></p>

+	 */

+	persist_items,

+	

+	/**

+	 * Whether to deliver notifications to available users only

+	 * 

+	 * <p><b>Value: boolean</b></p>

+	 */

+	presence_based_delivery,

+

+	/**

+	 * Defines who can publish to the node

+	 * 

+	 * <p><b>Value: {@link PublishModel}</b></p>

+	 */

+	publish_model,

+	

+	/**

+	 * The specific multi-user chat rooms to specify for replyroom

+	 * 

+	 * <p><b>Value: List of JIDs as Strings</b></p>

+	 */

+	replyroom,

+	

+	/**

+	 * The specific JID(s) to specify for replyto

+	 * 

+	 * <p><b>Value: List of JIDs as Strings</b></p>

+	 */

+	replyto,

+	

+	/**

+	 * The roster group(s) allowed to subscribe and retrieve items

+	 * 

+	 * <p><b>Value: List of strings</b></p>

+	 */

+	roster_groups_allowed,

+	

+	/**

+	 * Whether to allow subscriptions

+	 * 

+	 * <p><b>Value: boolean</b></p>

+	 */

+	subscribe,

+	

+	/**

+	 * A friendly name for the node

+	 * 

+	 * <p><b>Value: String</b></p>

+	 */

+	title,

+	

+	/**

+	 * The type of node data, ussually specified by the namespace 

+	 * of the payload(if any);MAY be a list-single rather than a 

+	 * text single

+	 * 

+	 * <p><b>Value: String</b></p>

+	 */

+	type;

+	

+	public String getFieldName()

+	{

+		return "pubsub#" + toString();

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/EmbeddedPacketExtension.java b/src/org/jivesoftware/smackx/pubsub/EmbeddedPacketExtension.java
new file mode 100644
index 0000000..b17a66a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/EmbeddedPacketExtension.java
@@ -0,0 +1,45 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.List;

+

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.util.PacketParserUtils;

+

+/**

+ * This interface defines {@link PacketExtension} implementations that contain other

+ * extensions.  This effectively extends the idea of an extension within one of the 

+ * top level {@link Packet} types to consider any embedded element to be an extension

+ * of its parent.  This more easily enables the usage of some of Smacks parsing

+ * utilities such as {@link PacketParserUtils#parsePacketExtension(String, String, org.xmlpull.v1.XmlPullParser)} to be used

+ * to parse any element of the XML being parsed.

+ * 

+ * <p>Top level extensions have only one element, but they can have multiple children, or

+ * their children can have multiple children.  This interface is a way of allowing extensions 

+ * to be embedded within one another as a partial or complete one to one mapping of extension 

+ * to element.

+ * 

+ * @author Robin Collier

+ */

+public interface EmbeddedPacketExtension extends PacketExtension

+{

+	/**

+	 * Get the list of embedded {@link PacketExtension} objects.

+	 *  

+	 * @return List of embedded {@link PacketExtension}

+	 */

+	List<PacketExtension> getExtensions();

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/EventElement.java b/src/org/jivesoftware/smackx/pubsub/EventElement.java
new file mode 100644
index 0000000..165970f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/EventElement.java
@@ -0,0 +1,74 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.Arrays;

+import java.util.List;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;

+

+/**

+ * Represents the top level element of a pubsub event extension.  All types of pubsub events are

+ * represented by this class.  The specific type can be found by {@link #getEventType()}.  The 

+ * embedded event information, which is specific to the event type, can be retrieved by the {@link #getEvent()}

+ * method.

+ * 

+ * @author Robin Collier

+ */

+public class EventElement implements EmbeddedPacketExtension

+{

+	private EventElementType type;

+	private NodeExtension ext;

+	

+	public EventElement(EventElementType eventType, NodeExtension eventExt)

+	{

+		type = eventType;

+		ext = eventExt;

+	}

+	

+	public EventElementType getEventType()

+	{

+		return type;

+	}

+

+	public List<PacketExtension> getExtensions()

+	{

+		return Arrays.asList(new PacketExtension[]{getEvent()});

+	}

+

+	public NodeExtension getEvent()

+	{

+		return ext;

+	}

+

+	public String getElementName()

+	{

+		return "event";

+	}

+

+	public String getNamespace()

+	{

+		return PubSubNamespace.EVENT.getXmlns();

+	}

+

+	public String toXML()

+	{

+		StringBuilder builder = new StringBuilder("<event xmlns='" + PubSubNamespace.EVENT.getXmlns() + "'>");

+

+		builder.append(ext.toXML());

+		builder.append("</event>");

+		return builder.toString();

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/EventElementType.java b/src/org/jivesoftware/smackx/pubsub/EventElementType.java
new file mode 100644
index 0000000..343edbe
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/EventElementType.java
@@ -0,0 +1,41 @@
+/**

+ * 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.smackx.pubsub;

+

+/**

+ * This enumeration defines the possible event types that are supported within pubsub

+ * event messages.

+ * 

+ * @author Robin Collier

+ */

+public enum EventElementType

+{

+	/** A node has been associated or dissassociated with a collection node */

+	collection, 

+

+	/** A node has had its configuration changed */

+	configuration, 

+	

+	/** A node has been deleted */

+	delete, 

+	

+	/** Items have been published to a node */

+	items, 

+	

+	/** All items have been purged from a node */

+	purge, 

+	

+	/** A node has been subscribed to */

+	subscription

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/FormNode.java b/src/org/jivesoftware/smackx/pubsub/FormNode.java
new file mode 100644
index 0000000..e08bed2
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/FormNode.java
@@ -0,0 +1,99 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smackx.Form;

+

+/**

+ * Generic packet extension which represents any pubsub form that is

+ * parsed from the incoming stream or being sent out to the server.

+ * 

+ * Form types are defined in {@link FormNodeType}.

+ * 

+ * @author Robin Collier

+ */

+public class FormNode extends NodeExtension

+{

+	private Form configForm;

+	

+	/**

+	 * Create a {@link FormNode} which contains the specified form.

+	 * 

+	 * @param formType The type of form being sent

+	 * @param submitForm The form

+	 */

+	public FormNode(FormNodeType formType, Form submitForm)

+	{

+		super(formType.getNodeElement());

+

+		if (submitForm == null)

+			throw new IllegalArgumentException("Submit form cannot be null");

+		configForm = submitForm;

+	}

+	

+	/**

+	 * Create a {@link FormNode} which contains the specified form, which is 

+	 * associated with the specified node.

+	 * 

+	 * @param formType The type of form being sent

+	 * @param nodeId The node the form is associated with

+	 * @param submitForm The form

+	 */

+	public FormNode(FormNodeType formType, String nodeId, Form submitForm)

+	{

+		super(formType.getNodeElement(), nodeId);

+

+		if (submitForm == null)

+			throw new IllegalArgumentException("Submit form cannot be null");

+		configForm = submitForm;

+	}

+	

+	/**

+	 * Get the Form that is to be sent, or was retrieved from the server.

+	 * 

+	 * @return The form

+	 */

+	public Form getForm()

+	{

+		return configForm;

+	}

+	

+	@Override

+	public String toXML()

+	{

+		if (configForm == null)

+		{

+			return super.toXML();

+		}

+		else

+		{

+			StringBuilder builder = new StringBuilder("<");

+			builder.append(getElementName());

+			

+			if (getNode() != null)

+			{

+				builder.append(" node='");

+				builder.append(getNode());

+				builder.append("'>");

+			}

+			else

+				builder.append('>');

+			builder.append(configForm.getDataFormToSend().toXML());

+			builder.append("</");

+			builder.append(getElementName() + '>');

+			return builder.toString();

+		}

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/FormNodeType.java b/src/org/jivesoftware/smackx/pubsub/FormNodeType.java
new file mode 100644
index 0000000..6a163ee
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/FormNodeType.java
@@ -0,0 +1,50 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;

+

+/**

+ * The types of forms supported by the pubsub specification.

+ * 

+ * @author Robin Collier

+ */

+public enum FormNodeType

+{

+	/** Form for configuring an existing node */

+	CONFIGURE_OWNER,

+	

+	/** Form for configuring a node during creation */

+	CONFIGURE,

+	

+	/** Form for configuring subscription options */

+	OPTIONS,

+

+	/** Form which represents the default node configuration options */

+	DEFAULT;

+	

+	public PubSubElementType getNodeElement()

+	{

+		return PubSubElementType.valueOf(toString());

+	}

+

+	public static FormNodeType valueOfFromElementName(String elem, String configNamespace)

+	{

+		if ("configure".equals(elem) && PubSubNamespace.OWNER.getXmlns().equals(configNamespace))

+		{

+			return CONFIGURE_OWNER;

+		}

+		return valueOf(elem.toUpperCase());

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/FormType.java b/src/org/jivesoftware/smackx/pubsub/FormType.java
new file mode 100644
index 0000000..e0fff51
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/FormType.java
@@ -0,0 +1,26 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smackx.Form;

+

+/**

+ * Defines the allowable types for a {@link Form}

+ * 

+ * @author Robin Collier

+ */

+public enum FormType

+{

+    form, submit, cancel, result;

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/pubsub/GetItemsRequest.java b/src/org/jivesoftware/smackx/pubsub/GetItemsRequest.java
new file mode 100644
index 0000000..341b7b5
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/GetItemsRequest.java
@@ -0,0 +1,85 @@
+/**
+ * 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.smackx.pubsub;
+
+/**
+ * Represents a request to subscribe to a node.
+ * 
+ * @author Robin Collier
+ */
+public class GetItemsRequest extends NodeExtension
+{
+	protected String subId;
+	protected int maxItems;
+	
+	public GetItemsRequest(String nodeId)
+	{
+		super(PubSubElementType.ITEMS, nodeId);
+	}
+	
+	public GetItemsRequest(String nodeId, String subscriptionId)
+	{
+		super(PubSubElementType.ITEMS, nodeId);
+		subId = subscriptionId;
+	}
+
+	public GetItemsRequest(String nodeId, int maxItemsToReturn)
+	{
+		super(PubSubElementType.ITEMS, nodeId);
+		maxItems = maxItemsToReturn;
+	}
+
+	public GetItemsRequest(String nodeId, String subscriptionId, int maxItemsToReturn)
+	{
+		this(nodeId, maxItemsToReturn);
+		subId = subscriptionId;
+	}
+
+	public String getSubscriptionId()
+	{
+		return subId;
+	}
+
+	public int getMaxItems()
+	{
+		return maxItems;
+	}
+
+	@Override
+	public String toXML()
+	{
+		StringBuilder builder = new StringBuilder("<");
+		builder.append(getElementName());
+		
+		builder.append(" node='");
+		builder.append(getNode());
+		builder.append("'");
+
+		if (getSubscriptionId() != null)
+		{
+			builder.append(" subid='");
+			builder.append(getSubscriptionId());
+			builder.append("'");
+		}
+
+		if (getMaxItems() > 0)
+		{
+			builder.append(" max_items='");
+			builder.append(getMaxItems());
+			builder.append("'");
+		}
+		builder.append("/>");
+		return builder.toString();
+	}
+}
diff --git a/src/org/jivesoftware/smackx/pubsub/Item.java b/src/org/jivesoftware/smackx/pubsub/Item.java
new file mode 100644
index 0000000..2ce0baa
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/Item.java
@@ -0,0 +1,132 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smack.packet.Message;

+import org.jivesoftware.smackx.pubsub.provider.ItemProvider;

+

+/**

+ * This class represents an item that has been, or will be published to a

+ * pubsub node.  An <tt>Item</tt> has several properties that are dependent

+ * on the configuration of the node to which it has been or will be published.

+ * 

+ * <h1>An Item received from a node (via {@link LeafNode#getItems()} or {@link LeafNode#addItemEventListener(org.jivesoftware.smackx.pubsub.listener.ItemEventListener)}</b>

+ * <li>Will always have an id (either user or server generated) unless node configuration has both

+ * {@link ConfigureForm#isPersistItems()} and {@link ConfigureForm#isDeliverPayloads()}set to false.

+ * <li>Will have a payload if the node configuration has {@link ConfigureForm#isDeliverPayloads()} set 

+ * to true, otherwise it will be null.

+ * 

+ * <h1>An Item created to send to a node (via {@link LeafNode#send()} or {@link LeafNode#publish()}</b>

+ * <li>The id is optional, since the server will generate one if necessary, but should be used if it is 

+ * meaningful in the context of the node.  This value must be unique within the node that it is sent to, since

+ * resending an item with the same id will overwrite the one that already exists if the items are persisted.

+ * <li>Will require payload if the node configuration has {@link ConfigureForm#isDeliverPayloads()} set

+ * to true. 

+ * 

+ * <p>To customise the payload object being returned from the {@link #getPayload()} method, you can

+ * add a custom parser as explained in {@link ItemProvider}.

+ * 

+ * @author Robin Collier

+ */

+public class Item extends NodeExtension

+{

+	private String id;

+	

+	/**

+	 * Create an empty <tt>Item</tt> with no id.  This is a valid item for nodes which are configured

+	 * so that {@link ConfigureForm#isDeliverPayloads()} is false.  In most cases an id will be generated by the server.

+	 * For nodes configured with {@link ConfigureForm#isDeliverPayloads()} and {@link ConfigureForm#isPersistItems()} 

+	 * set to false, no <tt>Item</tt> is sent to the node, you have to use {@link LeafNode#send()} or {@link LeafNode#publish()}

+	 * methods in this case. 

+	 */

+	public Item()

+	{

+		super(PubSubElementType.ITEM);

+	}

+	

+	/**

+	 * Create an <tt>Item</tt> with an id but no payload.  This is a valid item for nodes which are configured

+	 * so that {@link ConfigureForm#isDeliverPayloads()} is false.

+	 * 

+	 * @param itemId The id if the item.  It must be unique within the node unless overwriting and existing item.

+	 * Passing null is the equivalent of calling {@link #Item()}.

+	 */

+	public Item(String itemId)

+	{

+		// The element type is actually irrelevant since we override getNamespace() to return null

+		super(PubSubElementType.ITEM);

+		id = itemId;

+	}

+

+	/**

+	 * Create an <tt>Item</tt> with an id and a node id.  

+	 * <p>

+	 * <b>Note:</b> This is not valid for publishing an item to a node, only receiving from 

+	 * one as part of {@link Message}.  If used to create an Item to publish 

+	 * (via {@link LeafNode#publish(Item)}, the server <i>may</i> return an

+	 * error for an invalid packet.

+	 * 

+	 * @param itemId The id of the item.

+	 * @param nodeId The id of the node which the item was published to.

+	 */

+    public Item(String itemId, String nodeId) 

+    {

+    	super(PubSubElementType.ITEM_EVENT, nodeId);

+        id = itemId;

+    }

+	

+	/**

+	 * Get the item id.  Unique to the node it is associated with.

+	 * 

+	 * @return The id

+	 */

+	public String getId()

+	{

+		return id;

+	}

+	

+	@Override

+	public String getNamespace()

+	{

+		return null;

+	}

+

+	@Override

+	public String toXML()

+	{

+		StringBuilder builder = new StringBuilder("<item");

+		

+		if (id != null)

+		{

+			builder.append(" id='");

+			builder.append(id);

+			builder.append("'");

+		}

+		

+        if (getNode() != null) {

+            builder.append(" node='");

+            builder.append(getNode());

+            builder.append("'");

+        }

+		builder.append("/>");

+

+		return builder.toString();

+	}

+

+	@Override

+	public String toString()

+	{

+		return getClass().getName() + " | Content [" + toXML() + "]";

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/ItemDeleteEvent.java b/src/org/jivesoftware/smackx/pubsub/ItemDeleteEvent.java
new file mode 100644
index 0000000..82ab7df
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/ItemDeleteEvent.java
@@ -0,0 +1,62 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.Collections;

+import java.util.List;

+

+/**

+ * Represents an event in which items have been deleted from the node.

+ * 

+ * @author Robin Collier

+ */

+public class ItemDeleteEvent extends SubscriptionEvent

+{

+	private List<String> itemIds = Collections.EMPTY_LIST;

+	

+	/**

+	 * Constructs an <tt>ItemDeleteEvent</tt> that indicates the the supplied

+	 * items (by id) have been deleted, and that the event matches the listed

+	 * subscriptions.  The subscriptions would have been created by calling 

+	 * {@link LeafNode#subscribe(String)}.

+	 * 

+	 * @param nodeId The id of the node the event came from

+	 * @param deletedItemIds The item ids of the items that were deleted.

+	 * @param subscriptionIds The subscriptions that match the event.

+	 */

+	public ItemDeleteEvent(String nodeId, List<String> deletedItemIds, List<String> subscriptionIds)

+	{

+		super(nodeId, subscriptionIds);

+		

+		if (deletedItemIds == null)

+			throw new IllegalArgumentException("deletedItemIds cannot be null");

+		itemIds = deletedItemIds;

+	}

+	

+	/**

+	 * Get the item id's of the items that have been deleted.

+	 * 

+	 * @return List of item id's

+	 */

+	public List<String> getItemIds()

+	{

+		return Collections.unmodifiableList(itemIds);

+	}

+	

+	@Override

+	public String toString()

+	{

+		return getClass().getName() + "  [subscriptions: " + getSubscriptions() + "], [Deleted Items: " + itemIds + ']';

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/ItemPublishEvent.java b/src/org/jivesoftware/smackx/pubsub/ItemPublishEvent.java
new file mode 100644
index 0000000..1ef1f67
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/ItemPublishEvent.java
@@ -0,0 +1,123 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.Collections;

+import java.util.Date;

+import java.util.List;

+

+/**

+ * Represents an event generated by an item(s) being published to a node.

+ * 

+ * @author Robin Collier

+ */

+public class ItemPublishEvent <T extends Item> extends SubscriptionEvent

+{

+	private List<T> items;

+	private Date originalDate;

+	

+	/**

+	 * Constructs an <tt>ItemPublishEvent</tt> with the provided list

+	 * of {@link Item} that were published.

+	 * 

+	 * @param nodeId The id of the node the event came from

+	 * @param eventItems The list of {@link Item} that were published 

+	 */

+	public ItemPublishEvent(String nodeId, List<T> eventItems)

+	{

+		super(nodeId);

+		items = eventItems;

+	}

+	

+	/**

+	 * Constructs an <tt>ItemPublishEvent</tt> with the provided list

+	 * of {@link Item} that were published.  The list of subscription ids

+	 * represents the subscriptions that matched the event, in the case 

+	 * of the user having multiple subscriptions.

+	 * 

+	 * @param nodeId The id of the node the event came from

+	 * @param eventItems The list of {@link Item} that were published 

+	 * @param subscriptionIds The list of subscriptionIds

+	 */

+	public ItemPublishEvent(String nodeId, List<T> eventItems, List<String> subscriptionIds)

+	{

+		super(nodeId, subscriptionIds);

+		items = eventItems;

+	}

+	

+	/**

+	 * Constructs an <tt>ItemPublishEvent</tt> with the provided list

+	 * of {@link Item} that were published in the past.  The published

+	 * date signifies that this is delayed event.  The list of subscription ids

+	 * represents the subscriptions that matched the event, in the case 

+	 * of the user having multiple subscriptions. 

+	 *

+	 * @param nodeId The id of the node the event came from

+	 * @param eventItems The list of {@link Item} that were published 

+	 * @param subscriptionIds The list of subscriptionIds

+	 * @param publishedDate The original publishing date of the events

+	 */

+	public ItemPublishEvent(String nodeId, List<T> eventItems, List<String> subscriptionIds, Date publishedDate)

+	{

+		super(nodeId, subscriptionIds);

+		items = eventItems;

+		

+		if (publishedDate != null)

+			originalDate = publishedDate;

+	}

+	

+	/**

+	 * Get the list of {@link Item} that were published.

+	 * 

+	 * @return The list of published {@link Item}

+	 */

+	public List<T> getItems()

+	{

+		return Collections.unmodifiableList(items);

+	}

+	

+	/**

+	 * Indicates whether this event was delayed.  That is, the items

+	 * were published to the node at some time in the past.  This will 

+	 * typically happen if there is an item already published to the node

+	 * before a user subscribes to it.  In this case, when the user 

+	 * subscribes, the server may send the last item published to the node

+	 * with a delay date showing its time of original publication.

+	 * 

+	 * @return true if the items are delayed, false otherwise.

+	 */

+	public boolean isDelayed()

+	{

+		return (originalDate != null);

+	}

+	

+	/**

+	 * Gets the original date the items were published.  This is only 

+	 * valid if {@link #isDelayed()} is true.

+	 * 

+	 * @return Date items were published if {@link #isDelayed()} is true, null otherwise.

+	 */

+	public Date getPublishedDate()

+	{

+		return originalDate;

+	}

+

+	@Override

+	public String toString()

+	{

+		return getClass().getName() + "  [subscriptions: " + getSubscriptions() + "], [Delayed: " + 

+			(isDelayed() ? originalDate.toString() : "false") + ']';

+	}

+	

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/ItemReply.java b/src/org/jivesoftware/smackx/pubsub/ItemReply.java
new file mode 100644
index 0000000..3e090d9
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/ItemReply.java
@@ -0,0 +1,29 @@
+/**

+ * 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.smackx.pubsub;

+

+/**

+ * These are the options for the node configuration setting {@link ConfigureForm#setItemReply(ItemReply)},

+ * which defines who should receive replies to items.

+ * 

+ * @author Robin Collier

+ */

+public enum ItemReply

+{

+	/** The node owner */

+	owner,

+	

+	/** The item publisher */

+	publisher;

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/ItemsExtension.java b/src/org/jivesoftware/smackx/pubsub/ItemsExtension.java
new file mode 100644
index 0000000..c98d93a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/ItemsExtension.java
@@ -0,0 +1,196 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.List;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+

+/**

+ * This class is used to for multiple purposes.  

+ * <li>It can represent an event containing a list of items that have been published

+ * <li>It can represent an event containing a list of retracted (deleted) items.

+ * <li>It can represent a request to delete a list of items.

+ * <li>It can represent a request to get existing items.

+ * 

+ * <p><b>Please note, this class is used for internal purposes, and is not required for usage of 

+ * pubsub functionality.</b>

+ * 

+ * @author Robin Collier

+ */

+public class ItemsExtension extends NodeExtension implements EmbeddedPacketExtension

+{

+	protected ItemsElementType type;

+	protected Boolean notify;

+	protected List<? extends PacketExtension> items;

+

+	public enum ItemsElementType

+	{

+		/** An items element, which has an optional <b>max_items</b> attribute when requesting items */

+		items(PubSubElementType.ITEMS, "max_items"),

+		

+		/** A retract element, which has an optional <b>notify</b> attribute when publishing deletions */

+		retract(PubSubElementType.RETRACT, "notify");

+		

+		private PubSubElementType elem;

+		private String att;

+		

+		private ItemsElementType(PubSubElementType nodeElement, String attribute)

+		{

+			elem = nodeElement;

+			att = attribute;

+		}

+		

+		public PubSubElementType getNodeElement()

+		{

+			return elem;

+		}

+

+		public String getElementAttribute()

+		{

+			return att;

+		}

+	}

+

+	/**

+	 * Construct an instance with a list representing items that have been published or deleted.

+	 * 

+	 * <p>Valid scenarios are:

+	 * <li>Request items from node - itemsType = {@link ItemsElementType#items}, items = list of {@link Item} and an

+	 * optional value for the <b>max_items</b> attribute.

+	 * <li>Request to delete items - itemsType = {@link ItemsElementType#retract}, items = list of {@link Item} containing

+	 * only id's and an optional value for the <b>notify</b> attribute.

+	 * <li>Items published event - itemsType = {@link ItemsElementType#items}, items = list of {@link Item} and 

+	 * attributeValue = <code>null</code>

+	 * <li>Items deleted event -  itemsType = {@link ItemsElementType#items}, items = list of {@link RetractItem} and 

+	 * attributeValue = <code>null</code> 

+	 * 

+	 * @param itemsType Type of representation

+	 * @param nodeId The node to which the items are being sent or deleted

+	 * @param items The list of {@link Item} or {@link RetractItem}

+	 * @param attributeValue The value of the <b>max_items</b>  

+	 */

+	public ItemsExtension(ItemsElementType itemsType, String nodeId, List<? extends PacketExtension> items)

+	{

+		super(itemsType.getNodeElement(), nodeId);

+		type = itemsType;

+		this.items = items;

+	}

+	

+	/**

+	 * Construct an instance with a list representing items that have been published or deleted.

+	 * 

+	 * <p>Valid scenarios are:

+	 * <li>Request items from node - itemsType = {@link ItemsElementType#items}, items = list of {@link Item} and an

+	 * optional value for the <b>max_items</b> attribute.

+	 * <li>Request to delete items - itemsType = {@link ItemsElementType#retract}, items = list of {@link Item} containing

+	 * only id's and an optional value for the <b>notify</b> attribute.

+	 * <li>Items published event - itemsType = {@link ItemsElementType#items}, items = list of {@link Item} and 

+	 * attributeValue = <code>null</code>

+	 * <li>Items deleted event -  itemsType = {@link ItemsElementType#items}, items = list of {@link RetractItem} and 

+	 * attributeValue = <code>null</code> 

+	 * 

+	 * @param itemsType Type of representation

+	 * @param nodeId The node to which the items are being sent or deleted

+	 * @param items The list of {@link Item} or {@link RetractItem}

+	 * @param attributeValue The value of the <b>max_items</b>  

+	 */

+	public ItemsExtension(String nodeId, List<? extends PacketExtension> items, boolean notify)

+	{

+		super(ItemsElementType.retract.getNodeElement(), nodeId);

+		type = ItemsElementType.retract;

+		this.items = items; 

+		this.notify = notify;

+	}

+	

+	/**

+	 * Get the type of element

+	 * 

+	 * @return The element type

+	 */

+	public ItemsElementType getItemsElementType()

+	{

+		return type;

+	}

+	

+	public List<PacketExtension> getExtensions()

+	{

+		return (List<PacketExtension>)getItems();

+	}

+	

+	/**

+	 * Gets the items related to the type of request or event.

+	 * 

+	 * return List of {@link Item}, {@link RetractItem}, or null

+	 */

+	public List<? extends PacketExtension> getItems()

+	{

+		return items;

+	}

+

+	/**

+	 * Gets the value of the optional attribute related to the {@link ItemsElementType}.

+	 * 

+	 * @return The attribute value

+	 */

+	public boolean getNotify()

+	{

+		return notify;

+	}

+	

+	@Override

+	public String toXML()

+	{

+		if ((items == null) || (items.size() == 0))

+		{

+			return super.toXML();

+		}

+		else

+		{

+			StringBuilder builder = new StringBuilder("<");

+			builder.append(getElementName());

+			builder.append(" node='");

+			builder.append(getNode());

+			

+			if (notify != null)

+			{

+				builder.append("' ");

+				builder.append(type.getElementAttribute());

+				builder.append("='");

+				builder.append(notify.equals(Boolean.TRUE) ? 1 : 0);

+				builder.append("'>");

+			}

+			else

+			{

+				builder.append("'>");

+				for (PacketExtension item : items)

+				{

+					builder.append(item.toXML());

+				}

+			}

+			

+			builder.append("</");

+			builder.append(getElementName());

+			builder.append(">");

+			return builder.toString();

+		}

+	}

+

+	@Override

+	public String toString()

+	{

+		return getClass().getName() + "Content [" + toXML() + "]";

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/LeafNode.java b/src/org/jivesoftware/smackx/pubsub/LeafNode.java
new file mode 100644
index 0000000..eee6293
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/LeafNode.java
@@ -0,0 +1,352 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.List;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.packet.IQ.Type;

+import org.jivesoftware.smackx.packet.DiscoverItems;

+import org.jivesoftware.smackx.pubsub.packet.PubSub;

+import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;

+

+/**

+ * The main class for the majority of pubsub functionality.  In general

+ * almost all pubsub capabilities are related to the concept of a node.

+ * All items are published to a node, and typically subscribed to by other

+ * users.  These users then retrieve events based on this subscription.

+ * 

+ * @author Robin Collier

+ */

+public class LeafNode extends Node

+{

+	LeafNode(Connection connection, String nodeName)

+	{

+		super(connection, nodeName);

+	}

+	

+	/**

+	 * Get information on the items in the node in standard

+	 * {@link DiscoverItems} format.

+	 * 

+	 * @return The item details in {@link DiscoverItems} format

+	 * 

+	 * @throws XMPPException

+	 */

+	public DiscoverItems discoverItems()

+		throws XMPPException

+	{

+		DiscoverItems items = new DiscoverItems();

+		items.setTo(to);

+		items.setNode(getId());

+		return (DiscoverItems)SyncPacketSend.getReply(con, items);

+	}

+

+	/**

+	 * Get the current items stored in the node.

+	 * 

+	 * @return List of {@link Item} in the node

+	 * 

+	 * @throws XMPPException

+	 */

+	public <T extends Item> List<T> getItems()

+		throws XMPPException

+	{

+		PubSub request = createPubsubPacket(Type.GET, new GetItemsRequest(getId()));

+		

+		PubSub result = (PubSub)SyncPacketSend.getReply(con, request);

+		ItemsExtension itemsElem = (ItemsExtension)result.getExtension(PubSubElementType.ITEMS);

+		return (List<T>)itemsElem.getItems();

+	}

+	

+	/**

+	 * Get the current items stored in the node based

+	 * on the subscription associated with the provided 

+	 * subscription id.

+	 * 

+	 * @param subscriptionId -  The subscription id for the 

+	 * associated subscription.

+	 * @return List of {@link Item} in the node

+	 * 

+	 * @throws XMPPException

+	 */

+	public <T extends Item> List<T> getItems(String subscriptionId)

+		throws XMPPException

+	{

+		PubSub request = createPubsubPacket(Type.GET, new GetItemsRequest(getId(), subscriptionId));

+		

+		PubSub result = (PubSub)SyncPacketSend.getReply(con, request);

+		ItemsExtension itemsElem = (ItemsExtension)result.getExtension(PubSubElementType.ITEMS);

+		return (List<T>)itemsElem.getItems();

+	}

+

+	/**

+	 * Get the items specified from the node.  This would typically be

+	 * used when the server does not return the payload due to size 

+	 * constraints.  The user would be required to retrieve the payload 

+	 * after the items have been retrieved via {@link #getItems()} or an

+	 * event, that did not include the payload.

+	 * 

+	 * @param ids Item ids of the items to retrieve

+	 * 

+	 * @return The list of {@link Item} with payload

+	 * 

+	 * @throws XMPPException

+	 */

+	public <T extends Item> List<T> getItems(Collection<String> ids)

+		throws XMPPException

+	{

+		List<Item> itemList = new ArrayList<Item>(ids.size());

+		

+		for (String id : ids)

+		{

+			itemList.add(new Item(id));

+		}

+		PubSub request = createPubsubPacket(Type.GET, new ItemsExtension(ItemsExtension.ItemsElementType.items, getId(), itemList));

+		

+		PubSub result = (PubSub)SyncPacketSend.getReply(con, request);

+		ItemsExtension itemsElem = (ItemsExtension)result.getExtension(PubSubElementType.ITEMS);

+		return (List<T>)itemsElem.getItems();

+	}

+

+	/**

+	 * Get items persisted on the node, limited to the specified number.

+	 * 

+	 * @param maxItems Maximum number of items to return

+	 * 

+	 * @return List of {@link Item}

+	 * 

+	 * @throws XMPPException

+	 */

+	public <T extends Item> List<T> getItems(int maxItems)

+		throws XMPPException

+	{

+		PubSub request = createPubsubPacket(Type.GET, new GetItemsRequest(getId(), maxItems));

+		

+		PubSub result = (PubSub)SyncPacketSend.getReply(con, request);

+		ItemsExtension itemsElem = (ItemsExtension)result.getExtension(PubSubElementType.ITEMS);

+		return (List<T>)itemsElem.getItems();

+	}

+	

+	/**

+	 * Get items persisted on the node, limited to the specified number

+	 * based on the subscription associated with the provided subscriptionId.

+	 * 

+	 * @param maxItems Maximum number of items to return

+	 * @param subscriptionId The subscription which the retrieval is based

+	 * on.

+	 * 

+	 * @return List of {@link Item}

+	 * 

+	 * @throws XMPPException

+	 */

+	public <T extends Item> List<T> getItems(int maxItems, String subscriptionId)

+		throws XMPPException

+	{

+		PubSub request = createPubsubPacket(Type.GET, new GetItemsRequest(getId(), subscriptionId, maxItems));

+		

+		PubSub result = (PubSub)SyncPacketSend.getReply(con, request);

+		ItemsExtension itemsElem = (ItemsExtension)result.getExtension(PubSubElementType.ITEMS);

+		return (List<T>)itemsElem.getItems();

+	}

+	

+	/**

+	 * Publishes an event to the node.  This is an empty event

+	 * with no item.

+	 * 

+	 * This is only acceptable for nodes with {@link ConfigureForm#isPersistItems()}=false

+	 * and {@link ConfigureForm#isDeliverPayloads()}=false.

+	 * 

+	 * This is an asynchronous call which returns as soon as the 

+	 * packet has been sent.

+	 * 

+	 * For synchronous calls use {@link #send() send()}.

+	 */

+	public void publish()

+	{

+		PubSub packet = createPubsubPacket(Type.SET, new NodeExtension(PubSubElementType.PUBLISH, getId()));

+		

+		con.sendPacket(packet);

+	}

+	

+	/**

+	 * Publishes an event to the node.  This is a simple item

+	 * with no payload.

+	 * 

+	 * If the id is null, an empty item (one without an id) will be sent.

+	 * Please note that this is not the same as {@link #send()}, which

+	 * publishes an event with NO item.

+	 * 

+	 * This is an asynchronous call which returns as soon as the 

+	 * packet has been sent.

+	 * 

+	 * For synchronous calls use {@link #send(Item) send(Item))}.

+	 * 

+	 * @param item - The item being sent

+	 */

+	public <T extends Item> void publish(T item)

+	{

+		Collection<T> items = new ArrayList<T>(1);

+		items.add((T)(item == null ? new Item() : item));

+		publish(items);

+	}

+

+	/**

+	 * Publishes multiple events to the node.  Same rules apply as in {@link #publish(Item)}.

+	 * 

+	 * In addition, if {@link ConfigureForm#isPersistItems()}=false, only the last item in the input

+	 * list will get stored on the node, assuming it stores the last sent item.

+	 * 

+	 * This is an asynchronous call which returns as soon as the 

+	 * packet has been sent.

+	 * 

+	 * For synchronous calls use {@link #send(Collection) send(Collection))}.

+	 * 

+	 * @param items - The collection of items being sent

+	 */

+	public <T extends Item> void publish(Collection<T> items)

+	{

+		PubSub packet = createPubsubPacket(Type.SET, new PublishItem<T>(getId(), items));

+		

+		con.sendPacket(packet);

+	}

+

+	/**

+	 * Publishes an event to the node.  This is an empty event

+	 * with no item.

+	 * 

+	 * This is only acceptable for nodes with {@link ConfigureForm#isPersistItems()}=false

+	 * and {@link ConfigureForm#isDeliverPayloads()}=false.

+	 * 

+	 * This is a synchronous call which will throw an exception 

+	 * on failure.

+	 * 

+	 * For asynchronous calls, use {@link #publish() publish()}.

+	 * 

+	 * @throws XMPPException

+	 */

+	public void send()

+		throws XMPPException

+	{

+		PubSub packet = createPubsubPacket(Type.SET, new NodeExtension(PubSubElementType.PUBLISH, getId()));

+		

+		SyncPacketSend.getReply(con, packet);

+	}

+	

+	/**

+	 * Publishes an event to the node.  This can be either a simple item

+	 * with no payload, or one with it.  This is determined by the Node

+	 * configuration.

+	 * 

+	 * If the node has <b>deliver_payload=false</b>, the Item must not

+	 * have a payload.

+	 * 

+	 * If the id is null, an empty item (one without an id) will be sent.

+	 * Please note that this is not the same as {@link #send()}, which

+	 * publishes an event with NO item.

+	 * 

+	 * This is a synchronous call which will throw an exception 

+	 * on failure.

+	 * 

+	 * For asynchronous calls, use {@link #publish(Item) publish(Item)}.

+	 * 

+	 * @param item - The item being sent

+	 * 

+	 * @throws XMPPException

+	 */

+	public <T extends Item> void send(T item)

+		throws XMPPException

+	{

+		Collection<T> items = new ArrayList<T>(1);

+		items.add((item == null ? (T)new Item() : item));

+		send(items);

+	}

+	

+	/**

+	 * Publishes multiple events to the node.  Same rules apply as in {@link #send(Item)}.

+	 * 

+	 * In addition, if {@link ConfigureForm#isPersistItems()}=false, only the last item in the input

+	 * list will get stored on the node, assuming it stores the last sent item.

+	 *  

+	 * This is a synchronous call which will throw an exception 

+	 * on failure.

+	 * 

+	 * For asynchronous calls, use {@link #publish(Collection) publish(Collection))}.

+	 * 

+	 * @param items - The collection of {@link Item} objects being sent

+	 * 

+	 * @throws XMPPException

+	 */

+	public <T extends Item> void send(Collection<T> items)

+		throws XMPPException

+	{

+		PubSub packet = createPubsubPacket(Type.SET, new PublishItem<T>(getId(), items));

+		

+		SyncPacketSend.getReply(con, packet);

+	}

+	

+	/**

+	 * Purges the node of all items.

+	 *   

+	 * <p>Note: Some implementations may keep the last item

+	 * sent.

+	 * 

+	 * @throws XMPPException

+	 */

+	public void deleteAllItems()

+		throws XMPPException

+	{

+		PubSub request = createPubsubPacket(Type.SET, new NodeExtension(PubSubElementType.PURGE_OWNER, getId()), PubSubElementType.PURGE_OWNER.getNamespace());

+		

+		SyncPacketSend.getReply(con, request);

+	}

+	

+	/**

+	 * Delete the item with the specified id from the node.

+	 * 

+	 * @param itemId The id of the item

+	 * 

+	 * @throws XMPPException

+	 */

+	public void deleteItem(String itemId)

+		throws XMPPException

+	{

+		Collection<String> items = new ArrayList<String>(1);

+		items.add(itemId);

+		deleteItem(items);

+	}

+	

+	/**

+	 * Delete the items with the specified id's from the node.

+	 * 

+	 * @param itemIds The list of id's of items to delete

+	 * 

+	 * @throws XMPPException

+	 */

+	public void deleteItem(Collection<String> itemIds)

+		throws XMPPException

+	{

+		List<Item> items = new ArrayList<Item>(itemIds.size());

+		

+		for (String id : itemIds)

+		{

+			items.add(new Item(id));

+		}

+		PubSub request = createPubsubPacket(Type.SET, new ItemsExtension(ItemsExtension.ItemsElementType.retract, getId(), items));

+		SyncPacketSend.getReply(con, request);

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/Node.java b/src/org/jivesoftware/smackx/pubsub/Node.java
new file mode 100644
index 0000000..1b0ff5a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/Node.java
@@ -0,0 +1,541 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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.smackx.pubsub;

+

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.Iterator;

+import java.util.List;

+import java.util.concurrent.ConcurrentHashMap;

+

+import org.jivesoftware.smack.PacketListener;

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.filter.OrFilter;

+import org.jivesoftware.smack.filter.PacketFilter;

+import org.jivesoftware.smack.packet.Message;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.packet.IQ.Type;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.packet.DelayInformation;

+import org.jivesoftware.smackx.packet.DiscoverInfo;

+import org.jivesoftware.smackx.packet.Header;

+import org.jivesoftware.smackx.packet.HeadersExtension;

+import org.jivesoftware.smackx.pubsub.listener.ItemDeleteListener;

+import org.jivesoftware.smackx.pubsub.listener.ItemEventListener;

+import org.jivesoftware.smackx.pubsub.listener.NodeConfigListener;

+import org.jivesoftware.smackx.pubsub.packet.PubSub;

+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;

+import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;

+import org.jivesoftware.smackx.pubsub.util.NodeUtils;

+

+abstract public class Node

+{

+	protected Connection con;

+	protected String id;

+	protected String to;

+	

+	protected ConcurrentHashMap<ItemEventListener<Item>, PacketListener> itemEventToListenerMap = new ConcurrentHashMap<ItemEventListener<Item>, PacketListener>();

+	protected ConcurrentHashMap<ItemDeleteListener, PacketListener> itemDeleteToListenerMap = new ConcurrentHashMap<ItemDeleteListener, PacketListener>();

+	protected ConcurrentHashMap<NodeConfigListener, PacketListener> configEventToListenerMap = new ConcurrentHashMap<NodeConfigListener, PacketListener>();

+	

+	/**

+	 * Construct a node associated to the supplied connection with the specified 

+	 * node id.

+	 * 

+	 * @param connection The connection the node is associated with

+	 * @param nodeName The node id

+	 */

+	Node(Connection connection, String nodeName)

+	{

+		con = connection;

+		id = nodeName;

+	}

+

+	/**

+	 * Some XMPP servers may require a specific service to be addressed on the 

+	 * server.

+	 * 

+	 *   For example, OpenFire requires the server to be prefixed by <b>pubsub</b>

+	 */

+	void setTo(String toAddress)

+	{

+		to = toAddress;

+	}

+

+	/**

+	 * Get the NodeId

+	 * 

+	 * @return the node id

+	 */

+	public String getId() 

+	{

+		return id;

+	}

+	/**

+	 * Returns a configuration form, from which you can create an answer form to be submitted

+	 * via the {@link #sendConfigurationForm(Form)}.

+	 * 

+	 * @return the configuration form

+	 */

+	public ConfigureForm getNodeConfiguration()

+		throws XMPPException

+	{

+		Packet reply = sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.CONFIGURE_OWNER, getId()), PubSubNamespace.OWNER);

+		return NodeUtils.getFormFromPacket(reply, PubSubElementType.CONFIGURE_OWNER);

+	}

+	

+	/**

+	 * Update the configuration with the contents of the new {@link Form}

+	 * 

+	 * @param submitForm

+	 */

+	public void sendConfigurationForm(Form submitForm)

+		throws XMPPException

+	{

+		PubSub packet = createPubsubPacket(Type.SET, new FormNode(FormNodeType.CONFIGURE_OWNER, getId(), submitForm), PubSubNamespace.OWNER);

+		SyncPacketSend.getReply(con, packet);

+	}

+	

+	/**

+	 * Discover node information in standard {@link DiscoverInfo} format.

+	 * 

+	 * @return The discovery information about the node.

+	 * 

+	 * @throws XMPPException

+	 */

+	public DiscoverInfo discoverInfo()

+		throws XMPPException

+	{

+		DiscoverInfo info = new DiscoverInfo();

+		info.setTo(to);

+		info.setNode(getId());

+		return (DiscoverInfo)SyncPacketSend.getReply(con, info);

+	}

+	

+	/**

+	 * Get the subscriptions currently associated with this node.

+	 * 

+	 * @return List of {@link Subscription}

+	 * 

+	 * @throws XMPPException

+	 */

+	public List<Subscription> getSubscriptions()

+		throws XMPPException

+	{

+		PubSub reply = (PubSub)sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.SUBSCRIPTIONS, getId()));

+		SubscriptionsExtension subElem = (SubscriptionsExtension)reply.getExtension(PubSubElementType.SUBSCRIPTIONS);

+		return subElem.getSubscriptions();

+	}

+

+	/**

+	 * The user subscribes to the node using the supplied jid.  The

+	 * bare jid portion of this one must match the jid for the connection.

+	 * 

+	 * Please note that the {@link Subscription.State} should be checked 

+	 * on return since more actions may be required by the caller.

+	 * {@link Subscription.State#pending} - The owner must approve the subscription 

+	 * request before messages will be received.

+	 * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, 

+	 * the caller must configure the subscription before messages will be received.  If it is false

+	 * the caller can configure it but is not required to do so.

+	 * @param jid The jid to subscribe as.

+	 * @return The subscription

+	 * @exception XMPPException

+	 */

+	public Subscription subscribe(String jid)

+		throws XMPPException

+	{

+		PubSub reply = (PubSub)sendPubsubPacket(Type.SET, new SubscribeExtension(jid, getId()));

+		return (Subscription)reply.getExtension(PubSubElementType.SUBSCRIPTION);

+	}

+	

+	/**

+	 * The user subscribes to the node using the supplied jid and subscription

+	 * options.  The bare jid portion of this one must match the jid for the 

+	 * connection.

+	 * 

+	 * Please note that the {@link Subscription.State} should be checked 

+	 * on return since more actions may be required by the caller.

+	 * {@link Subscription.State#pending} - The owner must approve the subscription 

+	 * request before messages will be received.

+	 * {@link Subscription.State#unconfigured} - If the {@link Subscription#isConfigRequired()} is true, 

+	 * the caller must configure the subscription before messages will be received.  If it is false

+	 * the caller can configure it but is not required to do so.

+	 * @param jid The jid to subscribe as.

+	 * @return The subscription

+	 * @exception XMPPException

+	 */

+	public Subscription subscribe(String jid, SubscribeForm subForm)

+		throws XMPPException

+	{

+		PubSub request = createPubsubPacket(Type.SET, new SubscribeExtension(jid, getId()));

+		request.addExtension(new FormNode(FormNodeType.OPTIONS, subForm));

+		PubSub reply = (PubSub)PubSubManager.sendPubsubPacket(con, jid, Type.SET, request);

+		return (Subscription)reply.getExtension(PubSubElementType.SUBSCRIPTION);

+	}

+

+	/**

+	 * Remove the subscription related to the specified JID.  This will only 

+	 * work if there is only 1 subscription.  If there are multiple subscriptions,

+	 * use {@link #unsubscribe(String, String)}.

+	 * 

+	 * @param jid The JID used to subscribe to the node

+	 * 

+	 * @throws XMPPException

+	 */

+	public void unsubscribe(String jid)

+		throws XMPPException

+	{

+		unsubscribe(jid, null);

+	}

+	

+	/**

+	 * Remove the specific subscription related to the specified JID.

+	 * 

+	 * @param jid The JID used to subscribe to the node

+	 * @param subscriptionId The id of the subscription being removed

+	 * 

+	 * @throws XMPPException

+	 */

+	public void unsubscribe(String jid, String subscriptionId)

+		throws XMPPException

+	{

+		sendPubsubPacket(Type.SET, new UnsubscribeExtension(jid, getId(), subscriptionId));

+	}

+

+	/**

+	 * Returns a SubscribeForm for subscriptions, from which you can create an answer form to be submitted

+	 * via the {@link #sendConfigurationForm(Form)}.

+	 * 

+	 * @return A subscription options form

+	 * 

+	 * @throws XMPPException

+	 */

+	public SubscribeForm getSubscriptionOptions(String jid)

+		throws XMPPException

+	{

+		return getSubscriptionOptions(jid, null);

+	}

+

+

+	/**

+	 * Get the options for configuring the specified subscription.

+	 * 

+	 * @param jid JID the subscription is registered under

+	 * @param subscriptionId The subscription id

+	 * 

+	 * @return The subscription option form

+	 * 

+	 * @throws XMPPException

+	 */

+	public SubscribeForm getSubscriptionOptions(String jid, String subscriptionId)

+		throws XMPPException

+	{

+		PubSub packet = (PubSub)sendPubsubPacket(Type.GET, new OptionsExtension(jid, getId(), subscriptionId));

+		FormNode ext = (FormNode)packet.getExtension(PubSubElementType.OPTIONS);

+		return new SubscribeForm(ext.getForm());

+	}

+

+	/**

+	 * Register a listener for item publication events.  This 

+	 * listener will get called whenever an item is published to 

+	 * this node.

+	 * 

+	 * @param listener The handler for the event

+	 */

+	public void addItemEventListener(ItemEventListener listener)

+	{

+		PacketListener conListener = new ItemEventTranslator(listener); 

+		itemEventToListenerMap.put(listener, conListener);

+		con.addPacketListener(conListener, new EventContentFilter(EventElementType.items.toString(), "item"));

+	}

+

+	/**

+	 * Unregister a listener for publication events.

+	 * 

+	 * @param listener The handler to unregister

+	 */

+	public void removeItemEventListener(ItemEventListener listener)

+	{

+		PacketListener conListener = itemEventToListenerMap.remove(listener);

+		

+		if (conListener != null)

+			con.removePacketListener(conListener);

+	}

+

+	/**

+	 * Register a listener for configuration events.  This listener

+	 * will get called whenever the node's configuration changes.

+	 * 

+	 * @param listener The handler for the event

+	 */

+	public void addConfigurationListener(NodeConfigListener listener)

+	{

+		PacketListener conListener = new NodeConfigTranslator(listener); 

+		configEventToListenerMap.put(listener, conListener);

+		con.addPacketListener(conListener, new EventContentFilter(EventElementType.configuration.toString()));

+	}

+

+	/**

+	 * Unregister a listener for configuration events.

+	 * 

+	 * @param listener The handler to unregister

+	 */

+	public void removeConfigurationListener(NodeConfigListener listener)

+	{

+		PacketListener conListener = configEventToListenerMap .remove(listener);

+		

+		if (conListener != null)

+			con.removePacketListener(conListener);

+	}

+	

+	/**

+	 * Register an listener for item delete events.  This listener

+	 * gets called whenever an item is deleted from the node.

+	 * 

+	 * @param listener The handler for the event

+	 */

+	public void addItemDeleteListener(ItemDeleteListener listener)

+	{

+		PacketListener delListener = new ItemDeleteTranslator(listener); 

+		itemDeleteToListenerMap.put(listener, delListener);

+		EventContentFilter deleteItem = new EventContentFilter(EventElementType.items.toString(), "retract");

+		EventContentFilter purge = new EventContentFilter(EventElementType.purge.toString());

+		

+		con.addPacketListener(delListener, new OrFilter(deleteItem, purge));

+	}

+

+	/**

+	 * Unregister a listener for item delete events.

+	 * 

+	 * @param listener The handler to unregister

+	 */

+	public void removeItemDeleteListener(ItemDeleteListener listener)

+	{

+		PacketListener conListener = itemDeleteToListenerMap .remove(listener);

+		

+		if (conListener != null)

+			con.removePacketListener(conListener);

+	}

+

+	@Override

+	public String toString()

+	{

+		return super.toString() + " " + getClass().getName() + " id: " + id;

+	}

+	

+	protected PubSub createPubsubPacket(Type type, PacketExtension ext)

+	{

+		return createPubsubPacket(type, ext, null);

+	}

+	

+	protected PubSub createPubsubPacket(Type type, PacketExtension ext, PubSubNamespace ns)

+	{

+		return PubSubManager.createPubsubPacket(to, type, ext, ns);

+	}

+

+	protected Packet sendPubsubPacket(Type type, NodeExtension ext)

+		throws XMPPException

+	{

+		return PubSubManager.sendPubsubPacket(con, to, type, ext);

+	}

+

+	protected Packet sendPubsubPacket(Type type, NodeExtension ext, PubSubNamespace ns)

+		throws XMPPException

+	{

+		return PubSubManager.sendPubsubPacket(con, to, type, ext, ns);

+	}

+

+

+	private static List<String> getSubscriptionIds(Packet packet)

+	{

+		HeadersExtension headers = (HeadersExtension)packet.getExtension("headers", "http://jabber.org/protocol/shim");

+		List<String> values = null;

+		

+		if (headers != null)

+		{

+			values = new ArrayList<String>(headers.getHeaders().size());

+			

+			for (Header header : headers.getHeaders())

+			{

+				values.add(header.getValue());

+			}

+		}

+		return values;

+	}

+

+	/**

+	 * This class translates low level item publication events into api level objects for 

+	 * user consumption.

+	 * 

+	 * @author Robin Collier

+	 */

+	public class ItemEventTranslator implements PacketListener

+	{

+		private ItemEventListener listener;

+

+		public ItemEventTranslator(ItemEventListener eventListener)

+		{

+			listener = eventListener;

+		}

+		

+		public void processPacket(Packet packet)

+		{

+	        EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());

+			ItemsExtension itemsElem = (ItemsExtension)event.getEvent();

+			DelayInformation delay = (DelayInformation)packet.getExtension("delay", "urn:xmpp:delay");

+			

+			// If there was no delay based on XEP-0203, then try XEP-0091 for backward compatibility

+			if (delay == null)

+			{

+				delay = (DelayInformation)packet.getExtension("x", "jabber:x:delay");

+			}

+			ItemPublishEvent eventItems = new ItemPublishEvent(itemsElem.getNode(), (List<Item>)itemsElem.getItems(), getSubscriptionIds(packet), (delay == null ? null : delay.getStamp()));

+			listener.handlePublishedItems(eventItems);

+		}

+	}

+

+	/**

+	 * This class translates low level item deletion events into api level objects for 

+	 * user consumption.

+	 * 

+	 * @author Robin Collier

+	 */

+	public class ItemDeleteTranslator implements PacketListener

+	{

+		private ItemDeleteListener listener;

+

+		public ItemDeleteTranslator(ItemDeleteListener eventListener)

+		{

+			listener = eventListener;

+		}

+		

+		public void processPacket(Packet packet)

+		{

+	        EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());

+	        

+	        List<PacketExtension> extList = event.getExtensions();

+	        

+	        if (extList.get(0).getElementName().equals(PubSubElementType.PURGE_EVENT.getElementName()))

+	        {

+	        	listener.handlePurge();

+	        }

+	        else

+	        {

+				ItemsExtension itemsElem = (ItemsExtension)event.getEvent();

+				Collection<? extends PacketExtension> pubItems = itemsElem.getItems();

+				Iterator<RetractItem> it = (Iterator<RetractItem>)pubItems.iterator();

+				List<String> items = new ArrayList<String>(pubItems.size());

+

+				while (it.hasNext())

+				{

+					RetractItem item = it.next();

+					items.add(item.getId());

+				}

+

+				ItemDeleteEvent eventItems = new ItemDeleteEvent(itemsElem.getNode(), items, getSubscriptionIds(packet));

+				listener.handleDeletedItems(eventItems);

+	        }

+		}

+	}

+	

+	/**

+	 * This class translates low level node configuration events into api level objects for 

+	 * user consumption.

+	 * 

+	 * @author Robin Collier

+	 */

+	public class NodeConfigTranslator implements PacketListener

+	{

+		private NodeConfigListener listener;

+

+		public NodeConfigTranslator(NodeConfigListener eventListener)

+		{

+			listener = eventListener;

+		}

+		

+		public void processPacket(Packet packet)

+		{

+	        EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());

+			ConfigurationEvent config = (ConfigurationEvent)event.getEvent();

+

+			listener.handleNodeConfiguration(config);

+		}

+	}

+

+	/**

+	 * Filter for {@link PacketListener} to filter out events not specific to the 

+	 * event type expected for this node.

+	 * 

+	 * @author Robin Collier

+	 */

+	class EventContentFilter implements PacketFilter

+	{

+		private String firstElement;

+		private String secondElement;

+		

+		EventContentFilter(String elementName)

+		{

+			firstElement = elementName;

+		}

+

+		EventContentFilter(String firstLevelEelement, String secondLevelElement)

+		{

+			firstElement = firstLevelEelement;

+			secondElement = secondLevelElement;

+		}

+

+		public boolean accept(Packet packet)

+		{

+			if (!(packet instanceof Message))

+				return false;

+

+			EventElement event = (EventElement)packet.getExtension("event", PubSubNamespace.EVENT.getXmlns());

+			

+			if (event == null)

+				return false;

+

+			NodeExtension embedEvent = event.getEvent();

+			

+			if (embedEvent == null)

+				return false;

+			

+			if (embedEvent.getElementName().equals(firstElement))

+			{

+				if (!embedEvent.getNode().equals(getId()))

+					return false;

+				

+				if (secondElement == null)

+					return true;

+				

+				if (embedEvent instanceof EmbeddedPacketExtension)

+				{

+					List<PacketExtension> secondLevelList = ((EmbeddedPacketExtension)embedEvent).getExtensions();

+					

+					if (secondLevelList.size() > 0 && secondLevelList.get(0).getElementName().equals(secondElement))

+						return true;

+				}

+			}

+			return false;

+		}

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/NodeEvent.java b/src/org/jivesoftware/smackx/pubsub/NodeEvent.java
new file mode 100644
index 0000000..1392e85
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/NodeEvent.java
@@ -0,0 +1,35 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 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.smackx.pubsub;

+

+abstract public class NodeEvent

+{

+	private String nodeId;

+	

+	protected NodeEvent(String id)

+	{

+		nodeId = id;

+	}

+	

+	public String getNodeId()

+	{

+		return nodeId;

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/NodeExtension.java b/src/org/jivesoftware/smackx/pubsub/NodeExtension.java
new file mode 100644
index 0000000..7e4cdec
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/NodeExtension.java
@@ -0,0 +1,85 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+

+/**

+ * A class which represents a common element within the pubsub defined

+ * schemas.  One which has a <b>node</b> as an attribute.  This class is 

+ * used on its own as well as a base class for many others, since the 

+ * node is a central concept to most pubsub functionality.

+ * 

+ * @author Robin Collier

+ */

+public class NodeExtension implements PacketExtension

+{

+	private PubSubElementType element;

+	private String node;

+	

+	/**

+	 * Constructs a <tt>NodeExtension</tt> with an element name specified

+	 * by {@link PubSubElementType} and the specified node id.

+	 * 

+	 * @param elem Defines the element name and namespace

+	 * @param nodeId Specifies the id of the node

+	 */

+	public NodeExtension(PubSubElementType elem, String nodeId)

+	{

+		element = elem;

+		this.node = nodeId;

+	}

+

+	/**

+	 * Constructs a <tt>NodeExtension</tt> with an element name specified

+	 * by {@link PubSubElementType}.

+	 * 

+	 * @param elem Defines the element name and namespace

+	 */

+	public NodeExtension(PubSubElementType elem)

+	{

+		this(elem, null);

+	}

+

+	/**

+	 * Gets the node id

+	 * 

+	 * @return The node id

+	 */

+	public String getNode()

+	{

+		return node;

+	}

+	

+	public String getElementName()

+	{

+		return element.getElementName();

+	}

+

+	public String getNamespace()

+	{

+		return element.getNamespace().getXmlns();

+	}

+

+	public String toXML()

+	{

+		return '<' + getElementName() + (node == null ? "" : " node='" + node + '\'') + "/>";

+	}

+

+	@Override

+	public String toString()

+	{

+		return getClass().getName() + " - content [" + toXML() + "]";

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/NodeType.java b/src/org/jivesoftware/smackx/pubsub/NodeType.java
new file mode 100644
index 0000000..5ee5a05
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/NodeType.java
@@ -0,0 +1,25 @@
+/**

+ * 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.smackx.pubsub;

+

+/**

+ * Defines the available types of nodes

+ * 

+ * @author Robin Collier

+ */

+public enum NodeType

+{

+	leaf,

+	collection;

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/OptionsExtension.java b/src/org/jivesoftware/smackx/pubsub/OptionsExtension.java
new file mode 100644
index 0000000..32c0331
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/OptionsExtension.java
@@ -0,0 +1,72 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smackx.pubsub.util.XmlUtils;

+

+/**

+ * A packet extension representing the <b>options</b> element. 

+ * 

+ * @author Robin Collier

+ */

+public class OptionsExtension extends NodeExtension

+{

+	protected String jid;

+	protected String id;

+	

+	public OptionsExtension(String subscriptionJid)

+	{

+		this(subscriptionJid, null, null);

+	}

+	

+	public OptionsExtension(String subscriptionJid, String nodeId)

+	{

+		this(subscriptionJid, nodeId, null);

+	}

+	

+	public OptionsExtension(String jid, String nodeId, String subscriptionId)

+	{

+		super(PubSubElementType.OPTIONS, nodeId);

+		this.jid = jid;

+		id = subscriptionId;

+	}

+	

+	public String getJid()

+	{

+		return jid;

+	}

+	

+	public String getId()

+	{

+		return id;

+	}

+	

+	@Override

+	public String toXML()

+	{

+		StringBuilder builder = new StringBuilder("<");

+		builder.append(getElementName());

+		XmlUtils.appendAttribute(builder, "jid", jid);

+		

+		if (getNode() != null)

+			XmlUtils.appendAttribute(builder, "node", getNode());

+		

+		if (id != null)

+			XmlUtils.appendAttribute(builder, "subid", id);

+		

+		builder.append("/>");

+		return builder.toString();

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/PayloadItem.java b/src/org/jivesoftware/smackx/pubsub/PayloadItem.java
new file mode 100644
index 0000000..488fd97
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/PayloadItem.java
@@ -0,0 +1,138 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smack.packet.Message;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.pubsub.provider.ItemProvider;

+

+/**

+ * This class represents an item that has been, or will be published to a

+ * pubsub node.  An <tt>Item</tt> has several properties that are dependent

+ * on the configuration of the node to which it has been or will be published.

+ * 

+ * <h1>An Item received from a node (via {@link LeafNode#getItems()} or {@link LeafNode#addItemEventListener(org.jivesoftware.smackx.pubsub.listener.ItemEventListener)}</b>

+ * <li>Will always have an id (either user or server generated) unless node configuration has both

+ * {@link ConfigureForm#isPersistItems()} and {@link ConfigureForm#isDeliverPayloads()}set to false.

+ * <li>Will have a payload if the node configuration has {@link ConfigureForm#isDeliverPayloads()} set 

+ * to true, otherwise it will be null.

+ * 

+ * <h1>An Item created to send to a node (via {@link LeafNode#send()} or {@link LeafNode#publish()}</b>

+ * <li>The id is optional, since the server will generate one if necessary, but should be used if it is 

+ * meaningful in the context of the node.  This value must be unique within the node that it is sent to, since

+ * resending an item with the same id will overwrite the one that already exists if the items are persisted.

+ * <li>Will require payload if the node configuration has {@link ConfigureForm#isDeliverPayloads()} set

+ * to true. 

+ * 

+ * <p>To customise the payload object being returned from the {@link #getPayload()} method, you can

+ * add a custom parser as explained in {@link ItemProvider}.

+ * 

+ * @author Robin Collier

+ */

+public class PayloadItem<E extends PacketExtension> extends Item

+{

+	private E payload;

+	

+	/**

+	 * Create an <tt>Item</tt> with no id and a payload  The id will be set by the server.  

+	 * 

+	 * @param payloadExt A {@link PacketExtension} which represents the payload data.

+	 */

+	public PayloadItem(E payloadExt)

+	{

+		super();

+		

+		if (payloadExt == null)

+			throw new IllegalArgumentException("payload cannot be 'null'");

+		payload = payloadExt;

+	}

+

+	/**

+	 * Create an <tt>Item</tt> with an id and payload.  

+	 * 

+	 * @param itemId The id of this item.  It can be null if we want the server to set the id.

+	 * @param payloadExt A {@link PacketExtension} which represents the payload data.

+	 */

+	public PayloadItem(String itemId, E payloadExt)

+	{

+		super(itemId);

+		

+		if (payloadExt == null)

+			throw new IllegalArgumentException("payload cannot be 'null'");

+		payload = payloadExt;

+	}

+	

+	/**

+	 * Create an <tt>Item</tt> with an id, node id and payload.  

+	 * 

+	 * <p>

+	 * <b>Note:</b> This is not valid for publishing an item to a node, only receiving from 

+	 * one as part of {@link Message}.  If used to create an Item to publish 

+	 * (via {@link LeafNode#publish(Item)}, the server <i>may</i> return an

+	 * error for an invalid packet.

+	 * 

+	 * @param itemId The id of this item.

+	 * @param nodeId The id of the node the item was published to.

+	 * @param payloadExt A {@link PacketExtension} which represents the payload data.

+	 */

+	public PayloadItem(String itemId, String nodeId, E payloadExt)

+	{

+		super(itemId, nodeId);

+		

+		if (payloadExt == null)

+			throw new IllegalArgumentException("payload cannot be 'null'");

+		payload = payloadExt;

+	}

+	

+	/**

+	 * Get the payload associated with this <tt>Item</tt>.  Customising the payload

+	 * parsing from the server can be accomplished as described in {@link ItemProvider}.

+	 * 

+	 * @return The payload as a {@link PacketExtension}.

+	 */

+	public E getPayload()

+	{

+		return payload;

+	}

+	

+	@Override

+	public String toXML()

+	{

+		StringBuilder builder = new StringBuilder("<item");

+		

+		if (getId() != null)

+		{

+			builder.append(" id='");

+			builder.append(getId());

+			builder.append("'");

+		}

+		

+        if (getNode() != null) {

+            builder.append(" node='");

+            builder.append(getNode());

+            builder.append("'");

+        }

+		builder.append(">");

+		builder.append(payload.toXML());

+		builder.append("</item>");

+		

+		return builder.toString();

+	}

+

+	@Override

+	public String toString()

+	{

+		return getClass().getName() + " | Content [" + toXML() + "]";

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/PresenceState.java b/src/org/jivesoftware/smackx/pubsub/PresenceState.java
new file mode 100644
index 0000000..0612fc2
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/PresenceState.java
@@ -0,0 +1,25 @@
+/**

+ * 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.smackx.pubsub;

+

+/** 

+ * Defines the possible valid presence states for node subscription via

+ * {@link SubscribeForm#getShowValues()}.

+ * 

+ * @author Robin Collier

+ */

+public enum PresenceState

+{

+	chat, online, away, xa, dnd

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/PubSubElementType.java b/src/org/jivesoftware/smackx/pubsub/PubSubElementType.java
new file mode 100644
index 0000000..a887ca2
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/PubSubElementType.java
@@ -0,0 +1,80 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;

+

+/**

+ * Defines all the possible element types as defined for all the pubsub

+ * schemas in all 3 namespaces.

+ * 

+ * @author Robin Collier

+ */

+public enum PubSubElementType

+{

+	CREATE("create", PubSubNamespace.BASIC),

+	DELETE("delete", PubSubNamespace.OWNER),

+	DELETE_EVENT("delete", PubSubNamespace.EVENT),

+	CONFIGURE("configure", PubSubNamespace.BASIC),

+	CONFIGURE_OWNER("configure", PubSubNamespace.OWNER),

+	CONFIGURATION("configuration", PubSubNamespace.EVENT),

+	OPTIONS("options", PubSubNamespace.BASIC),

+	DEFAULT("default", PubSubNamespace.OWNER),	

+	ITEMS("items", PubSubNamespace.BASIC),

+	ITEMS_EVENT("items", PubSubNamespace.EVENT),

+	ITEM("item", PubSubNamespace.BASIC),

+	ITEM_EVENT("item", PubSubNamespace.EVENT),

+	PUBLISH("publish", PubSubNamespace.BASIC),

+	PUBLISH_OPTIONS("publish-options", PubSubNamespace.BASIC), 

+	PURGE_OWNER("purge", PubSubNamespace.OWNER),

+	PURGE_EVENT("purge", PubSubNamespace.EVENT),

+	RETRACT("retract", PubSubNamespace.BASIC), 

+	AFFILIATIONS("affiliations", PubSubNamespace.BASIC), 

+	SUBSCRIBE("subscribe", PubSubNamespace.BASIC), 

+	SUBSCRIPTION("subscription", PubSubNamespace.BASIC),

+	SUBSCRIPTIONS("subscriptions", PubSubNamespace.BASIC), 

+	UNSUBSCRIBE("unsubscribe", PubSubNamespace.BASIC);

+

+	private String eName;

+	private PubSubNamespace nSpace;

+	

+	private PubSubElementType(String elemName, PubSubNamespace ns)

+	{

+		eName = elemName;

+		nSpace = ns;

+	}

+	

+	public PubSubNamespace getNamespace()

+	{

+		return nSpace;

+	}

+	

+	public String getElementName()

+	{

+		return eName;

+	}

+	

+	public static PubSubElementType valueOfFromElemName(String elemName, String namespace)

+	{

+		int index = namespace.lastIndexOf('#');

+		String fragment = (index == -1 ? null : namespace.substring(index+1));

+		

+		if (fragment != null)

+		{

+			return valueOf((elemName + '_' + fragment).toUpperCase());

+		}

+		return valueOf(elemName.toUpperCase().replace('-', '_'));

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/PubSubManager.java b/src/org/jivesoftware/smackx/pubsub/PubSubManager.java
new file mode 100644
index 0000000..4fb0158
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/PubSubManager.java
@@ -0,0 +1,329 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.List;

+import java.util.Map;

+import java.util.concurrent.ConcurrentHashMap;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.XMPPException;

+import org.jivesoftware.smack.packet.IQ.Type;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.FormField;

+import org.jivesoftware.smackx.ServiceDiscoveryManager;

+import org.jivesoftware.smackx.packet.DiscoverInfo;

+import org.jivesoftware.smackx.packet.DiscoverItems;

+import org.jivesoftware.smackx.pubsub.packet.PubSub;

+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;

+import org.jivesoftware.smackx.pubsub.packet.SyncPacketSend;

+import org.jivesoftware.smackx.pubsub.util.NodeUtils;

+

+/**

+ * This is the starting point for access to the pubsub service.  It

+ * will provide access to general information about the service, as

+ * well as create or retrieve pubsub {@link LeafNode} instances.  These 

+ * instances provide the bulk of the functionality as defined in the 

+ * pubsub specification <a href="http://xmpp.org/extensions/xep-0060.html">XEP-0060</a>.

+ * 

+ * @author Robin Collier

+ */

+final public class PubSubManager

+{

+	private Connection con;

+	private String to;

+	private Map<String, Node> nodeMap = new ConcurrentHashMap<String, Node>();

+	

+	/**

+	 * Create a pubsub manager associated to the specified connection.  Defaults the service

+	 * name to <i>pubsub</i>

+	 * 

+	 * @param connection The XMPP connection

+	 */

+	public PubSubManager(Connection connection)

+	{

+		con = connection;

+		to = "pubsub." + connection.getServiceName();

+	}

+	

+	/**

+	 * Create a pubsub manager associated to the specified connection where

+	 * the pubsub requests require a specific to address for packets.

+	 * 

+	 * @param connection The XMPP connection

+	 * @param toAddress The pubsub specific to address (required for some servers)

+	 */

+	public PubSubManager(Connection connection, String toAddress)

+	{

+		con = connection;

+		to = toAddress;

+	}

+	

+	/**

+	 * Creates an instant node, if supported.

+	 * 

+	 * @return The node that was created

+	 * @exception XMPPException

+	 */

+	public LeafNode createNode()

+		throws XMPPException

+	{

+		PubSub reply = (PubSub)sendPubsubPacket(Type.SET, new NodeExtension(PubSubElementType.CREATE));

+		NodeExtension elem = (NodeExtension)reply.getExtension("create", PubSubNamespace.BASIC.getXmlns());

+		

+		LeafNode newNode = new LeafNode(con, elem.getNode());

+		newNode.setTo(to);

+		nodeMap.put(newNode.getId(), newNode);

+		

+		return newNode;

+	}

+	

+	/**

+	 * Creates a node with default configuration.

+	 * 

+	 * @param id The id of the node, which must be unique within the 

+	 * pubsub service

+	 * @return The node that was created

+	 * @exception XMPPException

+	 */

+	public LeafNode createNode(String id)

+		throws XMPPException

+	{

+		return (LeafNode)createNode(id, null);

+	}

+	

+	/**

+	 * Creates a node with specified configuration.

+	 * 

+	 * Note: This is the only way to create a collection node.

+	 * 

+	 * @param name The name of the node, which must be unique within the 

+	 * pubsub service

+	 * @param config The configuration for the node

+	 * @return The node that was created

+	 * @exception XMPPException

+	 */

+	public Node createNode(String name, Form config)

+		throws XMPPException

+	{

+		PubSub request = createPubsubPacket(to, Type.SET, new NodeExtension(PubSubElementType.CREATE, name));

+		boolean isLeafNode = true;

+		

+		if (config != null)

+		{

+			request.addExtension(new FormNode(FormNodeType.CONFIGURE, config));

+			FormField nodeTypeField = config.getField(ConfigureNodeFields.node_type.getFieldName());

+			

+			if (nodeTypeField != null)

+				isLeafNode = nodeTypeField.getValues().next().equals(NodeType.leaf.toString());

+		}

+

+		// Errors will cause exceptions in getReply, so it only returns

+		// on success.

+		sendPubsubPacket(con, to, Type.SET, request);

+		Node newNode = isLeafNode ? new LeafNode(con, name) : new CollectionNode(con, name);

+		newNode.setTo(to);

+		nodeMap.put(newNode.getId(), newNode);

+		

+		return newNode;

+	}

+

+	/**

+	 * Retrieves the requested node, if it exists.  It will throw an 

+	 * exception if it does not.

+	 * 

+	 * @param id - The unique id of the node

+	 * @return the node

+	 * @throws XMPPException The node does not exist

+	 */

+	public <T extends Node> T getNode(String id)

+		throws XMPPException

+	{

+		Node node = nodeMap.get(id);

+		

+		if (node == null)

+		{

+			DiscoverInfo info = new DiscoverInfo();

+			info.setTo(to);

+			info.setNode(id);

+			

+			DiscoverInfo infoReply = (DiscoverInfo)SyncPacketSend.getReply(con, info);

+			

+			if (infoReply.getIdentities().next().getType().equals(NodeType.leaf.toString()))

+				node = new LeafNode(con, id);

+			else

+				node = new CollectionNode(con, id);

+			node.setTo(to);

+			nodeMap.put(id, node);

+		}

+		return (T) node;

+	}

+	

+	/**

+	 * Get all the nodes that currently exist as a child of the specified

+	 * collection node.  If the service does not support collection nodes

+	 * then all nodes will be returned.

+	 * 

+	 * To retrieve contents of the root collection node (if it exists), 

+	 * or there is no root collection node, pass null as the nodeId.

+	 * 

+	 * @param nodeId - The id of the collection node for which the child 

+	 * nodes will be returned.  

+	 * @return {@link DiscoverItems} representing the existing nodes

+	 * 

+	 * @throws XMPPException

+	 */

+	public DiscoverItems discoverNodes(String nodeId)

+		throws XMPPException

+	{

+		DiscoverItems items = new DiscoverItems();

+		

+		if (nodeId != null)

+			items.setNode(nodeId);

+		items.setTo(to);

+		DiscoverItems nodeItems = (DiscoverItems)SyncPacketSend.getReply(con, items);

+		return nodeItems;

+	}

+	

+	/**

+	 * Gets the subscriptions on the root node.

+	 * 

+	 * @return List of exceptions

+	 * 

+	 * @throws XMPPException

+	 */

+	public List<Subscription> getSubscriptions()

+		throws XMPPException

+	{

+		Packet reply = sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.SUBSCRIPTIONS));

+		SubscriptionsExtension subElem = (SubscriptionsExtension)reply.getExtension(PubSubElementType.SUBSCRIPTIONS.getElementName(), PubSubElementType.SUBSCRIPTIONS.getNamespace().getXmlns());

+		return subElem.getSubscriptions();

+	}

+	

+	/**

+	 * Gets the affiliations on the root node.

+	 * 

+	 * @return List of affiliations

+	 * 

+	 * @throws XMPPException

+	 */

+	public List<Affiliation> getAffiliations()

+		throws XMPPException

+	{

+		PubSub reply = (PubSub)sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.AFFILIATIONS));

+		AffiliationsExtension listElem = (AffiliationsExtension)reply.getExtension(PubSubElementType.AFFILIATIONS);

+		return listElem.getAffiliations();

+	}

+

+	/**

+	 * Delete the specified node

+	 * 

+	 * @param nodeId

+	 * @throws XMPPException

+	 */

+	public void deleteNode(String nodeId)

+		throws XMPPException

+	{

+		sendPubsubPacket(Type.SET, new NodeExtension(PubSubElementType.DELETE, nodeId), PubSubElementType.DELETE.getNamespace());

+		nodeMap.remove(nodeId);

+	}

+	

+	/**

+	 * Returns the default settings for Node configuration.

+	 * 

+	 * @return configuration form containing the default settings.

+	 */

+	public ConfigureForm getDefaultConfiguration()

+		throws XMPPException

+	{

+		// Errors will cause exceptions in getReply, so it only returns

+		// on success.

+		PubSub reply = (PubSub)sendPubsubPacket(Type.GET, new NodeExtension(PubSubElementType.DEFAULT), PubSubElementType.DEFAULT.getNamespace());

+		return NodeUtils.getFormFromPacket(reply, PubSubElementType.DEFAULT);

+	}

+	

+	/**

+	 * Gets the supported features of the servers pubsub implementation

+	 * as a standard {@link DiscoverInfo} instance.

+	 * 

+	 * @return The supported features

+	 * 

+	 * @throws XMPPException

+	 */

+	public DiscoverInfo getSupportedFeatures()

+		throws XMPPException

+	{

+		ServiceDiscoveryManager mgr = ServiceDiscoveryManager.getInstanceFor(con);

+		return mgr.discoverInfo(to);

+	}

+	

+	private Packet sendPubsubPacket(Type type, PacketExtension ext, PubSubNamespace ns)

+		throws XMPPException

+	{

+		return sendPubsubPacket(con, to, type, ext, ns);

+	}

+

+	private Packet sendPubsubPacket(Type type, PacketExtension ext)

+		throws XMPPException

+	{

+		return sendPubsubPacket(type, ext, null);

+	}

+

+	static PubSub createPubsubPacket(String to, Type type, PacketExtension ext)

+	{

+		return createPubsubPacket(to, type, ext, null);

+	}

+	

+	static PubSub createPubsubPacket(String to, Type type, PacketExtension ext, PubSubNamespace ns)

+	{

+		PubSub request = new PubSub();

+		request.setTo(to);

+		request.setType(type);

+		

+		if (ns != null)

+		{

+			request.setPubSubNamespace(ns);

+		}

+		request.addExtension(ext);

+		

+		return request;

+	}

+

+	static Packet sendPubsubPacket(Connection con, String to, Type type, PacketExtension ext)

+		throws XMPPException

+	{

+		return sendPubsubPacket(con, to, type, ext, null);

+	}

+	

+	static Packet sendPubsubPacket(Connection con, String to, Type type, PacketExtension ext, PubSubNamespace ns)

+		throws XMPPException

+	{

+		return SyncPacketSend.getReply(con, createPubsubPacket(to, type, ext, ns));

+	}

+

+	static Packet sendPubsubPacket(Connection con, String to, Type type, PubSub packet)

+		throws XMPPException

+	{

+		return sendPubsubPacket(con, to, type, packet, null);

+	}

+

+	static Packet sendPubsubPacket(Connection con, String to, Type type, PubSub packet, PubSubNamespace ns)

+		throws XMPPException

+	{

+		return SyncPacketSend.getReply(con, packet);

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/PublishItem.java b/src/org/jivesoftware/smackx/pubsub/PublishItem.java
new file mode 100644
index 0000000..ffbd705
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/PublishItem.java
@@ -0,0 +1,70 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.ArrayList;

+import java.util.Collection;

+

+/**

+ * Represents a request to publish an item(s) to a specific node.

+ * 

+ * @author Robin Collier

+ */

+public class PublishItem <T extends Item> extends NodeExtension

+{

+	protected Collection<T> items;

+	

+	/**

+	 * Construct a request to publish an item to a node.

+	 * 

+	 * @param nodeId The node to publish to

+	 * @param toPublish The {@link Item} to publish

+	 */

+	public PublishItem(String nodeId, T toPublish)

+	{

+		super(PubSubElementType.PUBLISH, nodeId);

+		items = new ArrayList<T>(1);

+		items.add(toPublish);

+	}

+

+	/**

+	 * Construct a request to publish multiple items to a node.

+	 * 

+	 * @param nodeId The node to publish to

+	 * @param toPublish The list of {@link Item} to publish

+	 */

+	public PublishItem(String nodeId, Collection<T> toPublish)

+	{

+		super(PubSubElementType.PUBLISH, nodeId);

+		items = toPublish;

+	}

+

+	@Override

+	public String toXML()

+	{

+		StringBuilder builder = new StringBuilder("<");

+		builder.append(getElementName());

+		builder.append(" node='");

+		builder.append(getNode());

+		builder.append("'>");

+		

+		for (Item item : items)

+		{

+			builder.append(item.toXML());

+		}

+		builder.append("</publish>");

+		

+		return builder.toString();

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/PublishModel.java b/src/org/jivesoftware/smackx/pubsub/PublishModel.java
new file mode 100644
index 0000000..4b5a851
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/PublishModel.java
@@ -0,0 +1,32 @@
+/**

+ * 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.smackx.pubsub;

+

+/**

+ * Determines who may publish to a node.  Denotes possible values 

+ * for {@link ConfigureForm#setPublishModel(PublishModel)}.

+ * 

+ * @author Robin Collier

+ */

+public enum PublishModel

+{

+	/** Only publishers may publish */

+	publishers,

+	

+	/** Only subscribers may publish */

+	subscribers,

+	

+	/** Anyone may publish */

+	open;

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/RetractItem.java b/src/org/jivesoftware/smackx/pubsub/RetractItem.java
new file mode 100644
index 0000000..97db5cc
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/RetractItem.java
@@ -0,0 +1,59 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;

+

+/**

+ * Represents and item that has been deleted from a node.

+ * 

+ * @author Robin Collier

+ */

+public class RetractItem implements PacketExtension

+{

+	private String id;

+

+	/**

+	 * Construct a <tt>RetractItem</tt> with the specified id.

+	 * 

+	 * @param itemId The id if the item deleted

+	 */

+	public RetractItem(String itemId)

+	{

+		if (itemId == null)

+			throw new IllegalArgumentException("itemId must not be 'null'");

+		id = itemId;

+	}

+	

+	public String getId()

+	{

+		return id;

+	}

+

+	public String getElementName()

+	{

+		return "retract";

+	}

+

+	public String getNamespace()

+	{

+		return PubSubNamespace.EVENT.getXmlns();

+	}

+

+	public String toXML()

+	{

+		return "<retract id='" + id + "'/>";

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/SimplePayload.java b/src/org/jivesoftware/smackx/pubsub/SimplePayload.java
new file mode 100644
index 0000000..9d114b0
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/SimplePayload.java
@@ -0,0 +1,65 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+

+/**

+ * The default payload representation for {@link Item#getPayload()}.  It simply 

+ * stores the XML payload as a string.

+ *  

+ * @author Robin Collier

+ */

+public class SimplePayload implements PacketExtension

+{

+	private String elemName;

+	private String ns;

+	private String payload;

+	

+	/**

+	 * Construct a <tt>SimplePayload</tt> object with the specified element name, 

+	 * namespace and content.  The content must be well formed XML.

+	 * 

+	 * @param elementName The root element name (of the payload)

+	 * @param namespace The namespace of the payload, null if there is none

+	 * @param xmlPayload The payload data

+	 */

+	public SimplePayload(String elementName, String namespace, String xmlPayload)

+	{

+		elemName = elementName;

+		payload = xmlPayload;

+		ns = namespace;

+	}

+

+	public String getElementName()

+	{

+		return elemName;

+	}

+

+	public String getNamespace()

+	{

+		return ns;

+	}

+

+	public String toXML()

+	{

+		return payload;

+	}

+

+	@Override

+	public String toString()

+	{

+		return getClass().getName() + "payload [" + toXML() + "]";

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/SubscribeExtension.java b/src/org/jivesoftware/smackx/pubsub/SubscribeExtension.java
new file mode 100644
index 0000000..daf8db7
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/SubscribeExtension.java
@@ -0,0 +1,60 @@
+/**

+ * 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.smackx.pubsub;

+

+/**

+ * Represents a request to subscribe to a node.

+ * 

+ * @author Robin Collier

+ */

+public class SubscribeExtension extends NodeExtension

+{

+	protected String jid;

+	

+	public SubscribeExtension(String subscribeJid)

+	{

+		super(PubSubElementType.SUBSCRIBE);

+		jid = subscribeJid;

+	}

+	

+	public SubscribeExtension(String subscribeJid, String nodeId)

+	{

+		super(PubSubElementType.SUBSCRIBE, nodeId);

+		jid = subscribeJid;

+	}

+

+	public String getJid()

+	{

+		return jid;

+	}

+

+	@Override

+	public String toXML()

+	{

+		StringBuilder builder = new StringBuilder("<");

+		builder.append(getElementName());

+		

+		if (getNode() != null)

+		{

+			builder.append(" node='");

+			builder.append(getNode());

+			builder.append("'");

+		}

+		builder.append(" jid='");

+		builder.append(getJid());

+		builder.append("'/>");

+		

+		return builder.toString();

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/SubscribeForm.java b/src/org/jivesoftware/smackx/pubsub/SubscribeForm.java
new file mode 100644
index 0000000..53f2606
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/SubscribeForm.java
@@ -0,0 +1,241 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.text.ParseException;

+import java.text.SimpleDateFormat;

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.Date;

+import java.util.Iterator;

+import java.util.UnknownFormatConversionException;

+

+import org.jivesoftware.smack.util.StringUtils;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.FormField;

+import org.jivesoftware.smackx.packet.DataForm;

+

+/**

+ * A decorator for a {@link Form} to easily enable reading and updating

+ * of subscription options.  All operations read or update the underlying {@link DataForm}.

+ * 

+ * <p>Unlike the {@link Form}.setAnswer(XXX)} methods, which throw an exception if the field does not

+ * exist, all <b>SubscribeForm.setXXX</b> methods will create the field in the wrapped form

+ * if it does not already exist.

+ * 

+ * @author Robin Collier

+ */

+public class SubscribeForm extends Form

+{	

+	public SubscribeForm(DataForm configDataForm)

+	{

+		super(configDataForm);

+	}

+	

+	public SubscribeForm(Form subscribeOptionsForm)

+	{

+		super(subscribeOptionsForm.getDataFormToSend());

+	}

+	

+	public SubscribeForm(FormType formType)

+	{

+		super(formType.toString());

+	}

+	

+	/**

+	 * Determines if an entity wants to receive notifications.

+	 * 

+	 * @return true if want to receive, false otherwise

+	 */

+	public boolean isDeliverOn()

+	{

+		return parseBoolean(getFieldValue(SubscribeOptionFields.deliver));

+	}

+	

+	/**

+	 * Sets whether an entity wants to receive notifications.

+	 *

+	 * @param deliverNotifications

+	 */

+	public void setDeliverOn(boolean deliverNotifications)

+	{

+		addField(SubscribeOptionFields.deliver, FormField.TYPE_BOOLEAN);

+		setAnswer(SubscribeOptionFields.deliver.getFieldName(), deliverNotifications);

+	}

+

+	/**

+	 * Determines if notifications should be delivered as aggregations or not.

+	 * 

+	 * @return true to aggregate, false otherwise

+	 */

+	public boolean isDigestOn()

+	{

+		return parseBoolean(getFieldValue(SubscribeOptionFields.digest));

+	}

+	

+	/**

+	 * Sets whether notifications should be delivered as aggregations or not.

+	 * 

+	 * @param digestOn true to aggregate, false otherwise 

+	 */

+	public void setDigestOn(boolean digestOn)

+	{

+		addField(SubscribeOptionFields.deliver, FormField.TYPE_BOOLEAN);

+		setAnswer(SubscribeOptionFields.deliver.getFieldName(), digestOn);

+	}

+

+	/**

+	 * Gets the minimum number of milliseconds between sending notification digests

+	 * 

+	 * @return The frequency in milliseconds

+	 */

+	public int getDigestFrequency()

+	{

+		return Integer.parseInt(getFieldValue(SubscribeOptionFields.digest_frequency));

+	}

+

+	/**

+	 * Sets the minimum number of milliseconds between sending notification digests

+	 * 

+	 * @param frequency The frequency in milliseconds

+	 */

+	public void setDigestFrequency(int frequency)

+	{

+		addField(SubscribeOptionFields.digest_frequency, FormField.TYPE_TEXT_SINGLE);

+		setAnswer(SubscribeOptionFields.digest_frequency.getFieldName(), frequency);

+	}

+

+	/**

+	 * Get the time at which the leased subscription will expire, or has expired.

+	 * 

+	 * @return The expiry date

+	 */

+	public Date getExpiry()

+	{

+		String dateTime = getFieldValue(SubscribeOptionFields.expire);

+		try

+		{

+			return StringUtils.parseDate(dateTime);

+		}

+		catch (ParseException e)

+		{

+			UnknownFormatConversionException exc = new UnknownFormatConversionException(dateTime);

+			exc.initCause(e);

+			throw exc;

+		}

+	}

+	

+	/**

+	 * Sets the time at which the leased subscription will expire, or has expired.

+	 * 

+	 * @param expire The expiry date

+	 */

+	public void setExpiry(Date expire)

+	{

+		addField(SubscribeOptionFields.expire, FormField.TYPE_TEXT_SINGLE);

+		setAnswer(SubscribeOptionFields.expire.getFieldName(), StringUtils.formatXEP0082Date(expire));

+	}

+	

+	/**

+	 * Determines whether the entity wants to receive an XMPP message body in 

+	 * addition to the payload format.

+	 * 

+	 * @return true to receive the message body, false otherwise

+	 */

+	public boolean isIncludeBody()

+	{

+		return parseBoolean(getFieldValue(SubscribeOptionFields.include_body));

+	}

+	

+	/**

+	 * Sets whether the entity wants to receive an XMPP message body in 

+	 * addition to the payload format.

+	 * 

+	 * @param include true to receive the message body, false otherwise

+	 */

+	public void setIncludeBody(boolean include)

+	{

+		addField(SubscribeOptionFields.include_body, FormField.TYPE_BOOLEAN);

+		setAnswer(SubscribeOptionFields.include_body.getFieldName(), include);

+	}

+

+	/**

+	 * Gets the {@link PresenceState} for which an entity wants to receive 

+	 * notifications.

+	 * 

+	 * @return iterator over the list of states

+	 */

+	public Iterator<PresenceState> getShowValues()

+	{

+		ArrayList<PresenceState> result = new ArrayList<PresenceState>(5);

+		Iterator<String > it = getFieldValues(SubscribeOptionFields.show_values);

+		

+		while (it.hasNext())

+		{

+			String state = it.next();

+			result.add(PresenceState.valueOf(state));

+		}

+		return result.iterator();

+	}

+	

+	/**

+	 * Sets the list of {@link PresenceState} for which an entity wants

+	 * to receive notifications.

+	 * 

+	 * @param stateValues The list of states

+	 */

+	public void setShowValues(Collection<PresenceState> stateValues)

+	{

+		ArrayList<String> values = new ArrayList<String>(stateValues.size());

+		

+		for (PresenceState state : stateValues)

+		{

+			values.add(state.toString());

+		}

+		addField(SubscribeOptionFields.show_values, FormField.TYPE_LIST_MULTI);

+		setAnswer(SubscribeOptionFields.show_values.getFieldName(), values);

+	}

+	

+	

+	static private boolean parseBoolean(String fieldValue)

+	{

+		return ("1".equals(fieldValue) || "true".equals(fieldValue));

+	}

+

+	private String getFieldValue(SubscribeOptionFields field)

+	{

+		FormField formField = getField(field.getFieldName());

+		

+		return formField.getValues().next();

+	}

+

+	private Iterator<String> getFieldValues(SubscribeOptionFields field)

+	{

+		FormField formField = getField(field.getFieldName());

+		

+		return formField.getValues();

+	}

+

+	private void addField(SubscribeOptionFields nodeField, String type)

+	{

+		String fieldName = nodeField.getFieldName();

+		

+		if (getField(fieldName) == null)

+		{

+			FormField field = new FormField(fieldName);

+			field.setType(type);

+			addField(field);

+		}

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/SubscribeOptionFields.java b/src/org/jivesoftware/smackx/pubsub/SubscribeOptionFields.java
new file mode 100644
index 0000000..dfca601
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/SubscribeOptionFields.java
@@ -0,0 +1,99 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.Calendar;

+

+/**

+ * Defines the possible field options for a subscribe options form as defined 

+ * by <a href="http://xmpp.org/extensions/xep-0060.html#registrar-formtypes-subscribe">Section 16.4.2</a>.

+ * 

+ * @author Robin Collier

+ */

+public enum SubscribeOptionFields

+{

+	/**

+	 * Whether an entity wants to receive or disable notifications

+	 * 

+	 * <p><b>Value: boolean</b></p>

+	 */

+	deliver,

+

+	/**

+	 * Whether an entity wants to receive digests (aggregations) of 

+	 * notifications or all notifications individually.

+	 * 

+	 * <p><b>Value: boolean</b></p>

+	 */

+	digest,

+	

+	/**

+	 * The minimum number of seconds between sending any two notifications digests

+	 * 

+	 * <p><b>Value: int</b></p>

+	 */

+	digest_frequency,

+

+	/**

+	 * The DateTime at which a leased subsscription will end ro has ended.

+	 * 

+	 * <p><b>Value: {@link Calendar}</b></p>

+	 */

+	expire,

+

+	/**

+	 * Whether an entity wants to receive an XMPP message body in addition to 

+	 * the payload format.

+	 *

+	 * <p><b>Value: boolean</b></p>

+	 */

+	include_body,

+	

+	/**

+	 * The presence states for which an entity wants to receive notifications.

+	 *

+	 * <p><b>Value: {@link PresenceState}</b></p>

+	 */

+	show_values,

+	

+	/**

+	 * 

+	 * 

+	 * <p><b>Value: </b></p>

+	 */

+	subscription_type,

+	

+	/**

+	 * 

+	 * <p><b>Value: </b></p>

+	 */

+	subscription_depth;

+	

+	public String getFieldName()

+	{

+		if (this == show_values)

+			return "pubsub#" + toString().replace('_', '-');

+		return "pubsub#" + toString();

+	}

+	

+	static public SubscribeOptionFields valueOfFromElement(String elementName)

+	{

+		String portion = elementName.substring(elementName.lastIndexOf('#' + 1));

+		

+		if ("show-values".equals(portion))

+			return show_values;

+		else

+			return valueOf(portion);

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/Subscription.java b/src/org/jivesoftware/smackx/pubsub/Subscription.java
new file mode 100644
index 0000000..19ad8a8
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/Subscription.java
@@ -0,0 +1,160 @@
+/**

+ * 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.smackx.pubsub;

+

+/**

+ * Represents a subscription to node for both requests and replies.

+ * 

+ * @author Robin Collier

+ */

+public class Subscription extends NodeExtension

+{

+	protected String jid;

+	protected String id;

+	protected State state;

+	protected boolean configRequired = false;

+	

+	public enum State

+	{

+		subscribed, unconfigured, pending, none 

+	}

+

+	/**

+	 * Used to constructs a subscription request to the root node with the specified

+	 * JID.

+	 * 

+	 * @param subscriptionJid The subscriber JID

+	 */

+	public Subscription(String subscriptionJid)

+	{

+		this(subscriptionJid, null, null, null);

+	}

+	

+	/**

+	 * Used to constructs a subscription request to the specified node with the specified

+	 * JID.

+	 * 

+	 * @param subscriptionJid The subscriber JID

+	 * @param nodeId The node id

+	 */

+	public Subscription(String subscriptionJid, String nodeId)

+	{

+		this(subscriptionJid, nodeId, null, null);

+	}

+	

+	/**

+	 * Constructs a representation of a subscription reply to the specified node 

+	 * and JID.  The server	will have supplied the subscription id and current state.

+	 * 

+	 * @param jid The JID the request was made under

+	 * @param nodeId The node subscribed to

+	 * @param subscriptionId The id of this subscription

+	 * @param state The current state of the subscription

+	 */

+	public Subscription(String jid, String nodeId, String subscriptionId, State state)

+	{

+		super(PubSubElementType.SUBSCRIPTION, nodeId);

+		this.jid = jid;

+		id = subscriptionId;

+		this.state = state;

+	}

+	

+	/**

+	 * Constructs a representation of a subscription reply to the specified node 

+	 * and JID.  The server	will have supplied the subscription id and current state

+	 * and whether the subscription need to be configured.

+	 * 

+	 * @param jid The JID the request was made under

+	 * @param nodeId The node subscribed to

+	 * @param subscriptionId The id of this subscription

+	 * @param state The current state of the subscription

+	 * @param configRequired Is configuration required to complete the subscription 

+	 */

+	public Subscription(String jid, String nodeId, String subscriptionId, State state, boolean configRequired)

+	{

+		super(PubSubElementType.SUBSCRIPTION, nodeId);

+		this.jid = jid;

+		id = subscriptionId;

+		this.state = state;

+		this.configRequired = configRequired;

+	}

+	

+	/**

+	 * Gets the JID the subscription is created for

+	 * 

+	 * @return The JID

+	 */

+	public String getJid()

+	{

+		return jid;

+	}

+	

+	/**

+	 * Gets the subscription id

+	 * 

+	 * @return The subscription id

+	 */

+	public String getId()

+	{

+		return id;

+	}

+	

+	/**

+	 * Gets the current subscription state.

+	 * 

+	 * @return Current subscription state

+	 */

+	public State getState()

+	{

+		return state;

+	}

+

+	/**

+	 * This value is only relevant when the {@link #getState()} is {@link State#unconfigured}

+	 * 

+	 * @return true if configuration is required, false otherwise

+	 */

+	public boolean isConfigRequired()

+	{

+		return configRequired;

+	}

+	

+	public String toXML()

+	{

+		StringBuilder builder = new StringBuilder("<subscription");

+		appendAttribute(builder, "jid", jid);

+		

+		if (getNode() != null)

+			appendAttribute(builder, "node", getNode());

+		

+		if (id != null)

+			appendAttribute(builder, "subid", id);

+		

+		if (state != null)

+			appendAttribute(builder, "subscription", state.toString());

+		

+		builder.append("/>");

+		return builder.toString();

+	}

+

+	private void appendAttribute(StringBuilder builder, String att, String value)

+	{

+		builder.append(" ");

+		builder.append(att);

+		builder.append("='");

+		builder.append(value);

+		builder.append("'");

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/SubscriptionEvent.java b/src/org/jivesoftware/smackx/pubsub/SubscriptionEvent.java
new file mode 100644
index 0000000..99f18d5
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/SubscriptionEvent.java
@@ -0,0 +1,75 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.Collections;

+import java.util.List;

+

+/**

+ * Base class to represents events that are associated to subscriptions.

+ * 

+ * @author Robin Collier

+ */

+abstract public class SubscriptionEvent extends NodeEvent

+{

+	private List<String> subIds = Collections.EMPTY_LIST;

+

+	/**

+	 * Construct an event with no subscription id's.  This can 

+	 * occur when there is only one subscription to a node.  The

+	 * event may or may not report the subscription id along 

+	 * with the event.

+	 * 

+	 * @param nodeId The id of the node the event came from

+	 */

+	protected SubscriptionEvent(String nodeId)

+	{

+		super(nodeId);

+	}

+

+	/**

+	 * Construct an event with multiple subscriptions.

+	 * 

+	 * @param nodeId The id of the node the event came from

+	 * @param subscriptionIds The list of subscription id's

+	 */

+	protected SubscriptionEvent(String nodeId, List<String> subscriptionIds)

+	{

+		super(nodeId);

+		

+		if (subscriptionIds != null)

+			subIds = subscriptionIds;

+	}

+

+	/** 

+	 * Get the subscriptions this event is associated with.

+	 * 

+	 * @return List of subscription id's

+	 */

+	public List<String> getSubscriptions()

+	{

+		return Collections.unmodifiableList(subIds);

+	}

+	

+	/**

+	 * Set the list of subscription id's for this event.

+	 * 

+	 * @param subscriptionIds The list of subscription id's

+	 */

+	protected void setSubscriptions(List<String> subscriptionIds)

+	{

+		if (subscriptionIds != null)

+			subIds = subscriptionIds;

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/SubscriptionsExtension.java b/src/org/jivesoftware/smackx/pubsub/SubscriptionsExtension.java
new file mode 100644
index 0000000..a28cbe2
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/SubscriptionsExtension.java
@@ -0,0 +1,96 @@
+/**

+ * 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.smackx.pubsub;

+

+import java.util.Collections;

+import java.util.List;

+

+/**

+ * Represents the element holding the list of subscription elements.

+ * 

+ * @author Robin Collier

+ */

+public class SubscriptionsExtension extends NodeExtension

+{

+	protected List<Subscription> items = Collections.EMPTY_LIST;

+	

+	/**

+	 * Subscriptions to the root node

+	 * 

+	 * @param subList The list of subscriptions

+	 */

+	public SubscriptionsExtension(List<Subscription> subList)

+	{

+		super(PubSubElementType.SUBSCRIPTIONS);

+		

+		if (subList != null)

+			items = subList;

+	}

+

+	/**

+	 * Subscriptions to the specified node.

+	 * 

+	 * @param nodeId The node subscribed to

+	 * @param subList The list of subscriptions

+	 */

+	public SubscriptionsExtension(String nodeId, List<Subscription> subList)

+	{

+		super(PubSubElementType.SUBSCRIPTIONS, nodeId);

+

+		if (subList != null)

+			items = subList;

+	}

+

+	/**

+	 * Gets the list of subscriptions.

+	 * 

+	 * @return List of subscriptions

+	 */

+	public List<Subscription> getSubscriptions()

+	{

+		return items;

+	}

+

+	@Override

+	public String toXML()

+	{

+		if ((items == null) || (items.size() == 0))

+		{

+			return super.toXML();

+		}

+		else

+		{

+			StringBuilder builder = new StringBuilder("<");

+			builder.append(getElementName());

+			

+			if (getNode() != null)

+			{

+				builder.append(" node='");

+				builder.append(getNode());

+				builder.append("'");

+			}

+			builder.append(">");

+			

+			for (Subscription item : items)

+			{

+				builder.append(item.toXML());

+			}

+			

+			builder.append("</");

+			builder.append(getElementName());

+			builder.append(">");

+			return builder.toString();

+		}

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/UnsubscribeExtension.java b/src/org/jivesoftware/smackx/pubsub/UnsubscribeExtension.java
new file mode 100644
index 0000000..ac14c60
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/UnsubscribeExtension.java
@@ -0,0 +1,73 @@
+/**

+ * 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.smackx.pubsub;

+

+import org.jivesoftware.smackx.pubsub.util.XmlUtils;

+

+

+/**

+ * Represents an unsubscribe element.

+ * 

+ * @author Robin Collier

+ */

+public class UnsubscribeExtension extends NodeExtension

+{

+	protected String jid;

+	protected String id;

+	

+	public UnsubscribeExtension(String subscriptionJid)

+	{

+		this(subscriptionJid, null, null);

+	}

+	

+	public UnsubscribeExtension(String subscriptionJid, String nodeId)

+	{

+		this(subscriptionJid, nodeId, null);

+	}

+	

+	public UnsubscribeExtension(String jid, String nodeId, String subscriptionId)

+	{

+		super(PubSubElementType.UNSUBSCRIBE, nodeId);

+		this.jid = jid;

+		id = subscriptionId;

+	}

+	

+	public String getJid()

+	{

+		return jid;

+	}

+	

+	public String getId()

+	{

+		return id;

+	}

+	

+	@Override

+	public String toXML()

+	{

+		StringBuilder builder = new StringBuilder("<");

+		builder.append(getElementName());

+		XmlUtils.appendAttribute(builder, "jid", jid);

+		

+		if (getNode() != null)

+			XmlUtils.appendAttribute(builder, "node", getNode());

+		

+		if (id != null)

+			XmlUtils.appendAttribute(builder, "subid", id);

+		

+		builder.append("/>");

+		return builder.toString();

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/listener/ItemDeleteListener.java b/src/org/jivesoftware/smackx/pubsub/listener/ItemDeleteListener.java
new file mode 100644
index 0000000..d228e8f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/listener/ItemDeleteListener.java
@@ -0,0 +1,41 @@
+/**

+ * 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.smackx.pubsub.listener;

+

+import org.jivesoftware.smackx.pubsub.ItemDeleteEvent;

+import org.jivesoftware.smackx.pubsub.LeafNode;

+

+/**

+ * Defines the listener for item deletion events from a node.

+ * 

+ * @see LeafNode#addItemDeleteListener(ItemDeleteListener)

+ * 

+ * @author Robin Collier

+ */

+public interface ItemDeleteListener

+{

+	/**

+	 * Called when items are deleted from a node the listener is 

+	 * registered with.

+	 * 

+	 * @param items The event with item deletion details

+	 */

+	void handleDeletedItems(ItemDeleteEvent items);

+	

+	/**

+	 * Called when <b>all</b> items are deleted from a node the listener is 

+	 * registered with. 

+	 */

+	void handlePurge();

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/listener/ItemEventListener.java b/src/org/jivesoftware/smackx/pubsub/listener/ItemEventListener.java
new file mode 100644
index 0000000..714b2c0
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/listener/ItemEventListener.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.

+ */

+package org.jivesoftware.smackx.pubsub.listener;

+

+import org.jivesoftware.smackx.pubsub.Item;

+import org.jivesoftware.smackx.pubsub.ItemPublishEvent;

+import org.jivesoftware.smackx.pubsub.LeafNode;

+

+/**

+ * Defines the listener for items being published to a node.

+ * 

+ * @see LeafNode#addItemEventListener(ItemEventListener)

+ *

+ * @author Robin Collier

+ */

+public interface ItemEventListener <T extends Item> 

+{

+	/**

+	 * Called whenever an item is published to the node the listener

+	 * is registered with.

+	 * 

+	 * @param items The publishing details.

+	 */

+	void handlePublishedItems(ItemPublishEvent<T> items);

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/listener/NodeConfigListener.java b/src/org/jivesoftware/smackx/pubsub/listener/NodeConfigListener.java
new file mode 100644
index 0000000..39db5a5
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/listener/NodeConfigListener.java
@@ -0,0 +1,35 @@
+/**

+ * 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.smackx.pubsub.listener;

+

+import org.jivesoftware.smackx.pubsub.ConfigurationEvent;

+import org.jivesoftware.smackx.pubsub.LeafNode;

+

+/**

+ * Defines the listener for a node being configured.

+ * 

+ * @see LeafNode#addConfigurationListener(NodeConfigListener)

+ *

+ * @author Robin Collier

+ */

+public interface NodeConfigListener

+{

+	/**

+	 * Called whenever the node the listener

+	 * is registered with is configured.

+	 * 

+	 * @param config The configuration details.

+	 */

+	void handleNodeConfiguration(ConfigurationEvent config);

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/packet/PubSub.java b/src/org/jivesoftware/smackx/pubsub/packet/PubSub.java
new file mode 100644
index 0000000..5aa4865
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/packet/PubSub.java
@@ -0,0 +1,106 @@
+/**

+ * 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.smackx.pubsub.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.pubsub.PubSubElementType;

+

+/**

+ * The standard PubSub extension of an {@link IQ} packet.  This is the topmost

+ * element of all pubsub requests and replies as defined in the <a href="http://xmpp.org/extensions/xep-0060">Publish-Subscribe</a> 

+ * specification.

+ * 

+ * @author Robin Collier

+ */

+public class PubSub extends IQ

+{

+	private PubSubNamespace ns = PubSubNamespace.BASIC;

+	

+	/**

+    * Returns the XML element name of the extension sub-packet root element.

+    *

+    * @return the XML element name of the packet extension.

+    */

+    public String getElementName() {

+        return "pubsub";

+    }

+

+    /** 

+     * Returns the XML namespace of the extension sub-packet root element.

+     * According the specification the namespace is 

+     * http://jabber.org/protocol/pubsub with a specific fragment depending

+     * on the request.  The namespace is defined at <a href="http://xmpp.org/registrar/namespaces.html">XMPP Registrar</a> at

+     * 

+     * The default value has no fragment.

+     * 

+     * @return the XML namespace of the packet extension.

+     */

+    public String getNamespace() 

+    {

+        return ns.getXmlns();

+    }

+

+    /**

+     * Set the namespace for the packet if it something other than the default

+     * case of {@link PubSubNamespace#BASIC}.  The {@link #getNamespace()} method will return 

+     * the result of calling {@link PubSubNamespace#getXmlns()} on the specified enum.

+     * 

+     * @param ns - The new value for the namespace.

+     */

+	public void setPubSubNamespace(PubSubNamespace ns)

+	{

+		this.ns = ns;

+	}

+

+	public PacketExtension getExtension(PubSubElementType elem)

+	{

+		return getExtension(elem.getElementName(), elem.getNamespace().getXmlns());

+	}

+

+	/**

+	 * Returns the current value of the namespace.  The {@link #getNamespace()} method will return 

+     * the result of calling {@link PubSubNamespace#getXmlns()} this value.

+	 * 

+	 * @return The current value of the namespace.

+	 */

+	public PubSubNamespace getPubSubNamespace()

+	{

+		return ns;

+	}

+    /**

+     * Returns the XML representation of a pubsub element according the specification.

+     * 

+     * The XML representation will be inside of an iq packet like

+     * in the following example:

+     * <pre>

+     * &lt;iq type='set' id="MlIpV-4" to="pubsub.gato.home" from="gato3@gato.home/Smack"&gt;

+     *     &lt;pubsub xmlns="http://jabber.org/protocol/pubsub"&gt;

+     *                      :

+     *         Specific request extension

+     *                      :

+     *     &lt;/pubsub&gt;

+     * &lt;/iq&gt;

+     * </pre>

+     * 

+     */

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<").append(getElementName()).append(" xmlns=\"").append(getNamespace()).append("\">");

+        buf.append(getExtensionsXML());

+        buf.append("</").append(getElementName()).append(">");

+        return buf.toString();

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/packet/PubSubNamespace.java b/src/org/jivesoftware/smackx/pubsub/packet/PubSubNamespace.java
new file mode 100644
index 0000000..eecf959
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/packet/PubSubNamespace.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.smackx.pubsub.packet;

+

+/**

+ * Defines all the valid namespaces that are used with the {@link PubSub} packet

+ * as defined by the specification.

+ * 

+ * @author Robin Collier

+ */

+public enum PubSubNamespace

+{

+	BASIC(null),

+	ERROR("errors"),

+	EVENT("event"),

+	OWNER("owner");

+	

+	private String fragment;

+	

+	private PubSubNamespace(String fragment)

+	{

+		this.fragment = fragment;

+	}

+	

+	public String getXmlns()

+	{

+		String ns = "http://jabber.org/protocol/pubsub";

+		

+		if (fragment != null)

+			ns += '#' + fragment;

+		

+		return ns;

+	}

+	

+	public String getFragment()

+	{

+		return fragment;

+	}

+

+	public static PubSubNamespace valueOfFromXmlns(String ns)

+	{

+		int index = ns.lastIndexOf('#');

+		

+		if (index != -1)

+		{

+			String suffix = ns.substring(ns.lastIndexOf('#')+1);

+			return valueOf(suffix.toUpperCase());

+		}

+		else

+			return BASIC;

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/packet/SyncPacketSend.java b/src/org/jivesoftware/smackx/pubsub/packet/SyncPacketSend.java
new file mode 100644
index 0000000..080129b
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/packet/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.smackx.pubsub.packet;

+

+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/smackx/pubsub/provider/AffiliationProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/AffiliationProvider.java
new file mode 100644
index 0000000..892eec6
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/AffiliationProvider.java
@@ -0,0 +1,37 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;

+import org.jivesoftware.smackx.pubsub.Affiliation;

+

+/**

+ * Parses the affiliation element out of the reply stanza from the server

+ * as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">affiliation schema</a>.

+ * 

+ * @author Robin Collier

+ */

+public class AffiliationProvider extends EmbeddedExtensionProvider

+{

+	@Override

+	protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)

+	{

+		return new Affiliation(attributeMap.get("jid"), attributeMap.get("node"), Affiliation.Type.valueOf(attributeMap.get("affiliation")));

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/AffiliationsProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/AffiliationsProvider.java
new file mode 100644
index 0000000..ee7af05
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/AffiliationsProvider.java
@@ -0,0 +1,38 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;

+import org.jivesoftware.smackx.pubsub.Affiliation;

+import org.jivesoftware.smackx.pubsub.AffiliationsExtension;

+

+/**

+ * Parses the affiliations element out of the reply stanza from the server

+ * as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">affiliation schema</a>.

+ * 

+ * @author Robin Collier

+ */public class AffiliationsProvider extends EmbeddedExtensionProvider

+{

+	@Override

+	protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)

+	{

+        return new AffiliationsExtension(attributeMap.get("node"), (List<Affiliation>)content);

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/ConfigEventProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/ConfigEventProvider.java
new file mode 100644
index 0000000..30e3017
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/ConfigEventProvider.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.smackx.pubsub.provider;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.packet.DataForm;

+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;

+import org.jivesoftware.smackx.pubsub.ConfigurationEvent;

+import org.jivesoftware.smackx.pubsub.ConfigureForm;

+

+/**

+ * Parses the node configuration element out of the message event stanza from 

+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">configuration schema</a>.

+ * 

+ * @author Robin Collier

+ */

+public class ConfigEventProvider extends EmbeddedExtensionProvider

+{

+	@Override

+	protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attMap, List<? extends PacketExtension> content)

+	{

+		if (content.size() == 0)

+			return new ConfigurationEvent(attMap.get("node"));

+		else

+			return new ConfigurationEvent(attMap.get("node"), new ConfigureForm((DataForm)content.iterator().next()));

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/EventProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/EventProvider.java
new file mode 100644
index 0000000..ef5671e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/EventProvider.java
@@ -0,0 +1,38 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;

+import org.jivesoftware.smackx.pubsub.EventElement;

+import org.jivesoftware.smackx.pubsub.EventElementType;

+import org.jivesoftware.smackx.pubsub.NodeExtension;

+

+/**

+ * Parses the event element out of the message stanza from 

+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">event schema</a>.

+ * 

+ * @author Robin Collier

+ */

+public class EventProvider extends EmbeddedExtensionProvider

+{

+	@Override

+	protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attMap, List<? extends PacketExtension> content)

+	{

+	   	return new EventElement(EventElementType.valueOf(content.get(0).getElementName()), (NodeExtension)content.get(0));

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/FormNodeProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/FormNodeProvider.java
new file mode 100644
index 0000000..da75b24
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/FormNodeProvider.java
@@ -0,0 +1,39 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.packet.DataForm;

+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;

+import org.jivesoftware.smackx.pubsub.FormNode;

+import org.jivesoftware.smackx.pubsub.FormNodeType;

+

+/**

+ * Parses one of several elements used in pubsub that contain a form of some kind as a child element.  The

+ * elements and namespaces supported is defined in {@link FormNodeType}.

+ * 

+ * @author Robin Collier

+ */

+public class FormNodeProvider extends EmbeddedExtensionProvider

+{

+	@Override

+	protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)

+	{

+        return new FormNode(FormNodeType.valueOfFromElementName(currentElement, currentNamespace), attributeMap.get("node"), new Form((DataForm)content.iterator().next()));

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java
new file mode 100644
index 0000000..a6b8694
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/ItemProvider.java
@@ -0,0 +1,92 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.jivesoftware.smack.provider.ProviderManager;

+import org.jivesoftware.smack.util.PacketParserUtils;

+import org.jivesoftware.smackx.pubsub.Item;

+import org.jivesoftware.smackx.pubsub.PayloadItem;

+import org.jivesoftware.smackx.pubsub.SimplePayload;

+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Parses an <b>item</b> element as is defined in both the {@link PubSubNamespace#BASIC} and {@link PubSubNamespace#EVENT}

+ * namespaces.  To parse the item contents, it will use whatever {@link PacketExtensionProvider} is registered in

+ * <b>smack.providers</b> for its element name and namespace.  If no provider is registered, it will return a {@link SimplePayload}.

+ *

+ * @author Robin Collier

+ */

+public class ItemProvider implements PacketExtensionProvider {

+    public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+        String id = parser.getAttributeValue(null, "id");

+        String node = parser.getAttributeValue(null, "node");

+        String elem = parser.getName();

+

+        int tag = parser.next();

+

+        if (tag == XmlPullParser.END_TAG) {

+            return new Item(id, node);

+        } else {

+            String payloadElemName = parser.getName();

+            String payloadNS = parser.getNamespace();

+

+            if (ProviderManager.getInstance().getExtensionProvider(payloadElemName, payloadNS) == null) {

+                StringBuilder payloadText = new StringBuilder();

+                boolean done = false;

+                boolean isEmptyElement = false;

+

+                // Parse custom payload

+                while (!done) {

+                    if (tag == XmlPullParser.END_TAG && parser.getName().equals(elem)) {

+                        done = true;

+                    } else if (parser.getEventType() == XmlPullParser.START_TAG) {

+                        payloadText.append("<").append(parser.getName());

+                        if (parser.getName().equals(payloadElemName) && (!"".equals(payloadNS))) {

+                            payloadText.append(" xmlns=\"").append(payloadNS).append("\"");

+                        }

+                        int n = parser.getAttributeCount();

+                        for (int i = 0; i < n; i++) {

+                            payloadText.append(" ").append(parser.getAttributeName(i)).append("=\"")

+                                    .append(parser.getAttributeValue(i)).append("\"");

+                        }

+                        if (parser.isEmptyElementTag()) {

+                            payloadText.append("/>");

+                            isEmptyElement = true;

+                        } else {

+                            payloadText.append(">");

+                        }

+                    } else if (parser.getEventType() == XmlPullParser.END_TAG) {

+                        if (isEmptyElement) {

+                            isEmptyElement = false;

+                        } else {

+                            payloadText.append("</").append(parser.getName()).append(">");

+                        }

+                    } else if (parser.getEventType() == XmlPullParser.TEXT) {

+                        payloadText.append(parser.getText());

+                    }

+

+                    tag = parser.next();

+                }

+                return new PayloadItem<SimplePayload>(id, node, new SimplePayload(payloadElemName, payloadNS,

+                        payloadText.toString()));

+            } else {

+                return new PayloadItem<PacketExtension>(id, node, PacketParserUtils.parsePacketExtension(

+                        payloadElemName, payloadNS, parser));

+            }

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/ItemsProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/ItemsProvider.java
new file mode 100644
index 0000000..01cb9d4
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/ItemsProvider.java
@@ -0,0 +1,38 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;

+import org.jivesoftware.smackx.pubsub.ItemsExtension;

+

+/**

+ * Parses the <b>items</b> element out of the message event stanza from 

+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">items schema</a>.

+ * 

+ * @author Robin Collier

+ */

+public class ItemsProvider extends EmbeddedExtensionProvider

+{

+

+	@Override

+	protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)

+	{

+        return new ItemsExtension(ItemsExtension.ItemsElementType.items, attributeMap.get("node"), content);

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/PubSubProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/PubSubProvider.java
new file mode 100644
index 0000000..742f219
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/PubSubProvider.java
@@ -0,0 +1,62 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smack.util.PacketParserUtils;

+import org.jivesoftware.smackx.pubsub.packet.PubSub;

+import org.jivesoftware.smackx.pubsub.packet.PubSubNamespace;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Parses the root pubsub packet extensions of the {@link IQ} packet and returns

+ * a {@link PubSub} instance.

+ * 

+ * @author Robin Collier

+ */

+public class PubSubProvider implements IQProvider

+{

+	public IQ parseIQ(XmlPullParser parser) throws Exception

+	{

+        PubSub pubsub = new PubSub();

+        String namespace = parser.getNamespace();

+        pubsub.setPubSubNamespace(PubSubNamespace.valueOfFromXmlns(namespace));

+        boolean done = false;

+

+        while (!done) 

+        {

+            int eventType = parser.next();

+            

+            if (eventType == XmlPullParser.START_TAG) 

+            {

+            	PacketExtension ext = PacketParserUtils.parsePacketExtension(parser.getName(), namespace, parser);

+            	

+            	if (ext != null)

+            	{

+            		pubsub.addExtension(ext);

+            	}

+            }

+            else if (eventType == XmlPullParser.END_TAG) 

+            {

+                if (parser.getName().equals("pubsub")) 

+                {

+                    done = true;

+                }

+            }

+        }

+        return pubsub;

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/RetractEventProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/RetractEventProvider.java
new file mode 100644
index 0000000..8fa3337
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/RetractEventProvider.java
@@ -0,0 +1,38 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;

+import org.jivesoftware.smackx.pubsub.RetractItem;

+

+/**

+ * Parses the <b>retract</b> element out of the message event stanza from 

+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-event">retract schema</a>.

+ * This element is a child of the <b>items</b> element.

+ * 

+ * @author Robin Collier

+ */

+public class RetractEventProvider extends EmbeddedExtensionProvider

+{

+	@Override

+	protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)

+	{

+		return new RetractItem(attributeMap.get("id"));

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/SimpleNodeProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/SimpleNodeProvider.java
new file mode 100644
index 0000000..d2b7d30
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/SimpleNodeProvider.java
@@ -0,0 +1,37 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;

+import org.jivesoftware.smackx.pubsub.NodeExtension;

+import org.jivesoftware.smackx.pubsub.PubSubElementType;

+

+/**

+ * Parses simple elements that only contain a <b>node</b> attribute.  This is common amongst many of the 

+ * elements defined in the pubsub specification.  For this common case a {@link NodeExtension} is returned. 

+ * 

+ * @author Robin Collier

+ */

+public class SimpleNodeProvider extends EmbeddedExtensionProvider

+{

+	@Override

+	protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)

+	{

+        return new NodeExtension(PubSubElementType.valueOfFromElemName(currentElement, currentNamespace), attributeMap.get("node"));

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionProvider.java
new file mode 100644
index 0000000..eccbe08
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionProvider.java
@@ -0,0 +1,52 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.jivesoftware.smackx.pubsub.Subscription;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Parses the <b>subscription</b> element out of the pubsub IQ message from 

+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">subscription schema</a>.

+ * 

+ * @author Robin Collier

+ */

+public class SubscriptionProvider implements PacketExtensionProvider

+{

+	public PacketExtension parseExtension(XmlPullParser parser) throws Exception

+	{

+		String jid = parser.getAttributeValue(null, "jid");

+		String nodeId = parser.getAttributeValue(null, "node");

+		String subId = parser.getAttributeValue(null, "subid");

+		String state = parser.getAttributeValue(null, "subscription");

+		boolean isRequired = false;

+

+		int tag = parser.next();

+		

+		if ((tag == XmlPullParser.START_TAG) && parser.getName().equals("subscribe-options"))

+		{

+			tag = parser.next();

+			

+			if ((tag == XmlPullParser.START_TAG) && parser.getName().equals("required"))

+				isRequired = true;

+			

+			while (parser.next() != XmlPullParser.END_TAG && parser.getName() != "subscribe-options");

+		}

+		while (parser.getEventType() != XmlPullParser.END_TAG) parser.next();

+		return new Subscription(jid, nodeId, subId, (state == null ? null : Subscription.State.valueOf(state)), isRequired);

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionsProvider.java b/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionsProvider.java
new file mode 100644
index 0000000..94dc61d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/provider/SubscriptionsProvider.java
@@ -0,0 +1,38 @@
+/**

+ * 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.smackx.pubsub.provider;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smackx.provider.EmbeddedExtensionProvider;

+import org.jivesoftware.smackx.pubsub.Subscription;

+import org.jivesoftware.smackx.pubsub.SubscriptionsExtension;

+

+/**

+ * Parses the <b>subscriptions</b> element out of the pubsub IQ message from 

+ * the server as specified in the <a href="http://xmpp.org/extensions/xep-0060.html#schemas-pubsub">subscriptions schema</a>.

+ * 

+ * @author Robin Collier

+ */

+public class SubscriptionsProvider extends EmbeddedExtensionProvider

+{

+	@Override

+	protected PacketExtension createReturnExtension(String currentElement, String currentNamespace, Map<String, String> attributeMap, List<? extends PacketExtension> content)

+	{

+		return new SubscriptionsExtension(attributeMap.get("node"), (List<Subscription>)content);

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/util/NodeUtils.java b/src/org/jivesoftware/smackx/pubsub/util/NodeUtils.java
new file mode 100644
index 0000000..414601f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/util/NodeUtils.java
@@ -0,0 +1,43 @@
+/**

+ * 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.smackx.pubsub.util;

+

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.pubsub.ConfigureForm;

+import org.jivesoftware.smackx.pubsub.FormNode;

+import org.jivesoftware.smackx.pubsub.PubSubElementType;

+

+/**

+ * Utility for extracting information from packets.

+ * 

+ * @author Robin Collier

+ */

+public class NodeUtils

+{

+	/** 

+	 * Get a {@link ConfigureForm} from a packet.

+	 * 

+	 * @param packet

+	 * @param elem

+	 * @return The configuration form

+	 */

+	public static ConfigureForm getFormFromPacket(Packet packet, PubSubElementType elem)

+	{

+		FormNode config = (FormNode)packet.getExtension(elem.getElementName(), elem.getNamespace().getXmlns());

+		Form formReply = config.getForm();

+		return new ConfigureForm(formReply);

+

+	}

+}

diff --git a/src/org/jivesoftware/smackx/pubsub/util/XmlUtils.java b/src/org/jivesoftware/smackx/pubsub/util/XmlUtils.java
new file mode 100644
index 0000000..8e4a77c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/pubsub/util/XmlUtils.java
@@ -0,0 +1,35 @@
+/**

+ * 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.smackx.pubsub.util;

+

+import java.io.StringReader;

+

+/**

+ * Simple utility for pretty printing xml.

+ * 

+ * @author Robin Collier

+ */

+public class XmlUtils

+{

+

+	static public void appendAttribute(StringBuilder builder, String att, String value)

+	{

+		builder.append(" ");

+		builder.append(att);

+		builder.append("='");

+		builder.append(value);

+		builder.append("'");

+	}

+

+}

diff --git a/src/org/jivesoftware/smackx/receipts/DeliveryReceipt.java b/src/org/jivesoftware/smackx/receipts/DeliveryReceipt.java
new file mode 100644
index 0000000..9020556
--- /dev/null
+++ b/src/org/jivesoftware/smackx/receipts/DeliveryReceipt.java
@@ -0,0 +1,77 @@
+/**

+ * 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.smackx.receipts;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.EmbeddedExtensionProvider;

+

+/**

+ * Represents a <b>message delivery receipt</b> entry as specified by

+ * <a href="http://xmpp.org/extensions/xep-0184.html">Message Delivery Receipts</a>.

+ *

+ * @author Georg Lukas

+ */

+public class DeliveryReceipt implements PacketExtension

+{

+    public static final String NAMESPACE = "urn:xmpp:receipts";

+    public static final String ELEMENT = "received";

+

+    private String id; /// original ID of the delivered message

+

+    public DeliveryReceipt(String id)

+    {

+        this.id = id;

+    }

+

+    public String getId()

+    {

+        return id;

+    }

+

+    @Override

+    public String getElementName()

+    {

+        return ELEMENT;

+    }

+

+    @Override

+    public String getNamespace()

+    {

+        return NAMESPACE;

+    }

+

+    @Override

+    public String toXML()

+    {

+        return "<received xmlns='" + NAMESPACE + "' id='" + id + "'/>";

+    }

+

+    /**

+     * This Provider parses and returns DeliveryReceipt packets.

+     */

+    public static class Provider extends EmbeddedExtensionProvider

+    {

+

+        @Override

+        protected PacketExtension createReturnExtension(String currentElement, String currentNamespace,

+                Map<String, String> attributeMap, List<? extends PacketExtension> content)

+        {

+            return new DeliveryReceipt(attributeMap.get("id"));

+        }

+

+    }

+}

diff --git a/src/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java b/src/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java
new file mode 100644
index 0000000..125b87e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/receipts/DeliveryReceiptManager.java
@@ -0,0 +1,202 @@
+/**
+ * Copyright 2013 Georg Lukas
+ *
+ * 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.smackx.receipts;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.filter.PacketExtensionFilter;
+import org.jivesoftware.smack.packet.Message;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+
+/**
+ * Manager for XEP-0184: Message Delivery Receipts. This class implements
+ * the manager for {@link DeliveryReceipt} support, enabling and disabling of
+ * automatic DeliveryReceipt transmission.
+ *
+ * @author Georg Lukas
+ */
+public class DeliveryReceiptManager implements PacketListener {
+
+    private static Map<Connection, DeliveryReceiptManager> instances =
+            Collections.synchronizedMap(new WeakHashMap<Connection, DeliveryReceiptManager>());
+
+    static {
+        Connection.addConnectionCreationListener(new ConnectionCreationListener() {
+            public void connectionCreated(Connection connection) {
+                new DeliveryReceiptManager(connection);
+            }
+        });
+    }
+
+    private Connection connection;
+    private boolean auto_receipts_enabled = false;
+    private Set<ReceiptReceivedListener> receiptReceivedListeners = Collections
+            .synchronizedSet(new HashSet<ReceiptReceivedListener>());
+
+    private DeliveryReceiptManager(Connection connection) {
+        ServiceDiscoveryManager sdm = ServiceDiscoveryManager.getInstanceFor(connection);
+        sdm.addFeature(DeliveryReceipt.NAMESPACE);
+        this.connection = connection;
+        instances.put(connection, this);
+
+        // register listener for delivery receipts and requests
+        connection.addPacketListener(this, new PacketExtensionFilter(DeliveryReceipt.NAMESPACE));
+    }
+
+    /**
+     * Obtain the DeliveryReceiptManager responsible for a connection.
+     *
+     * @param connection the connection object.
+     *
+     * @return the DeliveryReceiptManager instance for the given connection
+     */
+    synchronized public static DeliveryReceiptManager getInstanceFor(Connection connection) {
+        DeliveryReceiptManager receiptManager = instances.get(connection);
+
+        if (receiptManager == null) {
+            receiptManager = new DeliveryReceiptManager(connection);
+        }
+
+        return receiptManager;
+    }
+
+    /**
+     * Returns true if Delivery Receipts are supported by a given JID
+     * 
+     * @param jid
+     * @return true if supported
+     */
+    public boolean isSupported(String jid) {
+        try {
+            DiscoverInfo result =
+                ServiceDiscoveryManager.getInstanceFor(connection).discoverInfo(jid);
+            return result.containsFeature(DeliveryReceipt.NAMESPACE);
+        }
+        catch (XMPPException e) {
+            return false;
+        }
+    }
+
+    // handle incoming receipts and receipt requests
+    @Override
+    public void processPacket(Packet packet) {
+        DeliveryReceipt dr = (DeliveryReceipt)packet.getExtension(
+                DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE);
+        if (dr != null) {
+            // notify listeners of incoming receipt
+            for (ReceiptReceivedListener l : receiptReceivedListeners) {
+                l.onReceiptReceived(packet.getFrom(), packet.getTo(), dr.getId());
+            }
+
+        }
+
+        // if enabled, automatically send a receipt
+        if (auto_receipts_enabled) {
+            DeliveryReceiptRequest drr = (DeliveryReceiptRequest)packet.getExtension(
+                    DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE);
+            if (drr != null) {
+                Message ack = new Message(packet.getFrom(), Message.Type.normal);
+                ack.addExtension(new DeliveryReceipt(packet.getPacketID()));
+                connection.sendPacket(ack);
+            }
+        }
+    }
+
+    /**
+     * Configure whether the {@link DeliveryReceiptManager} should automatically
+     * reply to incoming {@link DeliveryReceipt}s. By default, this feature is off.
+     *
+     * @param new_state whether automatic transmission of
+     *                  DeliveryReceipts should be enabled or disabled
+     */
+    public void setAutoReceiptsEnabled(boolean new_state) {
+        auto_receipts_enabled = new_state;
+    }
+
+    /**
+     * Helper method to enable automatic DeliveryReceipt transmission.
+     */
+    public void enableAutoReceipts() {
+        setAutoReceiptsEnabled(true);
+    }
+
+    /**
+     * Helper method to disable automatic DeliveryReceipt transmission.
+     */
+    public void disableAutoReceipts() {
+        setAutoReceiptsEnabled(false);
+    }
+
+    /**
+     * Check if AutoReceipts are enabled on this connection.
+     */
+    public boolean getAutoReceiptsEnabled() {
+        return this.auto_receipts_enabled;
+    }
+
+    /**
+     * Get informed about incoming delivery receipts with a {@link ReceiptReceivedListener}.
+     * 
+     * @param listener the listener to be informed about new receipts
+     */
+    public void addReceiptReceivedListener(ReceiptReceivedListener listener) {
+        receiptReceivedListeners.add(listener);
+    }
+
+    /**
+     * Stop getting informed about incoming delivery receipts.
+     * 
+     * @param listener the listener to be removed
+     */
+    public void removeReceiptReceivedListener(ReceiptReceivedListener listener) {
+        receiptReceivedListeners.remove(listener);
+    }
+
+    /**
+     * Test if a packet requires a delivery receipt.
+     *
+     * @param p Packet object to check for a DeliveryReceiptRequest
+     *
+     * @return true if a delivery receipt was requested
+     */
+    public static boolean hasDeliveryReceiptRequest(Packet p) {
+        return (p.getExtension(DeliveryReceiptRequest.ELEMENT,
+                    DeliveryReceipt.NAMESPACE) != null);
+    }
+
+    /**
+     * Add a delivery receipt request to an outgoing packet.
+     *
+     * Only message packets may contain receipt requests as of XEP-0184,
+     * therefore only allow Message as the parameter type.
+     *
+     * @param m Message object to add a request to
+     */
+    public static void addDeliveryReceiptRequest(Message m) {
+        m.addExtension(new DeliveryReceiptRequest());
+    }
+}
diff --git a/src/org/jivesoftware/smackx/receipts/DeliveryReceiptRequest.java b/src/org/jivesoftware/smackx/receipts/DeliveryReceiptRequest.java
new file mode 100644
index 0000000..1b5ed3b
--- /dev/null
+++ b/src/org/jivesoftware/smackx/receipts/DeliveryReceiptRequest.java
@@ -0,0 +1,54 @@
+/*

+ * 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.smackx.receipts;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Represents a <b>message delivery receipt request</b> entry as specified by

+ * <a href="http://xmpp.org/extensions/xep-0184.html">Message Delivery Receipts</a>.

+ *

+ * @author Georg Lukas

+ */

+public class DeliveryReceiptRequest implements PacketExtension

+{

+    public static final String ELEMENT = "request";

+

+    public String getElementName()

+    {

+        return ELEMENT;

+    }

+

+    public String getNamespace()

+    {

+        return DeliveryReceipt.NAMESPACE;

+    }

+

+    public String toXML()

+    {

+        return "<request xmlns='" + DeliveryReceipt.NAMESPACE + "'/>";

+    }

+

+    /**

+     * This Provider parses and returns DeliveryReceiptRequest packets.

+     */

+    public static class Provider implements PacketExtensionProvider {

+        @Override

+        public PacketExtension parseExtension(XmlPullParser parser) {

+            return new DeliveryReceiptRequest();

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/receipts/ReceiptReceivedListener.java b/src/org/jivesoftware/smackx/receipts/ReceiptReceivedListener.java
new file mode 100644
index 0000000..3183113
--- /dev/null
+++ b/src/org/jivesoftware/smackx/receipts/ReceiptReceivedListener.java
@@ -0,0 +1,26 @@
+/**

+ * Copyright 2013 Georg Lukas

+ *

+ * 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.smackx.receipts;

+

+/**

+ * Interface for received receipt notifications.

+ * 

+ * Implement this and add a listener to get notified. 

+ */

+public interface ReceiptReceivedListener {

+    void onReceiptReceived(String fromJid, String toJid, String receiptId);

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/search/SimpleUserSearch.java b/src/org/jivesoftware/smackx/search/SimpleUserSearch.java
new file mode 100644
index 0000000..74a70f0
--- /dev/null
+++ b/src/org/jivesoftware/smackx/search/SimpleUserSearch.java
@@ -0,0 +1,151 @@
+/**
+ * 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.smackx.search;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.ReportedData;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * SimpleUserSearch is used to support the non-dataform type of JEP 55. This provides
+ * the mechanism for allowing always type ReportedData to be returned by any search result,
+ * regardless of the form of the data returned from the server.
+ *
+ * @author Derek DeMoro
+ */
+class SimpleUserSearch extends IQ {
+
+    private Form form;
+    private ReportedData data;
+
+    public void setForm(Form form) {
+        this.form = form;
+    }
+
+    public ReportedData getReportedData() {
+        return data;
+    }
+
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"jabber:iq:search\">");
+        buf.append(getItemsToSearch());
+        buf.append("</query>");
+        return buf.toString();
+    }
+
+    private String getItemsToSearch() {
+        StringBuilder buf = new StringBuilder();
+
+        if (form == null) {
+            form = Form.getFormFrom(this);
+        }
+
+        if (form == null) {
+            return "";
+        }
+
+        Iterator<FormField> fields = form.getFields();
+        while (fields.hasNext()) {
+            FormField field = fields.next();
+            String name = field.getVariable();
+            String value = getSingleValue(field);
+            if (value.trim().length() > 0) {
+                buf.append("<").append(name).append(">").append(value).append("</").append(name).append(">");
+            }
+        }
+
+        return buf.toString();
+    }
+
+    private static String getSingleValue(FormField formField) {
+        Iterator<String> values = formField.getValues();
+        while (values.hasNext()) {
+            return values.next();
+        }
+        return "";
+    }
+
+    protected void parseItems(XmlPullParser parser) throws Exception {
+        ReportedData data = new ReportedData();
+        data.addColumn(new ReportedData.Column("JID", "jid", "text-single"));
+
+        boolean done = false;
+
+        List<ReportedData.Field> fields = new ArrayList<ReportedData.Field>();
+        while (!done) {
+            if (parser.getAttributeCount() > 0) {
+                String jid = parser.getAttributeValue("", "jid");
+                List<String> valueList = new ArrayList<String>();
+                valueList.add(jid);
+                ReportedData.Field field = new ReportedData.Field("jid", valueList);
+                fields.add(field);
+            }
+
+            int eventType = parser.next();
+
+            if (eventType == XmlPullParser.START_TAG && parser.getName().equals("item")) {
+                fields = new ArrayList<ReportedData.Field>();
+            }
+            else if (eventType == XmlPullParser.END_TAG && parser.getName().equals("item")) {
+                ReportedData.Row row = new ReportedData.Row(fields);
+                data.addRow(row);
+            }
+            else if (eventType == XmlPullParser.START_TAG) {
+                String name = parser.getName();
+                String value = parser.nextText();
+
+                List<String> valueList = new ArrayList<String>();
+                valueList.add(value);
+                ReportedData.Field field = new ReportedData.Field(name, valueList);
+                fields.add(field);
+
+                boolean exists = false;
+                Iterator<ReportedData.Column> cols = data.getColumns();
+                while (cols.hasNext()) {
+                    ReportedData.Column column = cols.next();
+                    if (column.getVariable().equals(name)) {
+                        exists = true;
+                    }
+                }
+
+                // Column name should be the same
+                if (!exists) {
+                    ReportedData.Column column = new ReportedData.Column(name, name, "text-single");
+                    data.addColumn(column);
+                }
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("query")) {
+                    done = true;
+                }
+            }
+
+        }
+
+        this.data = data;
+    }
+
+
+}
diff --git a/src/org/jivesoftware/smackx/search/UserSearch.java b/src/org/jivesoftware/smackx/search/UserSearch.java
new file mode 100644
index 0000000..781dd9a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/search/UserSearch.java
@@ -0,0 +1,255 @@
+/**
+ * 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.smackx.search;
+
+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.PacketIDFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.PacketParserUtils;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.FormField;
+import org.jivesoftware.smackx.ReportedData;
+import org.jivesoftware.smackx.packet.DataForm;
+import org.xmlpull.v1.XmlPullParser;
+
+/**
+ * Implements the protocol currently used to search information repositories on the Jabber network. To date, the jabber:iq:search protocol
+ * has been used mainly to search for people who have registered with user directories (e.g., the "Jabber User Directory" hosted at users.jabber.org).
+ * However, the jabber:iq:search protocol is not limited to user directories, and could be used to search other Jabber information repositories
+ * (such as chatroom directories) or even to provide a Jabber interface to conventional search engines.
+ * <p/>
+ * The basic functionality is to query an information repository regarding the possible search fields, to send a search query, and to receive search results.
+ *
+ * @author Derek DeMoro
+ */
+public class UserSearch extends IQ {
+
+    /**
+     * Creates a new instance of UserSearch.
+     */
+    public UserSearch() {
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+        buf.append("<query xmlns=\"jabber:iq:search\">");
+        buf.append(getExtensionsXML());
+        buf.append("</query>");
+        return buf.toString();
+    }
+
+    /**
+     * Returns the form for all search fields supported by the search service.
+     *
+     * @param con           the current Connection.
+     * @param searchService the search service to use. (ex. search.jivesoftware.com)
+     * @return the search form received by the server.
+     * @throws org.jivesoftware.smack.XMPPException
+     *          thrown if a server error has occurred.
+     */
+    public Form getSearchForm(Connection con, String searchService) throws XMPPException {
+        UserSearch search = new UserSearch();
+        search.setType(IQ.Type.GET);
+        search.setTo(searchService);
+
+        PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID()));
+        con.sendPacket(search);
+
+        IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+        // Cancel the collector.
+        collector.cancel();
+        if (response == null) {
+            throw new XMPPException("No response from server on status set.");
+        }
+        if (response.getError() != null) {
+            throw new XMPPException(response.getError());
+        }
+        return Form.getFormFrom(response);
+    }
+
+    /**
+     * Sends the filled out answer form to be sent and queried by the search service.
+     *
+     * @param con           the current Connection.
+     * @param searchForm    the <code>Form</code> to send for querying.
+     * @param searchService the search service to use. (ex. search.jivesoftware.com)
+     * @return ReportedData the data found from the query.
+     * @throws org.jivesoftware.smack.XMPPException
+     *          thrown if a server error has occurred.
+     */
+    public ReportedData sendSearchForm(Connection con, Form searchForm, String searchService) throws XMPPException {
+        UserSearch search = new UserSearch();
+        search.setType(IQ.Type.SET);
+        search.setTo(searchService);
+        search.addExtension(searchForm.getDataFormToSend());
+
+        PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID()));
+
+        con.sendPacket(search);
+
+        IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+        // Cancel the collector.
+        collector.cancel();
+        if (response == null) {
+            throw new XMPPException("No response from server on status set.");
+        }
+        if (response.getError() != null) {
+            return sendSimpleSearchForm(con, searchForm, searchService);
+        }
+
+
+        return ReportedData.getReportedDataFrom(response);
+    }
+
+    /**
+     * Sends the filled out answer form to be sent and queried by the search service.
+     *
+     * @param con           the current Connection.
+     * @param searchForm    the <code>Form</code> to send for querying.
+     * @param searchService the search service to use. (ex. search.jivesoftware.com)
+     * @return ReportedData the data found from the query.
+     * @throws org.jivesoftware.smack.XMPPException
+     *          thrown if a server error has occurred.
+     */
+    public ReportedData sendSimpleSearchForm(Connection con, Form searchForm, String searchService) throws XMPPException {
+        SimpleUserSearch search = new SimpleUserSearch();
+        search.setForm(searchForm);
+        search.setType(IQ.Type.SET);
+        search.setTo(searchService);
+
+        PacketCollector collector = con.createPacketCollector(new PacketIDFilter(search.getPacketID()));
+
+        con.sendPacket(search);
+
+        IQ response = (IQ) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+
+        // Cancel the collector.
+        collector.cancel();
+        if (response == null) {
+            throw new XMPPException("No response from server on status set.");
+        }
+        if (response.getError() != null) {
+            throw new XMPPException(response.getError());
+        }
+
+        if (response instanceof SimpleUserSearch) {
+            return ((SimpleUserSearch) response).getReportedData();
+        }
+        return null;
+    }
+
+    /**
+     * Internal Search service Provider.
+     */
+    public static class Provider implements IQProvider {
+
+        /**
+         * Provider Constructor.
+         */
+        public Provider() {
+            super();
+        }
+
+        public IQ parseIQ(XmlPullParser parser) throws Exception {
+            UserSearch search = null;
+            SimpleUserSearch simpleUserSearch = new SimpleUserSearch();
+
+            boolean done = false;
+            while (!done) {
+                int eventType = parser.next();
+                if (eventType == XmlPullParser.START_TAG && parser.getName().equals("instructions")) {
+                    buildDataForm(simpleUserSearch, parser.nextText(), parser);
+                    return simpleUserSearch;
+                }
+                else if (eventType == XmlPullParser.START_TAG && parser.getName().equals("item")) {
+                    simpleUserSearch.parseItems(parser);
+                    return simpleUserSearch;
+                }
+                else if (eventType == XmlPullParser.START_TAG && parser.getNamespace().equals("jabber:x:data")) {
+                    // Otherwise, it must be a packet extension.
+                    search = new UserSearch();
+                    search.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(),
+                            parser.getNamespace(), parser));
+
+                }
+                else if (eventType == XmlPullParser.END_TAG) {
+                    if (parser.getName().equals("query")) {
+                        done = true;
+                    }
+                }
+            }
+
+            if (search != null) {
+                return search;
+            }
+            return simpleUserSearch;
+        }
+    }
+
+    private static void buildDataForm(SimpleUserSearch search, String instructions, XmlPullParser parser) throws Exception {
+        DataForm dataForm = new DataForm(Form.TYPE_FORM);
+        boolean done = false;
+        dataForm.setTitle("User Search");
+        dataForm.addInstruction(instructions);
+        while (!done) {
+            int eventType = parser.next();
+
+            if (eventType == XmlPullParser.START_TAG && !parser.getNamespace().equals("jabber:x:data")) {
+                String name = parser.getName();
+                FormField field = new FormField(name);
+
+                // Handle hard coded values.
+                if(name.equals("first")){
+                    field.setLabel("First Name");
+                }
+                else if(name.equals("last")){
+                    field.setLabel("Last Name");
+                }
+                else if(name.equals("email")){
+                    field.setLabel("Email Address");
+                }
+                else if(name.equals("nick")){
+                    field.setLabel("Nickname");
+                }
+
+                field.setType(FormField.TYPE_TEXT_SINGLE);
+                dataForm.addField(field);
+            }
+            else if (eventType == XmlPullParser.END_TAG) {
+                if (parser.getName().equals("query")) {
+                    done = true;
+                }
+            }
+            else if (eventType == XmlPullParser.START_TAG && parser.getNamespace().equals("jabber:x:data")) {
+                search.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(),
+                        parser.getNamespace(), parser));
+                done = true;
+            }
+        }
+        if (search.getExtension("x", "jabber:x:data") == null) {
+            search.addExtension(dataForm);
+        }
+    }
+
+
+}
diff --git a/src/org/jivesoftware/smackx/search/UserSearchManager.java b/src/org/jivesoftware/smackx/search/UserSearchManager.java
new file mode 100644
index 0000000..858c2a7
--- /dev/null
+++ b/src/org/jivesoftware/smackx/search/UserSearchManager.java
@@ -0,0 +1,124 @@
+/**
+ * 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.smackx.search;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smackx.Form;
+import org.jivesoftware.smackx.ReportedData;
+import org.jivesoftware.smackx.ServiceDiscoveryManager;
+import org.jivesoftware.smackx.packet.DiscoverInfo;
+import org.jivesoftware.smackx.packet.DiscoverItems;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * The UserSearchManager is a facade built upon Jabber Search Services (JEP-055) to allow for searching
+ * repositories on a Jabber Server. This implementation allows for transparency of implementation of
+ * searching (DataForms or No DataForms), but allows the user to simply use the DataForm model for both
+ * types of support.
+ * <pre>
+ * Connection con = new XMPPConnection("jabber.org");
+ * con.login("john", "doe");
+ * UserSearchManager search = new UserSearchManager(con, "users.jabber.org");
+ * Form searchForm = search.getSearchForm();
+ * Form answerForm = searchForm.createAnswerForm();
+ * answerForm.setAnswer("last", "DeMoro");
+ * ReportedData data = search.getSearchResults(answerForm);
+ * // Use Returned Data
+ * </pre>
+ *
+ * @author Derek DeMoro
+ */
+public class UserSearchManager {
+
+    private Connection con;
+    private UserSearch userSearch;
+
+    /**
+     * Creates a new UserSearchManager.
+     *
+     * @param con the Connection to use.
+     */
+    public UserSearchManager(Connection con) {
+        this.con = con;
+        userSearch = new UserSearch();
+    }
+
+    /**
+     * Returns the form to fill out to perform a search.
+     *
+     * @param searchService the search service to query.
+     * @return the form to fill out to perform a search.
+     * @throws XMPPException thrown if a server error has occurred.
+     */
+    public Form getSearchForm(String searchService) throws XMPPException {
+        return userSearch.getSearchForm(con, searchService);
+    }
+
+    /**
+     * Submits a search form to the server and returns the resulting information
+     * in the form of <code>ReportedData</code>
+     *
+     * @param searchForm    the <code>Form</code> to submit for searching.
+     * @param searchService the name of the search service to use.
+     * @return the ReportedData returned by the server.
+     * @throws XMPPException thrown if a server error has occurred.
+     */
+    public ReportedData getSearchResults(Form searchForm, String searchService) throws XMPPException {
+        return userSearch.sendSearchForm(con, searchForm, searchService);
+    }
+
+
+    /**
+     * Returns a collection of search services found on the server.
+     *
+     * @return a Collection of search services found on the server.
+     * @throws XMPPException thrown if a server error has occurred.
+     */
+    public Collection<String> getSearchServices() throws XMPPException {
+        final List<String> searchServices = new ArrayList<String>();
+        ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(con);
+        DiscoverItems items = discoManager.discoverItems(con.getServiceName());
+        Iterator<DiscoverItems.Item> iter = items.getItems();
+        while (iter.hasNext()) {
+            DiscoverItems.Item item = iter.next();
+            try {
+                DiscoverInfo info;
+                try {
+                    info = discoManager.discoverInfo(item.getEntityID());
+                }
+                catch (XMPPException e) {
+                    // Ignore Case
+                    continue;
+                }
+
+                if (info.containsFeature("jabber:iq:search")) {
+                    searchServices.add(item.getEntityID());
+                }
+            }
+            catch (Exception e) {
+                // No info found.
+                break;
+            }
+        }
+        return searchServices;
+    }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/MetaData.java b/src/org/jivesoftware/smackx/workgroup/MetaData.java
new file mode 100644
index 0000000..115a79c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/MetaData.java
@@ -0,0 +1,68 @@
+/**

+ * $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.smackx.workgroup;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smackx.workgroup.util.MetaDataUtils;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+

+/**

+ * MetaData packet extension.

+ */

+public class MetaData implements PacketExtension {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "metadata";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    private Map<String, List<String>> metaData;

+

+    public MetaData(Map<String, List<String>> metaData) {

+        this.metaData = metaData;

+    }

+

+    /**

+     * @return the Map of metadata contained by this instance

+     */

+    public Map<String, List<String>> getMetaData() {

+        return metaData;

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    public String toXML() {

+        return MetaDataUtils.serializeMetaData(this.getMetaData());

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/QueueUser.java b/src/org/jivesoftware/smackx/workgroup/QueueUser.java
new file mode 100644
index 0000000..89a1899
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/QueueUser.java
@@ -0,0 +1,85 @@
+/**

+ * $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.smackx.workgroup;

+

+import java.util.Date;

+

+/**

+ * An immutable class which wraps up customer-in-queue data return from the server; depending on

+ * the type of information dispatched from the server, not all information will be available in

+ * any given instance.

+ *

+ * @author loki der quaeler

+ */

+public class QueueUser {

+

+    private String userID;

+

+    private int queuePosition;

+    private int estimatedTime;

+    private Date joinDate;

+

+    /**

+     * @param uid the user jid of the customer in the queue

+     * @param position the position customer sits in the queue

+     * @param time the estimate of how much longer the customer will be in the queue in seconds

+     * @param joinedAt the timestamp of when the customer entered the queue

+     */

+    public QueueUser (String uid, int position, int time, Date joinedAt) {

+        super();

+

+        this.userID = uid;

+        this.queuePosition = position;

+        this.estimatedTime = time;

+        this.joinDate = joinedAt;

+    }

+

+    /**

+     * @return the user jid of the customer in the queue

+     */

+    public String getUserID () {

+        return this.userID;

+    }

+

+    /**

+     * @return the position in the queue at which the customer sits, or -1 if the update which

+     *          this instance embodies is only a time update instead

+     */

+    public int getQueuePosition () {

+        return this.queuePosition;

+    }

+

+    /**

+     * @return the estimated time remaining of the customer in the queue in seconds, or -1 if

+     *          if the update which this instance embodies is only a position update instead

+     */

+    public int getEstimatedRemainingTime () {

+        return this.estimatedTime;

+    }

+

+    /**

+     * @return the timestamp of when this customer entered the queue, or null if the server did not

+     *          provide this information

+     */

+    public Date getQueueJoinTimestamp () {

+        return this.joinDate;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitation.java b/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitation.java
new file mode 100644
index 0000000..ac3b5b6
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitation.java
@@ -0,0 +1,134 @@
+/**

+ * $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.smackx.workgroup;

+

+import java.util.List;

+import java.util.Map;

+

+/**

+ * An immutable class wrapping up the basic information which comprises a group chat invitation.

+ *

+ * @author loki der quaeler

+ */

+public class WorkgroupInvitation {

+

+    protected String uniqueID;

+

+    protected String sessionID;

+

+    protected String groupChatName;

+    protected String issuingWorkgroupName;

+    protected String messageBody;

+    protected String invitationSender;

+    protected Map<String, List<String>> metaData;

+

+    /**

+     * This calls the 5-argument constructor with a null MetaData argument value

+     *

+     * @param jid the jid string with which the issuing AgentSession or Workgroup instance

+     *                  was created

+     * @param group the jid of the room to which the person is invited

+     * @param workgroup the jid of the workgroup issuing the invitation

+     * @param sessID the session id associated with the pending chat

+     * @param msgBody the body of the message which contained the invitation

+     * @param from the user jid who issued the invitation, if known, null otherwise

+     */

+    public WorkgroupInvitation (String jid, String group, String workgroup,

+                       String sessID, String msgBody, String from) {

+        this(jid, group, workgroup, sessID, msgBody, from, null);

+    }

+

+    /**

+     * @param jid the jid string with which the issuing AgentSession or Workgroup instance

+     *                  was created

+     * @param group the jid of the room to which the person is invited

+     * @param workgroup the jid of the workgroup issuing the invitation

+     * @param sessID the session id associated with the pending chat

+     * @param msgBody the body of the message which contained the invitation

+     * @param from the user jid who issued the invitation, if known, null otherwise

+     * @param metaData the metadata sent with the invitation

+     */

+    public WorkgroupInvitation (String jid, String group, String workgroup, String sessID, String msgBody,

+                       String from, Map<String, List<String>> metaData) {

+        super();

+

+        this.uniqueID = jid;

+        this.sessionID = sessID;

+        this.groupChatName = group;

+        this.issuingWorkgroupName = workgroup;

+        this.messageBody = msgBody;

+        this.invitationSender = from;

+        this.metaData = metaData;

+    }

+

+    /**

+     * @return the jid string with which the issuing AgentSession or Workgroup instance

+     *  was created.

+     */

+    public String getUniqueID () {

+        return this.uniqueID;

+    }

+

+    /**

+     * @return the session id associated with the pending chat; working backwards temporally,

+     *              this session id should match the session id to the corresponding offer request

+     *              which resulted in this invitation.

+     */

+    public String getSessionID () {

+        return this.sessionID;

+    }

+

+    /**

+     * @return the jid of the room to which the person is invited.

+     */

+    public String getGroupChatName () {

+        return this.groupChatName;

+    }

+

+    /**

+     * @return the name of the workgroup from which the invitation was issued.

+     */

+    public String getWorkgroupName () {

+        return this.issuingWorkgroupName;

+    }

+

+    /**

+     * @return the contents of the body-block of the message that housed this invitation.

+     */

+    public String getMessageBody () {

+        return this.messageBody;

+    }

+

+    /**

+     * @return the user who issued the invitation, or null if it wasn't known.

+     */

+    public String getInvitationSender () {

+        return this.invitationSender;

+    }

+

+    /**

+     * @return the meta data associated with the invitation, or null if this instance was

+     *              constructed with none

+     */

+    public Map<String, List<String>> getMetaData () {

+        return this.metaData;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitationListener.java b/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitationListener.java
new file mode 100644
index 0000000..bc73242
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/WorkgroupInvitationListener.java
@@ -0,0 +1,39 @@
+/**

+ * $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.smackx.workgroup;

+

+/**

+ * An interface which all classes interested in hearing about group chat invitations should

+ *  implement.

+ *

+ * @author loki der quaeler

+ */

+public interface WorkgroupInvitationListener {

+

+    /**

+     * The implementing class instance will be notified via this method when an invitation

+     *  to join a group chat has been received from the server.

+     *

+     * @param invitation an Invitation instance embodying the information pertaining to the

+     *                      invitation

+     */

+    public void invitationReceived(WorkgroupInvitation invitation);

+

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/Agent.java b/src/org/jivesoftware/smackx/workgroup/agent/Agent.java
new file mode 100644
index 0000000..bebac37
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/Agent.java
@@ -0,0 +1,138 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import org.jivesoftware.smackx.workgroup.packet.AgentInfo;

+import org.jivesoftware.smackx.workgroup.packet.AgentWorkgroups;

+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.PacketIDFilter;

+import org.jivesoftware.smack.packet.IQ;

+

+import java.util.Collection;

+

+/**

+ * The <code>Agent</code> class is used to represent one agent in a Workgroup Queue.

+ *

+ * @author Derek DeMoro

+ */

+public class Agent {

+    private Connection connection;

+    private String workgroupJID;

+

+    public static Collection<String> getWorkgroups(String serviceJID, String agentJID, Connection connection) throws XMPPException {

+        AgentWorkgroups request = new AgentWorkgroups(agentJID);

+        request.setTo(serviceJID);

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        // Send the request

+        connection.sendPacket(request);

+

+        AgentWorkgroups response = (AgentWorkgroups)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response.getWorkgroups();

+    }

+

+    /**

+     * Constructs an Agent.

+     */

+    Agent(Connection connection, String workgroupJID) {

+        this.connection = connection;

+        this.workgroupJID = workgroupJID;

+    }

+

+    /**

+     * Return the agents JID

+     *

+     * @return - the agents JID.

+     */

+    public String getUser() {

+        return connection.getUser();

+    }

+

+    /**

+     * Return the agents name.

+     *

+     * @return - the agents name.

+     */

+    public String getName() throws XMPPException {

+        AgentInfo agentInfo = new AgentInfo();

+        agentInfo.setType(IQ.Type.GET);

+        agentInfo.setTo(workgroupJID);

+        agentInfo.setFrom(getUser());

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(agentInfo.getPacketID()));

+        // Send the request

+        connection.sendPacket(agentInfo);

+

+        AgentInfo response = (AgentInfo)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response.getName();

+    }

+

+    /**

+     * Changes the name of the agent in the server. The server may have this functionality

+     * disabled for all the agents or for this agent in particular. If the agent is not

+     * allowed to change his name then an exception will be thrown with a service_unavailable

+     * error code.

+     *

+     * @param newName the new name of the agent.

+     * @throws XMPPException if the agent is not allowed to change his name or no response was

+     *                       obtained from the server.

+     */

+    public void setName(String newName) throws XMPPException {

+        AgentInfo agentInfo = new AgentInfo();

+        agentInfo.setType(IQ.Type.SET);

+        agentInfo.setTo(workgroupJID);

+        agentInfo.setFrom(getUser());

+        agentInfo.setName(newName);

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(agentInfo.getPacketID()));

+        // Send the request

+        connection.sendPacket(agentInfo);

+

+        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return;

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/AgentRoster.java b/src/org/jivesoftware/smackx/workgroup/agent/AgentRoster.java
new file mode 100644
index 0000000..70c95ee
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/AgentRoster.java
@@ -0,0 +1,386 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import org.jivesoftware.smackx.workgroup.packet.AgentStatus;

+import org.jivesoftware.smackx.workgroup.packet.AgentStatusRequest;

+import org.jivesoftware.smack.PacketListener;

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.filter.PacketFilter;

+import org.jivesoftware.smack.filter.PacketTypeFilter;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.packet.Presence;

+import org.jivesoftware.smack.util.StringUtils;

+

+import java.util.ArrayList;

+import java.util.Collections;

+import java.util.HashMap;

+import java.util.HashSet;

+import java.util.Iterator;

+import java.util.List;

+import java.util.Map;

+import java.util.Set;

+

+/**

+ * Manges information about the agents in a workgroup and their presence.

+ *

+ * @author Matt Tucker

+ * @see AgentSession#getAgentRoster()

+ */

+public class AgentRoster {

+

+    private static final int EVENT_AGENT_ADDED = 0;

+    private static final int EVENT_AGENT_REMOVED = 1;

+    private static final int EVENT_PRESENCE_CHANGED = 2;

+

+    private Connection connection;

+    private String workgroupJID;

+    private List<String> entries;

+    private List<AgentRosterListener> listeners;

+    private Map<String, Map<String, Presence>> presenceMap;

+    // The roster is marked as initialized when at least a single roster packet

+    // has been recieved and processed.

+    boolean rosterInitialized = false;

+

+    /**

+     * Constructs a new AgentRoster.

+     *

+     * @param connection an XMPP connection.

+     */

+    AgentRoster(Connection connection, String workgroupJID) {

+        this.connection = connection;

+        this.workgroupJID = workgroupJID;

+        entries = new ArrayList<String>();

+        listeners = new ArrayList<AgentRosterListener>();

+        presenceMap = new HashMap<String, Map<String, Presence>>();

+        // Listen for any roster packets.

+        PacketFilter rosterFilter = new PacketTypeFilter(AgentStatusRequest.class);

+        connection.addPacketListener(new AgentStatusListener(), rosterFilter);

+        // Listen for any presence packets.

+        connection.addPacketListener(new PresencePacketListener(),

+                new PacketTypeFilter(Presence.class));

+

+        // Send request for roster.

+        AgentStatusRequest request = new AgentStatusRequest();

+        request.setTo(workgroupJID);

+        connection.sendPacket(request);

+    }

+

+    /**

+     * Reloads the entire roster from the server. This is an asynchronous operation,

+     * which means the method will return immediately, and the roster will be

+     * reloaded at a later point when the server responds to the reload request.

+     */

+    public void reload() {

+        AgentStatusRequest request = new AgentStatusRequest();

+        request.setTo(workgroupJID);

+        connection.sendPacket(request);

+    }

+

+    /**

+     * Adds a listener to this roster. The listener will be fired anytime one or more

+     * changes to the roster are pushed from the server.

+     *

+     * @param listener an agent roster listener.

+     */

+    public void addListener(AgentRosterListener listener) {

+        synchronized (listeners) {

+            if (!listeners.contains(listener)) {

+                listeners.add(listener);

+

+                // Fire events for the existing entries and presences in the roster

+                for (Iterator<String> it = getAgents().iterator(); it.hasNext();) {

+                    String jid = it.next();

+                    // Check again in case the agent is no longer in the roster (highly unlikely

+                    // but possible)

+                    if (entries.contains(jid)) {

+                        // Fire the agent added event

+                        listener.agentAdded(jid);

+                        Map<String,Presence> userPresences = presenceMap.get(jid);

+                        if (userPresences != null) {

+                            Iterator<Presence> presences = userPresences.values().iterator();

+                            while (presences.hasNext()) {

+                                // Fire the presence changed event

+                                listener.presenceChanged(presences.next());

+                            }

+                        }

+                    }

+                }

+            }

+        }

+    }

+

+    /**

+     * Removes a listener from this roster. The listener will be fired anytime one or more

+     * changes to the roster are pushed from the server.

+     *

+     * @param listener a roster listener.

+     */

+    public void removeListener(AgentRosterListener listener) {

+        synchronized (listeners) {

+            listeners.remove(listener);

+        }

+    }

+

+    /**

+     * Returns a count of all agents in the workgroup.

+     *

+     * @return the number of agents in the workgroup.

+     */

+    public int getAgentCount() {

+        return entries.size();

+    }

+

+    /**

+     * Returns all agents (String JID values) in the workgroup.

+     *

+     * @return all entries in the roster.

+     */

+    public Set<String> getAgents() {

+        Set<String> agents = new HashSet<String>();

+        synchronized (entries) {

+            for (Iterator<String> i = entries.iterator(); i.hasNext();) {

+                agents.add(i.next());

+            }

+        }

+        return Collections.unmodifiableSet(agents);

+    }

+

+    /**

+     * Returns true if the specified XMPP address is an agent in the workgroup.

+     *

+     * @param jid the XMPP address of the agent (eg "jsmith@example.com"). The

+     *            address can be in any valid format (e.g. "domain/resource", "user@domain"

+     *            or "user@domain/resource").

+     * @return true if the XMPP address is an agent in the workgroup.

+     */

+    public boolean contains(String jid) {

+        if (jid == null) {

+            return false;

+        }

+        synchronized (entries) {

+            for (Iterator<String> i = entries.iterator(); i.hasNext();) {

+                String entry = i.next();

+                if (entry.toLowerCase().equals(jid.toLowerCase())) {

+                    return true;

+                }

+            }

+        }

+        return false;

+    }

+

+    /**

+     * Returns the presence info for a particular agent, or <tt>null</tt> if the agent

+     * is unavailable (offline) or if no presence information is available.<p>

+     *

+     * @param user a fully qualified xmpp JID. The address could be in any valid format (e.g.

+     *             "domain/resource", "user@domain" or "user@domain/resource").

+     * @return the agent's current presence, or <tt>null</tt> if the agent is unavailable

+     *         or if no presence information is available..

+     */

+    public Presence getPresence(String user) {

+        String key = getPresenceMapKey(user);

+        Map<String, Presence> userPresences = presenceMap.get(key);

+        if (userPresences == null) {

+            Presence presence = new Presence(Presence.Type.unavailable);

+            presence.setFrom(user);

+            return presence;

+        }

+        else {

+            // Find the resource with the highest priority

+            // Might be changed to use the resource with the highest availability instead.

+            Iterator<String> it = userPresences.keySet().iterator();

+            Presence p;

+            Presence presence = null;

+

+            while (it.hasNext()) {

+                p = (Presence)userPresences.get(it.next());

+                if (presence == null){

+                    presence = p;

+                }

+                else {

+                    if (p.getPriority() > presence.getPriority()) {

+                        presence = p;

+                    }

+                }

+            }

+            if (presence == null) {

+                presence = new Presence(Presence.Type.unavailable);

+                presence.setFrom(user);

+                return presence;

+            }

+            else {

+                return presence;

+            }

+        }

+    }

+

+    /**

+     * Returns the key to use in the presenceMap for a fully qualified xmpp ID. The roster

+     * can contain any valid address format such us "domain/resource", "user@domain" or

+     * "user@domain/resource". If the roster contains an entry associated with the fully qualified

+     * xmpp ID then use the fully qualified xmpp ID as the key in presenceMap, otherwise use the

+     * bare address. Note: When the key in presenceMap is a fully qualified xmpp ID, the

+     * userPresences is useless since it will always contain one entry for the user.

+     *

+     * @param user the fully qualified xmpp ID, e.g. jdoe@example.com/Work.

+     * @return the key to use in the presenceMap for the fully qualified xmpp ID.

+     */

+    private String getPresenceMapKey(String user) {

+        String key = user;

+        if (!contains(user)) {

+            key = StringUtils.parseBareAddress(user).toLowerCase();

+        }

+        return key;

+    }

+

+    /**

+     * Fires event to listeners.

+     */

+    private void fireEvent(int eventType, Object eventObject) {

+        AgentRosterListener[] listeners = null;

+        synchronized (this.listeners) {

+            listeners = new AgentRosterListener[this.listeners.size()];

+            this.listeners.toArray(listeners);

+        }

+        for (int i = 0; i < listeners.length; i++) {

+            switch (eventType) {

+                case EVENT_AGENT_ADDED:

+                    listeners[i].agentAdded((String)eventObject);

+                    break;

+                case EVENT_AGENT_REMOVED:

+                    listeners[i].agentRemoved((String)eventObject);

+                    break;

+                case EVENT_PRESENCE_CHANGED:

+                    listeners[i].presenceChanged((Presence)eventObject);

+                    break;

+            }

+        }

+    }

+

+    /**

+     * Listens for all presence packets and processes them.

+     */

+    private class PresencePacketListener implements PacketListener {

+        public void processPacket(Packet packet) {

+            Presence presence = (Presence)packet;

+            String from = presence.getFrom();

+            if (from == null) {

+                // TODO Check if we need to ignore these presences or this is a server bug?

+                System.out.println("Presence with no FROM: " + presence.toXML());

+                return;

+            }

+            String key = getPresenceMapKey(from);

+

+            // If an "available" packet, add it to the presence map. Each presence map will hold

+            // for a particular user a map with the presence packets saved for each resource.

+            if (presence.getType() == Presence.Type.available) {

+                // Ignore the presence packet unless it has an agent status extension.

+                AgentStatus agentStatus = (AgentStatus)presence.getExtension(

+                        AgentStatus.ELEMENT_NAME, AgentStatus.NAMESPACE);

+                if (agentStatus == null) {

+                    return;

+                }

+                // Ensure that this presence is coming from an Agent of the same workgroup

+                // of this Agent

+                else if (!workgroupJID.equals(agentStatus.getWorkgroupJID())) {

+                    return;

+                }

+                Map<String, Presence> userPresences;

+                // Get the user presence map

+                if (presenceMap.get(key) == null) {

+                    userPresences = new HashMap<String, Presence>();

+                    presenceMap.put(key, userPresences);

+                }

+                else {

+                    userPresences = presenceMap.get(key);

+                }

+                // Add the new presence, using the resources as a key.

+                synchronized (userPresences) {

+                    userPresences.put(StringUtils.parseResource(from), presence);

+                }

+                // Fire an event.

+                synchronized (entries) {

+                    for (Iterator<String> i = entries.iterator(); i.hasNext();) {

+                        String entry = i.next();

+                        if (entry.toLowerCase().equals(StringUtils.parseBareAddress(key).toLowerCase())) {

+                            fireEvent(EVENT_PRESENCE_CHANGED, packet);

+                        }

+                    }

+                }

+            }

+            // If an "unavailable" packet, remove any entries in the presence map.

+            else if (presence.getType() == Presence.Type.unavailable) {

+                if (presenceMap.get(key) != null) {

+                    Map<String,Presence> userPresences = presenceMap.get(key);

+                    synchronized (userPresences) {

+                        userPresences.remove(StringUtils.parseResource(from));

+                    }

+                    if (userPresences.isEmpty()) {

+                        presenceMap.remove(key);

+                    }

+                }

+                // Fire an event.

+                synchronized (entries) {

+                    for (Iterator<String> i = entries.iterator(); i.hasNext();) {

+                        String entry = (String)i.next();

+                        if (entry.toLowerCase().equals(StringUtils.parseBareAddress(key).toLowerCase())) {

+                            fireEvent(EVENT_PRESENCE_CHANGED, packet);

+                        }

+                    }

+                }

+            }

+        }

+    }

+

+    /**

+     * Listens for all roster packets and processes them.

+     */

+    private class AgentStatusListener implements PacketListener {

+

+        public void processPacket(Packet packet) {

+            if (packet instanceof AgentStatusRequest) {

+                AgentStatusRequest statusRequest = (AgentStatusRequest)packet;

+                for (Iterator<AgentStatusRequest.Item> i = statusRequest.getAgents().iterator(); i.hasNext();) {

+                    AgentStatusRequest.Item item = i.next();

+                    String agentJID = item.getJID();

+                    if ("remove".equals(item.getType())) {

+

+                        // Removing the user from the roster, so remove any presence information

+                        // about them.

+                        String key = StringUtils.parseName(StringUtils.parseName(agentJID) + "@" +

+                                StringUtils.parseServer(agentJID));

+                        presenceMap.remove(key);

+                        // Fire event for roster listeners.

+                        fireEvent(EVENT_AGENT_REMOVED, agentJID);

+                    }

+                    else {

+                        entries.add(agentJID);

+                        // Fire event for roster listeners.

+                        fireEvent(EVENT_AGENT_ADDED, agentJID);

+                    }

+                }

+

+                // Mark the roster as initialized.

+                rosterInitialized = true;

+            }

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/AgentRosterListener.java b/src/org/jivesoftware/smackx/workgroup/agent/AgentRosterListener.java
new file mode 100644
index 0000000..4db9203
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/AgentRosterListener.java
@@ -0,0 +1,35 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import org.jivesoftware.smack.packet.Presence;

+

+/**

+ *

+ * @author Matt Tucker

+ */

+public interface AgentRosterListener {

+

+    public void agentAdded(String jid);

+

+    public void agentRemoved(String jid);

+

+    public void presenceChanged(Presence presence);

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/AgentSession.java b/src/org/jivesoftware/smackx/workgroup/agent/AgentSession.java
new file mode 100644
index 0000000..46d19d0
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/AgentSession.java
@@ -0,0 +1,1185 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import org.jivesoftware.smackx.workgroup.MetaData;

+import org.jivesoftware.smackx.workgroup.QueueUser;

+import org.jivesoftware.smackx.workgroup.WorkgroupInvitation;

+import org.jivesoftware.smackx.workgroup.WorkgroupInvitationListener;

+import org.jivesoftware.smackx.workgroup.ext.history.AgentChatHistory;

+import org.jivesoftware.smackx.workgroup.ext.history.ChatMetadata;

+import org.jivesoftware.smackx.workgroup.ext.macros.MacroGroup;

+import org.jivesoftware.smackx.workgroup.ext.macros.Macros;

+import org.jivesoftware.smackx.workgroup.ext.notes.ChatNotes;

+import org.jivesoftware.smackx.workgroup.packet.*;

+import org.jivesoftware.smackx.workgroup.settings.GenericSettings;

+import org.jivesoftware.smackx.workgroup.settings.SearchSettings;

+import org.jivesoftware.smack.*;

+import org.jivesoftware.smack.filter.*;

+import org.jivesoftware.smack.packet.*;

+import org.jivesoftware.smack.util.StringUtils;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.ReportedData;

+import org.jivesoftware.smackx.packet.MUCUser;

+

+import java.util.*;

+

+/**

+ * This class embodies the agent's active presence within a given workgroup. The application

+ * should have N instances of this class, where N is the number of workgroups to which the

+ * owning agent of the application belongs. This class provides all functionality that a

+ * session within a given workgroup is expected to have from an agent's perspective -- setting

+ * the status, tracking the status of queues to which the agent belongs within the workgroup, and

+ * dequeuing customers.

+ *

+ * @author Matt Tucker

+ * @author Derek DeMoro

+ */

+public class AgentSession {

+

+    private Connection connection;

+

+    private String workgroupJID;

+

+    private boolean online = false;

+    private Presence.Mode presenceMode;

+    private int maxChats;

+    private final Map<String, List<String>> metaData;

+

+    private Map<String, WorkgroupQueue> queues;

+

+    private final List<OfferListener> offerListeners;

+    private final List<WorkgroupInvitationListener> invitationListeners;

+    private final List<QueueUsersListener> queueUsersListeners;

+

+    private AgentRoster agentRoster = null;

+    private TranscriptManager transcriptManager;

+    private TranscriptSearchManager transcriptSearchManager;

+    private Agent agent;

+    private PacketListener packetListener;

+

+    /**

+     * Constructs a new agent session instance. Note, the {@link #setOnline(boolean)}

+     * method must be called with an argument of <tt>true</tt> to mark the agent

+     * as available to accept chat requests.

+     *

+     * @param connection   a connection instance which must have already gone through

+     *                     authentication.

+     * @param workgroupJID the fully qualified JID of the workgroup.

+     */

+    public AgentSession(String workgroupJID, Connection connection) {

+        // Login must have been done before passing in connection.

+        if (!connection.isAuthenticated()) {

+            throw new IllegalStateException("Must login to server before creating workgroup.");

+        }

+

+        this.workgroupJID = workgroupJID;

+        this.connection = connection;

+        this.transcriptManager = new TranscriptManager(connection);

+        this.transcriptSearchManager = new TranscriptSearchManager(connection);

+

+        this.maxChats = -1;

+

+        this.metaData = new HashMap<String, List<String>>();

+

+        this.queues = new HashMap<String, WorkgroupQueue>();

+

+        offerListeners = new ArrayList<OfferListener>();

+        invitationListeners = new ArrayList<WorkgroupInvitationListener>();

+        queueUsersListeners = new ArrayList<QueueUsersListener>();

+

+        // Create a filter to listen for packets we're interested in.

+        OrFilter filter = new OrFilter();

+        filter.addFilter(new PacketTypeFilter(OfferRequestProvider.OfferRequestPacket.class));

+        filter.addFilter(new PacketTypeFilter(OfferRevokeProvider.OfferRevokePacket.class));

+        filter.addFilter(new PacketTypeFilter(Presence.class));

+        filter.addFilter(new PacketTypeFilter(Message.class));

+

+        packetListener = new PacketListener() {

+            public void processPacket(Packet packet) {

+                try {

+                    handlePacket(packet);

+                }

+                catch (Exception e) {

+                    e.printStackTrace();

+                }

+            }

+        };

+        connection.addPacketListener(packetListener, filter);

+        // Create the agent associated to this session

+        agent = new Agent(connection, workgroupJID);

+    }

+

+    /**

+     * Close the agent session. The underlying connection will remain opened but the

+     * packet listeners that were added by this agent session will be removed.

+     */

+    public void close() {

+        connection.removePacketListener(packetListener);

+    }

+

+    /**

+     * Returns the agent roster for the workgroup, which contains

+     *

+     * @return the AgentRoster

+     */

+    public AgentRoster getAgentRoster() {

+        if (agentRoster == null) {

+            agentRoster = new AgentRoster(connection, workgroupJID);

+        }

+

+        // This might be the first time the user has asked for the roster. If so, we

+        // want to wait up to 2 seconds for the server to send back the list of agents.

+        // This behavior shields API users from having to worry about the fact that the

+        // operation is asynchronous, although they'll still have to listen for changes

+        // to the roster.

+        int elapsed = 0;

+        while (!agentRoster.rosterInitialized && elapsed <= 2000) {

+            try {

+                Thread.sleep(500);

+            }

+            catch (Exception e) {

+                // Ignore

+            }

+            elapsed += 500;

+        }

+        return agentRoster;

+    }

+

+    /**

+     * Returns the agent's current presence mode.

+     *

+     * @return the agent's current presence mode.

+     */

+    public Presence.Mode getPresenceMode() {

+        return presenceMode;

+    }

+

+    /**

+     * Returns the maximum number of chats the agent can participate in.

+     *

+     * @return the maximum number of chats the agent can participate in.

+     */

+    public int getMaxChats() {

+        return maxChats;

+    }

+

+    /**

+     * Returns true if the agent is online with the workgroup.

+     *

+     * @return true if the agent is online with the workgroup.

+     */

+    public boolean isOnline() {

+        return online;

+    }

+

+    /**

+     * Allows the addition of a new key-value pair to the agent's meta data, if the value is

+     * new data, the revised meta data will be rebroadcast in an agent's presence broadcast.

+     *

+     * @param key the meta data key

+     * @param val the non-null meta data value

+     * @throws XMPPException if an exception occurs.

+     */

+    public void setMetaData(String key, String val) throws XMPPException {

+        synchronized (this.metaData) {

+            List<String> oldVals = metaData.get(key);

+

+            if ((oldVals == null) || (!oldVals.get(0).equals(val))) {

+                oldVals.set(0, val);

+

+                setStatus(presenceMode, maxChats);

+            }

+        }

+    }

+

+    /**

+     * Allows the removal of data from the agent's meta data, if the key represents existing data,

+     * the revised meta data will be rebroadcast in an agent's presence broadcast.

+     *

+     * @param key the meta data key.

+     * @throws XMPPException if an exception occurs.

+     */

+    public void removeMetaData(String key) throws XMPPException {

+        synchronized (this.metaData) {

+            List<String> oldVal = metaData.remove(key);

+

+            if (oldVal != null) {

+                setStatus(presenceMode, maxChats);

+            }

+        }

+    }

+

+    /**

+     * Allows the retrieval of meta data for a specified key.

+     *

+     * @param key the meta data key

+     * @return the meta data value associated with the key or <tt>null</tt> if the meta-data

+     *         doesn't exist..

+     */

+    public List<String> getMetaData(String key) {

+        return metaData.get(key);

+    }

+

+    /**

+     * Sets whether the agent is online with the workgroup. If the user tries to go online with

+     * the workgroup but is not allowed to be an agent, an XMPPError with error code 401 will

+     * be thrown.

+     *

+     * @param online true to set the agent as online with the workgroup.

+     * @throws XMPPException if an error occurs setting the online status.

+     */

+    public void setOnline(boolean online) throws XMPPException {

+        // If the online status hasn't changed, do nothing.

+        if (this.online == online) {

+            return;

+        }

+

+        Presence presence;

+

+        // If the user is going online...

+        if (online) {

+            presence = new Presence(Presence.Type.available);

+            presence.setTo(workgroupJID);

+            presence.addExtension(new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,

+                    AgentStatus.NAMESPACE));

+

+            PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupJID)));

+

+            connection.sendPacket(presence);

+

+            presence = (Presence)collector.nextResult(5000);

+            collector.cancel();

+            if (!presence.isAvailable()) {

+                throw new XMPPException("No response from server on status set.");

+            }

+

+            if (presence.getError() != null) {

+                throw new XMPPException(presence.getError());

+            }

+

+            // We can safely update this iv since we didn't get any error

+            this.online = online;

+        }

+        // Otherwise the user is going offline...

+        else {

+            // Update this iv now since we don't care at this point of any error

+            this.online = online;

+

+            presence = new Presence(Presence.Type.unavailable);

+            presence.setTo(workgroupJID);

+            presence.addExtension(new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,

+                    AgentStatus.NAMESPACE));

+            connection.sendPacket(presence);

+        }

+    }

+

+    /**

+     * Sets the agent's current status with the workgroup. The presence mode affects

+     * how offers are routed to the agent. The possible presence modes with their

+     * meanings are as follows:<ul>

+     * <p/>

+     * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats

+     * (equivalent to Presence.Mode.CHAT).

+     * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.

+     * However, special case, or extreme urgency chats may still be offered to the agent.

+     * <li>Presence.Mode.AWAY -- the agent is not available and should not

+     * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>

+     * <p/>

+     * The max chats value is the maximum number of chats the agent is willing to have

+     * routed to them at once. Some servers may be configured to only accept max chat

+     * values in a certain range; for example, between two and five. In that case, the

+     * maxChats value the agent sends may be adjusted by the server to a value within that

+     * range.

+     *

+     * @param presenceMode the presence mode of the agent.

+     * @param maxChats     the maximum number of chats the agent is willing to accept.

+     * @throws XMPPException         if an error occurs setting the agent status.

+     * @throws IllegalStateException if the agent is not online with the workgroup.

+     */

+    public void setStatus(Presence.Mode presenceMode, int maxChats) throws XMPPException {

+        setStatus(presenceMode, maxChats, null);

+    }

+

+    /**

+     * Sets the agent's current status with the workgroup. The presence mode affects how offers

+     * are routed to the agent. The possible presence modes with their meanings are as follows:<ul>

+     * <p/>

+     * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats

+     * (equivalent to Presence.Mode.CHAT).

+     * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.

+     * However, special case, or extreme urgency chats may still be offered to the agent.

+     * <li>Presence.Mode.AWAY -- the agent is not available and should not

+     * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>

+     * <p/>

+     * The max chats value is the maximum number of chats the agent is willing to have routed to

+     * them at once. Some servers may be configured to only accept max chat values in a certain

+     * range; for example, between two and five. In that case, the maxChats value the agent sends

+     * may be adjusted by the server to a value within that range.

+     *

+     * @param presenceMode the presence mode of the agent.

+     * @param maxChats     the maximum number of chats the agent is willing to accept.

+     * @param status       sets the status message of the presence update.

+     * @throws XMPPException         if an error occurs setting the agent status.

+     * @throws IllegalStateException if the agent is not online with the workgroup.

+     */

+    public void setStatus(Presence.Mode presenceMode, int maxChats, String status)

+            throws XMPPException {

+        if (!online) {

+            throw new IllegalStateException("Cannot set status when the agent is not online.");

+        }

+

+        if (presenceMode == null) {

+            presenceMode = Presence.Mode.available;

+        }

+        this.presenceMode = presenceMode;

+        this.maxChats = maxChats;

+

+        Presence presence = new Presence(Presence.Type.available);

+        presence.setMode(presenceMode);

+        presence.setTo(this.getWorkgroupJID());

+

+        if (status != null) {

+            presence.setStatus(status);

+        }

+        // Send information about max chats and current chats as a packet extension.

+        DefaultPacketExtension agentStatus = new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,

+                AgentStatus.NAMESPACE);

+        agentStatus.setValue("max-chats", "" + maxChats);

+        presence.addExtension(agentStatus);

+        presence.addExtension(new MetaData(this.metaData));

+

+        PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupJID)));

+

+        this.connection.sendPacket(presence);

+

+        presence = (Presence)collector.nextResult(5000);

+        collector.cancel();

+        if (!presence.isAvailable()) {

+            throw new XMPPException("No response from server on status set.");

+        }

+

+        if (presence.getError() != null) {

+            throw new XMPPException(presence.getError());

+        }

+    }

+

+    /**

+     * Sets the agent's current status with the workgroup. The presence mode affects how offers

+     * are routed to the agent. The possible presence modes with their meanings are as follows:<ul>

+     * <p/>

+     * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats

+     * (equivalent to Presence.Mode.CHAT).

+     * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.

+     * However, special case, or extreme urgency chats may still be offered to the agent.

+     * <li>Presence.Mode.AWAY -- the agent is not available and should not

+     * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>

+     *

+     * @param presenceMode the presence mode of the agent.

+     * @param status       sets the status message of the presence update.

+     * @throws XMPPException         if an error occurs setting the agent status.

+     * @throws IllegalStateException if the agent is not online with the workgroup.

+     */

+    public void setStatus(Presence.Mode presenceMode, String status) throws XMPPException {

+        if (!online) {

+            throw new IllegalStateException("Cannot set status when the agent is not online.");

+        }

+

+        if (presenceMode == null) {

+            presenceMode = Presence.Mode.available;

+        }

+        this.presenceMode = presenceMode;

+

+        Presence presence = new Presence(Presence.Type.available);

+        presence.setMode(presenceMode);

+        presence.setTo(this.getWorkgroupJID());

+

+        if (status != null) {

+            presence.setStatus(status);

+        }

+        presence.addExtension(new MetaData(this.metaData));

+

+        PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class),

+                new FromContainsFilter(workgroupJID)));

+

+        this.connection.sendPacket(presence);

+

+        presence = (Presence)collector.nextResult(5000);

+        collector.cancel();

+        if (!presence.isAvailable()) {

+            throw new XMPPException("No response from server on status set.");

+        }

+

+        if (presence.getError() != null) {

+            throw new XMPPException(presence.getError());

+        }

+    }

+

+    /**

+     * Removes a user from the workgroup queue. This is an administrative action that the

+     * <p/>

+     * The agent is not guaranteed of having privileges to perform this action; an exception

+     * denying the request may be thrown.

+     *

+     * @param userID the ID of the user to remove.

+     * @throws XMPPException if an exception occurs.

+     */

+    public void dequeueUser(String userID) throws XMPPException {

+        // todo: this method simply won't work right now.

+        DepartQueuePacket departPacket = new DepartQueuePacket(this.workgroupJID);

+

+        // PENDING

+        this.connection.sendPacket(departPacket);

+    }

+

+    /**

+     * Returns the transcripts of a given user. The answer will contain the complete history of

+     * conversations that a user had.

+     *

+     * @param userID the id of the user to get his conversations.

+     * @return the transcripts of a given user.

+     * @throws XMPPException if an error occurs while getting the information.

+     */

+    public Transcripts getTranscripts(String userID) throws XMPPException {

+        return transcriptManager.getTranscripts(workgroupJID, userID);

+    }

+

+    /**

+     * Returns the full conversation transcript of a given session.

+     *

+     * @param sessionID the id of the session to get the full transcript.

+     * @return the full conversation transcript of a given session.

+     * @throws XMPPException if an error occurs while getting the information.

+     */

+    public Transcript getTranscript(String sessionID) throws XMPPException {

+        return transcriptManager.getTranscript(workgroupJID, sessionID);

+    }

+

+    /**

+     * Returns the Form to use for searching transcripts. It is unlikely that the server

+     * will change the form (without a restart) so it is safe to keep the returned form

+     * for future submissions.

+     *

+     * @return the Form to use for searching transcripts.

+     * @throws XMPPException if an error occurs while sending the request to the server.

+     */

+    public Form getTranscriptSearchForm() throws XMPPException {

+        return transcriptSearchManager.getSearchForm(StringUtils.parseServer(workgroupJID));

+    }

+

+    /**

+     * Submits the completed form and returns the result of the transcript search. The result

+     * will include all the data returned from the server so be careful with the amount of

+     * data that the search may return.

+     *

+     * @param completedForm the filled out search form.

+     * @return the result of the transcript search.

+     * @throws XMPPException if an error occurs while submiting the search to the server.

+     */

+    public ReportedData searchTranscripts(Form completedForm) throws XMPPException {

+        return transcriptSearchManager.submitSearch(StringUtils.parseServer(workgroupJID),

+                completedForm);

+    }

+

+    /**

+     * Asks the workgroup for information about the occupants of the specified room. The returned

+     * information will include the real JID of the occupants, the nickname of the user in the

+     * room as well as the date when the user joined the room.

+     *

+     * @param roomID the room to get information about its occupants.

+     * @return information about the occupants of the specified room.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public OccupantsInfo getOccupantsInfo(String roomID) throws XMPPException {

+        OccupantsInfo request = new OccupantsInfo(roomID);

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+        OccupantsInfo response = (OccupantsInfo)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+    /**

+     * @return the fully-qualified name of the workgroup for which this session exists

+     */

+    public String getWorkgroupJID() {

+        return workgroupJID;

+    }

+

+    /**

+     * Returns the Agent associated to this session.

+     *

+     * @return the Agent associated to this session.

+     */

+    public Agent getAgent() {

+        return agent;

+    }

+

+    /**

+     * @param queueName the name of the queue

+     * @return an instance of WorkgroupQueue for the argument queue name, or null if none exists

+     */

+    public WorkgroupQueue getQueue(String queueName) {

+        return queues.get(queueName);

+    }

+

+    public Iterator<WorkgroupQueue> getQueues() {

+        return Collections.unmodifiableMap((new HashMap<String, WorkgroupQueue>(queues))).values().iterator();

+    }

+

+    public void addQueueUsersListener(QueueUsersListener listener) {

+        synchronized (queueUsersListeners) {

+            if (!queueUsersListeners.contains(listener)) {

+                queueUsersListeners.add(listener);

+            }

+        }

+    }

+

+    public void removeQueueUsersListener(QueueUsersListener listener) {

+        synchronized (queueUsersListeners) {

+            queueUsersListeners.remove(listener);

+        }

+    }

+

+    /**

+     * Adds an offer listener.

+     *

+     * @param offerListener the offer listener.

+     */

+    public void addOfferListener(OfferListener offerListener) {

+        synchronized (offerListeners) {

+            if (!offerListeners.contains(offerListener)) {

+                offerListeners.add(offerListener);

+            }

+        }

+    }

+

+    /**

+     * Removes an offer listener.

+     *

+     * @param offerListener the offer listener.

+     */

+    public void removeOfferListener(OfferListener offerListener) {

+        synchronized (offerListeners) {

+            offerListeners.remove(offerListener);

+        }

+    }

+

+    /**

+     * Adds an invitation listener.

+     *

+     * @param invitationListener the invitation listener.

+     */

+    public void addInvitationListener(WorkgroupInvitationListener invitationListener) {

+        synchronized (invitationListeners) {

+            if (!invitationListeners.contains(invitationListener)) {

+                invitationListeners.add(invitationListener);

+            }

+        }

+    }

+

+    /**

+     * Removes an invitation listener.

+     *

+     * @param invitationListener the invitation listener.

+     */

+    public void removeInvitationListener(WorkgroupInvitationListener invitationListener) {

+        synchronized (invitationListeners) {

+            invitationListeners.remove(invitationListener);

+        }

+    }

+

+    private void fireOfferRequestEvent(OfferRequestProvider.OfferRequestPacket requestPacket) {

+        Offer offer = new Offer(this.connection, this, requestPacket.getUserID(),

+                requestPacket.getUserJID(), this.getWorkgroupJID(),

+                new Date((new Date()).getTime() + (requestPacket.getTimeout() * 1000)),

+                requestPacket.getSessionID(), requestPacket.getMetaData(), requestPacket.getContent());

+

+        synchronized (offerListeners) {

+            for (OfferListener listener : offerListeners) {

+                listener.offerReceived(offer);

+            }

+        }

+    }

+

+    private void fireOfferRevokeEvent(OfferRevokeProvider.OfferRevokePacket orp) {

+        RevokedOffer revokedOffer = new RevokedOffer(orp.getUserJID(), orp.getUserID(),

+                this.getWorkgroupJID(), orp.getSessionID(), orp.getReason(), new Date());

+

+        synchronized (offerListeners) {

+            for (OfferListener listener : offerListeners) {

+                listener.offerRevoked(revokedOffer);

+            }

+        }

+    }

+

+    private void fireInvitationEvent(String groupChatJID, String sessionID, String body,

+                                     String from, Map<String, List<String>> metaData) {

+        WorkgroupInvitation invitation = new WorkgroupInvitation(connection.getUser(), groupChatJID,

+                workgroupJID, sessionID, body, from, metaData);

+

+        synchronized (invitationListeners) {

+            for (WorkgroupInvitationListener listener : invitationListeners) {

+                listener.invitationReceived(invitation);

+            }

+        }

+    }

+

+    private void fireQueueUsersEvent(WorkgroupQueue queue, WorkgroupQueue.Status status,

+                                     int averageWaitTime, Date oldestEntry, Set<QueueUser> users) {

+        synchronized (queueUsersListeners) {

+            for (QueueUsersListener listener : queueUsersListeners) {

+                if (status != null) {

+                    listener.statusUpdated(queue, status);

+                }

+                if (averageWaitTime != -1) {

+                    listener.averageWaitTimeUpdated(queue, averageWaitTime);

+                }

+                if (oldestEntry != null) {

+                    listener.oldestEntryUpdated(queue, oldestEntry);

+                }

+                if (users != null) {

+                    listener.usersUpdated(queue, users);

+                }

+            }

+        }

+    }

+

+    // PacketListener Implementation.

+

+    private void handlePacket(Packet packet) {

+        if (packet instanceof OfferRequestProvider.OfferRequestPacket) {

+            // Acknowledge the IQ set.

+            IQ reply = new IQ() {

+                public String getChildElementXML() {

+                    return null;

+                }

+            };

+            reply.setPacketID(packet.getPacketID());

+            reply.setTo(packet.getFrom());

+            reply.setType(IQ.Type.RESULT);

+            connection.sendPacket(reply);

+

+            fireOfferRequestEvent((OfferRequestProvider.OfferRequestPacket)packet);

+        }

+        else if (packet instanceof Presence) {

+            Presence presence = (Presence)packet;

+

+            // The workgroup can send us a number of different presence packets. We

+            // check for different packet extensions to see what type of presence

+            // packet it is.

+

+            String queueName = StringUtils.parseResource(presence.getFrom());

+            WorkgroupQueue queue = queues.get(queueName);

+            // If there isn't already an entry for the queue, create a new one.

+            if (queue == null) {

+                queue = new WorkgroupQueue(queueName);

+                queues.put(queueName, queue);

+            }

+

+            // QueueOverview packet extensions contain basic information about a queue.

+            QueueOverview queueOverview = (QueueOverview)presence.getExtension(QueueOverview.ELEMENT_NAME, QueueOverview.NAMESPACE);

+            if (queueOverview != null) {

+                if (queueOverview.getStatus() == null) {

+                    queue.setStatus(WorkgroupQueue.Status.CLOSED);

+                }

+                else {

+                    queue.setStatus(queueOverview.getStatus());

+                }

+                queue.setAverageWaitTime(queueOverview.getAverageWaitTime());

+                queue.setOldestEntry(queueOverview.getOldestEntry());

+                // Fire event.

+                fireQueueUsersEvent(queue, queueOverview.getStatus(),

+                        queueOverview.getAverageWaitTime(), queueOverview.getOldestEntry(),

+                        null);

+                return;

+            }

+

+            // QueueDetails packet extensions contain information about the users in

+            // a queue.

+            QueueDetails queueDetails = (QueueDetails)packet.getExtension(QueueDetails.ELEMENT_NAME, QueueDetails.NAMESPACE);

+            if (queueDetails != null) {

+                queue.setUsers(queueDetails.getUsers());

+                // Fire event.

+                fireQueueUsersEvent(queue, null, -1, null, queueDetails.getUsers());

+                return;

+            }

+

+            // Notify agent packets gives an overview of agent activity in a queue.

+            DefaultPacketExtension notifyAgents = (DefaultPacketExtension)presence.getExtension("notify-agents", "http://jabber.org/protocol/workgroup");

+            if (notifyAgents != null) {

+                int currentChats = Integer.parseInt(notifyAgents.getValue("current-chats"));

+                int maxChats = Integer.parseInt(notifyAgents.getValue("max-chats"));

+                queue.setCurrentChats(currentChats);

+                queue.setMaxChats(maxChats);

+                // Fire event.

+                // TODO: might need another event for current chats and max chats of queue

+                return;

+            }

+        }

+        else if (packet instanceof Message) {

+            Message message = (Message)packet;

+

+            // Check if a room invitation was sent and if the sender is the workgroup

+            MUCUser mucUser = (MUCUser)message.getExtension("x",

+                    "http://jabber.org/protocol/muc#user");

+            MUCUser.Invite invite = mucUser != null ? mucUser.getInvite() : null;

+            if (invite != null && workgroupJID.equals(invite.getFrom())) {

+                String sessionID = null;

+                Map<String, List<String>> metaData = null;

+

+                SessionID sessionIDExt = (SessionID)message.getExtension(SessionID.ELEMENT_NAME,

+                        SessionID.NAMESPACE);

+                if (sessionIDExt != null) {

+                    sessionID = sessionIDExt.getSessionID();

+                }

+

+                MetaData metaDataExt = (MetaData)message.getExtension(MetaData.ELEMENT_NAME,

+                        MetaData.NAMESPACE);

+                if (metaDataExt != null) {

+                    metaData = metaDataExt.getMetaData();

+                }

+

+                this.fireInvitationEvent(message.getFrom(), sessionID, message.getBody(),

+                        message.getFrom(), metaData);

+            }

+        }

+        else if (packet instanceof OfferRevokeProvider.OfferRevokePacket) {

+            // Acknowledge the IQ set.

+            IQ reply = new IQ() {

+                public String getChildElementXML() {

+                    return null;

+                }

+            };

+            reply.setPacketID(packet.getPacketID());

+            reply.setType(IQ.Type.RESULT);

+            connection.sendPacket(reply);

+

+            fireOfferRevokeEvent((OfferRevokeProvider.OfferRevokePacket)packet);

+        }

+    }

+

+    /**

+     * Creates a ChatNote that will be mapped to the given chat session.

+     *

+     * @param sessionID the session id of a Chat Session.

+     * @param note      the chat note to add.

+     * @throws XMPPException is thrown if an error occurs while adding the note.

+     */

+    public void setNote(String sessionID, String note) throws XMPPException {

+        note = ChatNotes.replace(note, "\n", "\\n");

+        note = StringUtils.escapeForXML(note);

+

+        ChatNotes notes = new ChatNotes();

+        notes.setType(IQ.Type.SET);

+        notes.setTo(workgroupJID);

+        notes.setSessionID(sessionID);

+        notes.setNotes(note);

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(notes.getPacketID()));

+        // Send the request

+        connection.sendPacket(notes);

+

+        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+    }

+

+    /**

+     * Retrieves the ChatNote associated with a given chat session.

+     *

+     * @param sessionID the sessionID of the chat session.

+     * @return the <code>ChatNote</code> associated with a given chat session.

+     * @throws XMPPException if an error occurs while retrieving the ChatNote.

+     */

+    public ChatNotes getNote(String sessionID) throws XMPPException {

+        ChatNotes request = new ChatNotes();

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+        request.setSessionID(sessionID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+        ChatNotes response = (ChatNotes)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+

+    }

+

+    /**

+     * Retrieves the AgentChatHistory associated with a particular agent jid.

+     *

+     * @param jid the jid of the agent.

+     * @param maxSessions the max number of sessions to retrieve.

+     * @param startDate the starting date of sessions to retrieve.

+     * @return the chat history associated with a given jid.

+     * @throws XMPPException if an error occurs while retrieving the AgentChatHistory.

+     */

+    public AgentChatHistory getAgentHistory(String jid, int maxSessions, Date startDate) throws XMPPException {

+        AgentChatHistory request;

+        if (startDate != null) {

+            request = new AgentChatHistory(jid, maxSessions, startDate);

+        }

+        else {

+            request = new AgentChatHistory(jid, maxSessions);

+        }

+

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+        AgentChatHistory response = (AgentChatHistory)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+    /**

+     * Asks the workgroup for it's Search Settings.

+     *

+     * @return SearchSettings the search settings for this workgroup.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public SearchSettings getSearchSettings() throws XMPPException {

+        SearchSettings request = new SearchSettings();

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+

+        SearchSettings response = (SearchSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+    /**

+     * Asks the workgroup for it's Global Macros.

+     *

+     * @param global true to retrieve global macros, otherwise false for personal macros.

+     * @return MacroGroup the root macro group.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public MacroGroup getMacros(boolean global) throws XMPPException {

+        Macros request = new Macros();

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+        request.setPersonal(!global);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+

+        Macros response = (Macros)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response.getRootGroup();

+    }

+

+    /**

+     * Persists the Personal Macro for an agent.

+     *

+     * @param group the macro group to save. 

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public void saveMacros(MacroGroup group) throws XMPPException {

+        Macros request = new Macros();

+        request.setType(IQ.Type.SET);

+        request.setTo(workgroupJID);

+        request.setPersonal(true);

+        request.setPersonalMacroGroup(group);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+

+        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+    }

+

+    /**

+     * Query for metadata associated with a session id.

+     *

+     * @param sessionID the sessionID to query for.

+     * @return Map a map of all metadata associated with the sessionID.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public Map<String, List<String>> getChatMetadata(String sessionID) throws XMPPException {

+        ChatMetadata request = new ChatMetadata();

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+        request.setSessionID(sessionID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+

+        ChatMetadata response = (ChatMetadata)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response.getMetadata();

+    }

+

+    /**

+     * Invites a user or agent to an existing session support. The provided invitee's JID can be of

+     * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service

+     * will decide the best agent to receive the invitation.<p>

+     *

+     * This method will return either when the service returned an ACK of the request or if an error occured

+     * while requesting the invitation. After sending the ACK the service will send the invitation to the target

+     * entity. When dealing with agents the common sequence of offer-response will be followed. However, when

+     * sending an invitation to a user a standard MUC invitation will be sent.<p>

+     *

+     * The agent or user that accepted the offer <b>MUST</b> join the room. Failing to do so will make

+     * the invitation to fail. The inviter will eventually receive a message error indicating that the invitee

+     * accepted the offer but failed to join the room.

+     *

+     * Different situations may lead to a failed invitation. Possible cases are: 1) all agents rejected the

+     * offer and ther are no agents available, 2) the agent that accepted the offer failed to join the room or

+     * 2) the user that received the MUC invitation never replied or joined the room. In any of these cases

+     * (or other failing cases) the inviter will get an error message with the failed notification.

+     *

+     * @param type type of entity that will get the invitation.

+     * @param invitee JID of entity that will get the invitation.

+     * @param sessionID ID of the support session that the invitee is being invited.

+     * @param reason the reason of the invitation.

+     * @throws XMPPException if the sender of the invitation is not an agent or the service failed to process

+     *         the request.

+     */

+    public void sendRoomInvitation(RoomInvitation.Type type, String invitee, String sessionID, String reason)

+            throws XMPPException {

+        final RoomInvitation invitation = new RoomInvitation(type, invitee, sessionID, reason);

+        IQ iq = new IQ() {

+

+            public String getChildElementXML() {

+                return invitation.toXML();

+            }

+        };

+        iq.setType(IQ.Type.SET);

+        iq.setTo(workgroupJID);

+        iq.setFrom(connection.getUser());

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(iq.getPacketID()));

+        connection.sendPacket(iq);

+

+        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+    }

+

+    /**

+     * Transfer an existing session support to another user or agent. The provided invitee's JID can be of

+     * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service

+     * will decide the best agent to receive the invitation.<p>

+     *

+     * This method will return either when the service returned an ACK of the request or if an error occured

+     * while requesting the transfer. After sending the ACK the service will send the invitation to the target

+     * entity. When dealing with agents the common sequence of offer-response will be followed. However, when

+     * sending an invitation to a user a standard MUC invitation will be sent.<p>

+     *

+     * Once the invitee joins the support room the workgroup service will kick the inviter from the room.<p>

+     *

+     * Different situations may lead to a failed transfers. Possible cases are: 1) all agents rejected the

+     * offer and there are no agents available, 2) the agent that accepted the offer failed to join the room

+     * or 2) the user that received the MUC invitation never replied or joined the room. In any of these cases

+     * (or other failing cases) the inviter will get an error message with the failed notification.

+     *

+     * @param type type of entity that will get the invitation.

+     * @param invitee JID of entity that will get the invitation.

+     * @param sessionID ID of the support session that the invitee is being invited.

+     * @param reason the reason of the invitation.

+     * @throws XMPPException if the sender of the invitation is not an agent or the service failed to process

+     *         the request.

+     */

+    public void sendRoomTransfer(RoomTransfer.Type type, String invitee, String sessionID, String reason)

+            throws XMPPException {

+        final RoomTransfer transfer = new RoomTransfer(type, invitee, sessionID, reason);

+        IQ iq = new IQ() {

+

+            public String getChildElementXML() {

+                return transfer.toXML();

+            }

+        };

+        iq.setType(IQ.Type.SET);

+        iq.setTo(workgroupJID);

+        iq.setFrom(connection.getUser());

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(iq.getPacketID()));

+        connection.sendPacket(iq);

+

+        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+    }

+

+    /**

+     * Returns the generic metadata of the workgroup the agent belongs to.

+     *

+     * @param con   the Connection to use.

+     * @param query an optional query object used to tell the server what metadata to retrieve. This can be null.

+     * @throws XMPPException if an error occurs while sending the request to the server.

+     * @return the settings for the workgroup.

+     */

+    public GenericSettings getGenericSettings(Connection con, String query) throws XMPPException {

+        GenericSettings setting = new GenericSettings();

+        setting.setType(IQ.Type.GET);

+        setting.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(setting.getPacketID()));

+        connection.sendPacket(setting);

+

+        GenericSettings response = (GenericSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+    public boolean hasMonitorPrivileges(Connection con) throws XMPPException {

+        MonitorPacket request = new MonitorPacket();

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+        MonitorPacket response = (MonitorPacket)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response.isMonitor();

+

+    }

+

+    public void makeRoomOwner(Connection con, String sessionID) throws XMPPException {

+        MonitorPacket request = new MonitorPacket();

+        request.setType(IQ.Type.SET);

+        request.setTo(workgroupJID);

+        request.setSessionID(sessionID);

+

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+        Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/InvitationRequest.java b/src/org/jivesoftware/smackx/workgroup/agent/InvitationRequest.java
new file mode 100644
index 0000000..16b324a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/InvitationRequest.java
@@ -0,0 +1,62 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+/**

+ * Request sent by an agent to invite another agent or user.

+ *

+ * @author Gaston Dombiak

+ */

+public class InvitationRequest extends OfferContent {

+

+    private String inviter;

+    private String room;

+    private String reason;

+

+    public InvitationRequest(String inviter, String room, String reason) {

+        this.inviter = inviter;

+        this.room = room;

+        this.reason = reason;

+    }

+

+    public String getInviter() {

+        return inviter;

+    }

+

+    public String getRoom() {

+        return room;

+    }

+

+    public String getReason() {

+        return reason;

+    }

+

+    boolean isUserRequest() {

+        return false;

+    }

+

+    boolean isInvitation() {

+        return true;

+    }

+

+    boolean isTransfer() {

+        return false;

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/Offer.java b/src/org/jivesoftware/smackx/workgroup/agent/Offer.java
new file mode 100644
index 0000000..ece8c6f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/Offer.java
@@ -0,0 +1,223 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+

+import java.util.Date;

+import java.util.List;

+import java.util.Map;

+

+/**

+ * A class embodying the semantic agent chat offer; specific instances allow the acceptance or

+ * rejecting of the offer.<br>

+ *

+ * @author Matt Tucker

+ * @author loki der quaeler

+ * @author Derek DeMoro

+ */

+public class Offer {

+

+    private Connection connection;

+    private AgentSession session;

+

+    private String sessionID;

+    private String userJID;

+    private String userID;

+    private String workgroupName;

+    private Date expiresDate;

+    private Map<String, List<String>> metaData;

+    private OfferContent content;

+

+    private boolean accepted = false;

+    private boolean rejected = false;

+

+    /**

+     * Creates a new offer.

+     *

+     * @param conn the XMPP connection with which the issuing session was created.

+     * @param agentSession the agent session instance through which this offer was issued.

+     * @param userID  the userID of the user from which the offer originates.

+     * @param userJID the XMPP address of the user from which the offer originates.

+     * @param workgroupName the fully qualified name of the workgroup.

+     * @param expiresDate the date at which this offer expires.

+     * @param sessionID the session id associated with the offer.

+     * @param metaData the metadata associated with the offer.

+     * @param content content of the offer. The content explains the reason for the offer

+     *        (e.g. user request, transfer)

+     */

+    Offer(Connection conn, AgentSession agentSession, String userID,

+            String userJID, String workgroupName, Date expiresDate,

+            String sessionID, Map<String, List<String>> metaData, OfferContent content)

+    {

+        this.connection = conn;

+        this.session = agentSession;

+        this.userID = userID;

+        this.userJID = userJID;

+        this.workgroupName = workgroupName;

+        this.expiresDate = expiresDate;

+        this.sessionID = sessionID;

+        this.metaData = metaData;

+        this.content = content;

+    }

+

+    /**

+     * Accepts the offer.

+     */

+    public void accept() {

+        Packet acceptPacket = new AcceptPacket(this.session.getWorkgroupJID());

+        connection.sendPacket(acceptPacket);

+        // TODO: listen for a reply.

+        accepted = true;

+    }

+

+    /**

+     * Rejects the offer.

+     */

+    public void reject() {

+        RejectPacket rejectPacket = new RejectPacket(this.session.getWorkgroupJID());

+        connection.sendPacket(rejectPacket);

+        // TODO: listen for a reply.

+        rejected = true;

+    }

+

+    /**

+     * Returns the userID that the offer originates from. In most cases, the

+     * userID will simply be the JID of the requesting user. However, users can

+     * also manually specify a userID for their request. In that case, that value will

+     * be returned.

+     *

+     * @return the userID of the user from which the offer originates.

+     */

+    public String getUserID() {

+        return userID;

+    }

+

+    /**

+     * Returns the JID of the user that made the offer request.

+     *

+     * @return the user's JID.

+     */

+    public String getUserJID() {

+        return userJID;

+    }

+

+    /**

+     * The fully qualified name of the workgroup (eg support@example.com).

+     *

+     * @return the name of the workgroup.

+     */

+    public String getWorkgroupName() {

+        return this.workgroupName;

+    }

+

+    /**

+     * The date when the offer will expire. The agent must {@link #accept()}

+     * the offer before the expiration date or the offer will lapse and be

+     * routed to another agent. Alternatively, the agent can {@link #reject()}

+     * the offer at any time if they don't wish to accept it..

+     *

+     * @return the date at which this offer expires.

+     */

+    public Date getExpiresDate() {

+        return this.expiresDate;

+    }

+

+    /**

+     * The session ID associated with the offer.

+     *

+     * @return the session id associated with the offer.

+     */

+    public String getSessionID() {

+        return this.sessionID;

+    }

+

+    /**

+     * The meta-data associated with the offer.

+     *

+     * @return the offer meta-data.

+     */

+    public Map<String, List<String>> getMetaData() {

+        return this.metaData;

+    }

+

+    /**

+     * Returns the content of the offer. The content explains the reason for the offer

+     * (e.g. user request, transfer)

+     *

+     * @return the content of the offer.

+     */

+    public OfferContent getContent() {

+        return content;

+    }

+

+    /**

+     * Returns true if the agent accepted this offer.

+     *

+     * @return true if the agent accepted this offer.

+     */

+    public boolean isAccepted() {

+        return accepted;

+    }

+

+    /**

+     * Return true if the agent rejected this offer.

+     *

+     * @return true if the agent rejected this offer.

+     */

+    public boolean isRejected() {

+        return rejected;

+    }

+

+    /**

+     * Packet for rejecting offers.

+     */

+    private class RejectPacket extends IQ {

+

+        RejectPacket(String workgroup) {

+            this.setTo(workgroup);

+            this.setType(IQ.Type.SET);

+        }

+

+        public String getChildElementXML() {

+            return "<offer-reject id=\"" + Offer.this.getSessionID() +

+                    "\" xmlns=\"http://jabber.org/protocol/workgroup" + "\"/>";

+        }

+    }

+

+    /**

+     * Packet for accepting an offer.

+     */

+    private class AcceptPacket extends IQ {

+

+        AcceptPacket(String workgroup) {

+            this.setTo(workgroup);

+            this.setType(IQ.Type.SET);

+        }

+

+        public String getChildElementXML() {

+            return "<offer-accept id=\"" + Offer.this.getSessionID() +

+                    "\" xmlns=\"http://jabber.org/protocol/workgroup" + "\"/>";

+        }

+    }

+

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmation.java b/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmation.java
new file mode 100644
index 0000000..f55d588
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmation.java
@@ -0,0 +1,114 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import org.jivesoftware.smack.Connection;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+

+public class OfferConfirmation extends IQ {

+    private String userJID;

+    private long sessionID;

+

+    public String getUserJID() {

+        return userJID;

+    }

+

+    public void setUserJID(String userJID) {

+        this.userJID = userJID;

+    }

+

+    public long getSessionID() {

+        return sessionID;

+    }

+

+    public void setSessionID(long sessionID) {

+        this.sessionID = sessionID;

+    }

+

+

+    public void notifyService(Connection con, String workgroup, String createdRoomName) {

+        NotifyServicePacket packet = new NotifyServicePacket(workgroup, createdRoomName);

+        con.sendPacket(packet);

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<offer-confirmation xmlns=\"http://jabber.org/protocol/workgroup\">");

+        buf.append("</offer-confirmation>");

+        return buf.toString();

+    }

+

+    public static class Provider implements IQProvider {

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            final OfferConfirmation confirmation = new OfferConfirmation();

+

+            boolean done = false;

+            while (!done) {

+                parser.next();

+                String elementName = parser.getName();

+                if (parser.getEventType() == XmlPullParser.START_TAG && "user-jid".equals(elementName)) {

+                    try {

+                        confirmation.setUserJID(parser.nextText());

+                    }

+                    catch (NumberFormatException nfe) {

+                    }

+                }

+                else if (parser.getEventType() == XmlPullParser.START_TAG && "session-id".equals(elementName)) {

+                    try {

+                        confirmation.setSessionID(Long.valueOf(parser.nextText()));

+                    }

+                    catch (NumberFormatException nfe) {

+                    }

+                }

+                else if (parser.getEventType() == XmlPullParser.END_TAG && "offer-confirmation".equals(elementName)) {

+                    done = true;

+                }

+            }

+

+

+            return confirmation;

+        }

+    }

+

+

+    /**

+     * Packet for notifying server of RoomName

+     */

+    private class NotifyServicePacket extends IQ {

+        String roomName;

+

+        NotifyServicePacket(String workgroup, String roomName) {

+            this.setTo(workgroup);

+            this.setType(IQ.Type.RESULT);

+

+            this.roomName = roomName;

+        }

+

+        public String getChildElementXML() {

+            return "<offer-confirmation  roomname=\"" + roomName + "\" xmlns=\"http://jabber.org/protocol/workgroup" + "\"/>";

+        }

+    }

+

+

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmationListener.java b/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmationListener.java
new file mode 100644
index 0000000..fb10550
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/OfferConfirmationListener.java
@@ -0,0 +1,32 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+public interface OfferConfirmationListener {

+

+

+    /**

+     * The implementing class instance will be notified via this when the AgentSession has confirmed

+     * the acceptance of the <code>Offer</code>. The instance will then have the ability to create the room and

+     * send the service the room name created for tracking.

+     *

+     * @param confirmedOffer the ConfirmedOffer

+     */

+    void offerConfirmed(OfferConfirmation confirmedOffer);

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/OfferContent.java b/src/org/jivesoftware/smackx/workgroup/agent/OfferContent.java
new file mode 100644
index 0000000..a11ddc3
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/OfferContent.java
@@ -0,0 +1,55 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+/**

+ * Type of content being included in the offer. The content actually explains the reason

+ * the agent is getting an offer.

+ *

+ * @author Gaston Dombiak

+ */

+public abstract class OfferContent {

+

+    /**

+     * Returns true if the content of the offer is related to a user request. This is the

+     * most common type of offers an agent should receive.

+     *

+     * @return true if the content of the offer is related to a user request.

+     */

+    abstract boolean isUserRequest();

+

+    /**

+     * Returns true if the content of the offer is related to a room invitation made by another

+     * agent. This type of offer include the room to join, metadata sent by the user while joining

+     * the queue and the reason why the agent is being invited.

+     *

+     * @return true if the content of the offer is related to a room invitation made by another agent.

+     */

+    abstract boolean isInvitation();

+

+    /**

+     * Returns true if the content of the offer is related to a service transfer made by another

+     * agent. This type of offers include the room to join, metadata sent by the user while joining the

+     * queue and the reason why the agent is receiving the transfer offer.

+     *

+     * @return true if the content of the offer is related to a service transfer made by another agent.

+     */

+    abstract boolean isTransfer();

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/OfferListener.java b/src/org/jivesoftware/smackx/workgroup/agent/OfferListener.java
new file mode 100644
index 0000000..5efde99
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/OfferListener.java
@@ -0,0 +1,49 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+/**

+ * An interface which all classes interested in hearing about chat offers associated to a particular

+ *  AgentSession instance should implement.<br>

+ *

+ * @author Matt Tucker

+ * @author loki der quaeler

+ * @see org.jivesoftware.smackx.workgroup.agent.AgentSession

+ */

+public interface OfferListener {

+

+    /**

+     * The implementing class instance will be notified via this when the AgentSession has received

+     *  an offer for a chat. The instance will then have the ability to accept, reject, or ignore

+     *  the request (resulting in a revocation-by-timeout).

+     *

+     * @param request the Offer instance embodying the details of the offer

+     */

+    public void offerReceived (Offer request);

+

+    /**

+     * The implementing class instance will be notified via this when the AgentSessino has received

+     *  a revocation of a previously extended offer.

+     *

+     * @param revokedOffer the RevokedOffer instance embodying the details of the revoked offer

+     */

+    public void offerRevoked (RevokedOffer revokedOffer);

+

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/QueueUsersListener.java b/src/org/jivesoftware/smackx/workgroup/agent/QueueUsersListener.java
new file mode 100644
index 0000000..9fcff9a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/QueueUsersListener.java
@@ -0,0 +1,60 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import java.util.Date;

+import java.util.Set;

+

+import org.jivesoftware.smackx.workgroup.QueueUser;

+

+public interface QueueUsersListener {

+

+    /**

+     * The status of the queue was updated.

+     *

+     * @param queue the workgroup queue.

+     * @param status the status of queue.

+     */

+    public void statusUpdated(WorkgroupQueue queue, WorkgroupQueue.Status status);

+

+    /**

+     * The average wait time of the queue was updated.

+     *

+     * @param queue the workgroup queue.

+     * @param averageWaitTime the average wait time of the queue.

+     */

+    public void averageWaitTimeUpdated(WorkgroupQueue queue, int averageWaitTime);

+

+    /**

+     * The date of oldest entry waiting in the queue was updated.

+     *

+     * @param queue the workgroup queue.

+     * @param oldestEntry the date of the oldest entry waiting in the queue.

+     */

+    public void oldestEntryUpdated(WorkgroupQueue queue, Date oldestEntry);

+

+    /**

+     * The list of users waiting in the queue was updated.

+     *

+     * @param queue the workgroup queue.

+     * @param users the list of users waiting in the queue.

+     */

+    public void usersUpdated(WorkgroupQueue queue, Set<QueueUser> users);

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/RevokedOffer.java b/src/org/jivesoftware/smackx/workgroup/agent/RevokedOffer.java
new file mode 100644
index 0000000..dab4d91
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/RevokedOffer.java
@@ -0,0 +1,98 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import java.util.Date;

+

+/**

+ * An immutable simple class to embody the information concerning a revoked offer, this is namely

+ *  the reason, the workgroup, the userJID, and the timestamp which the message was received.<br>

+ *

+ * @author loki der quaeler

+ */

+public class RevokedOffer {

+

+    private String userJID;

+    private String userID;

+    private String workgroupName;

+    private String sessionID;

+    private String reason;

+    private Date timestamp;

+

+    /**

+     *

+     * @param userJID the JID of the user for which this revocation was issued.

+     * @param userID the user ID of the user for which this revocation was issued.

+     * @param workgroupName the fully qualified name of the workgroup

+     * @param sessionID the session id attributed to this chain of packets

+     * @param reason the server issued message as to why this revocation was issued.

+     * @param timestamp the timestamp at which the revocation was issued

+     */

+    RevokedOffer(String userJID, String userID, String workgroupName, String sessionID,

+            String reason, Date timestamp) {

+        super();

+

+        this.userJID = userJID;

+        this.userID = userID;

+        this.workgroupName = workgroupName;

+        this.sessionID = sessionID;

+        this.reason = reason;

+        this.timestamp = timestamp;

+    }

+

+    public String getUserJID() {

+        return userJID;

+    }

+

+    /**

+     * @return the jid of the user for which this revocation was issued

+     */

+    public String getUserID() {

+        return this.userID;

+    }

+

+    /**

+     * @return the fully qualified name of the workgroup

+     */

+    public String getWorkgroupName() {

+        return this.workgroupName;

+    }

+

+    /**

+     * @return the session id which will associate all packets for the pending chat

+     */

+    public String getSessionID() {

+        return this.sessionID;

+    }

+

+    /**

+     * @return the server issued message as to why this revocation was issued

+     */

+    public String getReason() {

+        return this.reason;

+    }

+

+    /**

+     * @return the timestamp at which the revocation was issued

+     */

+    public Date getTimestamp() {

+        return this.timestamp;

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/agent/TranscriptManager.java b/src/org/jivesoftware/smackx/workgroup/agent/TranscriptManager.java
new file mode 100644
index 0000000..8a3801f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/TranscriptManager.java
@@ -0,0 +1,100 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import org.jivesoftware.smackx.workgroup.packet.Transcript;

+import org.jivesoftware.smackx.workgroup.packet.Transcripts;

+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.PacketIDFilter;

+

+/**

+ * A TranscriptManager helps to retrieve the full conversation transcript of a given session

+ * {@link #getTranscript(String, String)} or to retrieve a list with the summary of all the

+ * conversations that a user had {@link #getTranscripts(String, String)}.

+ *

+ * @author Gaston Dombiak

+ */

+public class TranscriptManager {

+    private Connection connection;

+

+    public TranscriptManager(Connection connection) {

+        this.connection = connection;

+    }

+

+    /**

+     * Returns the full conversation transcript of a given session.

+     *

+     * @param sessionID the id of the session to get the full transcript.

+     * @param workgroupJID the JID of the workgroup that will process the request.

+     * @return the full conversation transcript of a given session.

+     * @throws XMPPException if an error occurs while getting the information.

+     */

+    public Transcript getTranscript(String workgroupJID, String sessionID) throws XMPPException {

+        Transcript request = new Transcript(sessionID);

+        request.setTo(workgroupJID);

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        // Send the request

+        connection.sendPacket(request);

+

+        Transcript response = (Transcript) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+    /**

+     * Returns the transcripts of a given user. The answer will contain the complete history of

+     * conversations that a user had.

+     *

+     * @param userID the id of the user to get his conversations.

+     * @param workgroupJID the JID of the workgroup that will process the request.

+     * @return the transcripts of a given user.

+     * @throws XMPPException if an error occurs while getting the information.

+     */

+    public Transcripts getTranscripts(String workgroupJID, String userID) throws XMPPException {

+        Transcripts request = new Transcripts(userID);

+        request.setTo(workgroupJID);

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        // Send the request

+        connection.sendPacket(request);

+

+        Transcripts response = (Transcripts) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/TranscriptSearchManager.java b/src/org/jivesoftware/smackx/workgroup/agent/TranscriptSearchManager.java
new file mode 100644
index 0000000..8260cd6
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/TranscriptSearchManager.java
@@ -0,0 +1,111 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import org.jivesoftware.smackx.workgroup.packet.TranscriptSearch;

+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.PacketIDFilter;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.ReportedData;

+

+/**

+ * A TranscriptSearchManager helps to retrieve the form to use for searching transcripts

+ * {@link #getSearchForm(String)} or to submit a search form and return the results of

+ * the search {@link #submitSearch(String, Form)}.

+ *

+ * @author Gaston Dombiak

+ */

+public class TranscriptSearchManager {

+    private Connection connection;

+

+    public TranscriptSearchManager(Connection connection) {

+        this.connection = connection;

+    }

+

+    /**

+     * Returns the Form to use for searching transcripts. It is unlikely that the server

+     * will change the form (without a restart) so it is safe to keep the returned form

+     * for future submissions.

+     *

+     * @param serviceJID the address of the workgroup service.

+     * @return the Form to use for searching transcripts.

+     * @throws XMPPException if an error occurs while sending the request to the server.

+     */

+    public Form getSearchForm(String serviceJID) throws XMPPException {

+        TranscriptSearch search = new TranscriptSearch();

+        search.setType(IQ.Type.GET);

+        search.setTo(serviceJID);

+

+        PacketCollector collector = connection.createPacketCollector(

+                new PacketIDFilter(search.getPacketID()));

+        connection.sendPacket(search);

+

+        TranscriptSearch response = (TranscriptSearch) collector.nextResult(

+                SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return Form.getFormFrom(response);

+    }

+

+    /**

+     * Submits the completed form and returns the result of the transcript search. The result

+     * will include all the data returned from the server so be careful with the amount of

+     * data that the search may return.

+     *

+     * @param serviceJID    the address of the workgroup service.

+     * @param completedForm the filled out search form.

+     * @return the result of the transcript search.

+     * @throws XMPPException if an error occurs while submiting the search to the server.

+     */

+    public ReportedData submitSearch(String serviceJID, Form completedForm) throws XMPPException {

+        TranscriptSearch search = new TranscriptSearch();

+        search.setType(IQ.Type.GET);

+        search.setTo(serviceJID);

+        search.addExtension(completedForm.getDataFormToSend());

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(search.getPacketID()));

+        connection.sendPacket(search);

+

+        TranscriptSearch response = (TranscriptSearch) collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return ReportedData.getReportedDataFrom(response);

+    }

+}

+

+

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/TransferRequest.java b/src/org/jivesoftware/smackx/workgroup/agent/TransferRequest.java
new file mode 100644
index 0000000..a3abbaa
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/TransferRequest.java
@@ -0,0 +1,62 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+/**

+ * Request sent by an agent to transfer a support session to another agent or user.

+ *

+ * @author Gaston Dombiak

+ */

+public class TransferRequest extends OfferContent {

+

+    private String inviter;

+    private String room;

+    private String reason;

+

+    public TransferRequest(String inviter, String room, String reason) {

+        this.inviter = inviter;

+        this.room = room;

+        this.reason = reason;

+    }

+

+    public String getInviter() {

+        return inviter;

+    }

+

+    public String getRoom() {

+        return room;

+    }

+

+    public String getReason() {

+        return reason;

+    }

+

+    boolean isUserRequest() {

+        return false;

+    }

+

+    boolean isInvitation() {

+        return false;

+    }

+

+    boolean isTransfer() {

+        return true;

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/UserRequest.java b/src/org/jivesoftware/smackx/workgroup/agent/UserRequest.java
new file mode 100644
index 0000000..ccaaaf3
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/UserRequest.java
@@ -0,0 +1,47 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+/**

+ * Requests made by users to get support by some agent.

+ *

+ * @author Gaston Dombiak

+ */

+public class UserRequest extends OfferContent {

+    // TODO Do we want to use a singleton? Should we store the userID here?

+    private static UserRequest instance = new UserRequest();

+

+    public static OfferContent getInstance() {

+        return instance;

+    }

+

+    boolean isUserRequest() {

+        return true;

+    }

+

+    boolean isInvitation() {

+        return false;

+    }

+

+    boolean isTransfer() {

+        return false;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/agent/WorkgroupQueue.java b/src/org/jivesoftware/smackx/workgroup/agent/WorkgroupQueue.java
new file mode 100644
index 0000000..b43c826
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/agent/WorkgroupQueue.java
@@ -0,0 +1,224 @@
+/**

+ * $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.smackx.workgroup.agent;

+

+import java.util.*;

+

+import org.jivesoftware.smackx.workgroup.QueueUser;

+

+/**

+ * A queue in a workgroup, which is a pool of agents that are routed  a specific type of

+ * chat request.

+ */

+public class WorkgroupQueue {

+

+    private String name;

+    private Status status = Status.CLOSED;

+

+    private int averageWaitTime = -1;

+    private Date oldestEntry = null;

+    private Set<QueueUser> users = Collections.emptySet();

+

+    private int maxChats = 0;

+    private int currentChats = 0;

+

+    /**

+     * Creates a new workgroup queue instance.

+     *

+     * @param name the name of the queue.

+     */

+    WorkgroupQueue(String name) {

+        this.name = name;

+    }

+

+    /**

+     * Returns the name of the queue.

+     *

+     * @return the name of the queue.

+     */

+    public String getName() {

+        return name;

+    }

+

+    /**

+     * Returns the status of the queue.

+     *

+     * @return the status of the queue.

+     */

+    public Status getStatus() {

+        return status;

+    }

+

+    void setStatus(Status status) {

+        this.status = status;

+    }

+

+    /**

+     * Returns the number of users waiting in the queue waiting to be routed to

+     * an agent.

+     *

+     * @return the number of users waiting in the queue.

+     */

+    public int getUserCount() {

+        if (users == null) {

+            return 0;

+        }

+        return users.size();

+    }

+

+    /**

+     * Returns an Iterator for the users in the queue waiting to be routed to

+     * an agent (QueueUser instances).

+     *

+     * @return an Iterator for the users waiting in the queue.

+     */

+    public Iterator<QueueUser> getUsers() {

+        if (users == null) {

+            return new HashSet<QueueUser>().iterator();

+        }

+        return Collections.unmodifiableSet(users).iterator();

+    }

+

+    void setUsers(Set<QueueUser> users) {

+        this.users = users;

+    }

+

+    /**

+     * Returns the average amount of time users wait in the queue before being

+     * routed to an agent. If average wait time info isn't available, -1 will

+     * be returned.

+     *

+     * @return the average wait time

+     */

+    public int getAverageWaitTime() {

+        return averageWaitTime;

+    }

+

+    void setAverageWaitTime(int averageTime) {

+        this.averageWaitTime = averageTime;

+    }

+

+    /**

+     * Returns the date of the oldest request waiting in the queue. If there

+     * are no requests waiting to be routed, this method will return <tt>null</tt>.

+     *

+     * @return the date of the oldest request in the queue.

+     */

+    public Date getOldestEntry() {

+        return oldestEntry;

+    }

+

+    void setOldestEntry(Date oldestEntry) {

+        this.oldestEntry = oldestEntry;

+    }

+

+    /**

+     * Returns the maximum number of simultaneous chats the queue can handle.

+     *

+     * @return the max number of chats the queue can handle.

+     */

+    public int getMaxChats() {

+        return maxChats;

+    }

+

+    void setMaxChats(int maxChats) {

+        this.maxChats = maxChats;

+    }

+

+    /**

+     * Returns the current number of active chat sessions in the queue.

+     *

+     * @return the current number of active chat sessions in the queue.

+     */

+    public int getCurrentChats() {

+        return currentChats;

+    }

+

+    void setCurrentChats(int currentChats) {

+        this.currentChats = currentChats;

+    }

+

+    /**

+     * A class to represent the status of the workgroup. The possible values are:

+     *

+     * <ul>

+     *      <li>WorkgroupQueue.Status.OPEN -- the queue is active and accepting new chat requests.

+     *      <li>WorkgroupQueue.Status.ACTIVE -- the queue is active but NOT accepting new chat

+     *          requests.

+     *      <li>WorkgroupQueue.Status.CLOSED -- the queue is NOT active and NOT accepting new

+     *          chat requests.

+     * </ul>

+     */

+    public static class Status {

+

+        /**

+         * The queue is active and accepting new chat requests.

+         */

+        public static final Status OPEN = new Status("open");

+

+        /**

+         * The queue is active but NOT accepting new chat requests. This state might

+         * occur when the workgroup has closed because regular support hours have closed,

+         * but there are still several requests left in the queue.

+         */

+        public static final Status ACTIVE = new Status("active");

+

+        /**

+         * The queue is NOT active and NOT accepting new chat requests.

+         */

+        public static final Status CLOSED = new Status("closed");

+

+        /**

+         * Converts a String into the corresponding status. Valid String values

+         * that can be converted to a status are: "open", "active", and "closed".

+         *

+         * @param type the String value to covert.

+         * @return the corresponding Type.

+         */

+        public static Status fromString(String type) {

+            if (type == null) {

+                return null;

+            }

+            type = type.toLowerCase();

+            if (OPEN.toString().equals(type)) {

+                return OPEN;

+            }

+            else if (ACTIVE.toString().equals(type)) {

+                return ACTIVE;

+            }

+            else if (CLOSED.toString().equals(type)) {

+                return CLOSED;

+            }

+            else {

+                return null;

+            }

+        }

+

+        private String value;

+

+        private Status(String value) {

+            this.value = value;

+        }

+

+        public String toString() {

+            return value;

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/ext/forms/WorkgroupForm.java b/src/org/jivesoftware/smackx/workgroup/ext/forms/WorkgroupForm.java
new file mode 100644
index 0000000..f2dc08e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/ext/forms/WorkgroupForm.java
@@ -0,0 +1,82 @@
+/**

+ * $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.smackx.workgroup.ext.forms;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smack.util.PacketParserUtils;

+import org.xmlpull.v1.XmlPullParser;

+

+public class WorkgroupForm extends IQ {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "workgroup-form";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");

+        // Add packet extensions, if any are defined.

+        buf.append(getExtensionsXML());

+        buf.append("</").append(ELEMENT_NAME).append("> ");

+

+        return buf.toString();

+    }

+

+    /**

+     * An IQProvider for WebForm packets.

+     *

+     * @author Derek DeMoro

+     */

+    public static class InternalProvider implements IQProvider {

+

+        public InternalProvider() {

+            super();

+        }

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            WorkgroupForm answer = new WorkgroupForm();

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if (eventType == XmlPullParser.START_TAG) {

+                    // Parse the packet extension

+                    answer.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(),

+                            parser.getNamespace(), parser));

+                }

+                else if (eventType == XmlPullParser.END_TAG) {

+                    if (parser.getName().equals(ELEMENT_NAME)) {

+                        done = true;

+                    }

+                }

+            }

+

+            return answer;

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatHistory.java b/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatHistory.java
new file mode 100644
index 0000000..7b8d200
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatHistory.java
@@ -0,0 +1,155 @@
+/**

+ * $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.smackx.workgroup.ext.history;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.Date;

+import java.util.List;

+

+/**

+ * IQ provider used to retrieve individual agent information. Each chat session can be mapped

+ * to one or more jids and therefore retrievable.

+ */

+public class AgentChatHistory extends IQ {

+    private String agentJID;

+    private int maxSessions;

+    private long startDate;

+

+    private List<AgentChatSession> agentChatSessions = new ArrayList<AgentChatSession>();

+

+    public AgentChatHistory(String agentJID, int maxSessions, Date startDate) {

+        this.agentJID = agentJID;

+        this.maxSessions = maxSessions;

+        this.startDate = startDate.getTime();

+    }

+

+    public AgentChatHistory(String agentJID, int maxSessions) {

+        this.agentJID = agentJID;

+        this.maxSessions = maxSessions;

+        this.startDate = 0;

+    }

+

+    public AgentChatHistory() {

+    }

+

+    public void addChatSession(AgentChatSession chatSession) {

+        agentChatSessions.add(chatSession);

+    }

+

+    public Collection<AgentChatSession> getAgentChatSessions() {

+        return agentChatSessions;

+    }

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "chat-sessions";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=");

+        buf.append('"');

+        buf.append(NAMESPACE);

+        buf.append('"');

+        buf.append(" agentJID=\"" + agentJID + "\"");

+        buf.append(" maxSessions=\"" + maxSessions + "\"");

+        buf.append(" startDate=\"" + startDate + "\"");

+

+        buf.append("></").append(ELEMENT_NAME).append("> ");

+        return buf.toString();

+    }

+

+    /**

+     * Packet extension provider for AgentHistory packets.

+     */

+    public static class InternalProvider implements IQProvider {

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            if (parser.getEventType() != XmlPullParser.START_TAG) {

+                throw new IllegalStateException("Parser not in proper position, or bad XML.");

+            }

+

+            AgentChatHistory agentChatHistory = new AgentChatHistory();

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if ((eventType == XmlPullParser.START_TAG) && ("chat-session".equals(parser.getName()))) {

+                    agentChatHistory.addChatSession(parseChatSetting(parser));

+

+                }

+                else if (eventType == XmlPullParser.END_TAG && ELEMENT_NAME.equals(parser.getName())) {

+                    done = true;

+                }

+            }

+            return agentChatHistory;

+        }

+

+        private AgentChatSession parseChatSetting(XmlPullParser parser) throws Exception {

+

+            boolean done = false;

+            Date date = null;

+            long duration = 0;

+            String visitorsName = null;

+            String visitorsEmail = null;

+            String sessionID = null;

+            String question = null;

+

+            while (!done) {

+                int eventType = parser.next();

+                if ((eventType == XmlPullParser.START_TAG) && ("date".equals(parser.getName()))) {

+                    String dateStr = parser.nextText();

+                    long l = Long.valueOf(dateStr).longValue();

+                    date = new Date(l);

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("duration".equals(parser.getName()))) {

+                    duration = Long.valueOf(parser.nextText()).longValue();

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("visitorsName".equals(parser.getName()))) {

+                    visitorsName = parser.nextText();

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("visitorsEmail".equals(parser.getName()))) {

+                    visitorsEmail = parser.nextText();

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("sessionID".equals(parser.getName()))) {

+                    sessionID = parser.nextText();

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("question".equals(parser.getName()))) {

+                    question = parser.nextText();

+                }

+                else if (eventType == XmlPullParser.END_TAG && "chat-session".equals(parser.getName())) {

+                    done = true;

+                }

+            }

+            return new AgentChatSession(date, duration, visitorsName, visitorsEmail, sessionID, question);

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatSession.java b/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatSession.java
new file mode 100644
index 0000000..5113cda
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/ext/history/AgentChatSession.java
@@ -0,0 +1,93 @@
+/**

+ * $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.smackx.workgroup.ext.history;

+

+import java.util.Date;

+

+/**

+ * Represents one chat session for an agent.

+ */

+public class AgentChatSession {

+    public Date startDate;

+    public long duration;

+    public String visitorsName;

+    public String visitorsEmail;

+    public String sessionID;

+    public String question;

+

+    public AgentChatSession(Date date, long duration, String visitorsName, String visitorsEmail, String sessionID, String question) {

+        this.startDate = date;

+        this.duration = duration;

+        this.visitorsName = visitorsName;

+        this.visitorsEmail = visitorsEmail;

+        this.sessionID = sessionID;

+        this.question = question;

+    }

+

+    public Date getStartDate() {

+        return startDate;

+    }

+

+    public void setStartDate(Date startDate) {

+        this.startDate = startDate;

+    }

+

+    public long getDuration() {

+        return duration;

+    }

+

+    public void setDuration(long duration) {

+        this.duration = duration;

+    }

+

+    public String getVisitorsName() {

+        return visitorsName;

+    }

+

+    public void setVisitorsName(String visitorsName) {

+        this.visitorsName = visitorsName;

+    }

+

+    public String getVisitorsEmail() {

+        return visitorsEmail;

+    }

+

+    public void setVisitorsEmail(String visitorsEmail) {

+        this.visitorsEmail = visitorsEmail;

+    }

+

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    public void setSessionID(String sessionID) {

+        this.sessionID = sessionID;

+    }

+

+    public void setQuestion(String question){

+        this.question = question;

+    }

+

+    public String getQuestion(){

+        return question;

+    }

+

+

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/ext/history/ChatMetadata.java b/src/org/jivesoftware/smackx/workgroup/ext/history/ChatMetadata.java
new file mode 100644
index 0000000..301e1a5
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/ext/history/ChatMetadata.java
@@ -0,0 +1,116 @@
+/**

+ * $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.smackx.workgroup.ext.history;

+

+import org.jivesoftware.smackx.workgroup.util.MetaDataUtils;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.util.HashMap;

+import java.util.List;

+import java.util.Map;

+

+public class ChatMetadata extends IQ {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "chat-metadata";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+

+    private String sessionID;

+

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    public void setSessionID(String sessionID) {

+        this.sessionID = sessionID;

+    }

+

+

+    private Map<String, List<String>> map = new HashMap<String, List<String>>();

+

+    public void setMetadata(Map<String, List<String>> metadata){

+        this.map = metadata;

+    }

+

+    public Map<String, List<String>> getMetadata(){

+        return map;

+    }

+

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");

+        buf.append("<sessionID>").append(getSessionID()).append("</sessionID>");

+        buf.append("</").append(ELEMENT_NAME).append("> ");

+

+        return buf.toString();

+    }

+

+    /**

+     * An IQProvider for Metadata packets.

+     *

+     * @author Derek DeMoro

+     */

+    public static class Provider implements IQProvider {

+

+        public Provider() {

+            super();

+        }

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            final ChatMetadata chatM = new ChatMetadata();

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if (eventType == XmlPullParser.START_TAG) {

+                    if (parser.getName().equals("sessionID")) {

+                       chatM.setSessionID(parser.nextText());

+                    }

+                    else if (parser.getName().equals("metadata")) {

+                        Map<String, List<String>> map = MetaDataUtils.parseMetaData(parser);

+                        chatM.setMetadata(map);

+                    }

+                }

+                else if (eventType == XmlPullParser.END_TAG) {

+                    if (parser.getName().equals(ELEMENT_NAME)) {

+                        done = true;

+                    }

+                }

+            }

+

+            return chatM;

+        }

+    }

+}

+

+

+

+

diff --git a/src/org/jivesoftware/smackx/workgroup/ext/macros/Macro.java b/src/org/jivesoftware/smackx/workgroup/ext/macros/Macro.java
new file mode 100644
index 0000000..acf6196
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/ext/macros/Macro.java
@@ -0,0 +1,68 @@
+/**

+ * $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.smackx.workgroup.ext.macros;

+

+/**

+ * Macro datamodel.

+ */

+public class Macro {

+    public static final int TEXT = 0;

+    public static final int URL = 1;

+    public static final int IMAGE = 2;

+

+

+    private String title;

+    private String description;

+    private String response;

+    private int type;

+

+    public String getTitle() {

+        return title;

+    }

+

+    public void setTitle(String title) {

+        this.title = title;

+    }

+

+    public String getDescription() {

+        return description;

+    }

+

+    public void setDescription(String description) {

+        this.description = description;

+    }

+

+    public String getResponse() {

+        return response;

+    }

+

+    public void setResponse(String response) {

+        this.response = response;

+    }

+

+    public int getType() {

+        return type;

+    }

+

+    public void setType(int type) {

+        this.type = type;

+    }

+

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/ext/macros/MacroGroup.java b/src/org/jivesoftware/smackx/workgroup/ext/macros/MacroGroup.java
new file mode 100644
index 0000000..0742b3d
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/ext/macros/MacroGroup.java
@@ -0,0 +1,143 @@
+/**

+ * $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.smackx.workgroup.ext.macros;

+

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.Collections;

+import java.util.Iterator;

+import java.util.List;

+

+/**

+ * MacroGroup datamodel.

+ */

+public class MacroGroup {

+    private List<Macro> macros;

+    private List<MacroGroup> macroGroups;

+

+

+    // Define MacroGroup

+    private String title;

+

+    public MacroGroup() {

+        macros = new ArrayList<Macro>();

+        macroGroups = new ArrayList<MacroGroup>();

+    }

+

+    public void addMacro(Macro macro) {

+        macros.add(macro);

+    }

+

+    public void removeMacro(Macro macro) {

+        macros.remove(macro);

+    }

+

+    public Macro getMacroByTitle(String title) {

+        Collection<Macro> col = Collections.unmodifiableList(macros);

+        Iterator<Macro> iter = col.iterator();

+        while (iter.hasNext()) {

+            Macro macro = (Macro)iter.next();

+            if (macro.getTitle().equalsIgnoreCase(title)) {

+                return macro;

+            }

+        }

+        return null;

+    }

+

+    public void addMacroGroup(MacroGroup group) {

+        macroGroups.add(group);

+    }

+

+    public void removeMacroGroup(MacroGroup group) {

+        macroGroups.remove(group);

+    }

+

+    public Macro getMacro(int location) {

+        return (Macro)macros.get(location);

+    }

+

+    public MacroGroup getMacroGroupByTitle(String title) {

+        Collection<MacroGroup> col = Collections.unmodifiableList(macroGroups);

+        Iterator<MacroGroup> iter = col.iterator();

+        while (iter.hasNext()) {

+            MacroGroup group = (MacroGroup)iter.next();

+            if (group.getTitle().equalsIgnoreCase(title)) {

+                return group;

+            }

+        }

+        return null;

+    }

+

+    public MacroGroup getMacroGroup(int location) {

+        return (MacroGroup)macroGroups.get(location);

+    }

+

+

+    public List<Macro>  getMacros() {

+        return macros;

+    }

+

+    public void setMacros(List<Macro> macros) {

+        this.macros = macros;

+    }

+

+    public List<MacroGroup> getMacroGroups() {

+        return macroGroups;

+    }

+

+    public void setMacroGroups(List<MacroGroup> macroGroups) {

+        this.macroGroups = macroGroups;

+    }

+

+    public String getTitle() {

+        return title;

+    }

+

+    public void setTitle(String title) {

+        this.title = title;

+    }

+    

+    public String toXML() {

+    	StringBuilder buf = new StringBuilder();

+    	buf.append("<macrogroup>");

+    	buf.append("<title>" +  getTitle() + "</title>");

+    	buf.append("<macros>");

+    	for (Macro macro : getMacros())

+		{

+    		buf.append("<macro>");

+    		buf.append("<title>" + macro.getTitle() + "</title>");

+    		buf.append("<type>" + macro.getType() + "</type>");

+    		buf.append("<description>" + macro.getDescription() + "</description>");

+    		buf.append("<response>" + macro.getResponse() + "</response>");

+    		buf.append("</macro>");

+		}

+    	buf.append("</macros>");

+    	

+    	if (getMacroGroups().size() > 0) {

+    		buf.append("<macroGroups>");

+    		for (MacroGroup groups : getMacroGroups()) {

+    			buf.append(groups.toXML());

+    		}

+    		buf.append("</macroGroups>");

+    	}

+    	buf.append("</macrogroup>");

+    	return buf.toString(); 

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/ext/macros/Macros.java b/src/org/jivesoftware/smackx/workgroup/ext/macros/Macros.java
new file mode 100644
index 0000000..869ec57
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/ext/macros/Macros.java
@@ -0,0 +1,198 @@
+/**

+ * $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.smackx.workgroup.ext.macros;

+

+import java.io.StringReader;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smack.util.StringUtils;

+import org.xmlpull.v1.XmlPullParserFactory;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Macros iq is responsible for handling global and personal macros in the a Live Assistant

+ * Workgroup.

+ */

+public class Macros extends IQ {

+

+    private MacroGroup rootGroup;

+    private boolean personal;

+    private MacroGroup personalMacroGroup;

+

+    public MacroGroup getRootGroup() {

+        return rootGroup;

+    }

+

+    public void setRootGroup(MacroGroup rootGroup) {

+        this.rootGroup = rootGroup;

+    }

+

+    public boolean isPersonal() {

+        return personal;

+    }

+

+    public void setPersonal(boolean personal) {

+        this.personal = personal;

+    }

+

+    public MacroGroup getPersonalMacroGroup() {

+        return personalMacroGroup;

+    }

+

+    public void setPersonalMacroGroup(MacroGroup personalMacroGroup) {

+        this.personalMacroGroup = personalMacroGroup;

+    }

+

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "macros";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");

+        if (isPersonal()) {

+            buf.append("<personal>true</personal>");

+        }

+        if (getPersonalMacroGroup() != null) {        	

+        	buf.append("<personalMacro>");

+        	buf.append(StringUtils.escapeForXML(getPersonalMacroGroup().toXML()));

+        	buf.append("</personalMacro>");

+        }

+        buf.append("</").append(ELEMENT_NAME).append("> ");

+

+        return buf.toString();

+    }

+

+    /**

+     * An IQProvider for Macro packets.

+     *

+     * @author Derek DeMoro

+     */

+    public static class InternalProvider implements IQProvider {

+

+        public InternalProvider() {

+            super();

+        }

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            Macros macroGroup = new Macros();

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if (eventType == XmlPullParser.START_TAG) {

+                    if (parser.getName().equals("model")) {

+                        String macros = parser.nextText();

+                        MacroGroup group = parseMacroGroups(macros);

+                        macroGroup.setRootGroup(group);

+                    }

+                }

+                else if (eventType == XmlPullParser.END_TAG) {

+                    if (parser.getName().equals(ELEMENT_NAME)) {

+                        done = true;

+                    }

+                }

+            }

+

+            return macroGroup;

+        }

+        

+        public Macro parseMacro(XmlPullParser parser) throws Exception {

+        	Macro macro = new Macro();

+        	 boolean done = false;

+            while (!done) {

+	        	int eventType = parser.next();

+	        	if (eventType == XmlPullParser.START_TAG) {

+	        		if (parser.getName().equals("title")) {

+	        			parser.next();

+	        			macro.setTitle(parser.getText());

+	        		}

+	        		else if (parser.getName().equals("description")) {

+	        			macro.setDescription(parser.nextText());

+	        		}

+	        		else if (parser.getName().equals("response")) {

+	        			macro.setResponse(parser.nextText());

+	        		}

+	        		else if (parser.getName().equals("type")) {

+	        			macro.setType(Integer.valueOf(parser.nextText()).intValue());

+	        		}

+	        	}

+	            else if (eventType == XmlPullParser.END_TAG) {

+	                if (parser.getName().equals("macro")) {

+	                    done = true;

+	                }

+	            }

+            }

+        	return macro;

+        }

+        

+        public MacroGroup parseMacroGroup(XmlPullParser parser) throws Exception {

+        	MacroGroup group = new MacroGroup();

+        	

+            boolean done = false;

+            while (!done) {

+	        	int eventType = parser.next();

+	        	if (eventType == XmlPullParser.START_TAG) {

+	        		if (parser.getName().equals("macrogroup")) {

+	        			group.addMacroGroup(parseMacroGroup(parser));

+	        		}

+	        		if (parser.getName().equals("title")) {

+	        			group.setTitle(parser.nextText());

+	        		}

+	        		if (parser.getName().equals("macro")) {

+	        			group.addMacro(parseMacro(parser));

+	        		}

+	        	}

+	            else if (eventType == XmlPullParser.END_TAG) {

+	                if (parser.getName().equals("macrogroup")) {

+	                    done = true;

+	                }

+	            }

+            }

+        	return group; 

+        }

+        

+        public MacroGroup parseMacroGroups(String macros) throws Exception {

+

+        	MacroGroup group = null;

+        	XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();

+        	parser.setInput(new StringReader(macros));

+			int eventType = parser.getEventType();

+			while (eventType != XmlPullParser.END_DOCUMENT) {		

+				eventType = parser.next();

+				 if (eventType == XmlPullParser.START_TAG) {

+	                    if (parser.getName().equals("macrogroup")) {

+	                    	group = parseMacroGroup(parser);

+	                    }

+				 }

+			}

+			return group;

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/ext/notes/ChatNotes.java b/src/org/jivesoftware/smackx/workgroup/ext/notes/ChatNotes.java
new file mode 100644
index 0000000..eff3c6c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/ext/notes/ChatNotes.java
@@ -0,0 +1,155 @@
+/**

+ * $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.smackx.workgroup.ext.notes;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * IQ packet for retrieving and adding Chat Notes.

+ */

+public class ChatNotes extends IQ {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "chat-notes";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+

+    private String sessionID;

+    private String notes;

+

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    public void setSessionID(String sessionID) {

+        this.sessionID = sessionID;

+    }

+

+    public String getNotes() {

+        return notes;

+    }

+

+    public void setNotes(String notes) {

+        this.notes = notes;

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");

+        buf.append("<sessionID>").append(getSessionID()).append("</sessionID>");

+

+        if (getNotes() != null) {

+            buf.append("<notes>").append(getNotes()).append("</notes>");

+        }

+        buf.append("</").append(ELEMENT_NAME).append("> ");

+

+        return buf.toString();

+    }

+

+    /**

+     * An IQProvider for ChatNotes packets.

+     *

+     * @author Derek DeMoro

+     */

+    public static class Provider implements IQProvider {

+

+        public Provider() {

+            super();

+        }

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            ChatNotes chatNotes = new ChatNotes();

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if (eventType == XmlPullParser.START_TAG) {

+                    if (parser.getName().equals("sessionID")) {

+                        chatNotes.setSessionID(parser.nextText());

+                    }

+                    else if (parser.getName().equals("text")) {

+                        String note = parser.nextText();

+                        note = note.replaceAll("\\\\n", "\n");

+                        chatNotes.setNotes(note);

+                    }

+                }

+                else if (eventType == XmlPullParser.END_TAG) {

+                    if (parser.getName().equals(ELEMENT_NAME)) {

+                        done = true;

+                    }

+                }

+            }

+

+            return chatNotes;

+        }

+    }

+

+    /**

+     * Replaces all instances of oldString with newString in string.

+     *

+     * @param string    the String to search to perform replacements on

+     * @param oldString the String that should be replaced by newString

+     * @param newString the String that will replace all instances of oldString

+     * @return a String will all instances of oldString replaced by newString

+     */

+    public static final String replace(String string, String oldString, String newString) {

+        if (string == null) {

+            return null;

+        }

+        // If the newString is null or zero length, just return the string since there's nothing

+        // to replace.

+        if (newString == null) {

+            return string;

+        }

+        int i = 0;

+        // Make sure that oldString appears at least once before doing any processing.

+        if ((i = string.indexOf(oldString, i)) >= 0) {

+            // Use char []'s, as they are more efficient to deal with.

+            char[] string2 = string.toCharArray();

+            char[] newString2 = newString.toCharArray();

+            int oLength = oldString.length();

+            StringBuilder buf = new StringBuilder(string2.length);

+            buf.append(string2, 0, i).append(newString2);

+            i += oLength;

+            int j = i;

+            // Replace all remaining instances of oldString with newString.

+            while ((i = string.indexOf(oldString, i)) > 0) {

+                buf.append(string2, j, i - j).append(newString2);

+                i += oLength;

+                j = i;

+            }

+            buf.append(string2, j, string2.length - j);

+            return buf.toString();

+        }

+        return string;

+    }

+}

+

+

+

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/AgentInfo.java b/src/org/jivesoftware/smackx/workgroup/packet/AgentInfo.java
new file mode 100644
index 0000000..8b9d230
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/AgentInfo.java
@@ -0,0 +1,132 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * IQ packet for retrieving and changing the Agent personal information.

+ */

+public class AgentInfo extends IQ {

+

+    /**

+    * Element name of the packet extension.

+    */

+   public static final String ELEMENT_NAME = "agent-info";

+

+   /**

+    * Namespace of the packet extension.

+    */

+   public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    private String jid;

+    private String name;

+

+    /**

+     * Returns the Agent's jid.

+     *

+     * @return the Agent's jid.

+     */

+    public String getJid() {

+        return jid;

+    }

+

+    /**

+     * Sets the Agent's jid.

+     *

+     * @param jid the jid of the agent.

+     */

+    public void setJid(String jid) {

+        this.jid = jid;

+    }

+

+    /**

+     * Returns the Agent's name. The name of the agent may be different than the user's name.

+     * This property may be shown in the webchat client.

+     *

+     * @return the Agent's name.

+     */

+    public String getName() {

+        return name;

+    }

+

+    /**

+     * Sets the Agent's name. The name of the agent may be different than the user's name.

+     * This property may be shown in the webchat client.

+     *

+     * @param name the new name of the agent.

+     */

+    public void setName(String name) {

+        this.name = name;

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");

+        if (jid != null) {

+            buf.append("<jid>").append(getJid()).append("</jid>");

+        }

+        if (name != null) {

+            buf.append("<name>").append(getName()).append("</name>");

+        }

+        buf.append("</").append(ELEMENT_NAME).append("> ");

+

+        return buf.toString();

+    }

+

+    /**

+     * An IQProvider for AgentInfo packets.

+     *

+     * @author Gaston Dombiak

+     */

+    public static class Provider implements IQProvider {

+

+        public Provider() {

+            super();

+        }

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            AgentInfo answer = new AgentInfo();

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if (eventType == XmlPullParser.START_TAG) {

+                    if (parser.getName().equals("jid")) {

+                        answer.setJid(parser.nextText());

+                    }

+                    else if (parser.getName().equals("name")) {

+                        answer.setName(parser.nextText());

+                    }

+                }

+                else if (eventType == XmlPullParser.END_TAG) {

+                    if (parser.getName().equals(ELEMENT_NAME)) {

+                        done = true;

+                    }

+                }

+            }

+

+            return answer;

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/AgentStatus.java b/src/org/jivesoftware/smackx/workgroup/packet/AgentStatus.java
new file mode 100644
index 0000000..9f49033
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/AgentStatus.java
@@ -0,0 +1,266 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.text.ParseException;

+import java.text.SimpleDateFormat;

+import java.util.*;

+

+/**

+ * Agent status packet.

+ *

+ * @author Matt Tucker

+ */

+public class AgentStatus implements PacketExtension {

+

+    private static final SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");

+

+    static {

+        UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0"));

+    }

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "agent-status";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";

+

+    private String workgroupJID;

+    private List<ChatInfo> currentChats = new ArrayList<ChatInfo>();

+    private int maxChats = -1;

+

+    AgentStatus() {

+    }

+

+    public String getWorkgroupJID() {

+        return workgroupJID;

+    }

+

+    /**

+     * Returns a collection of ChatInfo where each ChatInfo represents a Chat where this agent

+     * is participating.

+     *

+     * @return a collection of ChatInfo where each ChatInfo represents a Chat where this agent

+     *         is participating.

+     */

+    public List<ChatInfo> getCurrentChats() {

+        return Collections.unmodifiableList(currentChats);

+    }

+

+    public int getMaxChats() {

+        return maxChats;

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    public String toXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\"");

+        if (workgroupJID != null) {

+            buf.append(" jid=\"").append(workgroupJID).append("\"");

+        }

+        buf.append(">");

+        if (maxChats != -1) {

+            buf.append("<max-chats>").append(maxChats).append("</max-chats>");

+        }

+        if (!currentChats.isEmpty()) {

+            buf.append("<current-chats xmlns= \"http://jivesoftware.com/protocol/workgroup\">");

+            for (Iterator<ChatInfo> it = currentChats.iterator(); it.hasNext();) {

+                buf.append(((ChatInfo)it.next()).toXML());

+            }

+            buf.append("</current-chats>");

+        }

+        buf.append("</").append(this.getElementName()).append("> ");

+

+        return buf.toString();

+    }

+

+    /**

+     * Represents information about a Chat where this Agent is participating.

+     *

+     * @author Gaston Dombiak

+     */

+    public static class ChatInfo {

+

+        private String sessionID;

+        private String userID;

+        private Date date;

+        private String email;

+        private String username;

+        private String question;

+

+        public ChatInfo(String sessionID, String userID, Date date, String email, String username, String question) {

+            this.sessionID = sessionID;

+            this.userID = userID;

+            this.date = date;

+            this.email = email;

+            this.username = username;

+            this.question = question;

+        }

+

+        /**

+         * Returns the sessionID associated to this chat. Each chat will have a unique sessionID

+         * that could be used for retrieving the whole transcript of the conversation.

+         *

+         * @return the sessionID associated to this chat.

+         */

+        public String getSessionID() {

+            return sessionID;

+        }

+

+        /**

+         * Returns the user unique identification of the user that made the initial request and

+         * for which this chat was generated. If the user joined using an anonymous connection

+         * then the userID will be the value of the ID attribute of the USER element. Otherwise,

+         * the userID will be the bare JID of the user that made the request.

+         *

+         * @return the user unique identification of the user that made the initial request.

+         */

+        public String getUserID() {

+            return userID;

+        }

+

+        /**

+         * Returns the date when this agent joined the chat.

+         *

+         * @return the date when this agent joined the chat.

+         */

+        public Date getDate() {

+            return date;

+        }

+

+        /**

+         * Returns the email address associated with the user.

+         *

+         * @return the email address associated with the user.

+         */

+        public String getEmail() {

+            return email;

+        }

+

+        /**

+         * Returns the username(nickname) associated with the user.

+         *

+         * @return the username associated with the user.

+         */

+        public String getUsername() {

+            return username;

+        }

+

+        /**

+         * Returns the question the user asked.

+         *

+         * @return the question the user asked, if any.

+         */

+        public String getQuestion() {

+            return question;

+        }

+

+        public String toXML() {

+            StringBuilder buf = new StringBuilder();

+

+            buf.append("<chat ");

+            if (sessionID != null) {

+                buf.append(" sessionID=\"").append(sessionID).append("\"");

+            }

+            if (userID != null) {

+                buf.append(" userID=\"").append(userID).append("\"");

+            }

+            if (date != null) {

+                buf.append(" startTime=\"").append(UTC_FORMAT.format(date)).append("\"");

+            }

+            if (email != null) {

+                buf.append(" email=\"").append(email).append("\"");

+            }

+            if (username != null) {

+                buf.append(" username=\"").append(username).append("\"");

+            }

+            if (question != null) {

+                buf.append(" question=\"").append(question).append("\"");

+            }

+            buf.append("/>");

+

+            return buf.toString();

+        }

+    }

+

+    /**

+     * Packet extension provider for AgentStatus packets.

+     */

+    public static class Provider implements PacketExtensionProvider {

+

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            AgentStatus agentStatus = new AgentStatus();

+

+            agentStatus.workgroupJID = parser.getAttributeValue("", "jid");

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+

+                if (eventType == XmlPullParser.START_TAG) {

+                    if ("chat".equals(parser.getName())) {

+                        agentStatus.currentChats.add(parseChatInfo(parser));

+                    }

+                    else if ("max-chats".equals(parser.getName())) {

+                        agentStatus.maxChats = Integer.parseInt(parser.nextText());

+                    }

+                }

+                else if (eventType == XmlPullParser.END_TAG &&

+                    ELEMENT_NAME.equals(parser.getName())) {

+                    done = true;

+                }

+            }

+            return agentStatus;

+        }

+

+        private ChatInfo parseChatInfo(XmlPullParser parser) {

+

+            String sessionID = parser.getAttributeValue("", "sessionID");

+            String userID = parser.getAttributeValue("", "userID");

+            Date date = null;

+            try {

+                date = UTC_FORMAT.parse(parser.getAttributeValue("", "startTime"));

+            }

+            catch (ParseException e) {

+            }

+

+            String email = parser.getAttributeValue("", "email");

+            String username = parser.getAttributeValue("", "username");

+            String question = parser.getAttributeValue("", "question");

+

+            return new ChatInfo(sessionID, userID, date, email, username, question);

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/AgentStatusRequest.java b/src/org/jivesoftware/smackx/workgroup/packet/AgentStatusRequest.java
new file mode 100644
index 0000000..48549d2
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/AgentStatusRequest.java
@@ -0,0 +1,163 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.util.Collections;

+import java.util.HashSet;

+import java.util.Iterator;

+import java.util.Set;

+

+/**

+ * Agent status request packet. This packet is used by agents to request the list of

+ * agents in a workgroup. The response packet contains a list of packets. Presence

+ * packets from individual agents follow.

+ *

+ * @author Matt Tucker

+ */

+public class AgentStatusRequest extends IQ {

+

+     /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "agent-status-request";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";

+

+    private Set<Item> agents;

+

+    public AgentStatusRequest() {

+        agents = new HashSet<Item>();

+    }

+

+    public int getAgentCount() {

+        return agents.size();

+    }

+

+    public Set<Item> getAgents() {

+        return Collections.unmodifiableSet(agents);

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");

+        synchronized (agents) {

+            for (Iterator<Item> i=agents.iterator(); i.hasNext(); ) {

+                Item item = (Item) i.next();

+                buf.append("<agent jid=\"").append(item.getJID()).append("\">");

+                if (item.getName() != null) {

+                    buf.append("<name xmlns=\""+ AgentInfo.NAMESPACE + "\">");

+                    buf.append(item.getName());

+                    buf.append("</name>");

+                }

+                buf.append("</agent>");

+            }

+        }

+        buf.append("</").append(this.getElementName()).append("> ");

+        return buf.toString();

+    }

+

+    public static class Item {

+

+        private String jid;

+        private String type;

+        private String name;

+

+        public Item(String jid, String type, String name) {

+            this.jid = jid;

+            this.type = type;

+            this.name = name;

+        }

+

+        public String getJID() {

+            return jid;

+        }

+

+        public String getType() {

+            return type;

+        }

+

+        public String getName() {

+            return name;

+        }

+    }

+

+    /**

+     * Packet extension provider for AgentStatusRequest packets.

+     */

+    public static class Provider implements IQProvider {

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            AgentStatusRequest statusRequest = new AgentStatusRequest();

+

+            if (parser.getEventType() != XmlPullParser.START_TAG) {

+                throw new IllegalStateException("Parser not in proper position, or bad XML.");

+            }

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if ((eventType == XmlPullParser.START_TAG) && ("agent".equals(parser.getName()))) {

+                    statusRequest.agents.add(parseAgent(parser));

+                }

+                else if (eventType == XmlPullParser.END_TAG &&

+                        "agent-status-request".equals(parser.getName()))

+                {

+                    done = true;

+                }

+            }

+            return statusRequest;

+        }

+

+        private Item parseAgent(XmlPullParser parser) throws Exception {

+

+            boolean done = false;

+            String jid = parser.getAttributeValue("", "jid");

+            String type = parser.getAttributeValue("", "type");

+            String name = null;

+            while (!done) {

+                int eventType = parser.next();

+                if ((eventType == XmlPullParser.START_TAG) && ("name".equals(parser.getName()))) {

+                    name = parser.nextText();

+                }

+                else if (eventType == XmlPullParser.END_TAG &&

+                        "agent".equals(parser.getName()))

+                {

+                    done = true;

+                }

+            }

+            return new Item(jid, type, name);

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/AgentWorkgroups.java b/src/org/jivesoftware/smackx/workgroup/packet/AgentWorkgroups.java
new file mode 100644
index 0000000..292a640
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/AgentWorkgroups.java
@@ -0,0 +1,129 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.util.ArrayList;

+import java.util.Collections;

+import java.util.Iterator;

+import java.util.List;

+

+/**

+ * Represents a request for getting the jid of the workgroups where an agent can work or could

+ * represent the result of such request which will contain the list of workgroups JIDs where the

+ * agent can work.

+ *

+ * @author Gaston Dombiak

+ */

+public class AgentWorkgroups extends IQ {

+

+    private String agentJID;

+    private List<String> workgroups;

+

+    /**

+     * Creates an AgentWorkgroups request for the given agent. This IQ will be sent and an answer

+     * will be received with the jid of the workgroups where the agent can work.

+     *

+     * @param agentJID the id of the agent to get his workgroups.

+     */

+    public AgentWorkgroups(String agentJID) {

+        this.agentJID = agentJID;

+        this.workgroups = new ArrayList<String>();

+    }

+

+    /**

+     * Creates an AgentWorkgroups which will contain the JIDs of the workgroups where an agent can

+     * work.

+     *

+     * @param agentJID the id of the agent that can work in the list of workgroups.

+     * @param workgroups the list of workgroup JIDs where the agent can work.

+     */

+    public AgentWorkgroups(String agentJID, List<String> workgroups) {

+        this.agentJID = agentJID;

+        this.workgroups = workgroups;

+    }

+

+    public String getAgentJID() {

+        return agentJID;

+    }

+

+    /**

+     * Returns a list of workgroup JIDs where the agent can work.

+     *

+     * @return a list of workgroup JIDs where the agent can work.

+     */

+    public List<String> getWorkgroups() {

+        return Collections.unmodifiableList(workgroups);

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<workgroups xmlns=\"http://jabber.org/protocol/workgroup\" jid=\"")

+                .append(agentJID)

+                .append("\">");

+

+        for (Iterator<String> it=workgroups.iterator(); it.hasNext();) {

+            String workgroupJID = it.next();

+            buf.append("<workgroup jid=\"" + workgroupJID + "\"/>");

+        }

+

+        buf.append("</workgroups>");

+

+        return buf.toString();

+    }

+

+    /**

+     * An IQProvider for AgentWorkgroups packets.

+     *

+     * @author Gaston Dombiak

+     */

+    public static class Provider implements IQProvider {

+

+        public Provider() {

+            super();

+        }

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            String agentJID = parser.getAttributeValue("", "jid");

+            List<String> workgroups = new ArrayList<String>();

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if (eventType == XmlPullParser.START_TAG) {

+                    if (parser.getName().equals("workgroup")) {

+                        workgroups.add(parser.getAttributeValue("", "jid"));

+                    }

+                }

+                else if (eventType == XmlPullParser.END_TAG) {

+                    if (parser.getName().equals("workgroups")) {

+                        done = true;

+                    }

+                }

+            }

+

+            return new AgentWorkgroups(agentJID, workgroups);

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/DepartQueuePacket.java b/src/org/jivesoftware/smackx/workgroup/packet/DepartQueuePacket.java
new file mode 100644
index 0000000..620291c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/DepartQueuePacket.java
@@ -0,0 +1,75 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+

+/**

+ * A IQ packet used to depart a workgroup queue. There are two cases for issuing a depart

+ * queue request:<ul>

+ *     <li>The user wants to leave the queue. In this case, an instance of this class

+ *         should be created without passing in a user address.

+ *     <li>An administrator or the server removes wants to remove a user from the queue.

+ *         In that case, the address of the user to remove from the queue should be

+ *         used to create an instance of this class.</ul>

+ *

+ * @author loki der quaeler

+ */

+public class DepartQueuePacket extends IQ {

+

+    private String user;

+

+    /**

+     * Creates a depart queue request packet to the specified workgroup.

+     *

+     * @param workgroup the workgroup to depart.

+     */

+    public DepartQueuePacket(String workgroup) {

+        this(workgroup, null);

+    }

+

+    /**

+     * Creates a depart queue request to the specified workgroup and for the

+     * specified user.

+     *

+     * @param workgroup the workgroup to depart.

+     * @param user the user to make depart from the queue.

+     */

+    public DepartQueuePacket(String workgroup, String user) {

+        this.user = user;

+

+        setTo(workgroup);

+        setType(IQ.Type.SET);

+        setFrom(user);

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder("<depart-queue xmlns=\"http://jabber.org/protocol/workgroup\"");

+

+        if (this.user != null) {

+            buf.append("><jid>").append(this.user).append("</jid></depart-queue>");

+        }

+        else {

+            buf.append("/>");

+        }

+

+        return buf.toString();

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/MetaDataProvider.java b/src/org/jivesoftware/smackx/workgroup/packet/MetaDataProvider.java
new file mode 100644
index 0000000..af76986
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/MetaDataProvider.java
@@ -0,0 +1,49 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import java.util.List;

+import java.util.Map;

+

+import org.jivesoftware.smackx.workgroup.MetaData;

+import org.jivesoftware.smackx.workgroup.util.MetaDataUtils;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * This provider parses meta data if it's not contained already in a larger extension provider.

+ *

+ * @author loki der quaeler

+ */

+public class MetaDataProvider implements PacketExtensionProvider {

+

+    /**

+     * PacketExtensionProvider implementation

+     */

+    public PacketExtension parseExtension (XmlPullParser parser)

+        throws Exception {

+        Map<String, List<String>> metaData = MetaDataUtils.parseMetaData(parser);

+

+        return new MetaData(metaData);

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/MonitorPacket.java b/src/org/jivesoftware/smackx/workgroup/packet/MonitorPacket.java
new file mode 100644
index 0000000..0ceecae
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/MonitorPacket.java
@@ -0,0 +1,113 @@
+/**
+ * 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.smackx.workgroup.packet;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+public class MonitorPacket extends IQ {
+
+    private String sessionID;
+
+    private boolean isMonitor;
+
+    public boolean isMonitor() {
+        return isMonitor;
+    }
+
+    public void setMonitor(boolean monitor) {
+        isMonitor = monitor;
+    }
+
+    public String getSessionID() {
+        return sessionID;
+    }
+
+    public void setSessionID(String sessionID) {
+        this.sessionID = sessionID;
+    }
+
+    /**
+     * Element name of the packet extension.
+     */
+    public static final String ELEMENT_NAME = "monitor";
+
+    /**
+     * Namespace of the packet extension.
+     */
+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+    public String getElementName() {
+        return ELEMENT_NAME;
+    }
+
+    public String getNamespace() {
+        return NAMESPACE;
+    }
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+
+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=");
+        buf.append('"');
+        buf.append(NAMESPACE);
+        buf.append('"');
+        buf.append(">");
+        if (sessionID != null) {
+            buf.append("<makeOwner sessionID=\""+sessionID+"\"></makeOwner>");
+        }
+        buf.append("</").append(ELEMENT_NAME).append("> ");
+        return buf.toString();
+    }
+
+
+    /**
+     * Packet extension provider for Monitor Packets.
+     */
+    public static class InternalProvider implements IQProvider {
+
+        public IQ parseIQ(XmlPullParser parser) throws Exception {
+            if (parser.getEventType() != XmlPullParser.START_TAG) {
+                throw new IllegalStateException("Parser not in proper position, or bad XML.");
+            }
+
+            MonitorPacket packet = new MonitorPacket();
+
+            boolean done = false;
+
+
+            while (!done) {
+                int eventType = parser.next();
+                if ((eventType == XmlPullParser.START_TAG) && ("isMonitor".equals(parser.getName()))) {
+                    String value = parser.nextText();
+                    if ("false".equalsIgnoreCase(value)) {
+                        packet.setMonitor(false);
+                    }
+                    else {
+                        packet.setMonitor(true);
+                    }
+                }
+                else if (eventType == XmlPullParser.END_TAG && "monitor".equals(parser.getName())) {
+                    done = true;
+                }
+            }
+
+            return packet;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/OccupantsInfo.java b/src/org/jivesoftware/smackx/workgroup/packet/OccupantsInfo.java
new file mode 100644
index 0000000..0f80866
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/OccupantsInfo.java
@@ -0,0 +1,173 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.text.SimpleDateFormat;

+import java.util.*;

+

+/**

+ * Packet used for requesting information about occupants of a room or for retrieving information

+ * such information.

+ *

+ * @author Gaston Dombiak

+ */

+public class OccupantsInfo extends IQ {

+

+    private static final SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");

+

+    static {

+        UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0"));

+    }

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "occupants-info";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    private String roomID;

+    private final Set<OccupantInfo> occupants;

+

+    public OccupantsInfo(String roomID) {

+        this.roomID = roomID;

+        this.occupants = new HashSet<OccupantInfo>();

+    }

+

+    public String getRoomID() {

+        return roomID;

+    }

+

+    public int getOccupantsCount() {

+        return occupants.size();

+    }

+

+    public Set<OccupantInfo> getOccupants() {

+        return Collections.unmodifiableSet(occupants);

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE);

+        buf.append("\" roomID=\"").append(roomID).append("\">");

+        synchronized (occupants) {

+            for (OccupantInfo occupant : occupants) {

+                buf.append("<occupant>");

+                // Add the occupant jid

+                buf.append("<jid>");

+                buf.append(occupant.getJID());

+                buf.append("</jid>");

+                // Add the occupant nickname

+                buf.append("<name>");

+                buf.append(occupant.getNickname());

+                buf.append("</name>");

+                // Add the date when the occupant joined the room

+                buf.append("<joined>");

+                buf.append(UTC_FORMAT.format(occupant.getJoined()));

+                buf.append("</joined>");

+                buf.append("</occupant>");

+            }

+        }

+        buf.append("</").append(ELEMENT_NAME).append("> ");

+        return buf.toString();

+    }

+

+    public static class OccupantInfo {

+

+        private String jid;

+        private String nickname;

+        private Date joined;

+

+        public OccupantInfo(String jid, String nickname, Date joined) {

+            this.jid = jid;

+            this.nickname = nickname;

+            this.joined = joined;

+        }

+

+        public String getJID() {

+            return jid;

+        }

+

+        public String getNickname() {

+            return nickname;

+        }

+

+        public Date getJoined() {

+            return joined;

+        }

+    }

+

+    /**

+     * Packet extension provider for AgentStatusRequest packets.

+     */

+    public static class Provider implements IQProvider {

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            if (parser.getEventType() != XmlPullParser.START_TAG) {

+                throw new IllegalStateException("Parser not in proper position, or bad XML.");

+            }

+            OccupantsInfo occupantsInfo = new OccupantsInfo(parser.getAttributeValue("", "roomID"));

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if ((eventType == XmlPullParser.START_TAG) &&

+                        ("occupant".equals(parser.getName()))) {

+                    occupantsInfo.occupants.add(parseOccupantInfo(parser));

+                } else if (eventType == XmlPullParser.END_TAG &&

+                        ELEMENT_NAME.equals(parser.getName())) {

+                    done = true;

+                }

+            }

+            return occupantsInfo;

+        }

+

+        private OccupantInfo parseOccupantInfo(XmlPullParser parser) throws Exception {

+

+            boolean done = false;

+            String jid = null;

+            String nickname = null;

+            Date joined = null;

+            while (!done) {

+                int eventType = parser.next();

+                if ((eventType == XmlPullParser.START_TAG) && ("jid".equals(parser.getName()))) {

+                    jid = parser.nextText();

+                } else if ((eventType == XmlPullParser.START_TAG) &&

+                        ("nickname".equals(parser.getName()))) {

+                    nickname = parser.nextText();

+                } else if ((eventType == XmlPullParser.START_TAG) &&

+                        ("joined".equals(parser.getName()))) {

+                    joined = UTC_FORMAT.parse(parser.nextText());

+                } else if (eventType == XmlPullParser.END_TAG &&

+                        "occupant".equals(parser.getName())) {

+                    done = true;

+                }

+            }

+            return new OccupantInfo(jid, nickname, joined);

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/OfferRequestProvider.java b/src/org/jivesoftware/smackx/workgroup/packet/OfferRequestProvider.java
new file mode 100644
index 0000000..8f56b78
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/OfferRequestProvider.java
@@ -0,0 +1,211 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smackx.workgroup.MetaData;

+import org.jivesoftware.smackx.workgroup.agent.InvitationRequest;

+import org.jivesoftware.smackx.workgroup.agent.OfferContent;

+import org.jivesoftware.smackx.workgroup.agent.TransferRequest;

+import org.jivesoftware.smackx.workgroup.agent.UserRequest;

+import org.jivesoftware.smackx.workgroup.util.MetaDataUtils;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smack.util.PacketParserUtils;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.util.HashMap;

+import java.util.List;

+import java.util.Map;

+

+/**

+ * An IQProvider for agent offer requests.

+ *

+ * @author loki der quaeler

+ */

+public class OfferRequestProvider implements IQProvider {

+

+    public OfferRequestProvider() {

+    }

+

+    public IQ parseIQ(XmlPullParser parser) throws Exception {

+        int eventType = parser.getEventType();

+        String sessionID = null;

+        int timeout = -1;

+        OfferContent content = null;

+        boolean done = false;

+        Map<String, List<String>> metaData = new HashMap<String, List<String>>();

+

+        if (eventType != XmlPullParser.START_TAG) {

+            // throw exception

+        }

+

+        String userJID = parser.getAttributeValue("", "jid");

+        // Default userID to the JID.

+        String userID = userJID;

+

+        while (!done) {

+            eventType = parser.next();

+

+            if (eventType == XmlPullParser.START_TAG) {

+                String elemName = parser.getName();

+

+                if ("timeout".equals(elemName)) {

+                    timeout = Integer.parseInt(parser.nextText());

+                }

+                else if (MetaData.ELEMENT_NAME.equals(elemName)) {

+                    metaData = MetaDataUtils.parseMetaData(parser);

+                }

+                else if (SessionID.ELEMENT_NAME.equals(elemName)) {

+                   sessionID = parser.getAttributeValue("", "id");

+                }

+                else if (UserID.ELEMENT_NAME.equals(elemName)) {

+                    userID = parser.getAttributeValue("", "id");

+                }

+                else if ("user-request".equals(elemName)) {

+                    content = UserRequest.getInstance();

+                }

+                else if (RoomInvitation.ELEMENT_NAME.equals(elemName)) {

+                    RoomInvitation invitation = (RoomInvitation) PacketParserUtils

+                            .parsePacketExtension(RoomInvitation.ELEMENT_NAME, RoomInvitation.NAMESPACE, parser);

+                    content = new InvitationRequest(invitation.getInviter(), invitation.getRoom(),

+                            invitation.getReason());

+                }

+                else if (RoomTransfer.ELEMENT_NAME.equals(elemName)) {

+                    RoomTransfer transfer = (RoomTransfer) PacketParserUtils

+                            .parsePacketExtension(RoomTransfer.ELEMENT_NAME, RoomTransfer.NAMESPACE, parser);

+                    content = new TransferRequest(transfer.getInviter(), transfer.getRoom(), transfer.getReason());

+                }

+            }

+            else if (eventType == XmlPullParser.END_TAG) {

+                if ("offer".equals(parser.getName())) {

+                    done = true;

+                }

+            }

+        }

+

+        OfferRequestPacket offerRequest =

+                new OfferRequestPacket(userJID, userID, timeout, metaData, sessionID, content);

+        offerRequest.setType(IQ.Type.SET);

+

+        return offerRequest;

+    }

+

+    public static class OfferRequestPacket extends IQ {

+

+        private int timeout;

+        private String userID;

+        private String userJID;

+        private Map<String, List<String>> metaData;

+        private String sessionID;

+        private OfferContent content;

+

+        public OfferRequestPacket(String userJID, String userID, int timeout, Map<String, List<String>> metaData,

+                String sessionID, OfferContent content)

+        {

+            this.userJID = userJID;

+            this.userID = userID;

+            this.timeout = timeout;

+            this.metaData = metaData;

+            this.sessionID = sessionID;

+            this.content = content;

+        }

+

+        /**

+         * Returns the userID, which is either the same as the userJID or a special

+         * value that the user provided as part of their "join queue" request.

+         *

+         * @return the user ID.

+         */

+        public String getUserID() {

+            return userID;

+        }

+

+        /**

+         * The JID of the user that made the "join queue" request.

+         *

+         * @return the user JID.

+         */

+        public String getUserJID() {

+            return userJID;

+        }

+

+        /**

+         * Returns the session ID associated with the request and ensuing chat. If the offer

+         * does not contain a session ID, <tt>null</tt> will be returned.

+         *

+         * @return the session id associated with the request.

+         */

+        public String getSessionID() {

+            return sessionID;

+        }

+

+        /**

+         * Returns the number of seconds the agent has to accept the offer before

+         * it times out.

+         *

+         * @return the offer timeout (in seconds).

+         */

+        public int getTimeout() {

+            return this.timeout;

+        }

+

+        public OfferContent getContent() {

+            return content;

+        }

+

+        /**

+         * Returns any meta-data associated with the offer.

+         *

+         * @return meta-data associated with the offer.

+         */

+        public Map<String, List<String>> getMetaData() {

+            return this.metaData;

+        }

+

+        public String getChildElementXML () {

+            StringBuilder buf = new StringBuilder();

+

+            buf.append("<offer xmlns=\"http://jabber.org/protocol/workgroup\" jid=\"").append(userJID).append("\">");

+            buf.append("<timeout>").append(timeout).append("</timeout>");

+

+            if (sessionID != null) {

+                buf.append('<').append(SessionID.ELEMENT_NAME);

+                buf.append(" session=\"");

+                buf.append(getSessionID()).append("\" xmlns=\"");

+                buf.append(SessionID.NAMESPACE).append("\"/>");

+            }

+

+            if (metaData != null) {

+                buf.append(MetaDataUtils.serializeMetaData(metaData));

+            }

+

+            if (userID != null) {

+                buf.append('<').append(UserID.ELEMENT_NAME);

+                buf.append(" id=\"");

+                buf.append(userID).append("\" xmlns=\"");

+                buf.append(UserID.NAMESPACE).append("\"/>");

+            }

+

+            buf.append("</offer>");

+

+            return buf.toString();

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/OfferRevokeProvider.java b/src/org/jivesoftware/smackx/workgroup/packet/OfferRevokeProvider.java
new file mode 100644
index 0000000..202824c
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/OfferRevokeProvider.java
@@ -0,0 +1,112 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * An IQProvider class which has savvy about the offer-revoke tag.<br>

+ *

+ * @author loki der quaeler

+ */

+public class OfferRevokeProvider implements IQProvider {

+

+    public IQ parseIQ (XmlPullParser parser) throws Exception {

+        // The parser will be positioned on the opening IQ tag, so get the JID attribute.

+        String userJID = parser.getAttributeValue("", "jid");

+        // Default the userID to the JID.

+        String userID = userJID;

+        String reason = null;

+        String sessionID = null;

+        boolean done = false;

+

+        while (!done) {

+            int eventType = parser.next();

+

+            if ((eventType == XmlPullParser.START_TAG) && parser.getName().equals("reason")) {

+                reason = parser.nextText();

+            }

+            else if ((eventType == XmlPullParser.START_TAG)

+                         && parser.getName().equals(SessionID.ELEMENT_NAME)) {

+                sessionID = parser.getAttributeValue("", "id");

+            }

+            else if ((eventType == XmlPullParser.START_TAG)

+                         && parser.getName().equals(UserID.ELEMENT_NAME)) {

+                userID = parser.getAttributeValue("", "id");

+            }

+            else if ((eventType == XmlPullParser.END_TAG) && parser.getName().equals(

+                    "offer-revoke"))

+            {

+                done = true;

+            }

+        }

+

+        return new OfferRevokePacket(userJID, userID, reason, sessionID);

+    }

+

+    public class OfferRevokePacket extends IQ {

+

+        private String userJID;

+        private String userID;

+        private String sessionID;

+        private String reason;

+

+        public OfferRevokePacket (String userJID, String userID, String cause, String sessionID) {

+            this.userJID = userJID;

+            this.userID = userID;

+            this.reason = cause;

+            this.sessionID = sessionID;

+        }

+

+        public String getUserJID() {

+            return userJID;

+        }

+

+        public String getUserID() {

+            return this.userID;

+        }

+

+        public String getReason() {

+            return this.reason;

+        }

+

+        public String getSessionID() {

+            return this.sessionID;

+        }

+

+        public String getChildElementXML () {

+            StringBuilder buf = new StringBuilder();

+            buf.append("<offer-revoke xmlns=\"http://jabber.org/protocol/workgroup\" jid=\"").append(userID).append("\">");

+            if (reason != null) {

+                buf.append("<reason>").append(reason).append("</reason>");

+            }

+            if (sessionID != null) {

+                buf.append(new SessionID(sessionID).toXML());

+            }

+            if (userID != null) {

+                buf.append(new UserID(userID).toXML());

+            }

+            buf.append("</offer-revoke>");

+            return buf.toString();

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java b/src/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java
new file mode 100644
index 0000000..ef11a78
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/QueueDetails.java
@@ -0,0 +1,199 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smackx.workgroup.QueueUser;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.text.SimpleDateFormat;

+import java.util.Date;

+import java.util.HashSet;

+import java.util.Iterator;

+import java.util.Set;

+

+/**

+ * Queue details packet extension, which contains details about the users

+ * currently in a queue.

+ */

+public class QueueDetails implements PacketExtension {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "notify-queue-details";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";

+

+    private static final String DATE_FORMAT = "yyyyMMdd'T'HH:mm:ss";

+

+    private SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);

+    /**

+     * The list of users in the queue.

+     */

+    private Set<QueueUser> users;

+

+    /**

+     * Creates a new QueueDetails packet

+     */

+    private QueueDetails() {

+        users = new HashSet<QueueUser>();

+    }

+

+    /**

+     * Returns the number of users currently in the queue that are waiting to

+     * be routed to an agent.

+     *

+     * @return the number of users in the queue.

+     */

+    public int getUserCount() {

+        return users.size();

+    }

+

+    /**

+     * Returns the set of users in the queue that are waiting to

+     * be routed to an agent (as QueueUser objects).

+     *

+     * @return a Set for the users waiting in a queue.

+     */

+    public Set<QueueUser> getUsers() {

+        synchronized (users) {

+            return users;

+        }

+    }

+

+    /**

+     * Adds a user to the packet.

+     *

+     * @param user the user.

+     */

+    private void addUser(QueueUser user) {

+        synchronized (users) {

+            users.add(user);

+        }

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    public String toXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");

+

+        synchronized (users) {

+            for (Iterator<QueueUser> i=users.iterator(); i.hasNext(); ) {

+                QueueUser user = (QueueUser)i.next();

+                int position = user.getQueuePosition();

+                int timeRemaining = user.getEstimatedRemainingTime();

+                Date timestamp = user.getQueueJoinTimestamp();

+

+                buf.append("<user jid=\"").append(user.getUserID()).append("\">");

+

+                if (position != -1) {

+                    buf.append("<position>").append(position).append("</position>");

+                }

+

+                if (timeRemaining != -1) {

+                    buf.append("<time>").append(timeRemaining).append("</time>");

+                }

+

+                if (timestamp != null) {

+                    buf.append("<join-time>");

+                    buf.append(dateFormat.format(timestamp));

+                    buf.append("</join-time>");

+                }

+

+                buf.append("</user>");

+            }

+        }

+        buf.append("</").append(ELEMENT_NAME).append(">");

+        return buf.toString();

+    }

+

+    /**

+     * Provider class for QueueDetails packet extensions.

+     */

+    public static class Provider implements PacketExtensionProvider {

+        

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            

+            SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);

+            QueueDetails queueDetails = new QueueDetails();

+

+            int eventType = parser.getEventType();

+            while (eventType != XmlPullParser.END_TAG &&

+                    "notify-queue-details".equals(parser.getName()))

+            {

+                eventType = parser.next();

+                while ((eventType == XmlPullParser.START_TAG) && "user".equals(parser.getName())) {

+                    String uid = null;

+                    int position = -1;

+                    int time = -1;

+                    Date joinTime = null;

+

+                    uid = parser.getAttributeValue("", "jid");

+               

+                    if (uid == null) {

+                        // throw exception

+                    }

+

+                    eventType = parser.next();

+                    while ((eventType != XmlPullParser.END_TAG)

+                                || (! "user".equals(parser.getName())))

+                    {                        

+                        if ("position".equals(parser.getName())) {

+                            position = Integer.parseInt(parser.nextText());

+                        }

+                        else if ("time".equals(parser.getName())) {

+                            time = Integer.parseInt(parser.nextText());

+                        }

+                        else if ("join-time".equals(parser.getName())) {

+                            joinTime = dateFormat.parse(parser.nextText());                            

+                        }

+                        else if( parser.getName().equals( "waitTime" ) ) {

+                            Date wait = dateFormat.parse(parser.nextText());

+                            System.out.println( wait );

+                        }

+

+                        eventType = parser.next();

+

+                        if (eventType != XmlPullParser.END_TAG) {

+                            // throw exception

+                        }

+                    }                   

+

+                    queueDetails.addUser(new QueueUser(uid, position, time, joinTime));

+

+                    eventType = parser.next();

+                }

+            }

+            return queueDetails;

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/QueueOverview.java b/src/org/jivesoftware/smackx/workgroup/packet/QueueOverview.java
new file mode 100644
index 0000000..a559579
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/QueueOverview.java
@@ -0,0 +1,160 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smackx.workgroup.agent.WorkgroupQueue;

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.text.SimpleDateFormat;

+import java.util.Date;

+

+public class QueueOverview implements PacketExtension {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static String ELEMENT_NAME = "notify-queue";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static String NAMESPACE = "http://jabber.org/protocol/workgroup";

+

+    private static final String DATE_FORMAT = "yyyyMMdd'T'HH:mm:ss";

+    private SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);

+

+    private int averageWaitTime;

+    private Date oldestEntry;

+    private int userCount;

+    private WorkgroupQueue.Status status;

+

+    QueueOverview() {

+        this.averageWaitTime = -1;

+        this.oldestEntry = null;

+        this.userCount = -1;

+        this.status = null;

+    }

+

+    void setAverageWaitTime(int averageWaitTime) {

+        this.averageWaitTime = averageWaitTime;

+    }

+

+    public int getAverageWaitTime () {

+        return averageWaitTime;

+    }

+

+    void setOldestEntry(Date oldestEntry) {

+        this.oldestEntry = oldestEntry;

+    }

+

+    public Date getOldestEntry() {

+        return oldestEntry;

+    }

+

+    void setUserCount(int userCount) {

+        this.userCount = userCount;

+    }

+

+    public int getUserCount() {

+        return userCount;

+    }

+

+    public WorkgroupQueue.Status getStatus() {

+        return status;

+    }

+

+    void setStatus(WorkgroupQueue.Status status) {

+        this.status = status;

+    }

+

+    public String getElementName () {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace () {

+        return NAMESPACE;

+    }

+

+    public String toXML () {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");

+

+        if (userCount != -1) {

+            buf.append("<count>").append(userCount).append("</count>");

+        }

+        if (oldestEntry != null) {

+            buf.append("<oldest>").append(dateFormat.format(oldestEntry)).append("</oldest>");

+        }

+        if (averageWaitTime != -1) {

+            buf.append("<time>").append(averageWaitTime).append("</time>");

+        }

+        if (status != null) {

+            buf.append("<status>").append(status).append("</status>");

+        }

+        buf.append("</").append(ELEMENT_NAME).append(">");

+

+        return buf.toString();

+    }

+

+    public static class Provider implements PacketExtensionProvider {

+

+        public PacketExtension parseExtension (XmlPullParser parser) throws Exception {

+            int eventType = parser.getEventType();

+            QueueOverview queueOverview = new QueueOverview();            

+            SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);

+

+            if (eventType != XmlPullParser.START_TAG) {

+                // throw exception

+            }

+

+            eventType = parser.next();

+            while ((eventType != XmlPullParser.END_TAG)

+                         || (!ELEMENT_NAME.equals(parser.getName())))

+            {

+                if ("count".equals(parser.getName())) {

+                    queueOverview.setUserCount(Integer.parseInt(parser.nextText()));

+                }

+                else if ("time".equals(parser.getName())) {

+                    queueOverview.setAverageWaitTime(Integer.parseInt(parser.nextText()));

+                }

+                else if ("oldest".equals(parser.getName())) {

+                    queueOverview.setOldestEntry((dateFormat.parse(parser.nextText())));                    

+                }

+                else if ("status".equals(parser.getName())) {

+                    queueOverview.setStatus(WorkgroupQueue.Status.fromString(parser.nextText()));

+                }

+

+                eventType = parser.next();

+

+                if (eventType != XmlPullParser.END_TAG) {

+                    // throw exception

+                }

+            }

+

+            if (eventType != XmlPullParser.END_TAG) {

+                // throw exception

+            }

+

+            return queueOverview;

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/QueueUpdate.java b/src/org/jivesoftware/smackx/workgroup/packet/QueueUpdate.java
new file mode 100644
index 0000000..c326a57
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/QueueUpdate.java
@@ -0,0 +1,122 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * An IQ packet that encapsulates both types of workgroup queue

+ * status notifications -- position updates, and estimated time

+ * left in the queue updates.

+ */

+public class QueueUpdate implements PacketExtension {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "queue-status";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";

+

+    private int position;

+    private int remainingTime;

+

+    public QueueUpdate(int position, int remainingTime) {

+        this.position = position;

+        this.remainingTime = remainingTime;

+    }

+

+    /**

+     * Returns the user's position in the workgroup queue, or -1 if the

+     * value isn't set on this packet.

+     *

+     * @return the position in the workgroup queue.

+     */

+    public int getPosition() {

+        return this.position;

+    }

+

+    /**

+     * Returns the user's estimated time left in the workgroup queue, or

+     * -1 if the value isn't set on this packet.

+     *

+     * @return the estimated time left in the workgroup queue.

+     */

+    public int getRemaingTime() {

+        return remainingTime;

+    }

+

+    public String toXML() {

+        StringBuilder buf = new StringBuilder();

+        buf.append("<queue-status xmlns=\"http://jabber.org/protocol/workgroup\">");

+        if (position != -1) {

+            buf.append("<position>").append(position).append("</position>");

+        }

+        if (remainingTime != -1) {

+            buf.append("<time>").append(remainingTime).append("</time>");

+        }

+        buf.append("</queue-status>");

+        return buf.toString();

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    public static class Provider implements PacketExtensionProvider {

+

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            boolean done = false;

+            int position = -1;

+            int timeRemaining = -1;

+            while (!done) {

+                parser.next();

+                String elementName = parser.getName();

+                if (parser.getEventType() == XmlPullParser.START_TAG && "position".equals(elementName)) {

+                    try {

+                        position = Integer.parseInt(parser.nextText());

+                    }

+                    catch (NumberFormatException nfe) {

+                    }

+                }

+                else if (parser.getEventType() == XmlPullParser.START_TAG && "time".equals(elementName)) {

+                    try {

+                        timeRemaining = Integer.parseInt(parser.nextText());

+                    }

+                    catch (NumberFormatException nfe) {

+                    }

+                }

+                else if (parser.getEventType() == XmlPullParser.END_TAG && "queue-status".equals(elementName)) {

+                    done = true;

+                }

+            }

+            return new QueueUpdate(position, timeRemaining);

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/packet/RoomInvitation.java b/src/org/jivesoftware/smackx/workgroup/packet/RoomInvitation.java
new file mode 100644
index 0000000..34555de
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/RoomInvitation.java
@@ -0,0 +1,177 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Packet extension for {@link org.jivesoftware.smackx.workgroup.agent.InvitationRequest}.

+ *

+ * @author Gaston Dombiak

+ */

+public class RoomInvitation implements PacketExtension {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "invite";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";

+

+    /**

+     * Type of entity being invited to a groupchat support session.

+     */

+    private Type type;

+    /**

+     * JID of the entity being invited. The entity could be another agent, user , a queue or a workgroup. In

+     * the case of a queue or a workgroup the server will select the best agent to invite. 

+     */

+    private String invitee;

+    /**

+     * Full JID of the user that sent the invitation.

+     */

+    private String inviter;

+    /**

+     * ID of the session that originated the initial user request.

+     */

+    private String sessionID;

+    /**

+     * JID of the room to join if offer is accepted.

+     */

+    private String room;

+    /**

+     * Text provided by the inviter explaining the reason why the invitee is invited. 

+     */

+    private String reason;

+

+    public RoomInvitation(Type type, String invitee, String sessionID, String reason) {

+        this.type = type;

+        this.invitee = invitee;

+        this.sessionID = sessionID;

+        this.reason = reason;

+    }

+

+    private RoomInvitation() {

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    public String getInviter() {

+        return inviter;

+    }

+

+    public String getRoom() {

+        return room;

+    }

+

+    public String getReason() {

+        return reason;

+    }

+

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    public String toXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE);

+        buf.append("\" type=\"").append(type).append("\">");

+        buf.append("<session xmlns=\"http://jivesoftware.com/protocol/workgroup\" id=\"").append(sessionID).append("\"></session>");

+        if (invitee != null) {

+            buf.append("<invitee>").append(invitee).append("</invitee>");

+        }

+        if (inviter != null) {

+            buf.append("<inviter>").append(inviter).append("</inviter>");

+        }

+        if (reason != null) {

+            buf.append("<reason>").append(reason).append("</reason>");

+        }

+        // Add packet extensions, if any are defined.

+        buf.append("</").append(ELEMENT_NAME).append("> ");

+

+        return buf.toString();

+    }

+

+    /**

+     * Type of entity being invited to a groupchat support session.

+     */

+    public static enum Type {

+        /**

+         * A user is being invited to a groupchat support session. The user could be another agent

+         * or just a regular XMPP user.

+         */

+        user,

+        /**

+         * Some agent of the specified queue will be invited to the groupchat support session.

+         */

+        queue,

+        /**

+         * Some agent of the specified workgroup will be invited to the groupchat support session.

+         */

+        workgroup

+    }

+

+    public static class Provider implements PacketExtensionProvider {

+

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            final RoomInvitation invitation = new RoomInvitation();

+            invitation.type = Type.valueOf(parser.getAttributeValue("", "type"));

+

+            boolean done = false;

+            while (!done) {

+                parser.next();

+                String elementName = parser.getName();

+                if (parser.getEventType() == XmlPullParser.START_TAG) {

+                    if ("session".equals(elementName)) {

+                        invitation.sessionID = parser.getAttributeValue("", "id");

+                    }

+                    else if ("invitee".equals(elementName)) {

+                        invitation.invitee = parser.nextText();

+                    }

+                    else if ("inviter".equals(elementName)) {

+                        invitation.inviter = parser.nextText();

+                    }

+                    else if ("reason".equals(elementName)) {

+                        invitation.reason = parser.nextText();

+                    }

+                    else if ("room".equals(elementName)) {

+                        invitation.room = parser.nextText();

+                    }

+                }

+                else if (parser.getEventType() == XmlPullParser.END_TAG && ELEMENT_NAME.equals(elementName)) {

+                    done = true;

+                }

+            }

+            return invitation;

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/RoomTransfer.java b/src/org/jivesoftware/smackx/workgroup/packet/RoomTransfer.java
new file mode 100644
index 0000000..d1e83e2
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/RoomTransfer.java
@@ -0,0 +1,177 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * Packet extension for {@link org.jivesoftware.smackx.workgroup.agent.TransferRequest}.

+ *

+ * @author Gaston Dombiak

+ */

+public class RoomTransfer implements PacketExtension {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "transfer";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";

+

+    /**

+     * Type of entity being invited to a groupchat support session.

+     */

+    private RoomTransfer.Type type;

+    /**

+     * JID of the entity being invited. The entity could be another agent, user , a queue or a workgroup. In

+     * the case of a queue or a workgroup the server will select the best agent to invite.

+     */

+    private String invitee;

+    /**

+     * Full JID of the user that sent the invitation.

+     */

+    private String inviter;

+    /**

+     * ID of the session that originated the initial user request.

+     */

+    private String sessionID;

+    /**

+     * JID of the room to join if offer is accepted.

+     */

+    private String room;

+    /**

+     * Text provided by the inviter explaining the reason why the invitee is invited.

+     */

+    private String reason;

+

+    public RoomTransfer(RoomTransfer.Type type, String invitee, String sessionID, String reason) {

+        this.type = type;

+        this.invitee = invitee;

+        this.sessionID = sessionID;

+        this.reason = reason;

+    }

+

+    private RoomTransfer() {

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    public String getInviter() {

+        return inviter;

+    }

+

+    public String getRoom() {

+        return room;

+    }

+

+    public String getReason() {

+        return reason;

+    }

+

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    public String toXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE);

+        buf.append("\" type=\"").append(type).append("\">");

+        buf.append("<session xmlns=\"http://jivesoftware.com/protocol/workgroup\" id=\"").append(sessionID).append("\"></session>");

+        if (invitee != null) {

+            buf.append("<invitee>").append(invitee).append("</invitee>");

+        }

+        if (inviter != null) {

+            buf.append("<inviter>").append(inviter).append("</inviter>");

+        }

+        if (reason != null) {

+            buf.append("<reason>").append(reason).append("</reason>");

+        }

+        // Add packet extensions, if any are defined.

+        buf.append("</").append(ELEMENT_NAME).append("> ");

+

+        return buf.toString();

+    }

+

+    /**

+     * Type of entity being invited to a groupchat support session.

+     */

+    public static enum Type {

+        /**

+         * A user is being invited to a groupchat support session. The user could be another agent

+         * or just a regular XMPP user.

+         */

+        user,

+        /**

+         * Some agent of the specified queue will be invited to the groupchat support session.

+         */

+        queue,

+        /**

+         * Some agent of the specified workgroup will be invited to the groupchat support session.

+         */

+        workgroup

+    }

+

+    public static class Provider implements PacketExtensionProvider {

+

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            final RoomTransfer invitation = new RoomTransfer();

+            invitation.type = RoomTransfer.Type.valueOf(parser.getAttributeValue("", "type"));

+

+            boolean done = false;

+            while (!done) {

+                parser.next();

+                String elementName = parser.getName();

+                if (parser.getEventType() == XmlPullParser.START_TAG) {

+                    if ("session".equals(elementName)) {

+                        invitation.sessionID = parser.getAttributeValue("", "id");

+                    }

+                    else if ("invitee".equals(elementName)) {

+                        invitation.invitee = parser.nextText();

+                    }

+                    else if ("inviter".equals(elementName)) {

+                        invitation.inviter = parser.nextText();

+                    }

+                    else if ("reason".equals(elementName)) {

+                        invitation.reason = parser.nextText();

+                    }

+                    else if ("room".equals(elementName)) {

+                        invitation.room = parser.nextText();

+                    }

+                }

+                else if (parser.getEventType() == XmlPullParser.END_TAG && ELEMENT_NAME.equals(elementName)) {

+                    done = true;

+                }

+            }

+            return invitation;

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/SessionID.java b/src/org/jivesoftware/smackx/workgroup/packet/SessionID.java
new file mode 100644
index 0000000..bfd7cfd
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/SessionID.java
@@ -0,0 +1,77 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+public class SessionID implements PacketExtension {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "session";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    private String sessionID;

+

+    public SessionID(String sessionID) {

+        this.sessionID = sessionID;

+    }

+

+    public String getSessionID() {

+        return this.sessionID;

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    public String toXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\" ");

+        buf.append("id=\"").append(this.getSessionID());

+        buf.append("\"/>");

+

+        return buf.toString();

+    }

+

+    public static class Provider implements PacketExtensionProvider {

+

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            String sessionID = parser.getAttributeValue("", "id");

+

+            // Advance to end of extension.

+            parser.next();

+

+            return new SessionID(sessionID);

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/Transcript.java b/src/org/jivesoftware/smackx/workgroup/packet/Transcript.java
new file mode 100644
index 0000000..7f8f29e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/Transcript.java
@@ -0,0 +1,98 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+

+import java.util.ArrayList;

+import java.util.Collections;

+import java.util.Iterator;

+import java.util.List;

+

+/**

+ * Represents the conversation transcript that occured in a group chat room between an Agent

+ * and a user that requested assistance. The transcript contains all the Messages that were sent

+ * to the room as well as the sent presences. 

+ *

+ * @author Gaston Dombiak

+ */

+public class Transcript extends IQ {

+    private String sessionID;

+    private List<Packet> packets;

+

+    /**

+     * Creates a transcript request for the given sessionID.

+     *

+     * @param sessionID the id of the session to get the conversation transcript.

+     */

+    public Transcript(String sessionID) {

+        this.sessionID = sessionID;

+        this.packets = new ArrayList<Packet>();

+    }

+

+    /**

+     * Creates a new transcript for the given sessionID and list of packets. The list of packets

+     * may include Messages and/or Presences.

+     *

+     * @param sessionID the id of the session that generated this conversation transcript.

+     * @param packets the list of messages and presences send to the room.

+     */

+    public Transcript(String sessionID, List<Packet> packets) {

+        this.sessionID = sessionID;

+        this.packets = packets;

+    }

+

+    /**

+     * Returns id of the session that generated this conversation transcript. The sessionID is a

+     * value generated by the server when a new request is received.

+     *

+     * @return id of the session that generated this conversation transcript.

+     */

+    public String getSessionID() {

+        return sessionID;

+    }

+

+    /**

+     * Returns the list of Messages and Presences that were sent to the room.

+     *

+     * @return the list of Messages and Presences that were sent to the room.

+     */

+    public List<Packet> getPackets() {

+        return Collections.unmodifiableList(packets);

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<transcript xmlns=\"http://jivesoftware.com/protocol/workgroup\" sessionID=\"")

+                .append(sessionID)

+                .append("\">");

+

+        for (Iterator<Packet> it=packets.iterator(); it.hasNext();) {

+            Packet packet = it.next();

+            buf.append(packet.toXML());

+        }

+

+        buf.append("</transcript>");

+

+        return buf.toString();

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/TranscriptProvider.java b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptProvider.java
new file mode 100644
index 0000000..791b06e
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptProvider.java
@@ -0,0 +1,66 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.packet.Packet;

+import org.jivesoftware.smack.util.PacketParserUtils;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.util.ArrayList;

+import java.util.List;

+

+/**

+ * An IQProvider for transcripts.

+ *

+ * @author Gaston Dombiak

+ */

+public class TranscriptProvider implements IQProvider {

+

+    public TranscriptProvider() {

+        super();

+    }

+

+    public IQ parseIQ(XmlPullParser parser) throws Exception {

+        String sessionID = parser.getAttributeValue("", "sessionID");

+        List<Packet> packets = new ArrayList<Packet>();

+

+        boolean done = false;

+        while (!done) {

+            int eventType = parser.next();

+            if (eventType == XmlPullParser.START_TAG) {

+                if (parser.getName().equals("message")) {

+                    packets.add(PacketParserUtils.parseMessage(parser));

+                }

+                else if (parser.getName().equals("presence")) {

+                    packets.add(PacketParserUtils.parsePresence(parser));

+                }

+            }

+            else if (eventType == XmlPullParser.END_TAG) {

+                if (parser.getName().equals("transcript")) {

+                    done = true;

+                }

+            }

+        }

+

+        return new Transcript(sessionID, packets);

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/TranscriptSearch.java b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptSearch.java
new file mode 100644
index 0000000..72693c4
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptSearch.java
@@ -0,0 +1,87 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.jivesoftware.smack.util.PacketParserUtils;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * IQ packet for retrieving the transcript search form, submiting the completed search form

+ * or retrieving the answer of a transcript search.

+ *

+ * @author Gaston Dombiak

+ */

+public class TranscriptSearch extends IQ {

+

+    /**

+    * Element name of the packet extension.

+    */

+   public static final String ELEMENT_NAME = "transcript-search";

+

+   /**

+    * Namespace of the packet extension.

+    */

+   public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\">");

+        // Add packet extensions, if any are defined.

+        buf.append(getExtensionsXML());

+        buf.append("</").append(ELEMENT_NAME).append("> ");

+

+        return buf.toString();

+    }

+

+    /**

+     * An IQProvider for TranscriptSearch packets.

+     *

+     * @author Gaston Dombiak

+     */

+    public static class Provider implements IQProvider {

+

+        public Provider() {

+            super();

+        }

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            TranscriptSearch answer = new TranscriptSearch();

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if (eventType == XmlPullParser.START_TAG) {

+                    // Parse the packet extension

+                    answer.addExtension(PacketParserUtils.parsePacketExtension(parser.getName(), parser.getNamespace(), parser));

+                }

+                else if (eventType == XmlPullParser.END_TAG) {

+                    if (parser.getName().equals(ELEMENT_NAME)) {

+                        done = true;

+                    }

+                }

+            }

+

+            return answer;

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/Transcripts.java b/src/org/jivesoftware/smackx/workgroup/packet/Transcripts.java
new file mode 100644
index 0000000..66ddaad
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/Transcripts.java
@@ -0,0 +1,247 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+

+import java.text.SimpleDateFormat;

+import java.util.*;

+

+/**

+ * Represents a list of conversation transcripts that a user had in all his history. Each

+ * transcript summary includes the sessionID which may be used for getting more detailed

+ * information about the conversation. {@link org.jivesoftware.smackx.workgroup.packet.Transcript}

+ *

+ * @author Gaston Dombiak

+ */

+public class Transcripts extends IQ {

+

+    private static final SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");

+    static {

+        UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0"));

+    }

+

+    private String userID;

+    private List<Transcripts.TranscriptSummary> summaries;

+

+

+    /**

+     * Creates a transcripts request for the given userID.

+     *

+     * @param userID the id of the user to get his conversations transcripts.

+     */

+    public Transcripts(String userID) {

+        this.userID = userID;

+        this.summaries = new ArrayList<Transcripts.TranscriptSummary>();

+    }

+

+    /**

+     * Creates a Transcripts which will contain the transcript summaries of the given user.

+     *

+     * @param userID the id of the user. Could be a real JID or a unique String that identifies

+     *        anonymous users.

+     * @param summaries the list of TranscriptSummaries.

+     */

+    public Transcripts(String userID, List<Transcripts.TranscriptSummary> summaries) {

+        this.userID = userID;

+        this.summaries = summaries;

+    }

+

+    /**

+     * Returns the id of the user that was involved in the conversations. The userID could be a

+     * real JID if the connected user was not anonymous. Otherwise, the userID will be a String

+     * that was provided by the anonymous user as a way to idenitify the user across many user

+     * sessions.

+     *

+     * @return the id of the user that was involved in the conversations.

+     */

+    public String getUserID() {

+        return userID;

+    }

+

+    /**

+     * Returns a list of TranscriptSummary. A TranscriptSummary does not contain the conversation

+     * transcript but some summary information like the sessionID and the time when the

+     * conversation started and finished. Once you have the sessionID it is possible to get the

+     * full conversation transcript.

+     *

+     * @return a list of TranscriptSummary.

+     */

+    public List<Transcripts.TranscriptSummary> getSummaries() {

+        return Collections.unmodifiableList(summaries);

+    }

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<transcripts xmlns=\"http://jivesoftware.com/protocol/workgroup\" userID=\"")

+                .append(userID)

+                .append("\">");

+

+        for (TranscriptSummary transcriptSummary : summaries) {

+            buf.append(transcriptSummary.toXML());

+        }

+

+        buf.append("</transcripts>");

+

+        return buf.toString();

+    }

+

+    /**

+     * A TranscriptSummary contains some information about a conversation such as the ID of the

+     * session or the date when the conversation started and finished. You will need to use the

+     * sessionID to get the full conversation transcript.

+     */

+    public static class TranscriptSummary {

+        private String sessionID;

+        private Date joinTime;

+        private Date leftTime;

+        private List<AgentDetail> agentDetails;

+

+        public TranscriptSummary(String sessionID, Date joinTime, Date leftTime, List<AgentDetail> agentDetails) {

+            this.sessionID = sessionID;

+            this.joinTime = joinTime;

+            this.leftTime = leftTime;

+            this.agentDetails = agentDetails;

+        }

+

+        /**

+         * Returns the ID of the session that is related to this conversation transcript. The

+         * sessionID could be used for getting the full conversation transcript.

+         *

+         * @return the ID of the session that is related to this conversation transcript.

+         */

+        public String getSessionID() {

+            return sessionID;

+        }

+

+        /**

+         * Returns the Date when the conversation started.

+         *

+         * @return the Date when the conversation started.

+         */

+        public Date getJoinTime() {

+            return joinTime;

+        }

+

+        /**

+         * Returns the Date when the conversation finished.

+         *

+         * @return the Date when the conversation finished.

+         */

+        public Date getLeftTime() {

+            return leftTime;

+        }

+

+        /**

+         * Returns a list of AgentDetails. For each Agent that was involved in the conversation

+         * the list will include an AgentDetail. An AgentDetail contains the JID of the agent

+         * as well as the time when the Agent joined and left the conversation.

+         *

+         * @return a list of AgentDetails.

+         */

+        public List<AgentDetail> getAgentDetails() {

+            return agentDetails;

+        }

+

+        public String toXML() {

+            StringBuilder buf = new StringBuilder();

+

+            buf.append("<transcript sessionID=\"")

+                    .append(sessionID)

+                    .append("\">");

+

+            if (joinTime != null) {

+                buf.append("<joinTime>").append(UTC_FORMAT.format(joinTime)).append("</joinTime>");

+            }

+            if (leftTime != null) {

+                buf.append("<leftTime>").append(UTC_FORMAT.format(leftTime)).append("</leftTime>");

+            }

+            buf.append("<agents>");

+            for (AgentDetail agentDetail : agentDetails) {

+                buf.append(agentDetail.toXML());

+            }

+            buf.append("</agents></transcript>");

+

+            return buf.toString();

+        }

+    }

+

+    /**

+     * An AgentDetail contains information of an Agent that was involved in a conversation. 

+     */

+    public static class AgentDetail {

+        private String agentJID;

+        private Date joinTime;

+        private Date leftTime;

+

+        public AgentDetail(String agentJID, Date joinTime, Date leftTime) {

+            this.agentJID = agentJID;

+            this.joinTime = joinTime;

+            this.leftTime = leftTime;

+        }

+

+        /**

+         * Returns the bare JID of the Agent that was involved in the conversation.

+         *

+         * @return the bared JID of the Agent that was involved in the conversation.

+         */

+        public String getAgentJID() {

+            return agentJID;

+        }

+

+        /**

+         * Returns the Date when the Agent joined the conversation.

+         *

+         * @return the Date when the Agent joined the conversation.

+         */

+        public Date getJoinTime() {

+            return joinTime;

+        }

+

+        /**

+         * Returns the Date when the Agent left the conversation.

+         *

+         * @return the Date when the Agent left the conversation.

+         */

+        public Date getLeftTime() {

+            return leftTime;

+        }

+

+        public String toXML() {

+            StringBuilder buf = new StringBuilder();

+

+            buf.append("<agent>");

+

+            if (agentJID != null) {

+                buf.append("<agentJID>").append(agentJID).append("</agentJID>");

+            }

+            if (joinTime != null) {

+                buf.append("<joinTime>").append(UTC_FORMAT.format(joinTime)).append("</joinTime>");

+            }

+            if (leftTime != null) {

+                buf.append("<leftTime>").append(UTC_FORMAT.format(leftTime)).append("</leftTime>");

+            }

+            buf.append("</agent>");

+

+            return buf.toString();

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/TranscriptsProvider.java b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptsProvider.java
new file mode 100644
index 0000000..cb8f429
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/TranscriptsProvider.java
@@ -0,0 +1,148 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+import org.xmlpull.v1.XmlPullParserException;

+

+import java.io.IOException;

+import java.text.ParseException;

+import java.text.SimpleDateFormat;

+import java.util.ArrayList;

+import java.util.Date;

+import java.util.List;

+import java.util.TimeZone;

+

+/**

+ * An IQProvider for transcripts summaries.

+ *

+ * @author Gaston Dombiak

+ */

+public class TranscriptsProvider implements IQProvider {

+

+    private static final SimpleDateFormat UTC_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HH:mm:ss");

+    static {

+        UTC_FORMAT.setTimeZone(TimeZone.getTimeZone("GMT+0"));

+    }

+

+    public TranscriptsProvider() {

+        super();

+    }

+

+    public IQ parseIQ(XmlPullParser parser) throws Exception {

+        String userID = parser.getAttributeValue("", "userID");

+        List<Transcripts.TranscriptSummary> summaries = new ArrayList<Transcripts.TranscriptSummary>();

+

+        boolean done = false;

+        while (!done) {

+            int eventType = parser.next();

+            if (eventType == XmlPullParser.START_TAG) {

+                if (parser.getName().equals("transcript")) {

+                    summaries.add(parseSummary(parser));

+                }

+            }

+            else if (eventType == XmlPullParser.END_TAG) {

+                if (parser.getName().equals("transcripts")) {

+                    done = true;

+                }

+            }

+        }

+

+        return new Transcripts(userID, summaries);

+    }

+

+    private Transcripts.TranscriptSummary parseSummary(XmlPullParser parser) throws IOException,

+            XmlPullParserException {

+        String sessionID =  parser.getAttributeValue("", "sessionID");

+        Date joinTime = null;

+        Date leftTime = null;

+        List<Transcripts.AgentDetail> agents = new ArrayList<Transcripts.AgentDetail>();

+

+        boolean done = false;

+        while (!done) {

+            int eventType = parser.next();

+            if (eventType == XmlPullParser.START_TAG) {

+                if (parser.getName().equals("joinTime")) {

+                    try {

+                        joinTime = UTC_FORMAT.parse(parser.nextText());

+                    } catch (ParseException e) {}

+                }

+                else if (parser.getName().equals("leftTime")) {

+                    try {

+                        leftTime = UTC_FORMAT.parse(parser.nextText());

+                    } catch (ParseException e) {}

+                }

+                else if (parser.getName().equals("agents")) {

+                    agents = parseAgents(parser);

+                }

+            }

+            else if (eventType == XmlPullParser.END_TAG) {

+                if (parser.getName().equals("transcript")) {

+                    done = true;

+                }

+            }

+        }

+

+        return new Transcripts.TranscriptSummary(sessionID, joinTime, leftTime, agents);

+    }

+

+    private List<Transcripts.AgentDetail> parseAgents(XmlPullParser parser) throws IOException, XmlPullParserException {

+        List<Transcripts.AgentDetail> agents = new ArrayList<Transcripts.AgentDetail>();

+        String agentJID =  null;

+        Date joinTime = null;

+        Date leftTime = null;

+

+        boolean done = false;

+        while (!done) {

+            int eventType = parser.next();

+            if (eventType == XmlPullParser.START_TAG) {

+                if (parser.getName().equals("agentJID")) {

+                    agentJID = parser.nextText();

+                }

+                else if (parser.getName().equals("joinTime")) {

+                    try {

+                        joinTime = UTC_FORMAT.parse(parser.nextText());

+                    } catch (ParseException e) {}

+                }

+                else if (parser.getName().equals("leftTime")) {

+                    try {

+                        leftTime = UTC_FORMAT.parse(parser.nextText());

+                    } catch (ParseException e) {}

+                }

+                else if (parser.getName().equals("agent")) {

+                    agentJID =  null;

+                    joinTime = null;

+                    leftTime = null;

+                }

+            }

+            else if (eventType == XmlPullParser.END_TAG) {

+                if (parser.getName().equals("agents")) {

+                    done = true;

+                }

+                else if (parser.getName().equals("agent")) {

+                    agents.add(new Transcripts.AgentDetail(agentJID, joinTime, leftTime));

+                }

+            }

+        }

+        return agents;

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/UserID.java b/src/org/jivesoftware/smackx/workgroup/packet/UserID.java
new file mode 100644
index 0000000..8bf4589
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/UserID.java
@@ -0,0 +1,77 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+public class UserID implements PacketExtension {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "user";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    private String userID;

+

+    public UserID(String userID) {

+        this.userID = userID;

+    }

+

+    public String getUserID() {

+        return this.userID;

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    public String toXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=\"").append(NAMESPACE).append("\" ");

+        buf.append("id=\"").append(this.getUserID());

+        buf.append("\"/>");

+

+        return buf.toString();

+    }

+

+    public static class Provider implements PacketExtensionProvider {

+

+        public PacketExtension parseExtension(XmlPullParser parser) throws Exception {

+            String userID = parser.getAttributeValue("", "id");

+

+            // Advance to end of extension.

+            parser.next();

+

+            return new UserID(userID);

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/packet/WorkgroupInformation.java b/src/org/jivesoftware/smackx/workgroup/packet/WorkgroupInformation.java
new file mode 100644
index 0000000..b0ea447
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/packet/WorkgroupInformation.java
@@ -0,0 +1,86 @@
+/**

+ * $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.smackx.workgroup.packet;

+

+import org.jivesoftware.smack.packet.PacketExtension;

+import org.jivesoftware.smack.provider.PacketExtensionProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+/**

+ * A packet extension that contains information about the user and agent in a

+ * workgroup chat. The packet extension is attached to group chat invitations.

+ */

+public class WorkgroupInformation implements PacketExtension {

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "workgroup";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jabber.org/protocol/workgroup";

+

+    private String workgroupJID;

+

+    public WorkgroupInformation(String workgroupJID){

+        this.workgroupJID = workgroupJID;

+    }

+

+    public String getWorkgroupJID() {

+        return workgroupJID;

+    }

+

+    public String getElementName() {

+        return ELEMENT_NAME;

+    }

+

+    public String getNamespace() {

+        return NAMESPACE;

+    }

+

+    public String toXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append('<').append(ELEMENT_NAME);

+        buf.append(" jid=\"").append(getWorkgroupJID()).append("\"");

+        buf.append(" xmlns=\"").append(NAMESPACE).append("\" />");

+

+        return buf.toString();

+    }

+

+    public static class Provider implements PacketExtensionProvider {

+

+        /**

+         * PacketExtensionProvider implementation

+         */

+        public PacketExtension parseExtension (XmlPullParser parser)

+            throws Exception {

+            String workgroupJID = parser.getAttributeValue("", "jid");

+

+            // since this is a start and end tag, and we arrive on the start, this should guarantee

+            //      we leave on the end

+            parser.next();

+

+            return new WorkgroupInformation(workgroupJID);

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/settings/ChatSetting.java b/src/org/jivesoftware/smackx/workgroup/settings/ChatSetting.java
new file mode 100644
index 0000000..921134a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/settings/ChatSetting.java
@@ -0,0 +1,56 @@
+/**

+ * $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.smackx.workgroup.settings;

+

+public class ChatSetting {

+    private String key;

+    private String value;

+    private int type;

+

+    public ChatSetting(String key, String value, int type){

+        setKey(key);

+        setValue(value);

+        setType(type);

+    }

+

+    public String getKey() {

+        return key;

+    }

+

+    public void setKey(String key) {

+        this.key = key;

+    }

+

+    public String getValue() {

+        return value;

+    }

+

+    public void setValue(String value) {

+        this.value = value;

+    }

+

+    public int getType() {

+        return type;

+    }

+

+    public void setType(int type) {

+        this.type = type;

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/settings/ChatSettings.java b/src/org/jivesoftware/smackx/workgroup/settings/ChatSettings.java
new file mode 100644
index 0000000..ccc7a40
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/settings/ChatSettings.java
@@ -0,0 +1,179 @@
+/**

+ * $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.smackx.workgroup.settings;

+

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+import java.util.ArrayList;

+import java.util.Collection;

+import java.util.Iterator;

+import java.util.List;

+

+public class ChatSettings extends IQ {

+

+    /**

+     * Defined as image type.

+     */

+    public static final int IMAGE_SETTINGS = 0;

+

+    /**

+     * Defined as Text settings type.

+     */

+    public static final int TEXT_SETTINGS = 1;

+

+    /**

+     * Defined as Bot settings type.

+     */

+    public static final int BOT_SETTINGS = 2;

+

+    private List<ChatSetting> settings;

+    private String key;

+    private int type = -1;

+

+    public ChatSettings() {

+        settings = new ArrayList<ChatSetting>();

+    }

+

+    public ChatSettings(String key) {

+        setKey(key);

+    }

+

+    public void setKey(String key) {

+        this.key = key;

+    }

+

+    public void setType(int type) {

+        this.type = type;

+    }

+

+    public void addSetting(ChatSetting setting) {

+        settings.add(setting);

+    }

+

+    public Collection<ChatSetting> getSettings() {

+        return settings;

+    }

+

+    public ChatSetting getChatSetting(String key) {

+        Collection<ChatSetting> col = getSettings();

+        if (col != null) {

+            Iterator<ChatSetting> iter = col.iterator();

+            while (iter.hasNext()) {

+                ChatSetting chatSetting = iter.next();

+                if (chatSetting.getKey().equals(key)) {

+                    return chatSetting;

+                }

+            }

+        }

+        return null;

+    }

+

+    public ChatSetting getFirstEntry() {

+        if (settings.size() > 0) {

+            return (ChatSetting)settings.get(0);

+        }

+        return null;

+    }

+

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "chat-settings";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=");

+        buf.append('"');

+        buf.append(NAMESPACE);

+        buf.append('"');

+        if (key != null) {

+            buf.append(" key=\"" + key + "\"");

+        }

+

+        if (type != -1) {

+            buf.append(" type=\"" + type + "\"");

+        }

+

+        buf.append("></").append(ELEMENT_NAME).append("> ");

+        return buf.toString();

+    }

+

+    /**

+     * Packet extension provider for AgentStatusRequest packets.

+     */

+    public static class InternalProvider implements IQProvider {

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            if (parser.getEventType() != XmlPullParser.START_TAG) {

+                throw new IllegalStateException("Parser not in proper position, or bad XML.");

+            }

+

+            ChatSettings chatSettings = new ChatSettings();

+

+            boolean done = false;

+            while (!done) {

+                int eventType = parser.next();

+                if ((eventType == XmlPullParser.START_TAG) && ("chat-setting".equals(parser.getName()))) {

+                    chatSettings.addSetting(parseChatSetting(parser));

+

+                }

+                else if (eventType == XmlPullParser.END_TAG && ELEMENT_NAME.equals(parser.getName())) {

+                    done = true;

+                }

+            }

+            return chatSettings;

+        }

+

+        private ChatSetting parseChatSetting(XmlPullParser parser) throws Exception {

+

+            boolean done = false;

+            String key = null;

+            String value = null;

+            int type = 0;

+

+            while (!done) {

+                int eventType = parser.next();

+                if ((eventType == XmlPullParser.START_TAG) && ("key".equals(parser.getName()))) {

+                    key = parser.nextText();

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("value".equals(parser.getName()))) {

+                    value = parser.nextText();

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("type".equals(parser.getName()))) {

+                    type = Integer.parseInt(parser.nextText());

+                }

+                else if (eventType == XmlPullParser.END_TAG && "chat-setting".equals(parser.getName())) {

+                    done = true;

+                }

+            }

+            return new ChatSetting(key, value, type);

+        }

+    }

+}

+

diff --git a/src/org/jivesoftware/smackx/workgroup/settings/GenericSettings.java b/src/org/jivesoftware/smackx/workgroup/settings/GenericSettings.java
new file mode 100644
index 0000000..702eeb7
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/settings/GenericSettings.java
@@ -0,0 +1,114 @@
+/**
+ * $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.smackx.workgroup.settings;
+
+import org.jivesoftware.smackx.workgroup.util.ModelUtil;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class GenericSettings extends IQ {
+
+    private Map<String, String> map = new HashMap<String, String>();
+
+    private String query;
+
+    public String getQuery() {
+        return query;
+    }
+
+    public void setQuery(String query) {
+        this.query = query;
+    }
+
+    public Map<String, String> getMap() {
+        return map;
+    }
+
+    public void setMap(Map<String, String> map) {
+        this.map = map;
+    }
+
+
+    /**
+     * Element name of the packet extension.
+     */
+    public static final String ELEMENT_NAME = "generic-metadata";
+
+    /**
+     * Namespace of the packet extension.
+     */
+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+
+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=");
+        buf.append('"');
+        buf.append(NAMESPACE);
+        buf.append('"');
+        buf.append(">");
+        if (ModelUtil.hasLength(getQuery())) {
+            buf.append("<query>" + getQuery() + "</query>");
+        }
+        buf.append("</").append(ELEMENT_NAME).append("> ");
+        return buf.toString();
+    }
+
+
+    /**
+     * Packet extension provider for SoundSetting Packets.
+     */
+    public static class InternalProvider implements IQProvider {
+
+        public IQ parseIQ(XmlPullParser parser) throws Exception {
+            if (parser.getEventType() != XmlPullParser.START_TAG) {
+                throw new IllegalStateException("Parser not in proper position, or bad XML.");
+            }
+
+            GenericSettings setting = new GenericSettings();
+
+            boolean done = false;
+
+
+            while (!done) {
+                int eventType = parser.next();
+                if ((eventType == XmlPullParser.START_TAG) && ("entry".equals(parser.getName()))) {
+                    eventType = parser.next();
+                    String name = parser.nextText();
+                    eventType = parser.next();
+                    String value = parser.nextText();
+                    setting.getMap().put(name, value);
+                }
+                else if (eventType == XmlPullParser.END_TAG && ELEMENT_NAME.equals(parser.getName())) {
+                    done = true;
+                }
+            }
+
+            return setting;
+        }
+    }
+
+
+}
+
diff --git a/src/org/jivesoftware/smackx/workgroup/settings/OfflineSettings.java b/src/org/jivesoftware/smackx/workgroup/settings/OfflineSettings.java
new file mode 100644
index 0000000..15136fd
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/settings/OfflineSettings.java
@@ -0,0 +1,155 @@
+/**

+ * $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.smackx.workgroup.settings;

+

+import org.jivesoftware.smackx.workgroup.util.ModelUtil;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+public class OfflineSettings extends IQ {

+    private String redirectURL;

+

+    private String offlineText;

+    private String emailAddress;

+    private String subject;

+

+    public String getRedirectURL() {

+        if (!ModelUtil.hasLength(redirectURL)) {

+            return "";

+        }

+        return redirectURL;

+    }

+

+    public void setRedirectURL(String redirectURL) {

+        this.redirectURL = redirectURL;

+    }

+

+    public String getOfflineText() {

+        if (!ModelUtil.hasLength(offlineText)) {

+            return "";

+        }

+        return offlineText;

+    }

+

+    public void setOfflineText(String offlineText) {

+        this.offlineText = offlineText;

+    }

+

+    public String getEmailAddress() {

+        if (!ModelUtil.hasLength(emailAddress)) {

+            return "";

+        }

+        return emailAddress;

+    }

+

+    public void setEmailAddress(String emailAddress) {

+        this.emailAddress = emailAddress;

+    }

+

+    public String getSubject() {

+        if (!ModelUtil.hasLength(subject)) {

+            return "";

+        }

+        return subject;

+    }

+

+    public void setSubject(String subject) {

+        this.subject = subject;

+    }

+

+    public boolean redirects() {

+        return (ModelUtil.hasLength(getRedirectURL()));

+    }

+

+    public boolean isConfigured(){

+        return ModelUtil.hasLength(getEmailAddress()) &&

+               ModelUtil.hasLength(getSubject()) &&

+               ModelUtil.hasLength(getOfflineText());

+    }

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "offline-settings";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=");

+        buf.append('"');

+        buf.append(NAMESPACE);

+        buf.append('"');

+        buf.append("></").append(ELEMENT_NAME).append("> ");

+        return buf.toString();

+    }

+

+

+    /**

+     * Packet extension provider for AgentStatusRequest packets.

+     */

+    public static class InternalProvider implements IQProvider {

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            if (parser.getEventType() != XmlPullParser.START_TAG) {

+                throw new IllegalStateException("Parser not in proper position, or bad XML.");

+            }

+

+            OfflineSettings offlineSettings = new OfflineSettings();

+

+            boolean done = false;

+            String redirectPage = null;

+            String subject = null;

+            String offlineText = null;

+            String emailAddress = null;

+

+            while (!done) {

+                int eventType = parser.next();

+                if ((eventType == XmlPullParser.START_TAG) && ("redirectPage".equals(parser.getName()))) {

+                    redirectPage = parser.nextText();

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("subject".equals(parser.getName()))) {

+                    subject = parser.nextText();

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("offlineText".equals(parser.getName()))) {

+                    offlineText = parser.nextText();

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("emailAddress".equals(parser.getName()))) {

+                    emailAddress = parser.nextText();

+                }

+                else if (eventType == XmlPullParser.END_TAG && "offline-settings".equals(parser.getName())) {

+                    done = true;

+                }

+            }

+

+            offlineSettings.setEmailAddress(emailAddress);

+            offlineSettings.setRedirectURL(redirectPage);

+            offlineSettings.setSubject(subject);

+            offlineSettings.setOfflineText(offlineText);

+            return offlineSettings;

+        }

+    }

+}

+

diff --git a/src/org/jivesoftware/smackx/workgroup/settings/SearchSettings.java b/src/org/jivesoftware/smackx/workgroup/settings/SearchSettings.java
new file mode 100644
index 0000000..98d59fc
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/settings/SearchSettings.java
@@ -0,0 +1,112 @@
+/**

+ * 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.smackx.workgroup.settings;

+

+import org.jivesoftware.smackx.workgroup.util.ModelUtil;

+import org.jivesoftware.smack.packet.IQ;

+import org.jivesoftware.smack.provider.IQProvider;

+import org.xmlpull.v1.XmlPullParser;

+

+public class SearchSettings extends IQ {

+    private String forumsLocation;

+    private String kbLocation;

+

+    public boolean isSearchEnabled() {

+        return ModelUtil.hasLength(getForumsLocation()) && ModelUtil.hasLength(getKbLocation());

+    }

+

+    public String getForumsLocation() {

+        return forumsLocation;

+    }

+

+    public void setForumsLocation(String forumsLocation) {

+        this.forumsLocation = forumsLocation;

+    }

+

+    public String getKbLocation() {

+        return kbLocation;

+    }

+

+    public void setKbLocation(String kbLocation) {

+        this.kbLocation = kbLocation;

+    }

+

+    public boolean hasKB(){

+        return ModelUtil.hasLength(getKbLocation());

+    }

+

+    public boolean hasForums(){

+        return ModelUtil.hasLength(getForumsLocation());

+    }

+

+

+    /**

+     * Element name of the packet extension.

+     */

+    public static final String ELEMENT_NAME = "search-settings";

+

+    /**

+     * Namespace of the packet extension.

+     */

+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";

+

+    public String getChildElementXML() {

+        StringBuilder buf = new StringBuilder();

+

+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=");

+        buf.append('"');

+        buf.append(NAMESPACE);

+        buf.append('"');

+        buf.append("></").append(ELEMENT_NAME).append("> ");

+        return buf.toString();

+    }

+

+

+    /**

+     * Packet extension provider for AgentStatusRequest packets.

+     */

+    public static class InternalProvider implements IQProvider {

+

+        public IQ parseIQ(XmlPullParser parser) throws Exception {

+            if (parser.getEventType() != XmlPullParser.START_TAG) {

+                throw new IllegalStateException("Parser not in proper position, or bad XML.");

+            }

+

+            SearchSettings settings = new SearchSettings();

+

+            boolean done = false;

+            String kb = null;

+            String forums = null;

+

+            while (!done) {

+                int eventType = parser.next();

+                if ((eventType == XmlPullParser.START_TAG) && ("forums".equals(parser.getName()))) {

+                    forums = parser.nextText();

+                }

+                else if ((eventType == XmlPullParser.START_TAG) && ("kb".equals(parser.getName()))) {

+                    kb = parser.nextText();

+                }

+                else if (eventType == XmlPullParser.END_TAG && "search-settings".equals(parser.getName())) {

+                    done = true;

+                }

+            }

+

+            settings.setForumsLocation(forums);

+            settings.setKbLocation(kb);

+            return settings;

+        }

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/settings/SoundSettings.java b/src/org/jivesoftware/smackx/workgroup/settings/SoundSettings.java
new file mode 100644
index 0000000..66bec35
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/settings/SoundSettings.java
@@ -0,0 +1,103 @@
+/**
+ * $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.smackx.workgroup.settings;
+
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.jivesoftware.smack.util.StringUtils;
+import org.xmlpull.v1.XmlPullParser;
+
+public class SoundSettings extends IQ {
+    private String outgoingSound;
+    private String incomingSound;
+
+
+    public void setOutgoingSound(String outgoingSound) {
+        this.outgoingSound = outgoingSound;
+    }
+
+    public void setIncomingSound(String incomingSound) {
+        this.incomingSound = incomingSound;
+    }
+
+    public byte[] getIncomingSoundBytes() {
+        return StringUtils.decodeBase64(incomingSound);
+    }
+
+    public byte[] getOutgoingSoundBytes() {
+        return StringUtils.decodeBase64(outgoingSound);
+    }
+
+
+    /**
+     * Element name of the packet extension.
+     */
+    public static final String ELEMENT_NAME = "sound-settings";
+
+    /**
+     * Namespace of the packet extension.
+     */
+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+
+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=");
+        buf.append('"');
+        buf.append(NAMESPACE);
+        buf.append('"');
+        buf.append("></").append(ELEMENT_NAME).append("> ");
+        return buf.toString();
+    }
+
+
+    /**
+     * Packet extension provider for SoundSetting Packets.
+     */
+    public static class InternalProvider implements IQProvider {
+
+        public IQ parseIQ(XmlPullParser parser) throws Exception {
+            if (parser.getEventType() != XmlPullParser.START_TAG) {
+                throw new IllegalStateException("Parser not in proper position, or bad XML.");
+            }
+
+            SoundSettings soundSettings = new SoundSettings();
+
+            boolean done = false;
+
+
+            while (!done) {
+                int eventType = parser.next();
+                if ((eventType == XmlPullParser.START_TAG) && ("outgoingSound".equals(parser.getName()))) {
+                    soundSettings.setOutgoingSound(parser.nextText());
+                }
+                else if ((eventType == XmlPullParser.START_TAG) && ("incomingSound".equals(parser.getName()))) {
+                    soundSettings.setIncomingSound(parser.nextText());
+                }
+                else if (eventType == XmlPullParser.END_TAG && "sound-settings".equals(parser.getName())) {
+                    done = true;
+                }
+            }
+
+            return soundSettings;
+        }
+    }
+}
+
diff --git a/src/org/jivesoftware/smackx/workgroup/settings/WorkgroupProperties.java b/src/org/jivesoftware/smackx/workgroup/settings/WorkgroupProperties.java
new file mode 100644
index 0000000..8e405bb
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/settings/WorkgroupProperties.java
@@ -0,0 +1,125 @@
+/**
+ * $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.smackx.workgroup.settings;
+
+import org.jivesoftware.smackx.workgroup.util.ModelUtil;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.provider.IQProvider;
+import org.xmlpull.v1.XmlPullParser;
+
+public class WorkgroupProperties extends IQ {
+
+    private boolean authRequired;
+    private String email;
+    private String fullName;
+    private String jid;
+
+    public boolean isAuthRequired() {
+        return authRequired;
+    }
+
+    public void setAuthRequired(boolean authRequired) {
+        this.authRequired = authRequired;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getFullName() {
+        return fullName;
+    }
+
+    public void setFullName(String fullName) {
+        this.fullName = fullName;
+    }
+
+    public String getJid() {
+        return jid;
+    }
+
+    public void setJid(String jid) {
+        this.jid = jid;
+    }
+
+
+    /**
+     * Element name of the packet extension.
+     */
+    public static final String ELEMENT_NAME = "workgroup-properties";
+
+    /**
+     * Namespace of the packet extension.
+     */
+    public static final String NAMESPACE = "http://jivesoftware.com/protocol/workgroup";
+
+    public String getChildElementXML() {
+        StringBuilder buf = new StringBuilder();
+
+        buf.append("<").append(ELEMENT_NAME).append(" xmlns=");
+        buf.append('"');
+        buf.append(NAMESPACE);
+        buf.append('"');
+        if (ModelUtil.hasLength(getJid())) {
+            buf.append("jid=\"" + getJid() + "\" ");
+        }
+        buf.append("></").append(ELEMENT_NAME).append("> ");
+        return buf.toString();
+    }
+
+    /**
+     * Packet extension provider for SoundSetting Packets.
+     */
+    public static class InternalProvider implements IQProvider {
+
+        public IQ parseIQ(XmlPullParser parser) throws Exception {
+            if (parser.getEventType() != XmlPullParser.START_TAG) {
+                throw new IllegalStateException("Parser not in proper position, or bad XML.");
+            }
+
+            WorkgroupProperties props = new WorkgroupProperties();
+
+            boolean done = false;
+
+
+            while (!done) {
+                int eventType = parser.next();
+                if ((eventType == XmlPullParser.START_TAG) && ("authRequired".equals(parser.getName()))) {
+                    props.setAuthRequired(new Boolean(parser.nextText()).booleanValue());
+                }
+                else if ((eventType == XmlPullParser.START_TAG) && ("email".equals(parser.getName()))) {
+                    props.setEmail(parser.nextText());
+                }
+                else if ((eventType == XmlPullParser.START_TAG) && ("name".equals(parser.getName()))) {
+                    props.setFullName(parser.nextText());
+                }
+                else if (eventType == XmlPullParser.END_TAG && "workgroup-properties".equals(parser.getName())) {
+                    done = true;
+                }
+            }
+
+            return props;
+        }
+    }
+}
diff --git a/src/org/jivesoftware/smackx/workgroup/user/QueueListener.java b/src/org/jivesoftware/smackx/workgroup/user/QueueListener.java
new file mode 100644
index 0000000..fa3e6a6
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/user/QueueListener.java
@@ -0,0 +1,55 @@
+/**

+ * $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.smackx.workgroup.user;

+

+/**

+ * Listener interface for those that wish to be notified of workgroup queue events.

+ *

+ * @see Workgroup#addQueueListener(QueueListener)

+ * @author loki der quaeler

+ */

+public interface QueueListener {

+

+    /**

+     * The user joined the workgroup queue.

+     */

+    public void joinedQueue();

+

+    /**

+     * The user departed the workgroup queue.

+     */

+    public void departedQueue();

+

+    /**

+     * The user's queue position has been updated to a new value.

+     *

+     * @param currentPosition the user's current position in the queue.

+     */

+    public void queuePositionUpdated(int currentPosition);

+

+    /**

+     * The user's estimated remaining wait time in the queue has been updated.

+     *

+     * @param secondsRemaining the estimated number of seconds remaining until the

+     *      the user is routed to the agent.

+     */

+    public void queueWaitTimeUpdated(int secondsRemaining);

+

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/user/Workgroup.java b/src/org/jivesoftware/smackx/workgroup/user/Workgroup.java
new file mode 100644
index 0000000..237337f
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/user/Workgroup.java
@@ -0,0 +1,868 @@
+/**

+ * 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.smackx.workgroup.user;

+

+import org.jivesoftware.smackx.workgroup.MetaData;

+import org.jivesoftware.smackx.workgroup.WorkgroupInvitation;

+import org.jivesoftware.smackx.workgroup.WorkgroupInvitationListener;

+import org.jivesoftware.smackx.workgroup.ext.forms.WorkgroupForm;

+import org.jivesoftware.smackx.workgroup.packet.DepartQueuePacket;

+import org.jivesoftware.smackx.workgroup.packet.QueueUpdate;

+import org.jivesoftware.smackx.workgroup.packet.SessionID;

+import org.jivesoftware.smackx.workgroup.packet.UserID;

+import org.jivesoftware.smackx.workgroup.settings.*;

+import org.jivesoftware.smack.*;

+import org.jivesoftware.smack.filter.*;

+import org.jivesoftware.smack.packet.*;

+import org.jivesoftware.smack.util.StringUtils;

+import org.jivesoftware.smackx.Form;

+import org.jivesoftware.smackx.FormField;

+import org.jivesoftware.smackx.ServiceDiscoveryManager;

+import org.jivesoftware.smackx.muc.MultiUserChat;

+import org.jivesoftware.smackx.packet.DataForm;

+import org.jivesoftware.smackx.packet.DiscoverInfo;

+import org.jivesoftware.smackx.packet.MUCUser;

+

+import java.util.ArrayList;

+import java.util.Iterator;

+import java.util.List;

+import java.util.Map;

+

+/**

+ * Provides workgroup services for users. Users can join the workgroup queue, depart the

+ * queue, find status information about their placement in the queue, and register to

+ * be notified when they are routed to an agent.<p>

+ * <p/>

+ * This class only provides a users perspective into a workgroup and is not intended

+ * for use by agents.

+ *

+ * @author Matt Tucker

+ * @author Derek DeMoro

+ */

+public class Workgroup {

+

+    private String workgroupJID;

+    private Connection connection;

+    private boolean inQueue;

+    private List<WorkgroupInvitationListener> invitationListeners;

+    private List<QueueListener> queueListeners;

+

+    private int queuePosition = -1;

+    private int queueRemainingTime = -1;

+

+    /**

+     * Creates a new workgroup instance using the specified workgroup JID

+     * (eg support@workgroup.example.com) and XMPP connection. The connection must have

+     * undergone a successful login before being used to construct an instance of

+     * this class.

+     *

+     * @param workgroupJID the JID of the workgroup.

+     * @param connection   an XMPP connection which must have already undergone a

+     *                     successful login.

+     */

+    public Workgroup(String workgroupJID, Connection connection) {

+        // Login must have been done before passing in connection.

+        if (!connection.isAuthenticated()) {

+            throw new IllegalStateException("Must login to server before creating workgroup.");

+        }

+

+        this.workgroupJID = workgroupJID;

+        this.connection = connection;

+        inQueue = false;

+        invitationListeners = new ArrayList<WorkgroupInvitationListener>();

+        queueListeners = new ArrayList<QueueListener>();

+

+        // Register as a queue listener for internal usage by this instance.

+        addQueueListener(new QueueListener() {

+            public void joinedQueue() {

+                inQueue = true;

+            }

+

+            public void departedQueue() {

+                inQueue = false;

+                queuePosition = -1;

+                queueRemainingTime = -1;

+            }

+

+            public void queuePositionUpdated(int currentPosition) {

+                queuePosition = currentPosition;

+            }

+

+            public void queueWaitTimeUpdated(int secondsRemaining) {

+                queueRemainingTime = secondsRemaining;

+            }

+        });

+

+        /**

+         * Internal handling of an invitation.Recieving an invitation removes the user from the queue.

+         */

+        MultiUserChat.addInvitationListener(connection,

+                new org.jivesoftware.smackx.muc.InvitationListener() {

+                    public void invitationReceived(Connection conn, String room, String inviter,

+                                                   String reason, String password, Message message) {

+                        inQueue = false;

+                        queuePosition = -1;

+                        queueRemainingTime = -1;

+                    }

+                });

+

+        // Register a packet listener for all the messages sent to this client.

+        PacketFilter typeFilter = new PacketTypeFilter(Message.class);

+

+        connection.addPacketListener(new PacketListener() {

+            public void processPacket(Packet packet) {

+                handlePacket(packet);

+            }

+        }, typeFilter);

+    }

+

+    /**

+     * Returns the name of this workgroup (eg support@example.com).

+     *

+     * @return the name of the workgroup.

+     */

+    public String getWorkgroupJID() {

+        return workgroupJID;

+    }

+

+    /**

+     * Returns true if the user is currently waiting in the workgroup queue.

+     *

+     * @return true if currently waiting in the queue.

+     */

+    public boolean isInQueue() {

+        return inQueue;

+    }

+

+    /**

+     * Returns true if the workgroup is available for receiving new requests. The workgroup will be

+     * available only when agents are available for this workgroup.

+     *

+     * @return true if the workgroup is available for receiving new requests.

+     */

+    public boolean isAvailable() {

+        Presence directedPresence = new Presence(Presence.Type.available);

+        directedPresence.setTo(workgroupJID);

+        PacketFilter typeFilter = new PacketTypeFilter(Presence.class);

+        PacketFilter fromFilter = new FromContainsFilter(workgroupJID);

+        PacketCollector collector = connection.createPacketCollector(new AndFilter(fromFilter,

+                typeFilter));

+

+        connection.sendPacket(directedPresence);

+

+        Presence response = (Presence)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            return false;

+        }

+        else if (response.getError() != null) {

+            return false;

+        }

+        else {

+            return Presence.Type.available == response.getType();

+        }

+    }

+

+    /**

+     * Returns the users current position in the workgroup queue. A value of 0 means

+     * the user is next in line to be routed; therefore, if the queue position

+     * is being displayed to the end user it is usually a good idea to add 1 to

+     * the value this method returns before display. If the user is not currently

+     * waiting in the workgroup, or no queue position information is available, -1

+     * will be returned.

+     *

+     * @return the user's current position in the workgroup queue, or -1 if the

+     *         position isn't available or if the user isn't in the queue.

+     */

+    public int getQueuePosition() {

+        return queuePosition;

+    }

+

+    /**

+     * Returns the estimated time (in seconds) that the user has to left wait in

+     * the workgroup queue before being routed. If the user is not currently waiting

+     * int he workgroup, or no queue time information is available, -1 will be

+     * returned.

+     *

+     * @return the estimated time remaining (in seconds) that the user has to

+     *         wait inthe workgroupu queue, or -1 if time information isn't available

+     *         or if the user isn't int the queue.

+     */

+    public int getQueueRemainingTime() {

+        return queueRemainingTime;

+    }

+

+    /**

+     * Joins the workgroup queue to wait to be routed to an agent. After joining

+     * the queue, queue status events will be sent to indicate the user's position and

+     * estimated time left in the queue. Once joining the queue, there are three ways

+     * the user can leave the queue: <ul>

+     * <p/>

+     * <li>The user is routed to an agent, which triggers a GroupChat invitation.

+     * <li>The user asks to leave the queue by calling the {@link #departQueue} method.

+     * <li>A server error occurs, or an administrator explicitly removes the user

+     * from the queue.

+     * </ul>

+     * <p/>

+     * A user cannot request to join the queue again if already in the queue. Therefore,

+     * this method will throw an IllegalStateException if the user is already in the queue.<p>

+     * <p/>

+     * Some servers may be configured to require certain meta-data in order to

+     * join the queue. In that case, the {@link #joinQueue(Form)} method should be

+     * used instead of this method so that meta-data may be passed in.<p>

+     * <p/>

+     * The server tracks the conversations that a user has with agents over time. By

+     * default, that tracking is done using the user's JID. However, this is not always

+     * possible. For example, when the user is logged in anonymously using a web client.

+     * In that case the user ID might be a randomly generated value put into a persistent

+     * cookie or a username obtained via the session. A userID can be explicitly

+     * passed in by using the {@link #joinQueue(Form, String)} method. When specified,

+     * that userID will be used instead of the user's JID to track conversations. The

+     * server will ignore a manually specified userID if the user's connection to the server

+     * is not anonymous.

+     *

+     * @throws XMPPException if an error occured joining the queue. An error may indicate

+     *                       that a connection failure occured or that the server explicitly rejected the

+     *                       request to join the queue.

+     */

+    public void joinQueue() throws XMPPException {

+        joinQueue(null);

+    }

+

+    /**

+     * Joins the workgroup queue to wait to be routed to an agent. After joining

+     * the queue, queue status events will be sent to indicate the user's position and

+     * estimated time left in the queue. Once joining the queue, there are three ways

+     * the user can leave the queue: <ul>

+     * <p/>

+     * <li>The user is routed to an agent, which triggers a GroupChat invitation.

+     * <li>The user asks to leave the queue by calling the {@link #departQueue} method.

+     * <li>A server error occurs, or an administrator explicitly removes the user

+     * from the queue.

+     * </ul>

+     * <p/>

+     * A user cannot request to join the queue again if already in the queue. Therefore,

+     * this method will throw an IllegalStateException if the user is already in the queue.<p>

+     * <p/>

+     * Some servers may be configured to require certain meta-data in order to

+     * join the queue.<p>

+     * <p/>

+     * The server tracks the conversations that a user has with agents over time. By

+     * default, that tracking is done using the user's JID. However, this is not always

+     * possible. For example, when the user is logged in anonymously using a web client.

+     * In that case the user ID might be a randomly generated value put into a persistent

+     * cookie or a username obtained via the session. A userID can be explicitly

+     * passed in by using the {@link #joinQueue(Form, String)} method. When specified,

+     * that userID will be used instead of the user's JID to track conversations. The

+     * server will ignore a manually specified userID if the user's connection to the server

+     * is not anonymous.

+     *

+     * @param answerForm the completed form the send for the join request.

+     * @throws XMPPException if an error occured joining the queue. An error may indicate

+     *                       that a connection failure occured or that the server explicitly rejected the

+     *                       request to join the queue.

+     */

+    public void joinQueue(Form answerForm) throws XMPPException {

+        joinQueue(answerForm, null);

+    }

+

+    /**

+     * <p>Joins the workgroup queue to wait to be routed to an agent. After joining

+     * the queue, queue status events will be sent to indicate the user's position and

+     * estimated time left in the queue. Once joining the queue, there are three ways

+     * the user can leave the queue: <ul>

+     * <p/>

+     * <li>The user is routed to an agent, which triggers a GroupChat invitation.

+     * <li>The user asks to leave the queue by calling the {@link #departQueue} method.

+     * <li>A server error occurs, or an administrator explicitly removes the user

+     * from the queue.

+     * </ul>

+     * <p/>

+     * A user cannot request to join the queue again if already in the queue. Therefore,

+     * this method will throw an IllegalStateException if the user is already in the queue.<p>

+     * <p/>

+     * Some servers may be configured to require certain meta-data in order to

+     * join the queue.<p>

+     * <p/>

+     * The server tracks the conversations that a user has with agents over time. By

+     * default, that tracking is done using the user's JID. However, this is not always

+     * possible. For example, when the user is logged in anonymously using a web client.

+     * In that case the user ID might be a randomly generated value put into a persistent

+     * cookie or a username obtained via the session. When specified, that userID will

+     * be used instead of the user's JID to track conversations. The server will ignore a

+     * manually specified userID if the user's connection to the server is not anonymous.

+     *

+     * @param answerForm the completed form associated with the join reqest.

+     * @param userID     String that represents the ID of the user when using anonymous sessions

+     *                   or <tt>null</tt> if a userID should not be used.

+     * @throws XMPPException if an error occured joining the queue. An error may indicate

+     *                       that a connection failure occured or that the server explicitly rejected the

+     *                       request to join the queue.

+     */

+    public void joinQueue(Form answerForm, String userID) throws XMPPException {

+        // If already in the queue ignore the join request.

+        if (inQueue) {

+            throw new IllegalStateException("Already in queue " + workgroupJID);

+        }

+

+        JoinQueuePacket joinPacket = new JoinQueuePacket(workgroupJID, answerForm, userID);

+

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(joinPacket.getPacketID()));

+

+        this.connection.sendPacket(joinPacket);

+

+        IQ response = (IQ)collector.nextResult(10000);

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from the server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+

+        // Notify listeners that we've joined the queue.

+        fireQueueJoinedEvent();

+    }

+

+    /**

+     * <p>Joins the workgroup queue to wait to be routed to an agent. After joining

+     * the queue, queue status events will be sent to indicate the user's position and

+     * estimated time left in the queue. Once joining the queue, there are three ways

+     * the user can leave the queue: <ul>

+     * <p/>

+     * <li>The user is routed to an agent, which triggers a GroupChat invitation.

+     * <li>The user asks to leave the queue by calling the {@link #departQueue} method.

+     * <li>A server error occurs, or an administrator explicitly removes the user

+     * from the queue.

+     * </ul>

+     * <p/>

+     * A user cannot request to join the queue again if already in the queue. Therefore,

+     * this method will throw an IllegalStateException if the user is already in the queue.<p>

+     * <p/>

+     * Some servers may be configured to require certain meta-data in order to

+     * join the queue.<p>

+     * <p/>

+     * The server tracks the conversations that a user has with agents over time. By

+     * default, that tracking is done using the user's JID. However, this is not always

+     * possible. For example, when the user is logged in anonymously using a web client.

+     * In that case the user ID might be a randomly generated value put into a persistent

+     * cookie or a username obtained via the session. When specified, that userID will

+     * be used instead of the user's JID to track conversations. The server will ignore a

+     * manually specified userID if the user's connection to the server is not anonymous.

+     *

+     * @param metadata metadata to create a dataform from.

+     * @param userID   String that represents the ID of the user when using anonymous sessions

+     *                 or <tt>null</tt> if a userID should not be used.

+     * @throws XMPPException if an error occured joining the queue. An error may indicate

+     *                       that a connection failure occured or that the server explicitly rejected the

+     *                       request to join the queue.

+     */

+    public void joinQueue(Map<String,Object> metadata, String userID) throws XMPPException {

+        // If already in the queue ignore the join request.

+        if (inQueue) {

+            throw new IllegalStateException("Already in queue " + workgroupJID);

+        }

+

+        // Build dataform from metadata

+        Form form = new Form(Form.TYPE_SUBMIT);

+        Iterator<String> iter = metadata.keySet().iterator();

+        while (iter.hasNext()) {

+            String name = iter.next();

+            String value = metadata.get(name).toString();

+

+            String escapedName = StringUtils.escapeForXML(name);

+            String escapedValue = StringUtils.escapeForXML(value);

+

+            FormField field = new FormField(escapedName);

+            field.setType(FormField.TYPE_TEXT_SINGLE);

+            form.addField(field);

+            form.setAnswer(escapedName, escapedValue);

+        }

+        joinQueue(form, userID);

+    }

+

+    /**

+     * Departs the workgroup queue. If the user is not currently in the queue, this

+     * method will do nothing.<p>

+     * <p/>

+     * Normally, the user would not manually leave the queue. However, they may wish to

+     * under certain circumstances -- for example, if they no longer wish to be routed

+     * to an agent because they've been waiting too long.

+     *

+     * @throws XMPPException if an error occured trying to send the depart queue

+     *                       request to the server.

+     */

+    public void departQueue() throws XMPPException {

+        // If not in the queue ignore the depart request.

+        if (!inQueue) {

+            return;

+        }

+

+        DepartQueuePacket departPacket = new DepartQueuePacket(this.workgroupJID);

+        PacketCollector collector = this.connection.createPacketCollector(new PacketIDFilter(departPacket.getPacketID()));

+

+        connection.sendPacket(departPacket);

+

+        IQ response = (IQ)collector.nextResult(5000);

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from the server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+

+        // Notify listeners that we're no longer in the queue.

+        fireQueueDepartedEvent();

+    }

+

+    /**

+     * Adds a queue listener that will be notified of queue events for the user

+     * that created this Workgroup instance.

+     *

+     * @param queueListener the queue listener.

+     */

+    public void addQueueListener(QueueListener queueListener) {

+        synchronized (queueListeners) {

+            if (!queueListeners.contains(queueListener)) {

+                queueListeners.add(queueListener);

+            }

+        }

+    }

+

+    /**

+     * Removes a queue listener.

+     *

+     * @param queueListener the queue listener.

+     */

+    public void removeQueueListener(QueueListener queueListener) {

+        synchronized (queueListeners) {

+            queueListeners.remove(queueListener);

+        }

+    }

+

+    /**

+     * Adds an invitation listener that will be notified of groupchat invitations

+     * from the workgroup for the the user that created this Workgroup instance.

+     *

+     * @param invitationListener the invitation listener.

+     */

+    public void addInvitationListener(WorkgroupInvitationListener invitationListener) {

+        synchronized (invitationListeners) {

+            if (!invitationListeners.contains(invitationListener)) {

+                invitationListeners.add(invitationListener);

+            }

+        }

+    }

+

+    /**

+     * Removes an invitation listener.

+     *

+     * @param invitationListener the invitation listener.

+     */

+    public void removeQueueListener(WorkgroupInvitationListener invitationListener) {

+        synchronized (invitationListeners) {

+            invitationListeners.remove(invitationListener);

+        }

+    }

+

+    private void fireInvitationEvent(WorkgroupInvitation invitation) {

+        synchronized (invitationListeners) {

+            for (Iterator<WorkgroupInvitationListener> i = invitationListeners.iterator(); i.hasNext();) {

+                WorkgroupInvitationListener listener = i.next();

+                listener.invitationReceived(invitation);

+            }

+        }

+    }

+

+    private void fireQueueJoinedEvent() {

+        synchronized (queueListeners) {

+            for (Iterator<QueueListener> i = queueListeners.iterator(); i.hasNext();) {

+                QueueListener listener = i.next();

+                listener.joinedQueue();

+            }

+        }

+    }

+

+    private void fireQueueDepartedEvent() {

+        synchronized (queueListeners) {

+            for (Iterator<QueueListener> i = queueListeners.iterator(); i.hasNext();) {

+                QueueListener listener = i.next();

+                listener.departedQueue();

+            }

+        }

+    }

+

+    private void fireQueuePositionEvent(int currentPosition) {

+        synchronized (queueListeners) {

+            for (Iterator<QueueListener> i = queueListeners.iterator(); i.hasNext();) {

+                QueueListener listener = i.next();

+                listener.queuePositionUpdated(currentPosition);

+            }

+        }

+    }

+

+    private void fireQueueTimeEvent(int secondsRemaining) {

+        synchronized (queueListeners) {

+            for (Iterator<QueueListener> i = queueListeners.iterator(); i.hasNext();) {

+                QueueListener listener = i.next();

+                listener.queueWaitTimeUpdated(secondsRemaining);

+            }

+        }

+    }

+

+    // PacketListener Implementation.

+

+    private void handlePacket(Packet packet) {

+        if (packet instanceof Message) {

+            Message msg = (Message)packet;

+            // Check to see if the user left the queue.

+            PacketExtension pe = msg.getExtension("depart-queue", "http://jabber.org/protocol/workgroup");

+            PacketExtension queueStatus = msg.getExtension("queue-status", "http://jabber.org/protocol/workgroup");

+

+            if (pe != null) {

+                fireQueueDepartedEvent();

+            }

+            else if (queueStatus != null) {

+                QueueUpdate queueUpdate = (QueueUpdate)queueStatus;

+                if (queueUpdate.getPosition() != -1) {

+                    fireQueuePositionEvent(queueUpdate.getPosition());

+                }

+                if (queueUpdate.getRemaingTime() != -1) {

+                    fireQueueTimeEvent(queueUpdate.getRemaingTime());

+                }

+            }

+

+            else {

+                // Check if a room invitation was sent and if the sender is the workgroup

+                MUCUser mucUser = (MUCUser)msg.getExtension("x", "http://jabber.org/protocol/muc#user");

+                MUCUser.Invite invite = mucUser != null ? mucUser.getInvite() : null;

+                if (invite != null && workgroupJID.equals(invite.getFrom())) {

+                    String sessionID = null;

+                    Map<String, List<String>> metaData = null;

+

+                    pe = msg.getExtension(SessionID.ELEMENT_NAME,

+                            SessionID.NAMESPACE);

+                    if (pe != null) {

+                        sessionID = ((SessionID)pe).getSessionID();

+                    }

+

+                    pe = msg.getExtension(MetaData.ELEMENT_NAME,

+                            MetaData.NAMESPACE);

+                    if (pe != null) {

+                        metaData = ((MetaData)pe).getMetaData();

+                    }

+

+                    WorkgroupInvitation inv = new WorkgroupInvitation(connection.getUser(), msg.getFrom(),

+                            workgroupJID, sessionID, msg.getBody(),

+                            msg.getFrom(), metaData);

+

+                    fireInvitationEvent(inv);

+                }

+            }

+        }

+    }

+

+    /**

+     * IQ packet to request joining the workgroup queue.

+     */

+    private class JoinQueuePacket extends IQ {

+

+        private String userID = null;

+        private DataForm form;

+

+        public JoinQueuePacket(String workgroup, Form answerForm, String userID) {

+            this.userID = userID;

+

+            setTo(workgroup);

+            setType(IQ.Type.SET);

+

+            form = answerForm.getDataFormToSend();

+            addExtension(form);

+        }

+

+        public String getChildElementXML() {

+            StringBuilder buf = new StringBuilder();

+

+            buf.append("<join-queue xmlns=\"http://jabber.org/protocol/workgroup\">");

+            buf.append("<queue-notifications/>");

+            // Add the user unique identification if the session is anonymous

+            if (connection.isAnonymous()) {

+                buf.append(new UserID(userID).toXML());

+            }

+

+            // Append data form text

+            buf.append(form.toXML());

+

+            buf.append("</join-queue>");

+

+            return buf.toString();

+        }

+    }

+

+    /**

+     * Returns a single chat setting based on it's identified key.

+     *

+     * @param key the key to find.

+     * @return the ChatSetting if found, otherwise false.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public ChatSetting getChatSetting(String key) throws XMPPException {

+        ChatSettings chatSettings = getChatSettings(key, -1);

+        return chatSettings.getFirstEntry();

+    }

+

+    /**

+     * Returns ChatSettings based on type.

+     *

+     * @param type the type of ChatSettings to return.

+     * @return the ChatSettings of given type, otherwise null.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public ChatSettings getChatSettings(int type) throws XMPPException {

+        return getChatSettings(null, type);

+    }

+

+    /**

+     * Returns all ChatSettings.

+     *

+     * @return all ChatSettings of a given workgroup.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public ChatSettings getChatSettings() throws XMPPException {

+        return getChatSettings(null, -1);

+    }

+

+

+    /**

+     * Asks the workgroup for it's Chat Settings.

+     *

+     * @return key specify a key to retrieve only that settings. Otherwise for all settings, key should be null.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    private ChatSettings getChatSettings(String key, int type) throws XMPPException {

+        ChatSettings request = new ChatSettings();

+        if (key != null) {

+            request.setKey(key);

+        }

+        if (type != -1) {

+            request.setType(type);

+        }

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+

+        ChatSettings response = (ChatSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+    /**

+     * The workgroup service may be configured to send email. This queries the Workgroup Service

+     * to see if the email service has been configured and is available.

+     *

+     * @return true if the email service is available, otherwise return false.

+     */

+    public boolean isEmailAvailable() {

+        ServiceDiscoveryManager discoManager = ServiceDiscoveryManager.getInstanceFor(connection);

+

+        try {

+            String workgroupService = StringUtils.parseServer(workgroupJID);

+            DiscoverInfo infoResult = discoManager.discoverInfo(workgroupService);

+            return infoResult.containsFeature("jive:email:provider");

+        }

+        catch (XMPPException e) {

+            return false;

+        }

+    }

+

+    /**

+     * Asks the workgroup for it's Offline Settings.

+     *

+     * @return offlineSettings the offline settings for this workgroup.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public OfflineSettings getOfflineSettings() throws XMPPException {

+        OfflineSettings request = new OfflineSettings();

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+

+        OfflineSettings response = (OfflineSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+    /**

+     * Asks the workgroup for it's Sound Settings.

+     *

+     * @return soundSettings the sound settings for the specified workgroup.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public SoundSettings getSoundSettings() throws XMPPException {

+        SoundSettings request = new SoundSettings();

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+

+        SoundSettings response = (SoundSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+    /**

+     * Asks the workgroup for it's Properties

+     *

+     * @return the WorkgroupProperties for the specified workgroup.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public WorkgroupProperties getWorkgroupProperties() throws XMPPException {

+        WorkgroupProperties request = new WorkgroupProperties();

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+

+        WorkgroupProperties response = (WorkgroupProperties)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+    /**

+     * Asks the workgroup for it's Properties

+     *

+     * @param jid the jid of the user who's information you would like the workgroup to retreive.

+     * @return the WorkgroupProperties for the specified workgroup.

+     * @throws XMPPException if an error occurs while getting information from the server.

+     */

+    public WorkgroupProperties getWorkgroupProperties(String jid) throws XMPPException {

+        WorkgroupProperties request = new WorkgroupProperties();

+        request.setJid(jid);

+        request.setType(IQ.Type.GET);

+        request.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));

+        connection.sendPacket(request);

+

+

+        WorkgroupProperties response = (WorkgroupProperties)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return response;

+    }

+

+

+    /**

+     * Returns the Form to use for all clients of a workgroup. It is unlikely that the server

+     * will change the form (without a restart) so it is safe to keep the returned form

+     * for future submissions.

+     *

+     * @return the Form to use for searching transcripts.

+     * @throws XMPPException if an error occurs while sending the request to the server.

+     */

+    public Form getWorkgroupForm() throws XMPPException {

+        WorkgroupForm workgroupForm = new WorkgroupForm();

+        workgroupForm.setType(IQ.Type.GET);

+        workgroupForm.setTo(workgroupJID);

+

+        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(workgroupForm.getPacketID()));

+        connection.sendPacket(workgroupForm);

+

+        WorkgroupForm response = (WorkgroupForm)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

+

+        // Cancel the collector.

+        collector.cancel();

+        if (response == null) {

+            throw new XMPPException("No response from server on status set.");

+        }

+        if (response.getError() != null) {

+            throw new XMPPException(response.getError());

+        }

+        return Form.getFormFrom(response);

+    }

+

+    /*

+    public static void main(String args[]) throws Exception {

+        Connection con = new XMPPConnection("anteros");

+        con.connect();

+        con.loginAnonymously();

+

+        Workgroup workgroup = new Workgroup("demo@workgroup.anteros", con);

+        WorkgroupProperties props = workgroup.getWorkgroupProperties("derek@anteros.com");

+

+        System.out.print(props);

+        con.disconnect();

+    }

+    */

+

+

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/util/ListenerEventDispatcher.java b/src/org/jivesoftware/smackx/workgroup/util/ListenerEventDispatcher.java
new file mode 100644
index 0000000..533b9a1
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/util/ListenerEventDispatcher.java
@@ -0,0 +1,132 @@
+/**

+ * 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.smackx.workgroup.util;

+

+import java.lang.reflect.Method;

+import java.util.ArrayList;

+import java.util.ListIterator;

+

+/**

+ * This class is a very flexible event dispatcher which implements Runnable so that it can

+ * dispatch easily from a newly created thread. The usage of this in code is more or less:

+ * create a new instance of this class, use addListenerTriplet to add as many listeners

+ * as desired to be messaged, create a new Thread using the instance of this class created

+ * as the argument to the constructor, start the new Thread instance.<p>

+ *

+ * Also, this is intended to be used to message methods that either return void, or have

+ * a return which the developer using this class is uninterested in receiving.

+ *

+ * @author loki der quaeler

+ */

+public class ListenerEventDispatcher

+    implements Runnable {

+

+    protected transient ArrayList<TripletContainer> triplets;

+

+    protected transient boolean hasFinishedDispatching;

+    protected transient boolean isRunning;

+

+    public ListenerEventDispatcher () {

+        super();

+

+        this.triplets = new ArrayList<TripletContainer>();

+

+        this.hasFinishedDispatching = false;

+        this.isRunning = false;

+    }

+

+    /**

+     * Add a listener triplet - the instance of the listener to be messaged, the Method on which

+     *  the listener should be messaged, and the Object array of arguments to be supplied to the

+     *  Method. No attempts are made to determine whether this triplet was already added.<br>

+     *

+     * Messages are dispatched in the order in which they're added via this method; so if triplet

+     *  X is added after triplet Z, then triplet Z will undergo messaging prior to triplet X.<br>

+     *

+     * This method should not be called once the owning Thread instance has been started; if it

+     *  is called, the triplet will not be added to the messaging queue.<br>

+     *

+     * @param listenerInstance the instance of the listener to receive the associated notification

+     * @param listenerMethod the Method instance representing the method through which notification

+     *                          will occur

+     * @param methodArguments the arguments supplied to the notification method

+     */

+    public void addListenerTriplet(Object listenerInstance, Method listenerMethod,

+            Object[] methodArguments)

+    {

+        if (!this.isRunning) {

+            this.triplets.add(new TripletContainer(listenerInstance, listenerMethod,

+                    methodArguments));

+        }

+    }

+

+    /**

+     * @return whether this instance has finished dispatching its messages

+     */

+    public boolean hasFinished() {

+        return this.hasFinishedDispatching;

+    }

+

+    public void run() {

+        ListIterator<TripletContainer> li = null;

+

+        this.isRunning = true;

+

+        li = this.triplets.listIterator();

+        while (li.hasNext()) {

+            TripletContainer tc = li.next();

+

+            try {

+                tc.getListenerMethod().invoke(tc.getListenerInstance(), tc.getMethodArguments());

+            } catch (Exception e) {

+                System.err.println("Exception dispatching an event: " + e);

+

+                e.printStackTrace();

+            }

+        }

+

+        this.hasFinishedDispatching = true;

+    }

+

+

+    protected class TripletContainer {

+

+        protected Object listenerInstance;

+        protected Method listenerMethod;

+        protected Object[] methodArguments;

+

+        protected TripletContainer (Object inst, Method meth, Object[] args) {

+            super();

+

+            this.listenerInstance = inst;

+            this.listenerMethod = meth;

+            this.methodArguments = args;

+        }

+

+        protected Object getListenerInstance() {

+            return this.listenerInstance;

+        }

+

+        protected Method getListenerMethod() {

+            return this.listenerMethod;

+        }

+

+        protected Object[] getMethodArguments() {

+            return this.methodArguments;

+        }

+    }

+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java b/src/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java
new file mode 100644
index 0000000..5be1c1a
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/util/MetaDataUtils.java
@@ -0,0 +1,103 @@
+/**

+ * 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.smackx.workgroup.util;

+

+import org.jivesoftware.smackx.workgroup.MetaData;

+import org.jivesoftware.smack.util.StringUtils;

+import org.xmlpull.v1.XmlPullParser;

+import org.xmlpull.v1.XmlPullParserException;

+

+import java.io.IOException;

+import java.util.*;

+

+/**

+ * Utility class for meta-data parsing and writing.

+ *

+ * @author Matt Tucker

+ */

+public class MetaDataUtils {

+

+    /**

+     * Parses any available meta-data and returns it as a Map of String name/value pairs. The

+     * parser must be positioned at an opening meta-data tag, or the an empty map will be returned.

+     *

+     * @param parser the XML parser positioned at an opening meta-data tag.

+     * @return the meta-data.

+     * @throws XmlPullParserException if an error occurs while parsing the XML.

+     * @throws IOException            if an error occurs while parsing the XML.

+     */

+    public static Map<String, List<String>> parseMetaData(XmlPullParser parser) throws XmlPullParserException, IOException {

+        int eventType = parser.getEventType();

+

+        // If correctly positioned on an opening meta-data tag, parse meta-data.

+        if ((eventType == XmlPullParser.START_TAG)

+                && parser.getName().equals(MetaData.ELEMENT_NAME)

+                && parser.getNamespace().equals(MetaData.NAMESPACE)) {

+            Map<String, List<String>> metaData = new Hashtable<String, List<String>>();

+

+            eventType = parser.nextTag();

+

+            // Keep parsing until we've gotten to end of meta-data.

+            while ((eventType != XmlPullParser.END_TAG)

+                    || (!parser.getName().equals(MetaData.ELEMENT_NAME))) {

+                String name = parser.getAttributeValue(0);

+                String value = parser.nextText();

+

+                if (metaData.containsKey(name)) {

+                    List<String> values = metaData.get(name);

+                    values.add(value);

+                }

+                else {

+                    List<String> values = new ArrayList<String>();

+                    values.add(value);

+                    metaData.put(name, values);

+                }

+

+                eventType = parser.nextTag();

+            }

+

+            return metaData;

+        }

+

+        return Collections.emptyMap();

+    }

+

+    /**

+     * Serializes a Map of String name/value pairs into the meta-data XML format.

+     *

+     * @param metaData the Map of meta-data as Map&lt;String,List&lt;String>>

+     * @return the meta-data values in XML form.

+     */

+    public static String serializeMetaData(Map<String, List<String>> metaData) {

+        StringBuilder buf = new StringBuilder();

+        if (metaData != null && metaData.size() > 0) {

+            buf.append("<metadata xmlns=\"http://jivesoftware.com/protocol/workgroup\">");

+            for (Iterator<String> i = metaData.keySet().iterator(); i.hasNext();) {

+                String key = i.next();

+                List<String> value = metaData.get(key);

+                for (Iterator<String> it = value.iterator(); it.hasNext();) {

+                    String v = it.next();

+                    buf.append("<value name=\"").append(key).append("\">");

+                    buf.append(StringUtils.escapeForXML(v));

+                    buf.append("</value>");

+                }

+            }

+            buf.append("</metadata>");

+        }

+        return buf.toString();

+    }

+}

diff --git a/src/org/jivesoftware/smackx/workgroup/util/ModelUtil.java b/src/org/jivesoftware/smackx/workgroup/util/ModelUtil.java
new file mode 100644
index 0000000..0a4df15
--- /dev/null
+++ b/src/org/jivesoftware/smackx/workgroup/util/ModelUtil.java
@@ -0,0 +1,322 @@
+/**

+ * 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.smackx.workgroup.util;

+

+import java.util.*;

+

+/**

+ * Utility methods frequently used by data classes and design-time

+ * classes.

+ */

+public final class ModelUtil {

+    private ModelUtil() {

+        //  Prevents instantiation.

+    }

+

+    /**

+     * This is a utility method that compares two objects when one or

+     * both of the objects might be <CODE>null</CODE>  The result of

+     * this method is determined as follows:

+     * <OL>

+     * <LI>If <CODE>o1</CODE> and <CODE>o2</CODE> are the same object

+     * according to the <CODE>==</CODE> operator, return

+     * <CODE>true</CODE>.

+     * <LI>Otherwise, if either <CODE>o1</CODE> or <CODE>o2</CODE> is

+     * <CODE>null</CODE>, return <CODE>false</CODE>.

+     * <LI>Otherwise, return <CODE>o1.equals(o2)</CODE>.

+     * </OL>

+     * <p/>

+     * This method produces the exact logically inverted result as the

+     * {@link #areDifferent(Object, Object)} method.<P>

+     * <p/>

+     * For array types, one of the <CODE>equals</CODE> methods in

+     * {@link java.util.Arrays} should be used instead of this method.

+     * Note that arrays with more than one dimension will require some

+     * custom code in order to implement <CODE>equals</CODE> properly.

+     */

+    public static final boolean areEqual(Object o1, Object o2) {

+        if (o1 == o2) {

+            return true;

+        }

+        else if (o1 == null || o2 == null) {

+            return false;

+        }

+        else {

+            return o1.equals(o2);

+        }

+    }

+

+    /**

+     * This is a utility method that compares two Booleans when one or

+     * both of the objects might be <CODE>null</CODE>  The result of

+     * this method is determined as follows:

+     * <OL>

+     * <LI>If <CODE>b1</CODE> and <CODE>b2</CODE> are both TRUE or

+     * neither <CODE>b1</CODE> nor <CODE>b2</CODE> is TRUE,

+     * return <CODE>true</CODE>.

+     * <LI>Otherwise, return <CODE>false</CODE>.

+     * </OL>

+     * <p/>

+     */

+    public static final boolean areBooleansEqual(Boolean b1, Boolean b2) {

+        // !jwetherb treat NULL the same as Boolean.FALSE

+        return (b1 == Boolean.TRUE && b2 == Boolean.TRUE) ||

+                (b1 != Boolean.TRUE && b2 != Boolean.TRUE);

+    }

+

+    /**

+     * This is a utility method that compares two objects when one or

+     * both of the objects might be <CODE>null</CODE>.  The result

+     * returned by this method is determined as follows:

+     * <OL>

+     * <LI>If <CODE>o1</CODE> and <CODE>o2</CODE> are the same object

+     * according to the <CODE>==</CODE> operator, return

+     * <CODE>false</CODE>.

+     * <LI>Otherwise, if either <CODE>o1</CODE> or <CODE>o2</CODE> is

+     * <CODE>null</CODE>, return <CODE>true</CODE>.

+     * <LI>Otherwise, return <CODE>!o1.equals(o2)</CODE>.

+     * </OL>

+     * <p/>

+     * This method produces the exact logically inverted result as the

+     * {@link #areEqual(Object, Object)} method.<P>

+     * <p/>

+     * For array types, one of the <CODE>equals</CODE> methods in

+     * {@link java.util.Arrays} should be used instead of this method.

+     * Note that arrays with more than one dimension will require some

+     * custom code in order to implement <CODE>equals</CODE> properly.

+     */

+    public static final boolean areDifferent(Object o1, Object o2) {

+        return !areEqual(o1, o2);

+    }

+

+

+    /**

+     * This is a utility method that compares two Booleans when one or

+     * both of the objects might be <CODE>null</CODE>  The result of

+     * this method is determined as follows:

+     * <OL>

+     * <LI>If <CODE>b1</CODE> and <CODE>b2</CODE> are both TRUE or

+     * neither <CODE>b1</CODE> nor <CODE>b2</CODE> is TRUE,

+     * return <CODE>false</CODE>.

+     * <LI>Otherwise, return <CODE>true</CODE>.

+     * </OL>

+     * <p/>

+     * This method produces the exact logically inverted result as the

+     * {@link #areBooleansEqual(Boolean, Boolean)} method.<P>

+     */

+    public static final boolean areBooleansDifferent(Boolean b1, Boolean b2) {

+        return !areBooleansEqual(b1, b2);

+    }

+

+

+    /**

+     * Returns <CODE>true</CODE> if the specified array is not null

+     * and contains a non-null element.  Returns <CODE>false</CODE>

+     * if the array is null or if all the array elements are null.

+     */

+    public static final boolean hasNonNullElement(Object[] array) {

+        if (array != null) {

+            final int n = array.length;

+            for (int i = 0; i < n; i++) {

+                if (array[i] != null) {

+                    return true;

+                }

+            }

+        }

+        return false;

+    }

+

+    /**

+     * Returns a single string that is the concatenation of all the

+     * strings in the specified string array.  A single space is

+     * put between each string array element.  Null array elements

+     * are skipped.  If the array itself is null, the empty string

+     * is returned.  This method is guaranteed to return a non-null

+     * value, if no expections are thrown.

+     */

+    public static final String concat(String[] strs) {

+        return concat(strs, " ");  //NOTRANS

+    }

+

+    /**

+     * Returns a single string that is the concatenation of all the

+     * strings in the specified string array.  The strings are separated

+     * by the specified delimiter.  Null array elements are skipped.  If

+     * the array itself is null, the empty string is returned.  This

+     * method is guaranteed to return a non-null value, if no expections

+     * are thrown.

+     */

+    public static final String concat(String[] strs, String delim) {

+        if (strs != null) {

+            final StringBuilder buf = new StringBuilder();

+            final int n = strs.length;

+            for (int i = 0; i < n; i++) {

+                final String str = strs[i];

+                if (str != null) {

+                    buf.append(str).append(delim);

+                }

+            }

+            final int length = buf.length();

+            if (length > 0) {

+                //  Trim trailing space.

+                buf.setLength(length - 1);

+            }

+            return buf.toString();

+        }

+        else {

+            return ""; // NOTRANS

+        }

+    }

+

+    /**

+     * Returns <CODE>true</CODE> if the specified {@link String} is not

+     * <CODE>null</CODE> and has a length greater than zero.  This is

+     * a very frequently occurring check.

+     */

+    public static final boolean hasLength(String s) {

+        return (s != null && s.length() > 0);

+    }

+

+

+    /**

+     * Returns <CODE>null</CODE> if the specified string is empty or

+     * <CODE>null</CODE>.  Otherwise the string itself is returned.

+     */

+    public static final String nullifyIfEmpty(String s) {

+        return ModelUtil.hasLength(s) ? s : null;

+    }

+

+    /**

+     * Returns <CODE>null</CODE> if the specified object is null

+     * or if its <CODE>toString()</CODE> representation is empty.

+     * Otherwise, the <CODE>toString()</CODE> representation of the

+     * object itself is returned.

+     */

+    public static final String nullifyingToString(Object o) {

+        return o != null ? nullifyIfEmpty(o.toString()) : null;

+    }

+

+    /**

+     * Determines if a string has been changed.

+     *

+     * @param oldString is the initial value of the String

+     * @param newString is the new value of the String

+     * @return true If both oldString and newString are null or if they are

+     *         both not null and equal to each other.  Otherwise returns false.

+     */

+    public static boolean hasStringChanged(String oldString, String newString) {

+        if (oldString == null && newString == null) {

+            return false;

+        }

+        else if ((oldString == null && newString != null)

+                || (oldString != null && newString == null)) {

+            return true;

+        }

+        else {

+            return !oldString.equals(newString);

+        }

+    }

+

+    public static String getTimeFromLong(long diff) {

+        final String HOURS = "h";

+        final String MINUTES = "min";

+        final String SECONDS = "sec";

+

+        final long MS_IN_A_DAY = 1000 * 60 * 60 * 24;

+        final long MS_IN_AN_HOUR = 1000 * 60 * 60;

+        final long MS_IN_A_MINUTE = 1000 * 60;

+        final long MS_IN_A_SECOND = 1000;

+        diff = diff % MS_IN_A_DAY;

+        long numHours = diff / MS_IN_AN_HOUR;

+        diff = diff % MS_IN_AN_HOUR;

+        long numMinutes = diff / MS_IN_A_MINUTE;

+        diff = diff % MS_IN_A_MINUTE;

+        long numSeconds = diff / MS_IN_A_SECOND;

+        diff = diff % MS_IN_A_SECOND;

+

+        StringBuilder buf = new StringBuilder();

+        if (numHours > 0) {

+            buf.append(numHours + " " + HOURS + ", ");

+        }

+

+        if (numMinutes > 0) {

+            buf.append(numMinutes + " " + MINUTES + ", ");

+        }

+

+        buf.append(numSeconds + " " + SECONDS);

+

+        String result = buf.toString();

+        return result;

+    }

+

+

+    /**

+     * Build a List of all elements in an Iterator.

+     */

+    public static <T> List<T> iteratorAsList(Iterator<T> i) {

+        ArrayList<T> list = new ArrayList<T>(10);

+        while (i.hasNext()) {

+            list.add(i.next());

+        }

+        return list;

+    }

+

+    /**

+     * Creates an Iterator that is the reverse of a ListIterator.

+     */

+    public static <T> Iterator<T> reverseListIterator(ListIterator<T> i) {

+        return new ReverseListIterator<T>(i);

+    }

+}

+

+/**

+ * An Iterator that is the reverse of a ListIterator.

+ */

+class ReverseListIterator<T> implements Iterator<T> {

+    private ListIterator<T> _i;

+

+    ReverseListIterator(ListIterator<T> i) {

+        _i = i;

+        while (_i.hasNext())

+            _i.next();

+    }

+

+    public boolean hasNext() {

+        return _i.hasPrevious();

+    }

+

+    public T next() {

+        return _i.previous();

+    }

+

+    public void remove() {

+        _i.remove();

+    }

+

+}

+

+

+

+

+

+

+

+

+

+

+

+
diff --git a/src/org/xbill/DNS/A6Record.java b/src/org/xbill/DNS/A6Record.java
new file mode 100644
index 0000000..a1c613a
--- /dev/null
+++ b/src/org/xbill/DNS/A6Record.java
@@ -0,0 +1,127 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.net.*;
+
+/**
+ * A6 Record - maps a domain name to an IPv6 address (experimental)
+ *
+ * @author Brian Wellington
+ */
+
+public class A6Record extends Record {
+
+private static final long serialVersionUID = -8815026887337346789L;
+
+private int prefixBits;
+private InetAddress suffix;
+private Name prefix;
+
+A6Record() {}
+
+Record
+getObject() {
+	return new A6Record();
+}
+
+/**
+ * Creates an A6 Record from the given data
+ * @param prefixBits The number of bits in the address prefix
+ * @param suffix The address suffix
+ * @param prefix The name of the prefix
+ */
+public
+A6Record(Name name, int dclass, long ttl, int prefixBits,
+	 InetAddress suffix, Name prefix)
+{
+	super(name, Type.A6, dclass, ttl);
+	this.prefixBits = checkU8("prefixBits", prefixBits);
+	if (suffix != null && Address.familyOf(suffix) != Address.IPv6)
+		throw new IllegalArgumentException("invalid IPv6 address");
+	this.suffix = suffix;
+	if (prefix != null)
+		this.prefix = checkName("prefix", prefix);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	prefixBits = in.readU8();
+	int suffixbits = 128 - prefixBits;
+	int suffixbytes = (suffixbits + 7) / 8;
+	if (prefixBits < 128) {
+		byte [] bytes = new byte[16];
+		in.readByteArray(bytes, 16 - suffixbytes, suffixbytes);
+		suffix = InetAddress.getByAddress(bytes);
+	}
+	if (prefixBits > 0)
+		prefix = new Name(in);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	prefixBits = st.getUInt8();
+	if (prefixBits > 128) {
+		throw st.exception("prefix bits must be [0..128]");
+	} else if (prefixBits < 128) {
+		String s = st.getString();
+		try {
+			suffix = Address.getByAddress(s, Address.IPv6);
+		}
+		catch (UnknownHostException e) {
+			throw st.exception("invalid IPv6 address: " + s);
+		}
+	}
+	if (prefixBits > 0)
+		prefix = st.getName(origin);
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(prefixBits);
+	if (suffix != null) {
+		sb.append(" ");
+		sb.append(suffix.getHostAddress());
+	}
+	if (prefix != null) {
+		sb.append(" ");
+		sb.append(prefix);
+	}
+	return sb.toString();
+}
+
+/** Returns the number of bits in the prefix */
+public int
+getPrefixBits() {
+	return prefixBits;
+}
+
+/** Returns the address suffix */
+public InetAddress
+getSuffix() {
+	return suffix;
+}
+
+/** Returns the address prefix */
+public Name
+getPrefix() {
+	return prefix;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU8(prefixBits);
+	if (suffix != null) {
+		int suffixbits = 128 - prefixBits;
+		int suffixbytes = (suffixbits + 7) / 8;
+		byte [] data = suffix.getAddress();
+		out.writeByteArray(data, 16 - suffixbytes, suffixbytes);
+	}
+	if (prefix != null)
+		prefix.toWire(out, null, canonical);
+}
+
+}
diff --git a/src/org/xbill/DNS/AAAARecord.java b/src/org/xbill/DNS/AAAARecord.java
new file mode 100644
index 0000000..4b637aa
--- /dev/null
+++ b/src/org/xbill/DNS/AAAARecord.java
@@ -0,0 +1,67 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.net.*;
+
+/**
+ * IPv6 Address Record - maps a domain name to an IPv6 address
+ *
+ * @author Brian Wellington
+ */
+
+public class AAAARecord extends Record {
+
+private static final long serialVersionUID = -4588601512069748050L;
+
+private InetAddress address;
+
+AAAARecord() {}
+
+Record
+getObject() {
+	return new AAAARecord();
+}
+
+/**
+ * Creates an AAAA Record from the given data
+ * @param address The address suffix
+ */
+public
+AAAARecord(Name name, int dclass, long ttl, InetAddress address) {
+	super(name, Type.AAAA, dclass, ttl);
+	if (Address.familyOf(address) != Address.IPv6)
+		throw new IllegalArgumentException("invalid IPv6 address");
+	this.address = address;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	address = InetAddress.getByAddress(name.toString(),
+					   in.readByteArray(16));
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	address = st.getAddress(Address.IPv6);
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	return address.getHostAddress();
+}
+
+/** Returns the address */
+public InetAddress
+getAddress() {
+	return address;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeByteArray(address.getAddress());
+}
+
+}
diff --git a/src/org/xbill/DNS/AFSDBRecord.java b/src/org/xbill/DNS/AFSDBRecord.java
new file mode 100644
index 0000000..4814faa
--- /dev/null
+++ b/src/org/xbill/DNS/AFSDBRecord.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * AFS Data Base Record - maps a domain name to the name of an AFS cell
+ * database server.
+ *
+ *
+ * @author Brian Wellington
+ */
+
+public class AFSDBRecord extends U16NameBase {
+
+private static final long serialVersionUID = 3034379930729102437L;
+
+AFSDBRecord() {}
+
+Record
+getObject() {
+	return new AFSDBRecord();
+}
+
+/**
+ * Creates an AFSDB Record from the given data.
+ * @param subtype Indicates the type of service provided by the host.
+ * @param host The host providing the service.
+ */
+public
+AFSDBRecord(Name name, int dclass, long ttl, int subtype, Name host) {
+	super(name, Type.AFSDB, dclass, ttl, subtype, "subtype", host, "host");
+}
+
+/** Gets the subtype indicating the service provided by the host. */
+public int
+getSubtype() {
+	return getU16Field();
+}
+
+/** Gets the host providing service for the domain. */
+public Name
+getHost() {
+	return getNameField();
+}
+
+}
diff --git a/src/org/xbill/DNS/APLRecord.java b/src/org/xbill/DNS/APLRecord.java
new file mode 100644
index 0000000..5940da2
--- /dev/null
+++ b/src/org/xbill/DNS/APLRecord.java
@@ -0,0 +1,287 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * APL - Address Prefix List.  See RFC 3123.
+ *
+ * @author Brian Wellington
+ */
+
+/*
+ * Note: this currently uses the same constants as the Address class;
+ * this could change if more constants are defined for APL records.
+ */
+
+public class APLRecord extends Record {
+
+public static class Element {
+	public final int family;
+	public final boolean negative;
+	public final int prefixLength;
+	public final Object address;
+
+	private
+	Element(int family, boolean negative, Object address, int prefixLength)
+	{
+		this.family = family;
+		this.negative = negative;
+		this.address = address;
+		this.prefixLength = prefixLength;
+		if (!validatePrefixLength(family, prefixLength)) {
+			throw new IllegalArgumentException("invalid prefix " +
+							   "length");
+		}
+	}
+
+	/**
+	 * Creates an APL element corresponding to an IPv4 or IPv6 prefix.
+	 * @param negative Indicates if this prefix is a negation.
+	 * @param address The IPv4 or IPv6 address.
+	 * @param prefixLength The length of this prefix, in bits.
+	 * @throws IllegalArgumentException The prefix length is invalid.
+	 */
+	public
+	Element(boolean negative, InetAddress address, int prefixLength) {
+		this(Address.familyOf(address), negative, address,
+		     prefixLength);
+	}
+
+	public String
+	toString() {
+		StringBuffer sb = new StringBuffer();
+		if (negative)
+			sb.append("!");
+		sb.append(family);
+		sb.append(":");
+		if (family == Address.IPv4 || family == Address.IPv6)
+			sb.append(((InetAddress) address).getHostAddress());
+		else
+			sb.append(base16.toString((byte []) address));
+		sb.append("/");
+		sb.append(prefixLength);
+		return sb.toString();
+	}
+
+	public boolean
+	equals(Object arg) {
+		if (arg == null || !(arg instanceof Element))
+			return false;
+		Element elt = (Element) arg;
+		return (family == elt.family &&
+			negative == elt.negative &&
+			prefixLength == elt.prefixLength &&
+			address.equals(elt.address));
+	}
+
+	public int
+	hashCode() {
+		return address.hashCode() + prefixLength + (negative ? 1 : 0);
+	}
+}
+
+private static final long serialVersionUID = -1348173791712935864L;
+
+private List elements;
+
+APLRecord() {} 
+
+Record
+getObject() {
+	return new APLRecord();
+}
+
+private static boolean
+validatePrefixLength(int family, int prefixLength) {
+	if (prefixLength < 0 || prefixLength >= 256)
+		return false;
+	if ((family == Address.IPv4 && prefixLength > 32) ||
+	    (family == Address.IPv6 && prefixLength > 128))
+		return false;
+	return true;
+}
+
+/**
+ * Creates an APL Record from the given data.
+ * @param elements The list of APL elements.
+ */
+public
+APLRecord(Name name, int dclass, long ttl, List elements) {
+	super(name, Type.APL, dclass, ttl);
+	this.elements = new ArrayList(elements.size());
+	for (Iterator it = elements.iterator(); it.hasNext(); ) {
+		Object o = it.next();
+		if (!(o instanceof Element)) {
+			throw new IllegalArgumentException("illegal element");
+		}
+		Element element = (Element) o;
+		if (element.family != Address.IPv4 &&
+		    element.family != Address.IPv6)
+		{
+			throw new IllegalArgumentException("unknown family");
+		}
+		this.elements.add(element);
+
+	}
+}
+
+private static byte []
+parseAddress(byte [] in, int length) throws WireParseException {
+	if (in.length > length)
+		throw new WireParseException("invalid address length");
+	if (in.length == length)
+		return in;
+	byte [] out = new byte[length];
+	System.arraycopy(in, 0, out, 0, in.length);
+	return out;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	elements = new ArrayList(1);
+	while (in.remaining() != 0) {
+		int family = in.readU16();
+		int prefix = in.readU8();
+		int length = in.readU8();
+		boolean negative = (length & 0x80) != 0;
+		length &= ~0x80;
+
+		byte [] data = in.readByteArray(length);
+		Element element;
+		if (!validatePrefixLength(family, prefix)) {
+			throw new WireParseException("invalid prefix length");
+		}
+
+		if (family == Address.IPv4 || family == Address.IPv6) {
+			data = parseAddress(data,
+					    Address.addressLength(family));
+			InetAddress addr = InetAddress.getByAddress(data);
+			element = new Element(negative, addr, prefix);
+		} else {
+			element = new Element(family, negative, data, prefix);
+		}
+		elements.add(element);
+
+	}
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	elements = new ArrayList(1);
+	while (true) {
+		Tokenizer.Token t = st.get();
+		if (!t.isString())
+			break;
+
+		boolean negative = false;
+		int family = 0;
+		int prefix = 0;
+
+		String s = t.value;
+		int start = 0;
+		if (s.startsWith("!")) {
+			negative = true;
+			start = 1;
+		}
+		int colon = s.indexOf(':', start);
+		if (colon < 0)
+			throw st.exception("invalid address prefix element");
+		int slash = s.indexOf('/', colon);
+		if (slash < 0)
+			throw st.exception("invalid address prefix element");
+
+		String familyString = s.substring(start, colon);
+		String addressString = s.substring(colon + 1, slash);
+		String prefixString = s.substring(slash + 1);
+
+		try {
+			family = Integer.parseInt(familyString);
+		}
+		catch (NumberFormatException e) {
+			throw st.exception("invalid family");
+		}
+		if (family != Address.IPv4 && family != Address.IPv6)
+			throw st.exception("unknown family");
+
+		try {
+			prefix = Integer.parseInt(prefixString);
+		}
+		catch (NumberFormatException e) {
+			throw st.exception("invalid prefix length");
+		}
+
+		if (!validatePrefixLength(family, prefix)) {
+			throw st.exception("invalid prefix length");
+		}
+
+		byte [] bytes = Address.toByteArray(addressString, family);
+		if (bytes == null)
+			throw st.exception("invalid IP address " +
+					   addressString);
+
+		InetAddress address = InetAddress.getByAddress(bytes);
+		elements.add(new Element(negative, address, prefix));
+	}
+	st.unget();
+}
+
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	for (Iterator it = elements.iterator(); it.hasNext(); ) {
+		Element element = (Element) it.next();
+		sb.append(element);
+		if (it.hasNext())
+			sb.append(" ");
+	}
+	return sb.toString();
+}
+
+/** Returns the list of APL elements. */
+public List
+getElements() {
+	return elements;
+}
+
+private static int
+addressLength(byte [] addr) {
+	for (int i = addr.length - 1; i >= 0; i--) {
+		if (addr[i] != 0)
+			return i + 1;
+	}
+	return 0;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	for (Iterator it = elements.iterator(); it.hasNext(); ) {
+		Element element = (Element) it.next();
+		int length = 0;
+		byte [] data;
+		if (element.family == Address.IPv4 ||
+		    element.family == Address.IPv6)
+		{
+			InetAddress addr = (InetAddress) element.address;
+			data = addr.getAddress();
+			length = addressLength(data);
+		} else {
+			data = (byte []) element.address;
+			length = data.length;
+		}
+		int wlength = length;
+		if (element.negative) {
+			wlength |= 0x80;
+		}
+		out.writeU16(element.family);
+		out.writeU8(element.prefixLength);
+		out.writeU8(wlength);
+		out.writeByteArray(data, 0, length);
+	}
+}
+
+}
diff --git a/src/org/xbill/DNS/ARecord.java b/src/org/xbill/DNS/ARecord.java
new file mode 100644
index 0000000..4e13aa7
--- /dev/null
+++ b/src/org/xbill/DNS/ARecord.java
@@ -0,0 +1,90 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.net.*;
+import java.io.*;
+
+/**
+ * Address Record - maps a domain name to an Internet address
+ *
+ * @author Brian Wellington
+ */
+
+public class ARecord extends Record {
+
+private static final long serialVersionUID = -2172609200849142323L;
+
+private int addr;
+
+ARecord() {}
+
+Record
+getObject() {
+	return new ARecord();
+}
+
+private static final int
+fromArray(byte [] array) {
+	return (((array[0] & 0xFF) << 24) |
+		((array[1] & 0xFF) << 16) |
+		((array[2] & 0xFF) << 8) |
+		(array[3] & 0xFF));
+}
+
+private static final byte []
+toArray(int addr) {
+	byte [] bytes = new byte[4];
+	bytes[0] = (byte) ((addr >>> 24) & 0xFF);
+	bytes[1] = (byte) ((addr >>> 16) & 0xFF);
+	bytes[2] = (byte) ((addr >>> 8) & 0xFF);
+	bytes[3] = (byte) (addr & 0xFF);
+	return bytes;
+}
+
+/**
+ * Creates an A Record from the given data
+ * @param address The address that the name refers to
+ */
+public
+ARecord(Name name, int dclass, long ttl, InetAddress address) {
+	super(name, Type.A, dclass, ttl);
+	if (Address.familyOf(address) != Address.IPv4)
+		throw new IllegalArgumentException("invalid IPv4 address");
+	addr = fromArray(address.getAddress());
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	addr = fromArray(in.readByteArray(4));
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	InetAddress address = st.getAddress(Address.IPv4);
+	addr = fromArray(address.getAddress());
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	return (Address.toDottedQuad(toArray(addr)));
+}
+
+/** Returns the Internet address */
+public InetAddress
+getAddress() {
+	try {
+		return InetAddress.getByAddress(name.toString(),
+						toArray(addr));
+	} catch (UnknownHostException e) {
+		return null;
+	}
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU32(((long)addr) & 0xFFFFFFFFL);
+}
+
+}
diff --git a/src/org/xbill/DNS/Address.java b/src/org/xbill/DNS/Address.java
new file mode 100644
index 0000000..799185b
--- /dev/null
+++ b/src/org/xbill/DNS/Address.java
@@ -0,0 +1,402 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.net.*;
+import java.net.Inet6Address;
+
+/**
+ * Routines dealing with IP addresses.  Includes functions similar to
+ * those in the java.net.InetAddress class.
+ *
+ * @author Brian Wellington
+ */
+
+public final class Address {
+
+public static final int IPv4 = 1;
+public static final int IPv6 = 2;
+
+private
+Address() {}
+
+private static byte []
+parseV4(String s) {
+	int numDigits;
+	int currentOctet;
+	byte [] values = new byte[4];
+	int currentValue;
+	int length = s.length();
+
+	currentOctet = 0;
+	currentValue = 0;
+	numDigits = 0;
+	for (int i = 0; i < length; i++) {
+		char c = s.charAt(i);
+		if (c >= '0' && c <= '9') {
+			/* Can't have more than 3 digits per octet. */
+			if (numDigits == 3)
+				return null;
+			/* Octets shouldn't start with 0, unless they are 0. */
+			if (numDigits > 0 && currentValue == 0)
+				return null;
+			numDigits++;
+			currentValue *= 10;
+			currentValue += (c - '0');
+			/* 255 is the maximum value for an octet. */
+			if (currentValue > 255)
+				return null;
+		} else if (c == '.') {
+			/* Can't have more than 3 dots. */
+			if (currentOctet == 3)
+				return null;
+			/* Two consecutive dots are bad. */
+			if (numDigits == 0)
+				return null;
+			values[currentOctet++] = (byte) currentValue;
+			currentValue = 0;
+			numDigits = 0;
+		} else
+			return null;
+	}
+	/* Must have 4 octets. */
+	if (currentOctet != 3)
+		return null;
+	/* The fourth octet can't be empty. */
+	if (numDigits == 0)
+		return null;
+	values[currentOctet] = (byte) currentValue;
+	return values;
+}
+
+private static byte []
+parseV6(String s) {
+	int range = -1;
+	byte [] data = new byte[16];
+
+	String [] tokens = s.split(":", -1);
+
+	int first = 0;
+	int last = tokens.length - 1;
+
+	if (tokens[0].length() == 0) {
+		// If the first two tokens are empty, it means the string
+		// started with ::, which is fine.  If only the first is
+		// empty, the string started with :, which is bad.
+		if (last - first > 0 && tokens[1].length() == 0)
+			first++;
+		else
+			return null;
+	}
+
+	if (tokens[last].length() == 0) {
+		// If the last two tokens are empty, it means the string
+		// ended with ::, which is fine.  If only the last is
+		// empty, the string ended with :, which is bad.
+		if (last - first > 0 && tokens[last - 1].length() == 0)
+			last--;
+		else
+			return null;
+	}
+
+	if (last - first + 1 > 8)
+		return null;
+
+	int i, j;
+	for (i = first, j = 0; i <= last; i++) {
+		if (tokens[i].length() == 0) {
+			if (range >= 0)
+				return null;
+			range = j;
+			continue;
+		}
+
+		if (tokens[i].indexOf('.') >= 0) {
+			// An IPv4 address must be the last component
+			if (i < last)
+				return null;
+			// There can't have been more than 6 components.
+			if (i > 6)
+				return null;
+			byte [] v4addr = Address.toByteArray(tokens[i], IPv4);
+			if (v4addr == null)
+				return null;
+			for (int k = 0; k < 4; k++)
+				data[j++] = v4addr[k];
+			break;
+		}
+
+		try {
+			for (int k = 0; k < tokens[i].length(); k++) {
+				char c = tokens[i].charAt(k);
+				if (Character.digit(c, 16) < 0)
+					return null;
+			}
+			int x = Integer.parseInt(tokens[i], 16);
+			if (x > 0xFFFF || x < 0)
+				return null;
+			data[j++] = (byte)(x >>> 8);
+			data[j++] = (byte)(x & 0xFF);
+		}
+		catch (NumberFormatException e) {
+			return null;
+		}
+	}
+
+	if (j < 16 && range < 0)
+		return null;
+
+	if (range >= 0) {
+		int empty = 16 - j;
+		System.arraycopy(data, range, data, range + empty, j - range);
+		for (i = range; i < range + empty; i++)
+			data[i] = 0;
+	}
+
+	return data;
+}
+
+/**
+ * Convert a string containing an IP address to an array of 4 or 16 integers.
+ * @param s The address, in text format.
+ * @param family The address family.
+ * @return The address
+ */
+public static int []
+toArray(String s, int family) {
+	byte [] byteArray = toByteArray(s, family);
+	if (byteArray == null)
+		return null;
+	int [] intArray = new int[byteArray.length];
+	for (int i = 0; i < byteArray.length; i++)
+		intArray[i] = byteArray[i] & 0xFF;
+	return intArray;
+}
+
+/**
+ * Convert a string containing an IPv4 address to an array of 4 integers.
+ * @param s The address, in text format.
+ * @return The address
+ */
+public static int []
+toArray(String s) {
+	return toArray(s, IPv4);
+}
+
+/**
+ * Convert a string containing an IP address to an array of 4 or 16 bytes.
+ * @param s The address, in text format.
+ * @param family The address family.
+ * @return The address
+ */
+public static byte []
+toByteArray(String s, int family) {
+	if (family == IPv4)
+		return parseV4(s);
+	else if (family == IPv6)
+		return parseV6(s);
+	else
+		throw new IllegalArgumentException("unknown address family");
+}
+
+/**
+ * Determines if a string contains a valid IP address.
+ * @param s The string
+ * @return Whether the string contains a valid IP address
+ */
+public static boolean
+isDottedQuad(String s) {
+	byte [] address = Address.toByteArray(s, IPv4);
+	return (address != null);
+}
+
+/**
+ * Converts a byte array containing an IPv4 address into a dotted quad string.
+ * @param addr The array
+ * @return The string representation
+ */
+public static String
+toDottedQuad(byte [] addr) {
+	return ((addr[0] & 0xFF) + "." + (addr[1] & 0xFF) + "." +
+		(addr[2] & 0xFF) + "." + (addr[3] & 0xFF));
+}
+
+/**
+ * Converts an int array containing an IPv4 address into a dotted quad string.
+ * @param addr The array
+ * @return The string representation
+ */
+public static String
+toDottedQuad(int [] addr) {
+	return (addr[0] + "." + addr[1] + "." + addr[2] + "." + addr[3]);
+}
+
+private static Record []
+lookupHostName(String name) throws UnknownHostException {
+	try {
+		Record [] records = new Lookup(name).run();
+		if (records == null)
+			throw new UnknownHostException("unknown host");
+		return records;
+	}
+	catch (TextParseException e) {
+		throw new UnknownHostException("invalid name");
+	}
+}
+
+private static InetAddress
+addrFromRecord(String name, Record r) throws UnknownHostException {
+	ARecord a = (ARecord) r;
+	return InetAddress.getByAddress(name, a.getAddress().getAddress());
+}
+
+/**
+ * Determines the IP address of a host
+ * @param name The hostname to look up
+ * @return The first matching IP address
+ * @exception UnknownHostException The hostname does not have any addresses
+ */
+public static InetAddress
+getByName(String name) throws UnknownHostException {
+	try {
+		return getByAddress(name);
+	} catch (UnknownHostException e) {
+		Record [] records = lookupHostName(name);
+		return addrFromRecord(name, records[0]);
+	}
+}
+
+/**
+ * Determines all IP address of a host
+ * @param name The hostname to look up
+ * @return All matching IP addresses
+ * @exception UnknownHostException The hostname does not have any addresses
+ */
+public static InetAddress []
+getAllByName(String name) throws UnknownHostException {
+	try {
+		InetAddress addr = getByAddress(name);
+		return new InetAddress[] {addr};
+	} catch (UnknownHostException e) {
+		Record [] records = lookupHostName(name);
+		InetAddress [] addrs = new InetAddress[records.length];
+		for (int i = 0; i < records.length; i++)
+			addrs[i] = addrFromRecord(name, records[i]);
+		return addrs;
+	}
+}
+
+/**
+ * Converts an address from its string representation to an IP address.
+ * The address can be either IPv4 or IPv6.
+ * @param addr The address, in string form
+ * @return The IP addresses
+ * @exception UnknownHostException The address is not a valid IP address.
+ */
+public static InetAddress
+getByAddress(String addr) throws UnknownHostException {
+	byte [] bytes;
+	bytes = toByteArray(addr, IPv4);
+	if (bytes != null)
+		return InetAddress.getByAddress(addr, bytes);
+	bytes = toByteArray(addr, IPv6);
+	if (bytes != null)
+		return InetAddress.getByAddress(addr, bytes);
+	throw new UnknownHostException("Invalid address: " + addr);
+}
+
+/**
+ * Converts an address from its string representation to an IP address in
+ * a particular family.
+ * @param addr The address, in string form
+ * @param family The address family, either IPv4 or IPv6.
+ * @return The IP addresses
+ * @exception UnknownHostException The address is not a valid IP address in
+ * the specified address family.
+ */
+public static InetAddress
+getByAddress(String addr, int family) throws UnknownHostException {
+	if (family != IPv4 && family != IPv6)
+		throw new IllegalArgumentException("unknown address family");
+	byte [] bytes;
+	bytes = toByteArray(addr, family);
+	if (bytes != null)
+		return InetAddress.getByAddress(addr, bytes);
+	throw new UnknownHostException("Invalid address: " + addr);
+}
+
+/**
+ * Determines the hostname for an address
+ * @param addr The address to look up
+ * @return The associated host name
+ * @exception UnknownHostException There is no hostname for the address
+ */
+public static String
+getHostName(InetAddress addr) throws UnknownHostException {
+	Name name = ReverseMap.fromAddress(addr);
+	Record [] records = new Lookup(name, Type.PTR).run();
+	if (records == null)
+		throw new UnknownHostException("unknown address");
+	PTRRecord ptr = (PTRRecord) records[0];
+	return ptr.getTarget().toString();
+}
+
+/**
+ * Returns the family of an InetAddress.
+ * @param address The supplied address.
+ * @return The family, either IPv4 or IPv6.
+ */
+public static int
+familyOf(InetAddress address) {
+	if (address instanceof Inet4Address)
+		return IPv4;
+	if (address instanceof Inet6Address)
+		return IPv6;
+	throw new IllegalArgumentException("unknown address family");
+}
+
+/**
+ * Returns the length of an address in a particular family.
+ * @param family The address family, either IPv4 or IPv6.
+ * @return The length of addresses in that family.
+ */
+public static int
+addressLength(int family) {
+	if (family == IPv4)
+		return 4;
+	if (family == IPv6)
+		return 16;
+	throw new IllegalArgumentException("unknown address family");
+}
+
+/**
+ * Truncates an address to the specified number of bits.  For example,
+ * truncating the address 10.1.2.3 to 8 bits would yield 10.0.0.0.
+ * @param address The source address
+ * @param maskLength The number of bits to truncate the address to.
+ */
+public static InetAddress
+truncate(InetAddress address, int maskLength)
+{
+	int family = familyOf(address);
+	int maxMaskLength = addressLength(family) * 8;
+	if (maskLength < 0 || maskLength > maxMaskLength)
+		throw new IllegalArgumentException("invalid mask length");
+	if (maskLength == maxMaskLength)
+		return address;
+	byte [] bytes = address.getAddress();
+	for (int i = maskLength / 8 + 1; i < bytes.length; i++)
+		bytes[i] = 0;
+	int maskBits = maskLength % 8;
+	int bitmask = 0;
+	for (int i = 0; i < maskBits; i++)
+		bitmask |= (1 << (7 - i));
+	bytes[maskLength / 8] &= bitmask;
+	try {
+		return InetAddress.getByAddress(bytes);
+	} catch (UnknownHostException e) {
+		throw new IllegalArgumentException("invalid address");
+	}
+}
+
+}
diff --git a/src/org/xbill/DNS/CERTRecord.java b/src/org/xbill/DNS/CERTRecord.java
new file mode 100644
index 0000000..39bcef3
--- /dev/null
+++ b/src/org/xbill/DNS/CERTRecord.java
@@ -0,0 +1,224 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * Certificate Record  - Stores a certificate associated with a name.  The
+ * certificate might also be associated with a KEYRecord.
+ * @see KEYRecord
+ *
+ * @author Brian Wellington
+ */
+
+public class CERTRecord extends Record {
+
+public static class CertificateType {
+	/** Certificate type identifiers.  See RFC 4398 for more detail. */
+
+	private CertificateType() {}
+
+	/** PKIX (X.509v3) */
+	public static final int PKIX = 1;
+
+	/** Simple Public Key Infrastructure */
+	public static final int SPKI = 2;
+
+	/** Pretty Good Privacy */
+	public static final int PGP = 3;
+
+	/** URL of an X.509 data object */
+	public static final int IPKIX = 4;
+
+	/** URL of an SPKI certificate */
+	public static final int ISPKI = 5;
+
+	/** Fingerprint and URL of an OpenPGP packet */
+	public static final int IPGP = 6;
+
+	/** Attribute Certificate */
+	public static final int ACPKIX = 7;
+
+	/** URL of an Attribute Certificate */
+	public static final int IACPKIX = 8;
+
+	/** Certificate format defined by URI */
+	public static final int URI = 253;
+
+	/** Certificate format defined by OID */
+	public static final int OID = 254;
+
+	private static Mnemonic types = new Mnemonic("Certificate type",
+						     Mnemonic.CASE_UPPER);
+
+	static {
+		types.setMaximum(0xFFFF);
+		types.setNumericAllowed(true);
+
+		types.add(PKIX, "PKIX");
+		types.add(SPKI, "SPKI");
+		types.add(PGP, "PGP");
+		types.add(PKIX, "IPKIX");
+		types.add(SPKI, "ISPKI");
+		types.add(PGP, "IPGP");
+		types.add(PGP, "ACPKIX");
+		types.add(PGP, "IACPKIX");
+		types.add(URI, "URI");
+		types.add(OID, "OID");
+	}
+
+	/**
+	 * Converts a certificate type into its textual representation
+	 */
+	public static String
+	string(int type) {
+		return types.getText(type);
+	}
+
+	/**
+	 * Converts a textual representation of an certificate type into its
+	 * numeric code.  Integers in the range 0..65535 are also accepted.
+	 * @param s The textual representation of the algorithm
+	 * @return The algorithm code, or -1 on error.
+	 */
+	public static int
+	value(String s) {
+		return types.getValue(s);
+	}
+}
+
+/** PKIX (X.509v3) */
+public static final int PKIX = CertificateType.PKIX;
+
+/** Simple Public Key Infrastructure  */
+public static final int SPKI = CertificateType.SPKI;
+
+/** Pretty Good Privacy */
+public static final int PGP = CertificateType.PGP;
+
+/** Certificate format defined by URI */
+public static final int URI = CertificateType.URI;
+
+/** Certificate format defined by IOD */
+public static final int OID = CertificateType.OID;
+
+private static final long serialVersionUID = 4763014646517016835L;
+
+private int certType, keyTag;
+private int alg;
+private byte [] cert;
+
+CERTRecord() {}
+
+Record
+getObject() {
+	return new CERTRecord();
+}
+
+/**
+ * Creates a CERT Record from the given data
+ * @param certType The type of certificate (see constants)
+ * @param keyTag The ID of the associated KEYRecord, if present
+ * @param alg The algorithm of the associated KEYRecord, if present
+ * @param cert Binary data representing the certificate
+ */
+public
+CERTRecord(Name name, int dclass, long ttl, int certType, int keyTag,
+	   int alg, byte []  cert)
+{
+	super(name, Type.CERT, dclass, ttl);
+	this.certType = checkU16("certType", certType);
+	this.keyTag = checkU16("keyTag", keyTag);
+	this.alg = checkU8("alg", alg);
+	this.cert = cert;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	certType = in.readU16();
+	keyTag = in.readU16();
+	alg = in.readU8();
+	cert = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	String certTypeString = st.getString();
+	certType = CertificateType.value(certTypeString);
+	if (certType < 0)
+		throw st.exception("Invalid certificate type: " +
+				   certTypeString);
+	keyTag = st.getUInt16();
+	String algString = st.getString();
+	alg = DNSSEC.Algorithm.value(algString);
+	if (alg < 0)
+		throw st.exception("Invalid algorithm: " + algString);
+	cert = st.getBase64();
+}
+
+/**
+ * Converts rdata to a String
+ */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append (certType);
+	sb.append (" ");
+	sb.append (keyTag);
+	sb.append (" ");
+	sb.append (alg);
+	if (cert != null) {
+		if (Options.check("multiline")) {
+			sb.append(" (\n");
+			sb.append(base64.formatString(cert, 64, "\t", true));
+		} else {
+			sb.append(" ");
+			sb.append(base64.toString(cert));
+		}
+	}
+	return sb.toString();
+}
+
+/**
+ * Returns the type of certificate
+ */
+public int
+getCertType() {
+	return certType;
+}
+
+/**
+ * Returns the ID of the associated KEYRecord, if present
+ */
+public int
+getKeyTag() {
+	return keyTag;
+}
+
+/**
+ * Returns the algorithm of the associated KEYRecord, if present
+ */
+public int
+getAlgorithm() {
+	return alg;
+}
+
+/**
+ * Returns the binary representation of the certificate
+ */
+public byte []
+getCert() {
+	return cert;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU16(certType);
+	out.writeU16(keyTag);
+	out.writeU8(alg);
+	out.writeByteArray(cert);
+}
+
+}
diff --git a/src/org/xbill/DNS/CNAMERecord.java b/src/org/xbill/DNS/CNAMERecord.java
new file mode 100644
index 0000000..8db9453
--- /dev/null
+++ b/src/org/xbill/DNS/CNAMERecord.java
@@ -0,0 +1,45 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * CNAME Record  - maps an alias to its real name
+ *
+ * @author Brian Wellington
+ */
+
+public class CNAMERecord extends SingleCompressedNameBase {
+
+private static final long serialVersionUID = -4020373886892538580L;
+
+CNAMERecord() {}
+
+Record
+getObject() {
+	return new CNAMERecord();
+}
+
+/**
+ * Creates a new CNAMERecord with the given data
+ * @param alias The name to which the CNAME alias points
+ */
+public
+CNAMERecord(Name name, int dclass, long ttl, Name alias) {
+	super(name, Type.CNAME, dclass, ttl, alias, "alias");
+}
+
+/**
+ * Gets the target of the CNAME Record
+ */
+public Name
+getTarget() {
+	return getSingleName();
+}
+
+/** Gets the alias specified by the CNAME Record */
+public Name
+getAlias() {
+	return getSingleName();
+}
+
+}
diff --git a/src/org/xbill/DNS/Cache.java b/src/org/xbill/DNS/Cache.java
new file mode 100644
index 0000000..5497f45
--- /dev/null
+++ b/src/org/xbill/DNS/Cache.java
@@ -0,0 +1,846 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * A cache of DNS records.  The cache obeys TTLs, so items are purged after
+ * their validity period is complete.  Negative answers are cached, to
+ * avoid repeated failed DNS queries.  The credibility of each RRset is
+ * maintained, so that more credible records replace less credible records,
+ * and lookups can specify the minimum credibility of data they are requesting.
+ * @see RRset
+ * @see Credibility
+ *
+ * @author Brian Wellington
+ */
+
+public class Cache {
+
+private interface Element {
+	public boolean expired();
+	public int compareCredibility(int cred);
+	public int getType();
+}
+
+private static int
+limitExpire(long ttl, long maxttl) {
+	if (maxttl >= 0 && maxttl < ttl)
+		ttl = maxttl;
+	long expire = (System.currentTimeMillis() / 1000) + ttl;
+	if (expire < 0 || expire > Integer.MAX_VALUE)
+		return Integer.MAX_VALUE;
+	return (int)expire;
+}
+
+private static class CacheRRset extends RRset implements Element {
+	private static final long serialVersionUID = 5971755205903597024L;
+	
+	int credibility;
+	int expire;
+
+	public
+	CacheRRset(Record rec, int cred, long maxttl) {
+		super();
+		this.credibility = cred;
+		this.expire = limitExpire(rec.getTTL(), maxttl);
+		addRR(rec);
+	}
+
+	public
+	CacheRRset(RRset rrset, int cred, long maxttl) {
+		super(rrset);
+		this.credibility = cred;
+		this.expire = limitExpire(rrset.getTTL(), maxttl);
+	}
+
+	public final boolean
+	expired() {
+		int now = (int)(System.currentTimeMillis() / 1000);
+		return (now >= expire);
+	}
+
+	public final int
+	compareCredibility(int cred) {
+		return credibility - cred;
+	}
+
+	public String
+	toString() {
+		StringBuffer sb = new StringBuffer();
+		sb.append(super.toString());
+		sb.append(" cl = ");
+		sb.append(credibility);
+		return sb.toString();
+	}
+}
+
+private static class NegativeElement implements Element {
+	int type;
+	Name name;
+	int credibility;
+	int expire;
+
+	public
+	NegativeElement(Name name, int type, SOARecord soa, int cred,
+			long maxttl)
+	{
+		this.name = name;
+		this.type = type;
+		long cttl = 0;
+		if (soa != null)
+			cttl = soa.getMinimum();
+		this.credibility = cred;
+		this.expire = limitExpire(cttl, maxttl);
+	}
+
+	public int
+	getType() {
+		return type;
+	}
+
+	public final boolean
+	expired() {
+		int now = (int)(System.currentTimeMillis() / 1000);
+		return (now >= expire);
+	}
+
+	public final int
+	compareCredibility(int cred) {
+		return credibility - cred;
+	}
+
+	public String
+	toString() {
+		StringBuffer sb = new StringBuffer();
+		if (type == 0)
+			sb.append("NXDOMAIN " + name);
+		else
+			sb.append("NXRRSET " + name + " " + Type.string(type));
+		sb.append(" cl = ");
+		sb.append(credibility);
+		return sb.toString();
+	}
+}
+
+private static class CacheMap extends LinkedHashMap {
+	private int maxsize = -1;
+
+	CacheMap(int maxsize) {
+		super(16, (float) 0.75, true);
+		this.maxsize = maxsize;
+	}
+
+	int
+	getMaxSize() {
+		return maxsize;
+	}
+
+	void
+	setMaxSize(int maxsize) {
+		/*
+		 * Note that this doesn't shrink the size of the map if
+		 * the maximum size is lowered, but it should shrink as
+		 * entries expire.
+		 */
+		this.maxsize = maxsize;
+	}
+
+	protected boolean removeEldestEntry(Map.Entry eldest) {
+		return maxsize >= 0 && size() > maxsize;
+	}
+}
+
+private CacheMap data;
+private int maxncache = -1;
+private int maxcache = -1;
+private int dclass;
+
+private static final int defaultMaxEntries = 50000;
+
+/**
+ * Creates an empty Cache
+ *
+ * @param dclass The DNS class of this cache
+ * @see DClass
+ */
+public
+Cache(int dclass) {
+	this.dclass = dclass;
+	data = new CacheMap(defaultMaxEntries);
+}
+
+/**
+ * Creates an empty Cache for class IN.
+ * @see DClass
+ */
+public
+Cache() {
+	this(DClass.IN);
+}
+
+/**
+ * Creates a Cache which initially contains all records in the specified file.
+ */
+public
+Cache(String file) throws IOException {
+	data = new CacheMap(defaultMaxEntries);
+	Master m = new Master(file);
+	Record record;
+	while ((record = m.nextRecord()) != null)
+		addRecord(record, Credibility.HINT, m);
+}
+
+private synchronized Object
+exactName(Name name) {
+	return data.get(name);
+}
+
+private synchronized void
+removeName(Name name) {
+	data.remove(name);
+}
+
+private synchronized Element []
+allElements(Object types) {
+	if (types instanceof List) {
+		List typelist = (List) types;
+		int size = typelist.size();
+		return (Element []) typelist.toArray(new Element[size]);
+	} else {
+		Element set = (Element) types;
+		return new Element[] {set};
+	}
+}
+
+private synchronized Element
+oneElement(Name name, Object types, int type, int minCred) {
+	Element found = null;
+
+	if (type == Type.ANY)
+		throw new IllegalArgumentException("oneElement(ANY)");
+	if (types instanceof List) {
+		List list = (List) types;
+		for (int i = 0; i < list.size(); i++) {
+			Element set = (Element) list.get(i);
+			if (set.getType() == type) {
+				found = set;
+				break;
+			}
+		}
+	} else {
+		Element set = (Element) types;
+		if (set.getType() == type)
+			found = set;
+	}
+	if (found == null)
+		return null;
+	if (found.expired()) {
+		removeElement(name, type);
+		return null;
+	}
+	if (found.compareCredibility(minCred) < 0)
+		return null;
+	return found;
+}
+
+private synchronized Element
+findElement(Name name, int type, int minCred) {
+	Object types = exactName(name);
+	if (types == null)
+		return null;
+	return oneElement(name, types, type, minCred);
+}
+
+private synchronized void
+addElement(Name name, Element element) {
+	Object types = data.get(name);
+	if (types == null) {
+		data.put(name, element);
+		return;
+	}
+	int type = element.getType();
+	if (types instanceof List) {
+		List list = (List) types;
+		for (int i = 0; i < list.size(); i++) {
+			Element elt = (Element) list.get(i);
+			if (elt.getType() == type) {
+				list.set(i, element);
+				return;
+			}
+		}
+		list.add(element);
+	} else {
+		Element elt = (Element) types;
+		if (elt.getType() == type)
+			data.put(name, element);
+		else {
+			LinkedList list = new LinkedList();
+			list.add(elt);
+			list.add(element);
+			data.put(name, list);
+		}
+	}
+}
+
+private synchronized void
+removeElement(Name name, int type) {
+	Object types = data.get(name);
+	if (types == null) {
+		return;
+	}
+	if (types instanceof List) {
+		List list = (List) types;
+		for (int i = 0; i < list.size(); i++) {
+			Element elt = (Element) list.get(i);
+			if (elt.getType() == type) {
+				list.remove(i);
+				if (list.size() == 0)
+					data.remove(name);
+				return;
+			}
+		}
+	} else {
+		Element elt = (Element) types;
+		if (elt.getType() != type)
+			return;
+		data.remove(name);
+	}
+}
+
+/** Empties the Cache. */
+public synchronized void
+clearCache() {
+	data.clear();
+}
+
+/**
+ * Adds a record to the Cache.
+ * @param r The record to be added
+ * @param cred The credibility of the record
+ * @param o The source of the record (this could be a Message, for example)
+ * @see Record
+ */
+public synchronized void
+addRecord(Record r, int cred, Object o) {
+	Name name = r.getName();
+	int type = r.getRRsetType();
+	if (!Type.isRR(type))
+		return;
+	Element element = findElement(name, type, cred);
+	if (element == null) {
+		CacheRRset crrset = new CacheRRset(r, cred, maxcache);
+		addRRset(crrset, cred);
+	} else if (element.compareCredibility(cred) == 0) {
+		if (element instanceof CacheRRset) {
+			CacheRRset crrset = (CacheRRset) element;
+			crrset.addRR(r);
+		}
+	}
+}
+
+/**
+ * Adds an RRset to the Cache.
+ * @param rrset The RRset to be added
+ * @param cred The credibility of these records
+ * @see RRset
+ */
+public synchronized void
+addRRset(RRset rrset, int cred) {
+	long ttl = rrset.getTTL();
+	Name name = rrset.getName();
+	int type = rrset.getType();
+	Element element = findElement(name, type, 0);
+	if (ttl == 0) {
+		if (element != null && element.compareCredibility(cred) <= 0)
+			removeElement(name, type);
+	} else {
+		if (element != null && element.compareCredibility(cred) <= 0)
+			element = null;
+		if (element == null) {
+			CacheRRset crrset;
+			if (rrset instanceof CacheRRset)
+				crrset = (CacheRRset) rrset;
+			else
+				crrset = new CacheRRset(rrset, cred, maxcache);
+			addElement(name, crrset);
+		}
+	}
+}
+
+/**
+ * Adds a negative entry to the Cache.
+ * @param name The name of the negative entry
+ * @param type The type of the negative entry
+ * @param soa The SOA record to add to the negative cache entry, or null.
+ * The negative cache ttl is derived from the SOA.
+ * @param cred The credibility of the negative entry
+ */
+public synchronized void
+addNegative(Name name, int type, SOARecord soa, int cred) {
+	long ttl = 0;
+	if (soa != null)
+		ttl = soa.getTTL();
+	Element element = findElement(name, type, 0);
+	if (ttl == 0) {
+		if (element != null && element.compareCredibility(cred) <= 0)
+			removeElement(name, type);
+	} else {
+		if (element != null && element.compareCredibility(cred) <= 0)
+			element = null;
+		if (element == null)
+			addElement(name, new NegativeElement(name, type,
+							     soa, cred,
+							     maxncache));
+	}
+}
+
+/**
+ * Finds all matching sets or something that causes the lookup to stop.
+ */
+protected synchronized SetResponse
+lookup(Name name, int type, int minCred) {
+	int labels;
+	int tlabels;
+	Element element;
+	Name tname;
+	Object types;
+	SetResponse sr;
+
+	labels = name.labels();
+
+	for (tlabels = labels; tlabels >= 1; tlabels--) {
+		boolean isRoot = (tlabels == 1);
+		boolean isExact = (tlabels == labels);
+
+		if (isRoot)
+			tname = Name.root;
+		else if (isExact)
+			tname = name;
+		else
+			tname = new Name(name, labels - tlabels);
+
+		types = data.get(tname);
+		if (types == null)
+			continue;
+
+		/*
+		 * If this is the name, look for the actual type or a CNAME
+		 * (unless it's an ANY query, where we return everything).
+		 * Otherwise, look for a DNAME.
+		 */
+		if (isExact && type == Type.ANY) {
+			sr = new SetResponse(SetResponse.SUCCESSFUL);
+			Element [] elements = allElements(types);
+			int added = 0;
+			for (int i = 0; i < elements.length; i++) {
+				element = elements[i];
+				if (element.expired()) {
+					removeElement(tname, element.getType());
+					continue;
+				}
+				if (!(element instanceof CacheRRset))
+					continue;
+				if (element.compareCredibility(minCred) < 0)
+					continue;
+				sr.addRRset((CacheRRset)element);
+				added++;
+			}
+			/* There were positive entries */
+			if (added > 0)
+				return sr;
+		} else if (isExact) {
+			element = oneElement(tname, types, type, minCred);
+			if (element != null &&
+			    element instanceof CacheRRset)
+			{
+				sr = new SetResponse(SetResponse.SUCCESSFUL);
+				sr.addRRset((CacheRRset) element);
+				return sr;
+			} else if (element != null) {
+				sr = new SetResponse(SetResponse.NXRRSET);
+				return sr;
+			}
+
+			element = oneElement(tname, types, Type.CNAME, minCred);
+			if (element != null &&
+			    element instanceof CacheRRset)
+			{
+				return new SetResponse(SetResponse.CNAME,
+						       (CacheRRset) element);
+			}
+		} else {
+			element = oneElement(tname, types, Type.DNAME, minCred);
+			if (element != null &&
+			    element instanceof CacheRRset)
+			{
+				return new SetResponse(SetResponse.DNAME,
+						       (CacheRRset) element);
+			}
+		}
+
+		/* Look for an NS */
+		element = oneElement(tname, types, Type.NS, minCred);
+		if (element != null && element instanceof CacheRRset)
+			return new SetResponse(SetResponse.DELEGATION,
+					       (CacheRRset) element);
+
+		/* Check for the special NXDOMAIN element. */
+		if (isExact) {
+			element = oneElement(tname, types, 0, minCred);
+			if (element != null)
+				return SetResponse.ofType(SetResponse.NXDOMAIN);
+		}
+
+	}
+	return SetResponse.ofType(SetResponse.UNKNOWN);
+}
+
+/**
+ * Looks up Records in the Cache.  This follows CNAMEs and handles negatively
+ * cached data.
+ * @param name The name to look up
+ * @param type The type to look up
+ * @param minCred The minimum acceptable credibility
+ * @return A SetResponse object
+ * @see SetResponse
+ * @see Credibility
+ */
+public SetResponse
+lookupRecords(Name name, int type, int minCred) {
+	return lookup(name, type, minCred);
+}
+
+private RRset []
+findRecords(Name name, int type, int minCred) {
+	SetResponse cr = lookupRecords(name, type, minCred);
+	if (cr.isSuccessful())
+		return cr.answers();
+	else
+		return null;
+}
+
+/**
+ * Looks up credible Records in the Cache (a wrapper around lookupRecords).
+ * Unlike lookupRecords, this given no indication of why failure occurred.
+ * @param name The name to look up
+ * @param type The type to look up
+ * @return An array of RRsets, or null
+ * @see Credibility
+ */
+public RRset []
+findRecords(Name name, int type) {
+	return findRecords(name, type, Credibility.NORMAL);
+}
+
+/**
+ * Looks up Records in the Cache (a wrapper around lookupRecords).  Unlike
+ * lookupRecords, this given no indication of why failure occurred.
+ * @param name The name to look up
+ * @param type The type to look up
+ * @return An array of RRsets, or null
+ * @see Credibility
+ */
+public RRset []
+findAnyRecords(Name name, int type) {
+	return findRecords(name, type, Credibility.GLUE);
+}
+
+private final int
+getCred(int section, boolean isAuth) {
+	if (section == Section.ANSWER) {
+		if (isAuth)
+			return Credibility.AUTH_ANSWER;
+		else
+			return Credibility.NONAUTH_ANSWER;
+	} else if (section == Section.AUTHORITY) {
+		if (isAuth)
+			return Credibility.AUTH_AUTHORITY;
+		else
+			return Credibility.NONAUTH_AUTHORITY;
+	} else if (section == Section.ADDITIONAL) {
+		return Credibility.ADDITIONAL;
+	} else
+		throw new IllegalArgumentException("getCred: invalid section");
+}
+
+private static void
+markAdditional(RRset rrset, Set names) {
+	Record first = rrset.first();
+	if (first.getAdditionalName() == null)
+		return;
+
+	Iterator it = rrset.rrs();
+	while (it.hasNext()) {
+		Record r = (Record) it.next();
+		Name name = r.getAdditionalName();
+		if (name != null)
+			names.add(name);
+	}
+}
+
+/**
+ * Adds all data from a Message into the Cache.  Each record is added with
+ * the appropriate credibility, and negative answers are cached as such.
+ * @param in The Message to be added
+ * @return A SetResponse that reflects what would be returned from a cache
+ * lookup, or null if nothing useful could be cached from the message.
+ * @see Message
+ */
+public SetResponse
+addMessage(Message in) {
+	boolean isAuth = in.getHeader().getFlag(Flags.AA);
+	Record question = in.getQuestion();
+	Name qname;
+	Name curname;
+	int qtype;
+	int qclass;
+	int cred;
+	int rcode = in.getHeader().getRcode();
+	boolean completed = false;
+	RRset [] answers, auth, addl;
+	SetResponse response = null;
+	boolean verbose = Options.check("verbosecache");
+	HashSet additionalNames;
+
+	if ((rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) ||
+	    question == null)
+		return null;
+
+	qname = question.getName();
+	qtype = question.getType();
+	qclass = question.getDClass();
+
+	curname = qname;
+
+	additionalNames = new HashSet();
+
+	answers = in.getSectionRRsets(Section.ANSWER);
+	for (int i = 0; i < answers.length; i++) {
+		if (answers[i].getDClass() != qclass)
+			continue;
+		int type = answers[i].getType();
+		Name name = answers[i].getName();
+		cred = getCred(Section.ANSWER, isAuth);
+		if ((type == qtype || qtype == Type.ANY) &&
+		    name.equals(curname))
+		{
+			addRRset(answers[i], cred);
+			completed = true;
+			if (curname == qname) {
+				if (response == null)
+					response = new SetResponse(
+							SetResponse.SUCCESSFUL);
+				response.addRRset(answers[i]);
+			}
+			markAdditional(answers[i], additionalNames);
+		} else if (type == Type.CNAME && name.equals(curname)) {
+			CNAMERecord cname;
+			addRRset(answers[i], cred);
+			if (curname == qname)
+				response = new SetResponse(SetResponse.CNAME,
+							   answers[i]);
+			cname = (CNAMERecord) answers[i].first();
+			curname = cname.getTarget();
+		} else if (type == Type.DNAME && curname.subdomain(name)) {
+			DNAMERecord dname;
+			addRRset(answers[i], cred);
+			if (curname == qname)
+				response = new SetResponse(SetResponse.DNAME,
+							   answers[i]);
+			dname = (DNAMERecord) answers[i].first();
+			try {
+				curname = curname.fromDNAME(dname);
+			}
+			catch (NameTooLongException e) {
+				break;
+			}
+		}
+	}
+
+	auth = in.getSectionRRsets(Section.AUTHORITY);
+	RRset soa = null, ns = null;
+	for (int i = 0; i < auth.length; i++) {
+		if (auth[i].getType() == Type.SOA &&
+		    curname.subdomain(auth[i].getName()))
+			soa = auth[i];
+		else if (auth[i].getType() == Type.NS &&
+			 curname.subdomain(auth[i].getName()))
+			ns = auth[i];
+	}
+	if (!completed) {
+		/* This is a negative response or a referral. */
+		int cachetype = (rcode == Rcode.NXDOMAIN) ? 0 : qtype;
+		if (rcode == Rcode.NXDOMAIN || soa != null || ns == null) {
+			/* Negative response */
+			cred = getCred(Section.AUTHORITY, isAuth);
+			SOARecord soarec = null;
+			if (soa != null)
+				soarec = (SOARecord) soa.first();
+			addNegative(curname, cachetype, soarec, cred);
+			if (response == null) {
+				int responseType;
+				if (rcode == Rcode.NXDOMAIN)
+					responseType = SetResponse.NXDOMAIN;
+				else
+					responseType = SetResponse.NXRRSET;
+				response = SetResponse.ofType(responseType);
+			}
+			/* DNSSEC records are not cached. */
+		} else {
+			/* Referral response */
+			cred = getCred(Section.AUTHORITY, isAuth);
+			addRRset(ns, cred);
+			markAdditional(ns, additionalNames);
+			if (response == null)
+				response = new SetResponse(
+							SetResponse.DELEGATION,
+							ns);
+		}
+	} else if (rcode == Rcode.NOERROR && ns != null) {
+		/* Cache the NS set from a positive response. */
+		cred = getCred(Section.AUTHORITY, isAuth);
+		addRRset(ns, cred);
+		markAdditional(ns, additionalNames);
+	}
+
+	addl = in.getSectionRRsets(Section.ADDITIONAL);
+	for (int i = 0; i < addl.length; i++) {
+		int type = addl[i].getType();
+		if (type != Type.A && type != Type.AAAA && type != Type.A6)
+			continue;
+		Name name = addl[i].getName();
+		if (!additionalNames.contains(name))
+			continue;
+		cred = getCred(Section.ADDITIONAL, isAuth);
+		addRRset(addl[i], cred);
+	}
+	if (verbose)
+		System.out.println("addMessage: " + response);
+	return (response);
+}
+
+/**
+ * Flushes an RRset from the cache
+ * @param name The name of the records to be flushed
+ * @param type The type of the records to be flushed
+ * @see RRset
+ */
+public void
+flushSet(Name name, int type) {
+	removeElement(name, type);
+}
+
+/**
+ * Flushes all RRsets with a given name from the cache
+ * @param name The name of the records to be flushed
+ * @see RRset
+ */
+public void
+flushName(Name name) {
+	removeName(name);
+}
+
+/**
+ * Sets the maximum length of time that a negative response will be stored
+ * in this Cache.  A negative value disables this feature (that is, sets
+ * no limit).
+ */
+public void
+setMaxNCache(int seconds) {
+	maxncache = seconds;
+}
+
+/**
+ * Gets the maximum length of time that a negative response will be stored
+ * in this Cache.  A negative value indicates no limit.
+ */
+public int
+getMaxNCache() {
+	return maxncache;
+}
+
+/**
+ * Sets the maximum length of time that records will be stored in this
+ * Cache.  A negative value disables this feature (that is, sets no limit).
+ */
+public void
+setMaxCache(int seconds) {
+	maxcache = seconds;
+}
+
+/**
+ * Gets the maximum length of time that records will be stored
+ * in this Cache.  A negative value indicates no limit.
+ */
+public int
+getMaxCache() {
+	return maxcache;
+}
+
+/**
+ * Gets the current number of entries in the Cache, where an entry consists
+ * of all records with a specific Name.
+ */
+public int
+getSize() {
+	return data.size();
+}
+
+/**
+ * Gets the maximum number of entries in the Cache, where an entry consists
+ * of all records with a specific Name.  A negative value is treated as an
+ * infinite limit.
+ */
+public int
+getMaxEntries() {
+	return data.getMaxSize();
+}
+
+/**
+ * Sets the maximum number of entries in the Cache, where an entry consists
+ * of all records with a specific Name.  A negative value is treated as an
+ * infinite limit.
+ *
+ * Note that setting this to a value lower than the current number
+ * of entries will not cause the Cache to shrink immediately.
+ *
+ * The default maximum number of entries is 50000.
+ *
+ * @param entries The maximum number of entries in the Cache.
+ */
+public void
+setMaxEntries(int entries) {
+	data.setMaxSize(entries);
+}
+
+/**
+ * Returns the DNS class of this cache.
+ */
+public int
+getDClass() {
+	return dclass;
+}
+
+/**
+ * Returns the contents of the Cache as a string.
+ */ 
+public String
+toString() {
+	StringBuffer sb = new StringBuffer();
+	synchronized (this) {
+		Iterator it = data.values().iterator();
+		while (it.hasNext()) {
+			Element [] elements = allElements(it.next());
+			for (int i = 0; i < elements.length; i++) {
+				sb.append(elements[i]);
+				sb.append("\n");
+			}
+		}
+	}
+	return sb.toString();
+}
+
+}
diff --git a/src/org/xbill/DNS/Client.java b/src/org/xbill/DNS/Client.java
new file mode 100644
index 0000000..2eef44f
--- /dev/null
+++ b/src/org/xbill/DNS/Client.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2005 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.net.*;
+import java.nio.channels.*;
+import org.xbill.DNS.utils.hexdump;
+
+class Client {
+
+protected long endTime;
+protected SelectionKey key;
+
+protected
+Client(SelectableChannel channel, long endTime) throws IOException {
+	boolean done = false;
+	Selector selector = null;
+	this.endTime = endTime;
+	try {
+		selector = Selector.open();
+		channel.configureBlocking(false);
+		key = channel.register(selector, SelectionKey.OP_READ);
+		done = true;
+	}
+	finally {
+		if (!done && selector != null)
+			selector.close();
+		if (!done)
+			channel.close();
+	}
+}
+
+static protected void
+blockUntil(SelectionKey key, long endTime) throws IOException {
+	long timeout = endTime - System.currentTimeMillis();
+	int nkeys = 0;
+	if (timeout > 0)
+		nkeys = key.selector().select(timeout);
+	else if (timeout == 0)
+		nkeys = key.selector().selectNow();
+	if (nkeys == 0)
+		throw new SocketTimeoutException();
+}
+
+static protected void
+verboseLog(String prefix, byte [] data) {
+	if (Options.check("verbosemsg"))
+		System.err.println(hexdump.dump(prefix, data));
+}
+
+void
+cleanup() throws IOException {
+	key.selector().close();
+	key.channel().close();
+}
+
+}
diff --git a/src/org/xbill/DNS/ClientSubnetOption.java b/src/org/xbill/DNS/ClientSubnetOption.java
new file mode 100644
index 0000000..4a98a12
--- /dev/null
+++ b/src/org/xbill/DNS/ClientSubnetOption.java
@@ -0,0 +1,175 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.net.*;
+import java.util.regex.*;
+
+/**
+ * The Client Subnet EDNS Option, defined in
+ * http://tools.ietf.org/html/draft-vandergaast-edns-client-subnet-00
+ * ("Client subnet in DNS requests").
+ *
+ * The option is used to convey information about the IP address of the
+ * originating client, so that an authoritative server can make decisions
+ * based on this address, rather than the address of the intermediate
+ * caching name server.
+ *
+ * The option is transmitted as part of an OPTRecord in the additional section
+ * of a DNS message, as defined by RFC 2671 (EDNS0).
+ * 
+ * An option code has not been assigned by IANA; the value 20730 (used here) is
+ * also used by several other implementations.
+ *
+ * The wire format of the option contains a 2-byte length field (1 for IPv4, 2
+ * for IPv6), a 1-byte source netmask, a 1-byte scope netmask, and an address
+ * truncated to the source netmask length (where the final octet is padded with
+ * bits set to 0)
+ * 
+ *
+ * @see OPTRecord
+ * 
+ * @author Brian Wellington
+ * @author Ming Zhou &lt;mizhou@bnivideo.com&gt;, Beaumaris Networks
+ */
+public class ClientSubnetOption extends EDNSOption {
+
+private static final long serialVersionUID = -3868158449890266347L;
+
+private int family;
+private int sourceNetmask;
+private int scopeNetmask;
+private InetAddress address;
+
+ClientSubnetOption() {
+	super(EDNSOption.Code.CLIENT_SUBNET);
+}
+
+private static int
+checkMaskLength(String field, int family, int val) {
+	int max = Address.addressLength(family) * 8;
+	if (val < 0 || val > max)
+		throw new IllegalArgumentException("\"" + field + "\" " + val +
+						   " must be in the range " +
+						   "[0.." + max + "]");
+	return val;
+}
+
+/**
+ * Construct a Client Subnet option.  Note that the number of significant bits in
+ * the address must not be greater than the supplied source netmask.
+ * XXX something about Java's mapped addresses
+ * @param sourceNetmask The length of the netmask pertaining to the query.
+ * In replies, it mirrors the same value as in the requests.
+ * @param scopeNetmask The length of the netmask pertaining to the reply.
+ * In requests, it MUST be set to 0.  In responses, this may or may not match
+ * the source netmask.
+ * @param address The address of the client.
+ */
+public 
+ClientSubnetOption(int sourceNetmask, int scopeNetmask, InetAddress address) {
+	super(EDNSOption.Code.CLIENT_SUBNET);
+
+	this.family = Address.familyOf(address);
+	this.sourceNetmask = checkMaskLength("source netmask", this.family,
+					     sourceNetmask);
+	this.scopeNetmask = checkMaskLength("scope netmask", this.family,
+					     scopeNetmask);
+	this.address = Address.truncate(address, sourceNetmask);
+
+	if (!address.equals(this.address))
+		throw new IllegalArgumentException("source netmask is not " +
+						   "valid for address");
+}
+
+/**
+ * Construct a Client Subnet option with scope netmask set to 0.
+ * @param sourceNetmask The length of the netmask pertaining to the query.
+ * In replies, it mirrors the same value as in the requests.
+ * @param address The address of the client.
+ * @see ClientSubnetOption
+ */
+public 
+ClientSubnetOption(int sourceNetmask, InetAddress address) {
+	this(sourceNetmask, 0, address);
+}
+
+/**
+ * Returns the family of the network address.  This will be either IPv4 (1)
+ * or IPv6 (2).
+ */
+public int 
+getFamily() {
+	return family;
+}
+
+/** Returns the source netmask. */
+public int 
+getSourceNetmask() {
+	return sourceNetmask;
+}
+
+/** Returns the scope netmask. */
+public int 
+getScopeNetmask() {
+	return scopeNetmask;
+}
+
+/** Returns the IP address of the client. */
+public InetAddress 
+getAddress() {
+	return address;
+}
+
+void 
+optionFromWire(DNSInput in) throws WireParseException {
+	family = in.readU16();
+	if (family != Address.IPv4 && family != Address.IPv6)
+		throw new WireParseException("unknown address family");
+	sourceNetmask = in.readU8();
+	if (sourceNetmask > Address.addressLength(family) * 8)
+		throw new WireParseException("invalid source netmask");
+	scopeNetmask = in.readU8();
+	if (scopeNetmask > Address.addressLength(family) * 8)
+		throw new WireParseException("invalid scope netmask");
+
+	// Read the truncated address
+	byte [] addr = in.readByteArray();
+	if (addr.length != (sourceNetmask + 7) / 8)
+		throw new WireParseException("invalid address");
+
+	// Convert it to a full length address.
+	byte [] fulladdr = new byte[Address.addressLength(family)];
+	System.arraycopy(addr, 0, fulladdr, 0, addr.length);
+
+	try {
+		address = InetAddress.getByAddress(fulladdr);
+	} catch (UnknownHostException e) {
+		throw new WireParseException("invalid address", e);
+	}
+
+	InetAddress tmp = Address.truncate(address, sourceNetmask);
+	if (!tmp.equals(address))
+		throw new WireParseException("invalid padding");
+}
+
+void 
+optionToWire(DNSOutput out) {
+	out.writeU16(family);
+	out.writeU8(sourceNetmask);
+	out.writeU8(scopeNetmask);
+	out.writeByteArray(address.getAddress(), 0, (sourceNetmask + 7) / 8);
+}
+
+String 
+optionToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(address.getHostAddress());
+	sb.append("/");
+	sb.append(sourceNetmask);
+	sb.append(", scope netmask ");
+	sb.append(scopeNetmask);
+	return sb.toString();
+}
+
+}
diff --git a/src/org/xbill/DNS/Compression.java b/src/org/xbill/DNS/Compression.java
new file mode 100644
index 0000000..e3e81c0
--- /dev/null
+++ b/src/org/xbill/DNS/Compression.java
@@ -0,0 +1,72 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * DNS Name Compression object.
+ * @see Message
+ * @see Name
+ *
+ * @author Brian Wellington
+ */
+
+public class Compression {
+
+private static class Entry {
+	Name name;
+	int pos;
+	Entry next;
+}
+
+private static final int TABLE_SIZE = 17;
+private static final int MAX_POINTER = 0x3FFF;
+private Entry [] table;
+private boolean verbose = Options.check("verbosecompression");
+
+/**
+ * Creates a new Compression object.
+ */
+public
+Compression() {
+	table = new Entry[TABLE_SIZE];
+}
+
+/**
+ * Adds a compression entry mapping a name to a position in a message.
+ * @param pos The position at which the name is added.
+ * @param name The name being added to the message.
+ */
+public void
+add(int pos, Name name) {
+	if (pos > MAX_POINTER)
+		return;
+	int row = (name.hashCode() & 0x7FFFFFFF) % TABLE_SIZE;
+	Entry entry = new Entry();
+	entry.name = name;
+	entry.pos = pos;
+	entry.next = table[row];
+	table[row] = entry;
+	if (verbose)
+		System.err.println("Adding " + name + " at " + pos);
+}
+
+/**
+ * Retrieves the position of the given name, if it has been previously
+ * included in the message.
+ * @param name The name to find in the compression table.
+ * @return The position of the name, or -1 if not found.
+ */
+public int
+get(Name name) {
+	int row = (name.hashCode() & 0x7FFFFFFF) % TABLE_SIZE;
+	int pos = -1;
+	for (Entry entry = table[row]; entry != null; entry = entry.next) {
+		if (entry.name.equals(name))
+			pos = entry.pos;
+	}
+	if (verbose)
+		System.err.println("Looking for " + name + ", found " + pos);
+	return pos;
+}
+
+}
diff --git a/src/org/xbill/DNS/Credibility.java b/src/org/xbill/DNS/Credibility.java
new file mode 100644
index 0000000..fa10686
--- /dev/null
+++ b/src/org/xbill/DNS/Credibility.java
@@ -0,0 +1,50 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Constants relating to the credibility of cached data, which is based on
+ * the data's source.  The constants NORMAL and ANY should be used by most
+ * callers.
+ * @see Cache
+ * @see Section
+ *
+ * @author Brian Wellington
+ */
+
+public final class Credibility {
+
+private
+Credibility() {}
+
+/** A hint or cache file on disk. */
+public static final int HINT			= 0;
+
+/** The additional section of a response. */
+public static final int ADDITIONAL		= 1;
+
+/** The additional section of a response. */
+public static final int GLUE			= 2;
+
+/** The authority section of a nonauthoritative response. */
+public static final int NONAUTH_AUTHORITY	= 3;
+
+/** The answer section of a nonauthoritative response. */
+public static final int NONAUTH_ANSWER		= 3;
+
+/** The authority section of an authoritative response. */
+public static final int AUTH_AUTHORITY		= 4;
+
+/** The answer section of a authoritative response. */
+public static final int AUTH_ANSWER		= 4;
+
+/** A zone. */
+public static final int ZONE			= 5;
+
+/** Credible data. */
+public static final int NORMAL			= 3;
+
+/** Data not required to be credible. */
+public static final int ANY			= 1;
+
+}
diff --git a/src/org/xbill/DNS/DClass.java b/src/org/xbill/DNS/DClass.java
new file mode 100644
index 0000000..22180cf
--- /dev/null
+++ b/src/org/xbill/DNS/DClass.java
@@ -0,0 +1,92 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Constants and functions relating to DNS classes.  This is called DClass
+ * to avoid confusion with Class.
+ *
+ * @author Brian Wellington
+ */
+
+public final class DClass {
+
+/** Internet */
+public static final int IN		= 1;
+
+/** Chaos network (MIT) */
+public static final int CH		= 3;
+
+/** Chaos network (MIT, alternate name) */
+public static final int CHAOS		= 3;
+
+/** Hesiod name server (MIT) */
+public static final int HS		= 4;
+
+/** Hesiod name server (MIT, alternate name) */
+public static final int HESIOD		= 4;
+
+/** Special value used in dynamic update messages */
+public static final int NONE		= 254;
+
+/** Matches any class */
+public static final int ANY		= 255;
+
+private static class DClassMnemonic extends Mnemonic {
+	public
+	DClassMnemonic() {
+		super("DClass", CASE_UPPER);
+		setPrefix("CLASS");
+	}
+
+	public void
+	check(int val) {
+		DClass.check(val);
+	}
+}
+
+private static Mnemonic classes = new DClassMnemonic();
+
+static {
+	classes.add(IN, "IN");
+	classes.add(CH, "CH");
+	classes.addAlias(CH, "CHAOS");
+	classes.add(HS, "HS");
+	classes.addAlias(HS, "HESIOD");
+	classes.add(NONE, "NONE");
+	classes.add(ANY, "ANY");
+}
+
+private
+DClass() {}
+
+/**
+ * Checks that a numeric DClass is valid.
+ * @throws InvalidDClassException The class is out of range.
+ */
+public static void
+check(int i) {
+	if (i < 0 || i > 0xFFFF)
+		throw new InvalidDClassException(i);
+}
+
+/**
+ * Converts a numeric DClass into a String
+ * @return The canonical string representation of the class
+ * @throws InvalidDClassException The class is out of range.
+ */
+public static String
+string(int i) {
+	return classes.getText(i);
+}
+
+/**
+ * Converts a String representation of a DClass into its numeric value
+ * @return The class code, or -1 on error.
+ */
+public static int
+value(String s) {
+	return classes.getValue(s);
+}
+
+}
diff --git a/src/org/xbill/DNS/DHCIDRecord.java b/src/org/xbill/DNS/DHCIDRecord.java
new file mode 100644
index 0000000..e160a8c
--- /dev/null
+++ b/src/org/xbill/DNS/DHCIDRecord.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2008 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import org.xbill.DNS.utils.base64;
+
+/**
+ * DHCID - Dynamic Host Configuration Protocol (DHCP) ID (RFC 4701)
+ *
+ * @author Brian Wellington
+ */
+
+public class DHCIDRecord extends Record {
+
+private static final long serialVersionUID = -8214820200808997707L;
+
+private byte [] data;
+
+DHCIDRecord() {}
+
+Record
+getObject() {
+	return new DHCIDRecord();
+}
+
+/**
+ * Creates an DHCID Record from the given data
+ * @param data The binary data, which is opaque to DNS.
+ */
+public
+DHCIDRecord(Name name, int dclass, long ttl, byte [] data) {
+	super(name, Type.DHCID, dclass, ttl);
+	this.data = data;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	data = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	data = st.getBase64();
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeByteArray(data);
+}
+
+String
+rrToString() {
+	return base64.toString(data);
+}
+
+/**
+ * Returns the binary data.
+ */
+public byte []
+getData() {
+	return data;
+}
+
+}
diff --git a/src/org/xbill/DNS/DLVRecord.java b/src/org/xbill/DNS/DLVRecord.java
new file mode 100644
index 0000000..8acc90f
--- /dev/null
+++ b/src/org/xbill/DNS/DLVRecord.java
@@ -0,0 +1,132 @@
+// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * DLV - contains a Delegation Lookaside Validation record, which acts
+ * as the equivalent of a DS record in a lookaside zone.
+ * @see DNSSEC
+ * @see DSRecord
+ *
+ * @author David Blacka
+ * @author Brian Wellington
+ */
+
+public class DLVRecord extends Record {
+
+public static final int SHA1_DIGEST_ID = DSRecord.Digest.SHA1;
+public static final int SHA256_DIGEST_ID = DSRecord.Digest.SHA1;
+
+private static final long serialVersionUID = 1960742375677534148L;
+
+private int footprint;
+private int alg;
+private int digestid;
+private byte [] digest;
+
+DLVRecord() {}
+
+Record
+getObject() {
+	return new DLVRecord();
+}
+
+/**
+ * Creates a DLV Record from the given data
+ * @param footprint The original KEY record's footprint (keyid).
+ * @param alg The original key algorithm.
+ * @param digestid The digest id code.
+ * @param digest A hash of the original key.
+ */
+public
+DLVRecord(Name name, int dclass, long ttl, int footprint, int alg,
+	  int digestid, byte [] digest)
+{
+	super(name, Type.DLV, dclass, ttl);
+	this.footprint = checkU16("footprint", footprint);
+	this.alg = checkU8("alg", alg);
+	this.digestid = checkU8("digestid", digestid);
+	this.digest = digest;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	footprint = in.readU16();
+	alg = in.readU8();
+	digestid = in.readU8();
+	digest = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	footprint = st.getUInt16();
+	alg = st.getUInt8();
+	digestid = st.getUInt8();
+	digest = st.getHex();
+}
+
+/**
+ * Converts rdata to a String
+ */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(footprint);
+	sb.append(" ");
+	sb.append(alg);
+	sb.append(" ");
+	sb.append(digestid);
+	if (digest != null) {
+		sb.append(" ");
+		sb.append(base16.toString(digest));
+	}
+
+	return sb.toString();
+}
+
+/**
+ * Returns the key's algorithm.
+ */
+public int
+getAlgorithm() {
+	return alg;
+}
+
+/**
+ *  Returns the key's Digest ID.
+ */
+public int
+getDigestID()
+{
+	return digestid;
+}
+  
+/**
+ * Returns the binary hash of the key.
+ */
+public byte []
+getDigest() {
+	return digest;
+}
+
+/**
+ * Returns the key's footprint.
+ */
+public int
+getFootprint() {
+	return footprint;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU16(footprint);
+	out.writeU8(alg);
+	out.writeU8(digestid);
+	if (digest != null)
+		out.writeByteArray(digest);
+}
+
+}
diff --git a/src/org/xbill/DNS/DNAMERecord.java b/src/org/xbill/DNS/DNAMERecord.java
new file mode 100644
index 0000000..cbb322f
--- /dev/null
+++ b/src/org/xbill/DNS/DNAMERecord.java
@@ -0,0 +1,45 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * DNAME Record  - maps a nonterminal alias (subtree) to a different domain
+ *
+ * @author Brian Wellington
+ */
+
+public class DNAMERecord extends SingleNameBase {
+
+private static final long serialVersionUID = 2670767677200844154L;
+
+DNAMERecord() {}
+
+Record
+getObject() {
+	return new DNAMERecord();
+}
+
+/**
+ * Creates a new DNAMERecord with the given data
+ * @param alias The name to which the DNAME alias points
+ */
+public
+DNAMERecord(Name name, int dclass, long ttl, Name alias) {
+	super(name, Type.DNAME, dclass, ttl, alias, "alias");
+}
+
+/**
+ * Gets the target of the DNAME Record
+ */
+public Name
+getTarget() {
+	return getSingleName();
+}
+
+/** Gets the alias specified by the DNAME Record */
+public Name
+getAlias() {
+	return getSingleName();
+}
+
+}
diff --git a/src/org/xbill/DNS/DNSInput.java b/src/org/xbill/DNS/DNSInput.java
new file mode 100644
index 0000000..d3134ed
--- /dev/null
+++ b/src/org/xbill/DNS/DNSInput.java
@@ -0,0 +1,239 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * An class for parsing DNS messages.
+ *
+ * @author Brian Wellington
+ */
+
+public class DNSInput {
+
+private byte [] array;
+private int pos;
+private int end;
+private int saved_pos;
+private int saved_end;
+
+/**
+ * Creates a new DNSInput
+ * @param input The byte array to read from
+ */
+public
+DNSInput(byte [] input) {
+	array = input;
+	pos = 0;
+	end = array.length;
+	saved_pos = -1;
+	saved_end = -1;
+}
+
+/**
+ * Returns the current position.
+ */
+public int
+current() {
+	return pos;
+}
+
+/**
+ * Returns the number of bytes that can be read from this stream before
+ * reaching the end.
+ */
+public int
+remaining() {
+	return end - pos;
+}
+
+private void
+require(int n) throws WireParseException{
+	if (n > remaining()) {
+		throw new WireParseException("end of input");
+	}
+}
+
+/**
+ * Marks the following bytes in the stream as active.
+ * @param len The number of bytes in the active region.
+ * @throws IllegalArgumentException The number of bytes in the active region
+ * is longer than the remainder of the input.
+ */
+public void
+setActive(int len) {
+	if (len > array.length - pos) {
+		throw new IllegalArgumentException("cannot set active " +
+						   "region past end of input");
+	}
+	end = pos + len;
+}
+
+/**
+ * Clears the active region of the string.  Further operations are not
+ * restricted to part of the input.
+ */
+public void
+clearActive() {
+	end = array.length;
+}
+
+/**
+ * Returns the position of the end of the current active region.
+ */
+public int
+saveActive() {
+	return end;
+}
+
+/**
+ * Restores the previously set active region.  This differs from setActive() in
+ * that restoreActive() takes an absolute position, and setActive takes an
+ * offset from the current location.
+ * @param pos The end of the active region.
+ */
+public void
+restoreActive(int pos) {
+	if (pos > array.length) {
+		throw new IllegalArgumentException("cannot set active " +
+						   "region past end of input");
+	}
+	end = pos;
+}
+
+/**
+ * Resets the current position of the input stream to the specified index,
+ * and clears the active region.
+ * @param index The position to continue parsing at.
+ * @throws IllegalArgumentException The index is not within the input.
+ */
+public void
+jump(int index) {
+	if (index >= array.length) {
+		throw new IllegalArgumentException("cannot jump past " +
+						   "end of input");
+	}
+	pos = index;
+	end = array.length;
+}
+
+/**
+ * Saves the current state of the input stream.  Both the current position and
+ * the end of the active region are saved.
+ * @throws IllegalArgumentException The index is not within the input.
+ */
+public void
+save() {
+	saved_pos = pos;
+	saved_end = end;
+}
+
+/**
+ * Restores the input stream to its state before the call to {@link #save}.
+ */
+public void
+restore() {
+	if (saved_pos < 0) {
+		throw new IllegalStateException("no previous state");
+	}
+	pos = saved_pos;
+	end = saved_end;
+	saved_pos = -1;
+	saved_end = -1;
+}
+
+/**
+ * Reads an unsigned 8 bit value from the stream, as an int.
+ * @return An unsigned 8 bit value.
+ * @throws WireParseException The end of the stream was reached.
+ */
+public int
+readU8() throws WireParseException {
+	require(1);
+	return (array[pos++] & 0xFF);
+}
+
+/**
+ * Reads an unsigned 16 bit value from the stream, as an int.
+ * @return An unsigned 16 bit value.
+ * @throws WireParseException The end of the stream was reached.
+ */
+public int
+readU16() throws WireParseException {
+	require(2);
+	int b1 = array[pos++] & 0xFF;
+	int b2 = array[pos++] & 0xFF;
+	return ((b1 << 8) + b2);
+}
+
+/**
+ * Reads an unsigned 32 bit value from the stream, as a long.
+ * @return An unsigned 32 bit value.
+ * @throws WireParseException The end of the stream was reached.
+ */
+public long
+readU32() throws WireParseException {
+	require(4);
+	int b1 = array[pos++] & 0xFF;
+	int b2 = array[pos++] & 0xFF;
+	int b3 = array[pos++] & 0xFF;
+	int b4 = array[pos++] & 0xFF;
+	return (((long)b1 << 24) + (b2 << 16) + (b3 << 8) + b4);
+}
+
+/**
+ * Reads a byte array of a specified length from the stream into an existing
+ * array.
+ * @param b The array to read into.
+ * @param off The offset of the array to start copying data into.
+ * @param len The number of bytes to copy.
+ * @throws WireParseException The end of the stream was reached.
+ */
+public void
+readByteArray(byte [] b, int off, int len) throws WireParseException {
+	require(len);
+	System.arraycopy(array, pos, b, off, len);
+	pos += len;
+}
+
+/**
+ * Reads a byte array of a specified length from the stream.
+ * @return The byte array.
+ * @throws WireParseException The end of the stream was reached.
+ */
+public byte []
+readByteArray(int len) throws WireParseException {
+	require(len);
+	byte [] out = new byte[len];
+	System.arraycopy(array, pos, out, 0, len);
+	pos += len;
+	return out;
+}
+
+/**
+ * Reads a byte array consisting of the remainder of the stream (or the
+ * active region, if one is set.
+ * @return The byte array.
+ */
+public byte []
+readByteArray() {
+	int len = remaining();
+	byte [] out = new byte[len];
+	System.arraycopy(array, pos, out, 0, len);
+	pos += len;
+	return out;
+}
+
+/**
+ * Reads a counted string from the stream.  A counted string is a one byte
+ * value indicating string length, followed by bytes of data.
+ * @return A byte array containing the string.
+ * @throws WireParseException The end of the stream was reached.
+ */
+public byte []
+readCountedString() throws WireParseException {
+	require(1);
+	int len = array[pos++] & 0xFF;
+	return readByteArray(len);
+}
+
+}
diff --git a/src/org/xbill/DNS/DNSKEYRecord.java b/src/org/xbill/DNS/DNSKEYRecord.java
new file mode 100644
index 0000000..6e9bafd
--- /dev/null
+++ b/src/org/xbill/DNS/DNSKEYRecord.java
@@ -0,0 +1,91 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.security.PublicKey;
+
+/**
+ * Key - contains a cryptographic public key for use by DNS.
+ * The data can be converted to objects implementing
+ * java.security.interfaces.PublicKey
+ * @see DNSSEC
+ *
+ * @author Brian Wellington
+ */
+
+public class DNSKEYRecord extends KEYBase {
+
+public static class Protocol {
+	private Protocol() {}
+
+	/** Key will be used for DNSSEC */
+	public static final int DNSSEC = 3;
+}
+
+public static class Flags {
+	private Flags() {}
+
+	/** Key is a zone key */
+	public static final int ZONE_KEY = 0x100;
+
+	/** Key is a secure entry point key */
+	public static final int SEP_KEY = 0x1;
+
+	/** Key has been revoked */
+	public static final int REVOKE = 0x80;
+}
+
+private static final long serialVersionUID = -8679800040426675002L;
+
+DNSKEYRecord() {}
+
+Record
+getObject() {
+	return new DNSKEYRecord();
+}
+
+/**
+ * Creates a DNSKEY Record from the given data
+ * @param flags Flags describing the key's properties
+ * @param proto The protocol that the key was created for
+ * @param alg The key's algorithm
+ * @param key Binary representation of the key
+ */
+public
+DNSKEYRecord(Name name, int dclass, long ttl, int flags, int proto, int alg,
+	     byte [] key)
+{
+	super(name, Type.DNSKEY, dclass, ttl, flags, proto, alg, key);
+}
+
+/**
+ * Creates a DNSKEY Record from the given data
+ * @param flags Flags describing the key's properties
+ * @param proto The protocol that the key was created for
+ * @param alg The key's algorithm
+ * @param key The key as a PublicKey
+ * @throws DNSSEC.DNSSECException The PublicKey could not be converted into DNS
+ * format.
+ */
+public
+DNSKEYRecord(Name name, int dclass, long ttl, int flags, int proto, int alg,
+	     PublicKey key) throws DNSSEC.DNSSECException
+{
+	super(name, Type.DNSKEY, dclass, ttl, flags, proto, alg,
+	      DNSSEC.fromPublicKey(key, alg));
+	publicKey = key;
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	flags = st.getUInt16();
+	proto = st.getUInt8();
+	String algString = st.getString();
+	alg = DNSSEC.Algorithm.value(algString);
+	if (alg < 0)
+		throw st.exception("Invalid algorithm: " + algString);
+	key = st.getBase64();
+}
+
+}
diff --git a/src/org/xbill/DNS/DNSOutput.java b/src/org/xbill/DNS/DNSOutput.java
new file mode 100644
index 0000000..29a8f68
--- /dev/null
+++ b/src/org/xbill/DNS/DNSOutput.java
@@ -0,0 +1,203 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * A class for rendering DNS messages.
+ *
+ * @author Brian Wellington
+ */
+
+
+public class DNSOutput {
+
+private byte [] array;
+private int pos;
+private int saved_pos;
+
+/**
+ * Create a new DNSOutput with a specified size.
+ * @param size The initial size
+ */
+public
+DNSOutput(int size) {
+	array = new byte[size];
+	pos = 0;
+	saved_pos = -1;
+}
+
+/**
+ * Create a new DNSOutput
+ */
+public
+DNSOutput() {
+	this(32);
+}
+
+/**
+ * Returns the current position.
+ */
+public int
+current() {
+	return pos;
+}
+
+private void
+check(long val, int bits) {
+	long max = 1;
+	max <<= bits;
+	if (val < 0 || val > max) {
+		throw new IllegalArgumentException(val + " out of range for " +
+						   bits + " bit value");
+	}
+}
+
+private void
+need(int n) {
+	if (array.length - pos >= n) {
+		return;
+	}
+	int newsize = array.length * 2;
+	if (newsize < pos + n) {
+		newsize = pos + n;
+	}
+	byte [] newarray = new byte[newsize];
+	System.arraycopy(array, 0, newarray, 0, pos);
+	array = newarray;
+}
+
+/**
+ * Resets the current position of the output stream to the specified index.
+ * @param index The new current position.
+ * @throws IllegalArgumentException The index is not within the output.
+ */
+public void
+jump(int index) {
+	if (index > pos) {
+		throw new IllegalArgumentException("cannot jump past " +
+						   "end of data");
+	}
+	pos = index;
+}
+
+/**
+ * Saves the current state of the output stream.
+ * @throws IllegalArgumentException The index is not within the output.
+ */
+public void
+save() {
+	saved_pos = pos;
+}
+
+/**
+ * Restores the input stream to its state before the call to {@link #save}.
+ */
+public void
+restore() {
+	if (saved_pos < 0) {
+		throw new IllegalStateException("no previous state");
+	}
+	pos = saved_pos;
+	saved_pos = -1;
+}
+
+/**
+ * Writes an unsigned 8 bit value to the stream.
+ * @param val The value to be written
+ */
+public void
+writeU8(int val) {
+	check(val, 8);
+	need(1);
+	array[pos++] = (byte)(val & 0xFF);
+}
+
+/**
+ * Writes an unsigned 16 bit value to the stream.
+ * @param val The value to be written
+ */
+public void
+writeU16(int val) {
+	check(val, 16);
+	need(2);
+	array[pos++] = (byte)((val >>> 8) & 0xFF);
+	array[pos++] = (byte)(val & 0xFF);
+}
+
+/**
+ * Writes an unsigned 16 bit value to the specified position in the stream.
+ * @param val The value to be written
+ * @param where The position to write the value.
+ */
+public void
+writeU16At(int val, int where) {
+	check(val, 16);
+	if (where > pos - 2)
+		throw new IllegalArgumentException("cannot write past " +
+						   "end of data");
+	array[where++] = (byte)((val >>> 8) & 0xFF);
+	array[where++] = (byte)(val & 0xFF);
+}
+
+/**
+ * Writes an unsigned 32 bit value to the stream.
+ * @param val The value to be written
+ */
+public void
+writeU32(long val) {
+	check(val, 32);
+	need(4);
+	array[pos++] = (byte)((val >>> 24) & 0xFF);
+	array[pos++] = (byte)((val >>> 16) & 0xFF);
+	array[pos++] = (byte)((val >>> 8) & 0xFF);
+	array[pos++] = (byte)(val & 0xFF);
+}
+
+/**
+ * Writes a byte array to the stream.
+ * @param b The array to write.
+ * @param off The offset of the array to start copying data from.
+ * @param len The number of bytes to write.
+ */
+public void
+writeByteArray(byte [] b, int off, int len) {
+	need(len);
+	System.arraycopy(b, off, array, pos, len);
+	pos += len;
+}
+
+/**
+ * Writes a byte array to the stream.
+ * @param b The array to write.
+ */
+public void
+writeByteArray(byte [] b) {
+	writeByteArray(b, 0, b.length);
+}
+
+/**
+ * Writes a counted string from the stream.  A counted string is a one byte
+ * value indicating string length, followed by bytes of data.
+ * @param s The string to write.
+ */
+public void
+writeCountedString(byte [] s) {
+	if (s.length > 0xFF) {
+		throw new IllegalArgumentException("Invalid counted string");
+	}
+	need(1 + s.length);
+	array[pos++] = (byte)(s.length & 0xFF);
+	writeByteArray(s, 0, s.length);
+}
+
+/**
+ * Returns a byte array containing the current contents of the stream.
+ */
+public byte []
+toByteArray() {
+	byte [] out = new byte[pos];
+	System.arraycopy(array, 0, out, 0, pos);
+	return out;
+}
+
+}
diff --git a/src/org/xbill/DNS/DNSSEC.java b/src/org/xbill/DNS/DNSSEC.java
new file mode 100644
index 0000000..2707947
--- /dev/null
+++ b/src/org/xbill/DNS/DNSSEC.java
@@ -0,0 +1,1026 @@
+// Copyright (c) 1999-2010 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.math.*;
+import java.security.*;
+import java.security.interfaces.*;
+import java.security.spec.*;
+import java.util.*;
+
+/**
+ * Constants and methods relating to DNSSEC.
+ *
+ * DNSSEC provides authentication for DNS information.
+ * @see RRSIGRecord
+ * @see DNSKEYRecord
+ * @see RRset
+ *
+ * @author Brian Wellington
+ */
+
+public class DNSSEC {
+
+public static class Algorithm {
+	private Algorithm() {}
+
+	/** RSA/MD5 public key (deprecated) */
+	public static final int RSAMD5 = 1;
+
+	/** Diffie Hellman key */
+	public static final int DH = 2;
+
+	/** DSA public key */
+	public static final int DSA = 3;
+
+	/** RSA/SHA1 public key */
+	public static final int RSASHA1 = 5;
+
+	/** DSA/SHA1, NSEC3-aware public key */
+	public static final int DSA_NSEC3_SHA1 = 6;
+
+	/** RSA/SHA1, NSEC3-aware public key */
+	public static final int RSA_NSEC3_SHA1 = 7;
+
+	/** RSA/SHA256 public key */
+	public static final int RSASHA256 = 8;
+
+	/** RSA/SHA512 public key */
+	public static final int RSASHA512 = 10;
+
+	/** ECDSA Curve P-256 with SHA-256 public key **/
+	public static final int ECDSAP256SHA256 = 13;
+
+	/** ECDSA Curve P-384 with SHA-384 public key **/
+	public static final int ECDSAP384SHA384 = 14;
+
+	/** Indirect keys; the actual key is elsewhere. */
+	public static final int INDIRECT = 252;
+
+	/** Private algorithm, specified by domain name */
+	public static final int PRIVATEDNS = 253;
+
+	/** Private algorithm, specified by OID */
+	public static final int PRIVATEOID = 254;
+
+	private static Mnemonic algs = new Mnemonic("DNSSEC algorithm",
+						    Mnemonic.CASE_UPPER);
+
+	static {
+		algs.setMaximum(0xFF);
+		algs.setNumericAllowed(true);
+
+		algs.add(RSAMD5, "RSAMD5");
+		algs.add(DH, "DH");
+		algs.add(DSA, "DSA");
+		algs.add(RSASHA1, "RSASHA1");
+		algs.add(DSA_NSEC3_SHA1, "DSA-NSEC3-SHA1");
+		algs.add(RSA_NSEC3_SHA1, "RSA-NSEC3-SHA1");
+		algs.add(RSASHA256, "RSASHA256");
+		algs.add(RSASHA512, "RSASHA512");
+		algs.add(ECDSAP256SHA256, "ECDSAP256SHA256");
+		algs.add(ECDSAP384SHA384, "ECDSAP384SHA384");
+		algs.add(INDIRECT, "INDIRECT");
+		algs.add(PRIVATEDNS, "PRIVATEDNS");
+		algs.add(PRIVATEOID, "PRIVATEOID");
+	}
+
+	/**
+	 * Converts an algorithm into its textual representation
+	 */
+	public static String
+	string(int alg) {
+		return algs.getText(alg);
+	}
+
+	/**
+	 * Converts a textual representation of an algorithm into its numeric
+	 * code.  Integers in the range 0..255 are also accepted.
+	 * @param s The textual representation of the algorithm
+	 * @return The algorithm code, or -1 on error.
+	 */
+	public static int
+	value(String s) {
+		return algs.getValue(s);
+	}
+}
+
+private
+DNSSEC() { }
+
+private static void
+digestSIG(DNSOutput out, SIGBase sig) {
+	out.writeU16(sig.getTypeCovered());
+	out.writeU8(sig.getAlgorithm());
+	out.writeU8(sig.getLabels());
+	out.writeU32(sig.getOrigTTL());
+	out.writeU32(sig.getExpire().getTime() / 1000);
+	out.writeU32(sig.getTimeSigned().getTime() / 1000);
+	out.writeU16(sig.getFootprint());
+	sig.getSigner().toWireCanonical(out);
+}
+
+/**
+ * Creates a byte array containing the concatenation of the fields of the
+ * SIG record and the RRsets to be signed/verified.  This does not perform
+ * a cryptographic digest.
+ * @param rrsig The RRSIG record used to sign/verify the rrset.
+ * @param rrset The data to be signed/verified.
+ * @return The data to be cryptographically signed or verified.
+ */
+public static byte []
+digestRRset(RRSIGRecord rrsig, RRset rrset) {
+	DNSOutput out = new DNSOutput();
+	digestSIG(out, rrsig);
+
+	int size = rrset.size();
+	Record [] records = new Record[size];
+
+	Iterator it = rrset.rrs();
+	Name name = rrset.getName();
+	Name wild = null;
+	int sigLabels = rrsig.getLabels() + 1; // Add the root label back.
+	if (name.labels() > sigLabels)
+		wild = name.wild(name.labels() - sigLabels);
+	while (it.hasNext())
+		records[--size] = (Record) it.next();
+	Arrays.sort(records);
+
+	DNSOutput header = new DNSOutput();
+	if (wild != null)
+		wild.toWireCanonical(header);
+	else
+		name.toWireCanonical(header);
+	header.writeU16(rrset.getType());
+	header.writeU16(rrset.getDClass());
+	header.writeU32(rrsig.getOrigTTL());
+	for (int i = 0; i < records.length; i++) {
+		out.writeByteArray(header.toByteArray());
+		int lengthPosition = out.current();
+		out.writeU16(0);
+		out.writeByteArray(records[i].rdataToWireCanonical());
+		int rrlength = out.current() - lengthPosition - 2;
+		out.save();
+		out.jump(lengthPosition);
+		out.writeU16(rrlength);
+		out.restore();
+	}
+	return out.toByteArray();
+}
+
+/**
+ * Creates a byte array containing the concatenation of the fields of the
+ * SIG(0) record and the message to be signed.  This does not perform
+ * a cryptographic digest.
+ * @param sig The SIG record used to sign the rrset.
+ * @param msg The message to be signed.
+ * @param previous If this is a response, the signature from the query.
+ * @return The data to be cryptographically signed.
+ */
+public static byte []
+digestMessage(SIGRecord sig, Message msg, byte [] previous) {
+	DNSOutput out = new DNSOutput();
+	digestSIG(out, sig);
+
+	if (previous != null)
+		out.writeByteArray(previous);
+
+	msg.toWire(out);
+	return out.toByteArray();
+}
+
+/**
+ * A DNSSEC exception.
+ */
+public static class DNSSECException extends Exception {
+	DNSSECException(String s) {
+		super(s);
+	}
+}
+
+/**
+ * An algorithm is unsupported by this DNSSEC implementation.
+ */
+public static class UnsupportedAlgorithmException extends DNSSECException {
+	UnsupportedAlgorithmException(int alg) {
+		super("Unsupported algorithm: " + alg);
+	}
+}
+
+/**
+ * The cryptographic data in a DNSSEC key is malformed.
+ */
+public static class MalformedKeyException extends DNSSECException {
+	MalformedKeyException(KEYBase rec) {
+		super("Invalid key data: " + rec.rdataToString());
+	}
+}
+
+/**
+ * A DNSSEC verification failed because fields in the DNSKEY and RRSIG records
+ * do not match.
+ */
+public static class KeyMismatchException extends DNSSECException {
+	private KEYBase key;
+	private SIGBase sig;
+
+	KeyMismatchException(KEYBase key, SIGBase sig) {
+		super("key " +
+		      key.getName() + "/" +
+		      DNSSEC.Algorithm.string(key.getAlgorithm()) + "/" +
+		      key.getFootprint() + " " +
+		      "does not match signature " +
+		      sig.getSigner() + "/" +
+		      DNSSEC.Algorithm.string(sig.getAlgorithm()) + "/" +
+		      sig.getFootprint());
+	}
+}
+
+/**
+ * A DNSSEC verification failed because the signature has expired.
+ */
+public static class SignatureExpiredException extends DNSSECException {
+	private Date when, now;
+
+	SignatureExpiredException(Date when, Date now) {
+		super("signature expired");
+		this.when = when;
+		this.now = now;
+	}
+
+	/**
+	 * @return When the signature expired
+	 */
+	public Date
+	getExpiration() {
+		return when;
+	}
+
+	/**
+	 * @return When the verification was attempted
+	 */
+	public Date
+	getVerifyTime() {
+		return now;
+	}
+}
+
+/**
+ * A DNSSEC verification failed because the signature has not yet become valid.
+ */
+public static class SignatureNotYetValidException extends DNSSECException {
+	private Date when, now;
+
+	SignatureNotYetValidException(Date when, Date now) {
+		super("signature is not yet valid");
+		this.when = when;
+		this.now = now;
+	}
+
+	/**
+	 * @return When the signature will become valid
+	 */
+	public Date
+	getExpiration() {
+		return when;
+	}
+
+	/**
+	 * @return When the verification was attempted
+	 */
+	public Date
+	getVerifyTime() {
+		return now;
+	}
+}
+
+/**
+ * A DNSSEC verification failed because the cryptographic signature
+ * verification failed.
+ */
+public static class SignatureVerificationException extends DNSSECException {
+	SignatureVerificationException() {
+		super("signature verification failed");
+	}
+}
+
+/**
+ * The key data provided is inconsistent.
+ */
+public static class IncompatibleKeyException extends IllegalArgumentException {
+	IncompatibleKeyException() {
+		super("incompatible keys");
+	}
+}
+
+private static int
+BigIntegerLength(BigInteger i) {
+	return (i.bitLength() + 7) / 8;
+}
+
+private static BigInteger
+readBigInteger(DNSInput in, int len) throws IOException {
+	byte [] b = in.readByteArray(len);
+	return new BigInteger(1, b);
+}
+
+private static BigInteger
+readBigInteger(DNSInput in) {
+	byte [] b = in.readByteArray();
+	return new BigInteger(1, b);
+}
+
+private static void
+writeBigInteger(DNSOutput out, BigInteger val) {
+	byte [] b = val.toByteArray();
+	if (b[0] == 0)
+		out.writeByteArray(b, 1, b.length - 1);
+	else
+		out.writeByteArray(b);
+}
+
+private static PublicKey
+toRSAPublicKey(KEYBase r) throws IOException, GeneralSecurityException {
+	DNSInput in = new DNSInput(r.getKey());
+	int exponentLength = in.readU8();
+	if (exponentLength == 0)
+		exponentLength = in.readU16();
+	BigInteger exponent = readBigInteger(in, exponentLength);
+	BigInteger modulus = readBigInteger(in);
+
+	KeyFactory factory = KeyFactory.getInstance("RSA");
+	return factory.generatePublic(new RSAPublicKeySpec(modulus, exponent));
+}
+
+private static PublicKey
+toDSAPublicKey(KEYBase r) throws IOException, GeneralSecurityException,
+	MalformedKeyException
+{
+	DNSInput in = new DNSInput(r.getKey());
+
+	int t = in.readU8();
+	if (t > 8)
+		throw new MalformedKeyException(r);
+
+	BigInteger q = readBigInteger(in, 20);
+	BigInteger p = readBigInteger(in, 64 + t*8);
+	BigInteger g = readBigInteger(in, 64 + t*8);
+	BigInteger y = readBigInteger(in, 64 + t*8);
+
+	KeyFactory factory = KeyFactory.getInstance("DSA");
+	return factory.generatePublic(new DSAPublicKeySpec(y, p, q, g));
+}
+
+private static class ECKeyInfo {
+	int length;
+	public BigInteger p, a, b, gx, gy, n;
+	EllipticCurve curve;
+	ECParameterSpec spec;
+
+	ECKeyInfo(int length, String p_str, String a_str, String b_str,
+		  String gx_str, String gy_str, String n_str)
+	{
+		this.length = length;
+		p = new BigInteger(p_str, 16);
+		a = new BigInteger(a_str, 16);
+		b = new BigInteger(b_str, 16);
+		gx = new BigInteger(gx_str, 16);
+		gy = new BigInteger(gy_str, 16);
+		n = new BigInteger(n_str, 16);
+		curve = new EllipticCurve(new ECFieldFp(p), a, b);
+		spec = new ECParameterSpec(curve, new ECPoint(gx, gy), n, 1);
+	}
+}
+
+// RFC 5114 Section 2.6
+private static final ECKeyInfo ECDSA_P256 = new ECKeyInfo(32,
+	"FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF",
+	"FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC",
+	"5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B",
+	"6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296",
+	"4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5",
+	"FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551");
+
+// RFC 5114 Section 2.7
+private static final ECKeyInfo ECDSA_P384 = new ECKeyInfo(48,
+	"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF",
+	"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC",
+	"B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF",
+	"AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7",
+	"3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F",
+	"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973");
+
+private static PublicKey
+toECDSAPublicKey(KEYBase r, ECKeyInfo keyinfo) throws IOException,
+	GeneralSecurityException, MalformedKeyException
+{
+	DNSInput in = new DNSInput(r.getKey());
+
+	// RFC 6605 Section 4
+	BigInteger x = readBigInteger(in, keyinfo.length);
+	BigInteger y = readBigInteger(in, keyinfo.length);
+	ECPoint q = new ECPoint(x, y);
+
+	KeyFactory factory = KeyFactory.getInstance("EC");
+	return factory.generatePublic(new ECPublicKeySpec(q, keyinfo.spec));
+}
+
+/** Converts a KEY/DNSKEY record into a PublicKey */
+static PublicKey
+toPublicKey(KEYBase r) throws DNSSECException {
+	int alg = r.getAlgorithm();
+	try {
+		switch (alg) {
+		case Algorithm.RSAMD5:
+		case Algorithm.RSASHA1:
+		case Algorithm.RSA_NSEC3_SHA1:
+		case Algorithm.RSASHA256:
+		case Algorithm.RSASHA512:
+			return toRSAPublicKey(r);
+		case Algorithm.DSA:
+		case Algorithm.DSA_NSEC3_SHA1:
+			return toDSAPublicKey(r);
+		case Algorithm.ECDSAP256SHA256:
+			return toECDSAPublicKey(r, ECDSA_P256);
+		case Algorithm.ECDSAP384SHA384:
+			return toECDSAPublicKey(r, ECDSA_P384);
+		default:
+			throw new UnsupportedAlgorithmException(alg);
+		}
+	}
+	catch (IOException e) {
+		throw new MalformedKeyException(r);
+	}
+	catch (GeneralSecurityException e) {
+		throw new DNSSECException(e.toString());
+	}
+}
+
+private static byte []
+fromRSAPublicKey(RSAPublicKey key) {
+	DNSOutput out = new DNSOutput();
+	BigInteger exponent = key.getPublicExponent();
+	BigInteger modulus = key.getModulus();
+	int exponentLength = BigIntegerLength(exponent);
+
+	if (exponentLength < 256)
+		out.writeU8(exponentLength);
+	else {
+		out.writeU8(0);
+		out.writeU16(exponentLength);
+	}
+	writeBigInteger(out, exponent);
+	writeBigInteger(out, modulus);
+
+	return out.toByteArray();
+}
+
+private static byte []
+fromDSAPublicKey(DSAPublicKey key) {
+	DNSOutput out = new DNSOutput();
+	BigInteger q = key.getParams().getQ();
+	BigInteger p = key.getParams().getP();
+	BigInteger g = key.getParams().getG();
+	BigInteger y = key.getY();
+	int t = (p.toByteArray().length - 64) / 8;
+
+	out.writeU8(t);
+	writeBigInteger(out, q);
+	writeBigInteger(out, p);
+	writeBigInteger(out, g);
+	writeBigInteger(out, y);
+
+	return out.toByteArray();
+}
+
+private static byte []
+fromECDSAPublicKey(ECPublicKey key) {
+	DNSOutput out = new DNSOutput();
+
+	BigInteger x = key.getW().getAffineX();
+	BigInteger y = key.getW().getAffineY();
+
+	writeBigInteger(out, x);
+	writeBigInteger(out, y);
+
+	return out.toByteArray();
+}
+
+/** Builds a DNSKEY record from a PublicKey */
+static byte []
+fromPublicKey(PublicKey key, int alg) throws DNSSECException
+{
+
+	switch (alg) {
+	case Algorithm.RSAMD5:
+	case Algorithm.RSASHA1:
+	case Algorithm.RSA_NSEC3_SHA1:
+	case Algorithm.RSASHA256:
+	case Algorithm.RSASHA512:
+		if (! (key instanceof RSAPublicKey))
+			throw new IncompatibleKeyException();
+		return fromRSAPublicKey((RSAPublicKey) key);
+	case Algorithm.DSA:
+	case Algorithm.DSA_NSEC3_SHA1:
+		if (! (key instanceof DSAPublicKey))
+			throw new IncompatibleKeyException();
+		return fromDSAPublicKey((DSAPublicKey) key);
+	case Algorithm.ECDSAP256SHA256:
+	case Algorithm.ECDSAP384SHA384:
+		if (! (key instanceof ECPublicKey))
+			throw new IncompatibleKeyException();
+		return fromECDSAPublicKey((ECPublicKey) key);
+	default:
+		throw new UnsupportedAlgorithmException(alg);
+	}
+}
+
+/**
+ * Convert an algorithm number to the corresponding JCA string.
+ * @param alg The algorithm number.
+ * @throws UnsupportedAlgorithmException The algorithm is unknown.
+ */
+public static String
+algString(int alg) throws UnsupportedAlgorithmException {
+	switch (alg) {
+	case Algorithm.RSAMD5:
+		return "MD5withRSA";
+	case Algorithm.DSA:
+	case Algorithm.DSA_NSEC3_SHA1:
+		return "SHA1withDSA";
+	case Algorithm.RSASHA1:
+	case Algorithm.RSA_NSEC3_SHA1:
+		return "SHA1withRSA";
+	case Algorithm.RSASHA256:
+		return "SHA256withRSA";
+	case Algorithm.RSASHA512:
+		return "SHA512withRSA";
+	case Algorithm.ECDSAP256SHA256:
+		return "SHA256withECDSA";
+	case Algorithm.ECDSAP384SHA384:
+		return "SHA384withECDSA";
+	default:
+		throw new UnsupportedAlgorithmException(alg);
+	}
+}
+
+private static final int ASN1_SEQ = 0x30;
+private static final int ASN1_INT = 0x2;
+
+private static final int DSA_LEN = 20;
+
+private static byte []
+DSASignaturefromDNS(byte [] dns) throws DNSSECException, IOException {
+	if (dns.length != 1 + DSA_LEN * 2)
+		throw new SignatureVerificationException();
+
+	DNSInput in = new DNSInput(dns);
+	DNSOutput out = new DNSOutput();
+
+	int t = in.readU8();
+
+	byte [] r = in.readByteArray(DSA_LEN);
+	int rlen = DSA_LEN;
+	if (r[0] < 0)
+		rlen++;
+
+	byte [] s = in.readByteArray(DSA_LEN);
+        int slen = DSA_LEN;
+        if (s[0] < 0)
+                slen++;
+
+	out.writeU8(ASN1_SEQ);
+	out.writeU8(rlen + slen + 4);
+
+	out.writeU8(ASN1_INT);
+	out.writeU8(rlen);
+	if (rlen > DSA_LEN)
+		out.writeU8(0);
+	out.writeByteArray(r);
+
+	out.writeU8(ASN1_INT);
+	out.writeU8(slen);
+	if (slen > DSA_LEN)
+		out.writeU8(0);
+	out.writeByteArray(s);
+
+	return out.toByteArray();
+}
+
+private static byte []
+DSASignaturetoDNS(byte [] signature, int t) throws IOException {
+	DNSInput in = new DNSInput(signature);
+	DNSOutput out = new DNSOutput();
+
+	out.writeU8(t);
+
+	int tmp = in.readU8();
+	if (tmp != ASN1_SEQ)
+		throw new IOException();
+	int seqlen = in.readU8();
+
+	tmp = in.readU8();
+	if (tmp != ASN1_INT)
+		throw new IOException();
+	int rlen = in.readU8();
+	if (rlen == DSA_LEN + 1) {
+		if (in.readU8() != 0)
+			throw new IOException();
+	} else if (rlen != DSA_LEN)
+		throw new IOException();
+	byte [] bytes = in.readByteArray(DSA_LEN);
+	out.writeByteArray(bytes);
+
+	tmp = in.readU8();
+	if (tmp != ASN1_INT)
+		throw new IOException();
+	int slen = in.readU8();
+	if (slen == DSA_LEN + 1) {
+		if (in.readU8() != 0)
+			throw new IOException();
+	} else if (slen != DSA_LEN)
+		throw new IOException();
+	bytes = in.readByteArray(DSA_LEN);
+	out.writeByteArray(bytes);
+
+	return out.toByteArray();
+}
+
+private static byte []
+ECDSASignaturefromDNS(byte [] signature, ECKeyInfo keyinfo)
+	throws DNSSECException, IOException
+{
+	if (signature.length != keyinfo.length * 2)
+		throw new SignatureVerificationException();
+
+	DNSInput in = new DNSInput(signature);
+	DNSOutput out = new DNSOutput();
+
+	byte [] r = in.readByteArray(keyinfo.length);
+	int rlen = keyinfo.length;
+	if (r[0] < 0)
+		rlen++;
+
+	byte [] s = in.readByteArray(keyinfo.length);
+	int slen = keyinfo.length;
+	if (s[0] < 0)
+		slen++;
+
+	out.writeU8(ASN1_SEQ);
+	out.writeU8(rlen + slen + 4);
+
+	out.writeU8(ASN1_INT);
+	out.writeU8(rlen);
+	if (rlen > keyinfo.length)
+		out.writeU8(0);
+	out.writeByteArray(r);
+
+	out.writeU8(ASN1_INT);
+	out.writeU8(slen);
+	if (slen > keyinfo.length)
+		out.writeU8(0);
+	out.writeByteArray(s);
+
+	return out.toByteArray();
+}
+
+private static byte []
+ECDSASignaturetoDNS(byte [] signature, ECKeyInfo keyinfo) throws IOException {
+	DNSInput in = new DNSInput(signature);
+	DNSOutput out = new DNSOutput();
+
+	int tmp = in.readU8();
+	if (tmp != ASN1_SEQ)
+		throw new IOException();
+	int seqlen = in.readU8();
+
+	tmp = in.readU8();
+	if (tmp != ASN1_INT)
+		throw new IOException();
+	int rlen = in.readU8();
+	if (rlen == keyinfo.length + 1) {
+		if (in.readU8() != 0)
+			throw new IOException();
+	} else if (rlen != keyinfo.length)
+		throw new IOException();
+	byte[] bytes = in.readByteArray(keyinfo.length);
+	out.writeByteArray(bytes);
+
+	tmp = in.readU8();
+	if (tmp != ASN1_INT)
+		throw new IOException();
+	int slen = in.readU8();
+	if (slen == keyinfo.length + 1) {
+		if (in.readU8() != 0)
+			throw new IOException();
+	} else if (slen != keyinfo.length)
+		throw new IOException();
+	bytes = in.readByteArray(keyinfo.length);
+	out.writeByteArray(bytes);
+
+	return out.toByteArray();
+}
+
+private static void
+verify(PublicKey key, int alg, byte [] data, byte [] signature)
+throws DNSSECException
+{
+	if (key instanceof DSAPublicKey) {
+		try {
+			signature = DSASignaturefromDNS(signature);
+		}
+		catch (IOException e) {
+			throw new IllegalStateException();
+		}
+	} else if (key instanceof ECPublicKey) {
+		try {
+			switch (alg) {
+			case Algorithm.ECDSAP256SHA256:
+				signature = ECDSASignaturefromDNS(signature,
+								  ECDSA_P256);
+				break;
+			case Algorithm.ECDSAP384SHA384:
+				signature = ECDSASignaturefromDNS(signature,
+								  ECDSA_P384);
+				break;
+			default:
+				throw new UnsupportedAlgorithmException(alg);
+			}
+		}
+		catch (IOException e) {
+			throw new IllegalStateException();
+		}
+	}
+
+	try {
+		Signature s = Signature.getInstance(algString(alg));
+		s.initVerify(key);
+		s.update(data);
+		if (!s.verify(signature))
+			throw new SignatureVerificationException();
+	}
+	catch (GeneralSecurityException e) {
+		throw new DNSSECException(e.toString());
+	}
+}
+
+private static boolean
+matches(SIGBase sig, KEYBase key)
+{
+	return (key.getAlgorithm() == sig.getAlgorithm() &&
+		key.getFootprint() == sig.getFootprint() &&
+		key.getName().equals(sig.getSigner()));
+}
+
+/**
+ * Verify a DNSSEC signature.
+ * @param rrset The data to be verified.
+ * @param rrsig The RRSIG record containing the signature.
+ * @param key The DNSKEY record to verify the signature with.
+ * @throws UnsupportedAlgorithmException The algorithm is unknown
+ * @throws MalformedKeyException The key is malformed
+ * @throws KeyMismatchException The key and signature do not match
+ * @throws SignatureExpiredException The signature has expired
+ * @throws SignatureNotYetValidException The signature is not yet valid
+ * @throws SignatureVerificationException The signature does not verify.
+ * @throws DNSSECException Some other error occurred.
+ */
+public static void
+verify(RRset rrset, RRSIGRecord rrsig, DNSKEYRecord key) throws DNSSECException
+{
+	if (!matches(rrsig, key))
+		throw new KeyMismatchException(key, rrsig);
+
+	Date now = new Date();
+	if (now.compareTo(rrsig.getExpire()) > 0)
+		throw new SignatureExpiredException(rrsig.getExpire(), now);
+	if (now.compareTo(rrsig.getTimeSigned()) < 0)
+		throw new SignatureNotYetValidException(rrsig.getTimeSigned(),
+							now);
+
+	verify(key.getPublicKey(), rrsig.getAlgorithm(),
+	       digestRRset(rrsig, rrset), rrsig.getSignature());
+}
+
+private static byte []
+sign(PrivateKey privkey, PublicKey pubkey, int alg, byte [] data,
+     String provider) throws DNSSECException
+{
+	byte [] signature;
+	try {
+		Signature s;
+		if (provider != null)
+			s = Signature.getInstance(algString(alg), provider);
+		else
+			s = Signature.getInstance(algString(alg));
+		s.initSign(privkey);
+		s.update(data);
+		signature = s.sign();
+	}
+	catch (GeneralSecurityException e) {
+		throw new DNSSECException(e.toString());
+	}
+
+	if (pubkey instanceof DSAPublicKey) {
+		try {
+			DSAPublicKey dsa = (DSAPublicKey) pubkey;
+			BigInteger P = dsa.getParams().getP();
+			int t = (BigIntegerLength(P) - 64) / 8;
+			signature = DSASignaturetoDNS(signature, t);
+		}
+		catch (IOException e) {
+			throw new IllegalStateException();
+		}
+	} else if (pubkey instanceof ECPublicKey) {
+		try {
+			switch (alg) {
+			case Algorithm.ECDSAP256SHA256:
+				signature = ECDSASignaturetoDNS(signature,
+								ECDSA_P256);
+				break;
+			case Algorithm.ECDSAP384SHA384:
+				signature = ECDSASignaturetoDNS(signature,
+								ECDSA_P384);
+				break;
+			default:
+				throw new UnsupportedAlgorithmException(alg);
+			}
+		}
+		catch (IOException e) {
+			throw new IllegalStateException();
+		}
+	}
+
+	return signature;
+}
+static void
+checkAlgorithm(PrivateKey key, int alg) throws UnsupportedAlgorithmException
+{
+	switch (alg) {
+	case Algorithm.RSAMD5:
+	case Algorithm.RSASHA1:
+	case Algorithm.RSA_NSEC3_SHA1:
+	case Algorithm.RSASHA256:
+	case Algorithm.RSASHA512:
+		if (! (key instanceof RSAPrivateKey))
+			throw new IncompatibleKeyException();
+		break;
+	case Algorithm.DSA:
+	case Algorithm.DSA_NSEC3_SHA1:
+		if (! (key instanceof DSAPrivateKey))
+			throw new IncompatibleKeyException();
+		break;
+	case Algorithm.ECDSAP256SHA256:
+	case Algorithm.ECDSAP384SHA384:
+		if (! (key instanceof ECPrivateKey))
+			throw new IncompatibleKeyException();
+		break;
+	default:
+		throw new UnsupportedAlgorithmException(alg);
+	}
+}
+
+/**
+ * Generate a DNSSEC signature.  key and privateKey must refer to the
+ * same underlying cryptographic key.
+ * @param rrset The data to be signed
+ * @param key The DNSKEY record to use as part of signing
+ * @param privkey The PrivateKey to use when signing
+ * @param inception The time at which the signatures should become valid
+ * @param expiration The time at which the signatures should expire
+ * @throws UnsupportedAlgorithmException The algorithm is unknown
+ * @throws MalformedKeyException The key is malformed
+ * @throws DNSSECException Some other error occurred.
+ * @return The generated signature
+ */
+public static RRSIGRecord
+sign(RRset rrset, DNSKEYRecord key, PrivateKey privkey,
+     Date inception, Date expiration) throws DNSSECException
+{
+	return sign(rrset, key, privkey, inception, expiration, null);
+}
+
+/**
+ * Generate a DNSSEC signature.  key and privateKey must refer to the
+ * same underlying cryptographic key.
+ * @param rrset The data to be signed
+ * @param key The DNSKEY record to use as part of signing
+ * @param privkey The PrivateKey to use when signing
+ * @param inception The time at which the signatures should become valid
+ * @param expiration The time at which the signatures should expire
+ * @param provider The name of the JCA provider.  If non-null, it will be
+ * passed to JCA getInstance() methods.
+ * @throws UnsupportedAlgorithmException The algorithm is unknown
+ * @throws MalformedKeyException The key is malformed
+ * @throws DNSSECException Some other error occurred.
+ * @return The generated signature
+ */
+public static RRSIGRecord
+sign(RRset rrset, DNSKEYRecord key, PrivateKey privkey,
+     Date inception, Date expiration, String provider) throws DNSSECException
+{
+	int alg = key.getAlgorithm();
+	checkAlgorithm(privkey, alg);
+
+	RRSIGRecord rrsig = new RRSIGRecord(rrset.getName(), rrset.getDClass(),
+					    rrset.getTTL(), rrset.getType(),
+					    alg, rrset.getTTL(),
+					    expiration, inception,
+					    key.getFootprint(),
+					    key.getName(), null);
+
+	rrsig.setSignature(sign(privkey, key.getPublicKey(), alg,
+				digestRRset(rrsig, rrset), provider));
+	return rrsig;
+}
+
+static SIGRecord
+signMessage(Message message, SIGRecord previous, KEYRecord key,
+	    PrivateKey privkey, Date inception, Date expiration)
+	throws DNSSECException
+{
+	int alg = key.getAlgorithm();
+	checkAlgorithm(privkey, alg);
+
+	SIGRecord sig = new SIGRecord(Name.root, DClass.ANY, 0, 0,
+					    alg, 0, expiration, inception,
+					    key.getFootprint(),
+					    key.getName(), null);
+	DNSOutput out = new DNSOutput();
+	digestSIG(out, sig);
+	if (previous != null)
+		out.writeByteArray(previous.getSignature());
+	message.toWire(out);
+
+	sig.setSignature(sign(privkey, key.getPublicKey(),
+			      alg, out.toByteArray(), null));
+	return sig;
+}
+
+static void
+verifyMessage(Message message, byte [] bytes, SIGRecord sig, SIGRecord previous,
+	      KEYRecord key) throws DNSSECException
+{
+	if (!matches(sig, key))
+		throw new KeyMismatchException(key, sig);
+
+	Date now = new Date();
+
+	if (now.compareTo(sig.getExpire()) > 0)
+		throw new SignatureExpiredException(sig.getExpire(), now);
+	if (now.compareTo(sig.getTimeSigned()) < 0)
+		throw new SignatureNotYetValidException(sig.getTimeSigned(),
+							now);
+
+	DNSOutput out = new DNSOutput();
+	digestSIG(out, sig);
+	if (previous != null)
+		out.writeByteArray(previous.getSignature());
+
+	Header header = (Header) message.getHeader().clone();
+	header.decCount(Section.ADDITIONAL);
+	out.writeByteArray(header.toWire());
+
+	out.writeByteArray(bytes, Header.LENGTH,
+			   message.sig0start - Header.LENGTH);
+
+	verify(key.getPublicKey(), sig.getAlgorithm(),
+	       out.toByteArray(), sig.getSignature());
+}
+
+/**
+ * Generate the digest value for a DS key
+ * @param key Which is covered by the DS record
+ * @param digestid The type of digest
+ * @return The digest value as an array of bytes
+ */
+static byte []
+generateDSDigest(DNSKEYRecord key, int digestid)
+{
+	MessageDigest digest;
+	try {
+		switch (digestid) {
+		case DSRecord.Digest.SHA1:
+			digest = MessageDigest.getInstance("sha-1");
+			break;
+		case DSRecord.Digest.SHA256:
+			digest = MessageDigest.getInstance("sha-256");
+			break;
+		case DSRecord.Digest.SHA384:
+			digest = MessageDigest.getInstance("sha-384");
+			break;
+		default:
+			throw new IllegalArgumentException(
+					"unknown DS digest type " + digestid);
+		}
+	}
+	catch (NoSuchAlgorithmException e) {
+		throw new IllegalStateException("no message digest support");
+	}
+	digest.update(key.getName().toWire());
+	digest.update(key.rdataToWireCanonical());
+	return digest.digest();
+}
+
+}
diff --git a/src/org/xbill/DNS/DSRecord.java b/src/org/xbill/DNS/DSRecord.java
new file mode 100644
index 0000000..4c1ebce
--- /dev/null
+++ b/src/org/xbill/DNS/DSRecord.java
@@ -0,0 +1,157 @@
+// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * DS - contains a Delegation Signer record, which acts as a
+ * placeholder for KEY records in the parent zone.
+ * @see DNSSEC
+ *
+ * @author David Blacka
+ * @author Brian Wellington
+ */
+
+public class DSRecord extends Record {
+
+public static class Digest {
+	private Digest() {}
+
+	/** SHA-1 */
+	public static final int SHA1 = 1;
+
+	/** SHA-256 */
+	public static final int SHA256 = 2;
+
+	/** SHA-384 */
+	public static final int SHA384 = 4;
+}
+
+public static final int SHA1_DIGEST_ID = Digest.SHA1;
+public static final int SHA256_DIGEST_ID = Digest.SHA256;
+public static final int SHA384_DIGEST_ID = Digest.SHA384;
+
+private static final long serialVersionUID = -9001819329700081493L;
+
+private int footprint;
+private int alg;
+private int digestid;
+private byte [] digest;
+
+DSRecord() {}
+
+Record
+getObject() {
+	return new DSRecord();
+}
+
+/**
+ * Creates a DS Record from the given data
+ * @param footprint The original KEY record's footprint (keyid).
+ * @param alg The original key algorithm.
+ * @param digestid The digest id code.
+ * @param digest A hash of the original key.
+ */
+public
+DSRecord(Name name, int dclass, long ttl, int footprint, int alg,
+	 int digestid, byte [] digest)
+{
+	super(name, Type.DS, dclass, ttl);
+	this.footprint = checkU16("footprint", footprint);
+	this.alg = checkU8("alg", alg);
+	this.digestid = checkU8("digestid", digestid);
+	this.digest = digest;
+}
+
+/**
+ * Creates a DS Record from the given data
+ * @param digestid The digest id code.
+ * @param key The key to digest
+ */
+public
+DSRecord(Name name, int dclass, long ttl, int digestid, DNSKEYRecord key)
+{
+	this(name, dclass, ttl, key.getFootprint(), key.getAlgorithm(),
+	     digestid, DNSSEC.generateDSDigest(key, digestid));
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	footprint = in.readU16();
+	alg = in.readU8();
+	digestid = in.readU8();
+	digest = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	footprint = st.getUInt16();
+	alg = st.getUInt8();
+	digestid = st.getUInt8();
+	digest = st.getHex();
+}
+
+/**
+ * Converts rdata to a String
+ */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(footprint);
+	sb.append(" ");
+	sb.append(alg);
+	sb.append(" ");
+	sb.append(digestid);
+	if (digest != null) {
+		sb.append(" ");
+		sb.append(base16.toString(digest));
+	}
+
+	return sb.toString();
+}
+
+/**
+ * Returns the key's algorithm.
+ */
+public int
+getAlgorithm() {
+	return alg;
+}
+
+/**
+ *  Returns the key's Digest ID.
+ */
+public int
+getDigestID()
+{
+	return digestid;
+}
+  
+/**
+ * Returns the binary hash of the key.
+ */
+public byte []
+getDigest() {
+	return digest;
+}
+
+/**
+ * Returns the key's footprint.
+ */
+public int
+getFootprint() {
+	return footprint;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU16(footprint);
+	out.writeU8(alg);
+	out.writeU8(digestid);
+	if (digest != null)
+		out.writeByteArray(digest);
+}
+
+}
diff --git a/src/org/xbill/DNS/EDNSOption.java b/src/org/xbill/DNS/EDNSOption.java
new file mode 100644
index 0000000..a65bd19
--- /dev/null
+++ b/src/org/xbill/DNS/EDNSOption.java
@@ -0,0 +1,215 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.Arrays;
+
+/**
+ * DNS extension options, as described in RFC 2671.  The rdata of an OPT record
+ * is defined as a list of options; this represents a single option.
+ * 
+ * @author Brian Wellington
+ * @author Ming Zhou &lt;mizhou@bnivideo.com&gt;, Beaumaris Networks
+ */
+public abstract class EDNSOption {
+
+public static class Code {
+	private Code() {}
+
+	/** Name Server Identifier, RFC 5001 */
+	public final static int NSID = 3;
+
+	/** Client Subnet, defined in draft-vandergaast-edns-client-subnet-00 */
+	public final static int CLIENT_SUBNET = 20730;
+
+	private static Mnemonic codes = new Mnemonic("EDNS Option Codes",
+						     Mnemonic.CASE_UPPER);
+
+	static {
+		codes.setMaximum(0xFFFF);
+		codes.setPrefix("CODE");
+		codes.setNumericAllowed(true);
+
+		codes.add(NSID, "NSID");
+		codes.add(CLIENT_SUBNET, "CLIENT_SUBNET");
+	}
+
+	/**
+	 * Converts an EDNS Option Code into its textual representation
+	 */
+	public static String
+	string(int code) {
+		return codes.getText(code);
+	}
+
+	/**
+	 * Converts a textual representation of an EDNS Option Code into its
+	 * numeric value.
+	 * @param s The textual representation of the option code
+	 * @return The option code, or -1 on error.
+	 */
+	public static int
+	value(String s) {
+		return codes.getValue(s);
+	}
+}
+
+private final int code;
+
+/**
+ * 
+ * Creates an option with the given option code and data.
+ */
+public 
+EDNSOption(int code) {
+	this.code = Record.checkU16("code", code);
+}
+
+public String 
+toString() {
+	StringBuffer sb = new StringBuffer();
+
+	sb.append("{");
+	sb.append(EDNSOption.Code.string(code));
+	sb.append(": ");
+	sb.append(optionToString());
+	sb.append("}");
+
+	return sb.toString();
+}
+
+/**
+ * Returns the EDNS Option's code.
+ *
+ * @return the option code
+ */
+public int 
+getCode() {
+	return code;
+}
+
+/**
+ * Returns the EDNS Option's data, as a byte array.
+ * 
+ * @return the option data
+ */
+byte [] 
+getData() {
+	DNSOutput out = new DNSOutput();
+	optionToWire(out);
+	return out.toByteArray();
+}
+
+/**
+ * Converts the wire format of an EDNS Option (the option data only) into the
+ * type-specific format.
+ * @param in The input Stream.
+ */
+abstract void 
+optionFromWire(DNSInput in) throws IOException;
+
+/**
+ * Converts the wire format of an EDNS Option (including code and length) into
+ * the type-specific format.
+ * @param out The input stream.
+ */
+static EDNSOption
+fromWire(DNSInput in) throws IOException {
+	int code, length;
+
+	code = in.readU16();
+	length = in.readU16();
+	if (in.remaining() < length)
+		throw new WireParseException("truncated option");
+	int save = in.saveActive();
+	in.setActive(length);
+	EDNSOption option;
+	switch (code) {
+	case Code.NSID:
+		option = new NSIDOption();
+		break;
+	case Code.CLIENT_SUBNET:
+		option = new ClientSubnetOption();
+		break;
+	default:
+		option = new GenericEDNSOption(code);
+		break;
+	}
+	option.optionFromWire(in);
+	in.restoreActive(save);
+
+	return option;
+}
+
+/**
+ * Converts the wire format of an EDNS Option (including code and length) into
+ * the type-specific format.
+ * @return The option, in wire format.
+ */
+public static EDNSOption
+fromWire(byte [] b) throws IOException {
+	return fromWire(new DNSInput(b));
+}
+
+/**
+ * Converts an EDNS Option (the type-specific option data only) into wire format.
+ * @param out The output stream.
+ */
+abstract void 
+optionToWire(DNSOutput out);
+
+/**
+ * Converts an EDNS Option (including code and length) into wire format.
+ * @param out The output stream.
+ */
+void
+toWire(DNSOutput out) {
+	out.writeU16(code);
+	int lengthPosition = out.current();
+	out.writeU16(0); /* until we know better */
+	optionToWire(out);
+	int length = out.current() - lengthPosition - 2;
+	out.writeU16At(length, lengthPosition);
+}
+
+/**
+ * Converts an EDNS Option (including code and length) into wire format.
+ * @return The option, in wire format.
+ */
+public byte []
+toWire() throws IOException {
+	DNSOutput out = new DNSOutput();
+	toWire(out);
+	return out.toByteArray();
+}
+
+/**
+ * Determines if two EDNS Options are identical.
+ * @param arg The option to compare to
+ * @return true if the options are equal, false otherwise.
+ */
+public boolean
+equals(Object arg) {
+	if (arg == null || !(arg instanceof EDNSOption))
+		return false;
+	EDNSOption opt = (EDNSOption) arg;
+	if (code != opt.code)
+		return false;
+	return Arrays.equals(getData(), opt.getData());
+}
+
+/**
+ * Generates a hash code based on the EDNS Option's data.
+ */
+public int
+hashCode() {
+	byte [] array = getData();
+	int hashval = 0;
+	for (int i = 0; i < array.length; i++)
+		hashval += ((hashval << 3) + (array[i] & 0xFF));
+	return hashval;
+}
+
+abstract String optionToString();
+
+}
diff --git a/src/org/xbill/DNS/EmptyRecord.java b/src/org/xbill/DNS/EmptyRecord.java
new file mode 100644
index 0000000..f5e61e8
--- /dev/null
+++ b/src/org/xbill/DNS/EmptyRecord.java
@@ -0,0 +1,42 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * A class implementing Records with no data; that is, records used in
+ * the question section of messages and meta-records in dynamic update.
+ *
+ * @author Brian Wellington
+ */
+
+class EmptyRecord extends Record {
+
+private static final long serialVersionUID = 3601852050646429582L;
+
+EmptyRecord() {}
+
+Record
+getObject() {
+	return new EmptyRecord();
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+}
+
+String
+rrToString() {
+	return "";
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+}
+
+}
diff --git a/src/org/xbill/DNS/ExtendedFlags.java b/src/org/xbill/DNS/ExtendedFlags.java
new file mode 100644
index 0000000..8f3bbab
--- /dev/null
+++ b/src/org/xbill/DNS/ExtendedFlags.java
@@ -0,0 +1,45 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Constants and functions relating to EDNS flags.
+ *
+ * @author Brian Wellington
+ */
+
+public final class ExtendedFlags {
+
+private static Mnemonic extflags = new Mnemonic("EDNS Flag",
+						Mnemonic.CASE_LOWER);
+
+/** dnssec ok */
+public static final int DO		= 0x8000;
+
+static {
+	extflags.setMaximum(0xFFFF);
+	extflags.setPrefix("FLAG");
+	extflags.setNumericAllowed(true);
+
+	extflags.add(DO, "do");
+}
+
+private
+ExtendedFlags() {}
+
+/** Converts a numeric extended flag into a String */
+public static String
+string(int i) {
+	return extflags.getText(i);
+}
+
+/**
+ * Converts a textual representation of an extended flag into its numeric
+ * value
+ */
+public static int
+value(String s) {
+	return extflags.getValue(s);
+}
+
+}
diff --git a/src/org/xbill/DNS/ExtendedResolver.java b/src/org/xbill/DNS/ExtendedResolver.java
new file mode 100644
index 0000000..f762b84
--- /dev/null
+++ b/src/org/xbill/DNS/ExtendedResolver.java
@@ -0,0 +1,419 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+import java.io.*;
+import java.net.*;
+
+/**
+ * An implementation of Resolver that can send queries to multiple servers,
+ * sending the queries multiple times if necessary.
+ * @see Resolver
+ *
+ * @author Brian Wellington
+ */
+
+public class ExtendedResolver implements Resolver {
+
+private static class Resolution implements ResolverListener {
+	Resolver [] resolvers;
+	int [] sent;
+	Object [] inprogress;
+	int retries;
+	int outstanding;
+	boolean done;
+	Message query;
+	Message response;
+	Throwable thrown;
+	ResolverListener listener;
+
+	public
+	Resolution(ExtendedResolver eres, Message query) {
+		List l = eres.resolvers;
+		resolvers = (Resolver []) l.toArray (new Resolver[l.size()]);
+		if (eres.loadBalance) {
+			int nresolvers = resolvers.length;
+			/*
+			 * Note: this is not synchronized, since the
+			 * worst thing that can happen is a random
+			 * ordering, which is ok.
+			 */
+			int start = eres.lbStart++ % nresolvers;
+			if (eres.lbStart > nresolvers)
+				eres.lbStart %= nresolvers;
+			if (start > 0) {
+				Resolver [] shuffle = new Resolver[nresolvers];
+				for (int i = 0; i < nresolvers; i++) {
+					int pos = (i + start) % nresolvers;
+					shuffle[i] = resolvers[pos];
+				}
+				resolvers = shuffle;
+			}
+		}
+		sent = new int[resolvers.length];
+		inprogress = new Object[resolvers.length];
+		retries = eres.retries;
+		this.query = query;
+	}
+
+	/* Asynchronously sends a message. */
+	public void
+	send(int n) {
+		sent[n]++;
+		outstanding++;
+		try {
+			inprogress[n] = resolvers[n].sendAsync(query, this);
+		}
+		catch (Throwable t) {
+			synchronized (this) {
+				thrown = t;
+				done = true;
+				if (listener == null) {
+					notifyAll();
+					return;
+				}
+			}
+		}
+	}
+
+	/* Start a synchronous resolution */
+	public Message
+	start() throws IOException {
+		try {
+			/*
+			 * First, try sending synchronously.  If this works,
+			 * we're done.  Otherwise, we'll get an exception
+			 * and continue.  It would be easier to call send(0),
+			 * but this avoids a thread creation.  If and when
+			 * SimpleResolver.sendAsync() can be made to not
+			 * create a thread, this could be changed.
+			 */
+			sent[0]++;
+			outstanding++;
+			inprogress[0] = new Object();
+			return resolvers[0].send(query);
+		}
+		catch (Exception e) {
+			/*
+			 * This will either cause more queries to be sent
+			 * asynchronously or will set the 'done' flag.
+			 */
+			handleException(inprogress[0], e);
+		}
+		/*
+		 * Wait for a successful response or for each
+		 * subresolver to fail.
+		 */
+		synchronized (this) {
+			while (!done) {
+				try {
+					wait();
+				}
+				catch (InterruptedException e) {
+				}
+			}
+		}
+		/* Return the response or throw an exception */
+		if (response != null)
+			return response;
+		else if (thrown instanceof IOException)
+			throw (IOException) thrown;
+		else if (thrown instanceof RuntimeException)
+			throw (RuntimeException) thrown;
+		else if (thrown instanceof Error)
+			throw (Error) thrown;
+		else
+			throw new IllegalStateException
+				("ExtendedResolver failure");
+	}
+
+	/* Start an asynchronous resolution */
+	public void
+	startAsync(ResolverListener listener) {
+		this.listener = listener;
+		send(0);
+	}
+
+	/*
+	 * Receive a response.  If the resolution hasn't been completed,
+	 * either wake up the blocking thread or call the callback.
+	 */
+	public void
+	receiveMessage(Object id, Message m) {
+		if (Options.check("verbose"))
+			System.err.println("ExtendedResolver: " +
+					   "received message");
+		synchronized (this) {
+			if (done)
+				return;
+			response = m;
+			done = true;
+			if (listener == null) {
+				notifyAll();
+				return;
+			}
+		}
+		listener.receiveMessage(this, response);
+	}
+
+	/*
+	 * Receive an exception.  If the resolution has been completed,
+	 * do nothing.  Otherwise make progress.
+	 */
+	public void
+	handleException(Object id, Exception e) {
+		if (Options.check("verbose"))
+			System.err.println("ExtendedResolver: got " + e);
+		synchronized (this) {
+			outstanding--;
+			if (done)
+				return;
+			int n;
+			for (n = 0; n < inprogress.length; n++)
+				if (inprogress[n] == id)
+					break;
+			/* If we don't know what this is, do nothing. */
+			if (n == inprogress.length)
+				return;
+			boolean startnext = false;
+			/*
+			 * If this is the first response from server n, 
+			 * we should start sending queries to server n + 1.
+			 */
+			if (sent[n] == 1 && n < resolvers.length - 1)
+				startnext = true;
+			if (e instanceof InterruptedIOException) {
+				/* Got a timeout; resend */
+				if (sent[n] < retries)
+					send(n);
+				if (thrown == null)
+					thrown = e;
+			} else if (e instanceof SocketException) {
+				/*
+				 * Problem with the socket; don't resend
+				 * on it
+				 */
+				if (thrown == null ||
+				    thrown instanceof InterruptedIOException)
+					thrown = e;
+			} else {
+				/*
+				 * Problem with the response; don't resend
+				 * on the same socket.
+				 */
+				thrown = e;
+			}
+			if (done)
+				return;
+			if (startnext)
+				send(n + 1);
+			if (done)
+				return;
+			if (outstanding == 0) {
+				/*
+				 * If we're done and this is synchronous,
+				 * wake up the blocking thread.
+				 */
+				done = true;
+				if (listener == null) {
+					notifyAll();
+					return;
+				}
+			}
+			if (!done)
+				return;
+		}
+		/* If we're done and this is asynchronous, call the callback. */
+		if (!(thrown instanceof Exception))
+			thrown = new RuntimeException(thrown.getMessage());
+		listener.handleException(this, (Exception) thrown);
+	}
+}
+
+private static final int quantum = 5;
+
+private List resolvers;
+private boolean loadBalance = false;
+private int lbStart = 0;
+private int retries = 3;
+
+private void
+init() {
+	resolvers = new ArrayList();
+}
+
+/**
+ * Creates a new Extended Resolver.  The default ResolverConfig is used to
+ * determine the servers for which SimpleResolver contexts should be
+ * initialized.
+ * @see SimpleResolver
+ * @see ResolverConfig
+ * @exception UnknownHostException Failure occured initializing SimpleResolvers
+ */
+public
+ExtendedResolver() throws UnknownHostException {
+	init();
+	String [] servers = ResolverConfig.getCurrentConfig().servers();
+	if (servers != null) {
+		for (int i = 0; i < servers.length; i++) {
+			Resolver r = new SimpleResolver(servers[i]);
+			r.setTimeout(quantum);
+			resolvers.add(r);
+		}
+	}
+	else
+		resolvers.add(new SimpleResolver());
+}
+
+/**
+ * Creates a new Extended Resolver
+ * @param servers An array of server names for which SimpleResolver
+ * contexts should be initialized.
+ * @see SimpleResolver
+ * @exception UnknownHostException Failure occured initializing SimpleResolvers
+ */
+public
+ExtendedResolver(String [] servers) throws UnknownHostException {
+	init();
+	for (int i = 0; i < servers.length; i++) {
+		Resolver r = new SimpleResolver(servers[i]);
+		r.setTimeout(quantum);
+		resolvers.add(r);
+	}
+}
+
+/**
+ * Creates a new Extended Resolver
+ * @param res An array of pre-initialized Resolvers is provided.
+ * @see SimpleResolver
+ * @exception UnknownHostException Failure occured initializing SimpleResolvers
+ */
+public
+ExtendedResolver(Resolver [] res) throws UnknownHostException {
+	init();
+	for (int i = 0; i < res.length; i++)
+		resolvers.add(res[i]);
+}
+
+public void
+setPort(int port) {
+	for (int i = 0; i < resolvers.size(); i++)
+		((Resolver)resolvers.get(i)).setPort(port);
+}
+
+public void
+setTCP(boolean flag) {
+	for (int i = 0; i < resolvers.size(); i++)
+		((Resolver)resolvers.get(i)).setTCP(flag);
+}
+
+public void
+setIgnoreTruncation(boolean flag) {
+	for (int i = 0; i < resolvers.size(); i++)
+		((Resolver)resolvers.get(i)).setIgnoreTruncation(flag);
+}
+
+public void
+setEDNS(int level) {
+	for (int i = 0; i < resolvers.size(); i++)
+		((Resolver)resolvers.get(i)).setEDNS(level);
+}
+
+public void
+setEDNS(int level, int payloadSize, int flags, List options) {
+	for (int i = 0; i < resolvers.size(); i++)
+		((Resolver)resolvers.get(i)).setEDNS(level, payloadSize,
+						     flags, options);
+}
+
+public void
+setTSIGKey(TSIG key) {
+	for (int i = 0; i < resolvers.size(); i++)
+		((Resolver)resolvers.get(i)).setTSIGKey(key);
+}
+
+public void
+setTimeout(int secs, int msecs) {
+	for (int i = 0; i < resolvers.size(); i++)
+		((Resolver)resolvers.get(i)).setTimeout(secs, msecs);
+}
+
+public void
+setTimeout(int secs) {
+	setTimeout(secs, 0);
+}
+
+/**
+ * Sends a message and waits for a response.  Multiple servers are queried,
+ * and queries are sent multiple times until either a successful response
+ * is received, or it is clear that there is no successful response.
+ * @param query The query to send.
+ * @return The response.
+ * @throws IOException An error occurred while sending or receiving.
+ */
+public Message
+send(Message query) throws IOException {
+	Resolution res = new Resolution(this, query);
+	return res.start();
+}
+
+/**
+ * Asynchronously sends a message to multiple servers, potentially multiple
+ * times, registering a listener to receive a callback on success or exception.
+ * Multiple asynchronous lookups can be performed in parallel.  Since the
+ * callback may be invoked before the function returns, external
+ * synchronization is necessary.
+ * @param query The query to send
+ * @param listener The object containing the callbacks.
+ * @return An identifier, which is also a parameter in the callback
+ */
+public Object
+sendAsync(final Message query, final ResolverListener listener) {
+	Resolution res = new Resolution(this, query);
+	res.startAsync(listener);
+	return res;
+}
+
+/** Returns the nth resolver used by this ExtendedResolver */
+public Resolver
+getResolver(int n) {
+	if (n < resolvers.size())
+		return (Resolver)resolvers.get(n);
+	return null;
+}
+
+/** Returns all resolvers used by this ExtendedResolver */
+public Resolver []
+getResolvers() {
+	return (Resolver []) resolvers.toArray(new Resolver[resolvers.size()]);
+}
+
+/** Adds a new resolver to be used by this ExtendedResolver */
+public void
+addResolver(Resolver r) {
+	resolvers.add(r);
+}
+
+/** Deletes a resolver used by this ExtendedResolver */
+public void
+deleteResolver(Resolver r) {
+	resolvers.remove(r);
+}
+
+/** Sets whether the servers should be load balanced.
+ * @param flag If true, servers will be tried in round-robin order.  If false,
+ * servers will always be queried in the same order.
+ */
+public void
+setLoadBalance(boolean flag) {
+	loadBalance = flag;
+}
+
+/** Sets the number of retries sent to each server per query */
+public void
+setRetries(int retries) {
+	this.retries = retries;
+}
+
+}
diff --git a/src/org/xbill/DNS/Flags.java b/src/org/xbill/DNS/Flags.java
new file mode 100644
index 0000000..964ce23
--- /dev/null
+++ b/src/org/xbill/DNS/Flags.java
@@ -0,0 +1,81 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Constants and functions relating to flags in the DNS header.
+ *
+ * @author Brian Wellington
+ */
+
+public final class Flags {
+
+private static Mnemonic flags = new Mnemonic("DNS Header Flag",
+					     Mnemonic.CASE_LOWER);
+
+/** query/response */
+public static final byte QR		= 0;
+
+/** authoritative answer */
+public static final byte AA		= 5;
+
+/** truncated */
+public static final byte TC		= 6;
+
+/** recursion desired */
+public static final byte RD		= 7;
+
+/** recursion available */
+public static final byte RA		= 8;
+
+/** authenticated data */
+public static final byte AD		= 10;
+
+/** (security) checking disabled */
+public static final byte CD		= 11;
+
+/** dnssec ok (extended) */
+public static final int DO		= ExtendedFlags.DO;
+
+static {
+	flags.setMaximum(0xF);
+	flags.setPrefix("FLAG");
+	flags.setNumericAllowed(true);
+
+	flags.add(QR, "qr");
+	flags.add(AA, "aa");
+	flags.add(TC, "tc");
+	flags.add(RD, "rd");
+	flags.add(RA, "ra");
+	flags.add(AD, "ad");
+	flags.add(CD, "cd");
+}
+
+private
+Flags() {}
+
+/** Converts a numeric Flag into a String */
+public static String
+string(int i) {
+	return flags.getText(i);
+}
+
+/** Converts a String representation of an Flag into its numeric value */
+public static int
+value(String s) {
+	return flags.getValue(s);
+}
+
+/**
+ * Indicates if a bit in the flags field is a flag or not.  If it's part of
+ * the rcode or opcode, it's not.
+ */
+public static boolean
+isFlag(int index) {
+	flags.check(index);
+	if ((index >= 1 && index <= 4) || (index >= 12))
+		return false;
+	return true;
+}
+
+}
diff --git a/src/org/xbill/DNS/FormattedTime.java b/src/org/xbill/DNS/FormattedTime.java
new file mode 100644
index 0000000..c76a846
--- /dev/null
+++ b/src/org/xbill/DNS/FormattedTime.java
@@ -0,0 +1,79 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Routines for converting time values to and from YYYYMMDDHHMMSS format.
+ *
+ * @author Brian Wellington
+ */
+
+import java.util.*;
+import java.text.*;
+
+final class FormattedTime {
+
+private static NumberFormat w2, w4;
+
+static {
+	w2 = new DecimalFormat();
+	w2.setMinimumIntegerDigits(2);
+
+	w4 = new DecimalFormat();
+	w4.setMinimumIntegerDigits(4);
+	w4.setGroupingUsed(false);
+}
+
+private
+FormattedTime() {}
+
+/**
+ * Converts a Date into a formatted string.
+ * @param date The Date to convert.
+ * @return The formatted string.
+ */
+public static String
+format(Date date) {
+	Calendar c = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+	StringBuffer sb = new StringBuffer();
+
+	c.setTime(date);
+	sb.append(w4.format(c.get(Calendar.YEAR)));
+	sb.append(w2.format(c.get(Calendar.MONTH)+1));
+	sb.append(w2.format(c.get(Calendar.DAY_OF_MONTH)));
+	sb.append(w2.format(c.get(Calendar.HOUR_OF_DAY)));
+	sb.append(w2.format(c.get(Calendar.MINUTE)));
+	sb.append(w2.format(c.get(Calendar.SECOND)));
+	return sb.toString();
+}
+
+/**
+ * Parses a formatted time string into a Date.
+ * @param s The string, in the form YYYYMMDDHHMMSS.
+ * @return The Date object.
+ * @throws TextParseExcetption The string was invalid.
+ */
+public static Date
+parse(String s) throws TextParseException {
+	if (s.length() != 14) {
+		throw new TextParseException("Invalid time encoding: " + s);
+	}
+
+	Calendar c = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+	c.clear();
+	try {
+		int year = Integer.parseInt(s.substring(0, 4));
+		int month = Integer.parseInt(s.substring(4, 6)) - 1;
+		int date = Integer.parseInt(s.substring(6, 8));
+		int hour = Integer.parseInt(s.substring(8, 10));
+		int minute = Integer.parseInt(s.substring(10, 12));
+		int second = Integer.parseInt(s.substring(12, 14));
+		c.set(year, month, date, hour, minute, second);
+	}
+	catch (NumberFormatException e) {
+		throw new TextParseException("Invalid time encoding: " + s);
+	}
+	return c.getTime();
+}
+
+}
diff --git a/src/org/xbill/DNS/GPOSRecord.java b/src/org/xbill/DNS/GPOSRecord.java
new file mode 100644
index 0000000..688d567
--- /dev/null
+++ b/src/org/xbill/DNS/GPOSRecord.java
@@ -0,0 +1,178 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * Geographical Location - describes the physical location of a host.
+ *
+ * @author Brian Wellington
+ */
+
+public class GPOSRecord extends Record {
+
+private static final long serialVersionUID = -6349714958085750705L;
+
+private byte [] latitude, longitude, altitude;
+
+GPOSRecord() {}
+
+Record
+getObject() {
+	return new GPOSRecord();
+}
+
+private void
+validate(double longitude, double latitude) throws IllegalArgumentException
+{
+       if (longitude < -90.0 || longitude > 90.0) {
+               throw new IllegalArgumentException("illegal longitude " +
+                                                  longitude);
+       }
+       if (latitude < -180.0 || latitude > 180.0) {
+               throw new IllegalArgumentException("illegal latitude " +
+                                                  latitude);
+       }
+}
+
+/**
+ * Creates an GPOS Record from the given data
+ * @param longitude The longitude component of the location.
+ * @param latitude The latitude component of the location.
+ * @param altitude The altitude component of the location (in meters above sea
+ * level).
+*/
+public
+GPOSRecord(Name name, int dclass, long ttl, double longitude, double latitude,
+	   double altitude)
+{
+	super(name, Type.GPOS, dclass, ttl);
+	validate(longitude, latitude);
+	this.longitude = Double.toString(longitude).getBytes();
+	this.latitude = Double.toString(latitude).getBytes();
+	this.altitude = Double.toString(altitude).getBytes();
+}
+
+/**
+ * Creates an GPOS Record from the given data
+ * @param longitude The longitude component of the location.
+ * @param latitude The latitude component of the location.
+ * @param altitude The altitude component of the location (in meters above sea
+ * level).
+*/
+public
+GPOSRecord(Name name, int dclass, long ttl, String longitude, String latitude,
+	   String altitude)
+{
+	super(name, Type.GPOS, dclass, ttl);
+	try {
+		this.longitude = byteArrayFromString(longitude);
+		this.latitude = byteArrayFromString(latitude);
+		validate(getLongitude(), getLatitude());
+		this.altitude = byteArrayFromString(altitude);
+	}
+	catch (TextParseException e) {
+		throw new IllegalArgumentException(e.getMessage());
+	}
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	longitude = in.readCountedString();
+	latitude = in.readCountedString();
+	altitude = in.readCountedString();
+	try {
+		validate(getLongitude(), getLatitude());
+	}
+	catch(IllegalArgumentException e) {
+		throw new WireParseException(e.getMessage());
+	}
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	try {
+		longitude = byteArrayFromString(st.getString());
+		latitude = byteArrayFromString(st.getString());
+		altitude = byteArrayFromString(st.getString());
+	}
+	catch (TextParseException e) {
+		throw st.exception(e.getMessage());
+	}
+	try {
+		validate(getLongitude(), getLatitude());
+	}
+	catch(IllegalArgumentException e) {
+		throw new WireParseException(e.getMessage());
+	}
+}
+
+/** Convert to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(byteArrayToString(longitude, true));
+	sb.append(" ");
+	sb.append(byteArrayToString(latitude, true));
+	sb.append(" ");
+	sb.append(byteArrayToString(altitude, true));
+	return sb.toString();
+}
+
+/** Returns the longitude as a string */
+public String
+getLongitudeString() {
+	return byteArrayToString(longitude, false);
+}
+
+/**
+ * Returns the longitude as a double
+ * @throws NumberFormatException The string does not contain a valid numeric
+ * value.
+ */
+public double
+getLongitude() {
+	return Double.parseDouble(getLongitudeString());
+}
+
+/** Returns the latitude as a string */
+public String
+getLatitudeString() {
+	return byteArrayToString(latitude, false);
+}
+
+/**
+ * Returns the latitude as a double
+ * @throws NumberFormatException The string does not contain a valid numeric
+ * value.
+ */
+public double
+getLatitude() {
+	return Double.parseDouble(getLatitudeString());
+}
+
+/** Returns the altitude as a string */
+public String
+getAltitudeString() {
+	return byteArrayToString(altitude, false);
+}
+
+/**
+ * Returns the altitude as a double
+ * @throws NumberFormatException The string does not contain a valid numeric
+ * value.
+ */
+public double
+getAltitude() {
+	return Double.parseDouble(getAltitudeString());
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeCountedString(longitude);
+	out.writeCountedString(latitude);
+	out.writeCountedString(altitude);
+}
+
+}
diff --git a/src/org/xbill/DNS/Generator.java b/src/org/xbill/DNS/Generator.java
new file mode 100644
index 0000000..a08d343
--- /dev/null
+++ b/src/org/xbill/DNS/Generator.java
@@ -0,0 +1,264 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * A representation of a $GENERATE statement in a master file.
+ *
+ * @author Brian Wellington
+ */
+
+public class Generator {
+
+/** The start of the range. */
+public long start;
+
+/** The end of the range. */
+public long end;
+
+/** The step value of the range. */
+public long step;
+
+/** The pattern to use for generating record names. */
+public final String namePattern;
+
+/** The type of the generated records. */
+public final int type;
+
+/** The class of the generated records. */
+public final int dclass;
+
+/** The ttl of the generated records. */
+public final long ttl;
+
+/** The pattern to use for generating record data. */
+public final String rdataPattern;
+
+/** The origin to append to relative names. */
+public final Name origin;
+
+private long current;
+
+/**
+ * Indicates whether generation is supported for this type.
+ * @throws InvalidTypeException The type is out of range.
+ */
+public static boolean
+supportedType(int type) {
+	Type.check(type);
+	return (type == Type.PTR || type == Type.CNAME || type == Type.DNAME ||
+		type == Type.A || type == Type.AAAA || type == Type.NS);
+}
+
+/**
+ * Creates a specification for generating records, as a $GENERATE
+ * statement in a master file.
+ * @param start The start of the range.
+ * @param end The end of the range.
+ * @param step The step value of the range.
+ * @param namePattern The pattern to use for generating record names.
+ * @param type The type of the generated records.  The supported types are
+ * PTR, CNAME, DNAME, A, AAAA, and NS.
+ * @param dclass The class of the generated records.
+ * @param ttl The ttl of the generated records.
+ * @param rdataPattern The pattern to use for generating record data.
+ * @param origin The origin to append to relative names.
+ * @throws IllegalArgumentException The range is invalid.
+ * @throws IllegalArgumentException The type does not support generation.
+ * @throws IllegalArgumentException The dclass is not a valid class.
+ */
+public
+Generator(long start, long end, long step, String namePattern,
+	  int type, int dclass, long ttl, String rdataPattern, Name origin)
+{
+	if (start < 0 || end < 0 || start > end || step <= 0)
+		throw new IllegalArgumentException
+				("invalid range specification");
+	if (!supportedType(type))
+		throw new IllegalArgumentException("unsupported type");
+	DClass.check(dclass);
+
+	this.start = start;
+	this.end = end;
+	this.step = step;
+	this.namePattern = namePattern;
+	this.type = type;
+	this.dclass = dclass;
+	this.ttl = ttl;
+	this.rdataPattern = rdataPattern;
+	this.origin = origin;
+	this.current = start;
+}
+
+private String
+substitute(String spec, long n) throws IOException {
+	boolean escaped = false;
+	byte [] str = spec.getBytes();
+	StringBuffer sb = new StringBuffer();
+
+	for (int i = 0; i < str.length; i++) {
+		char c = (char)(str[i] & 0xFF);
+		if (escaped) {
+			sb.append(c);
+			escaped = false;
+		} else if (c == '\\') {
+			if (i + 1 == str.length)
+				throw new TextParseException
+						("invalid escape character");
+			escaped = true;
+		} else if (c == '$') {
+			boolean negative = false;
+			long offset = 0;
+			long width = 0;
+			long base = 10;
+			boolean wantUpperCase = false;
+			if (i + 1 < str.length && str[i + 1] == '$') {
+				// '$$' == literal '$' for backwards
+				// compatibility with old versions of BIND.
+				c = (char)(str[++i] & 0xFF);
+				sb.append(c);
+				continue;
+			} else if (i + 1 < str.length && str[i + 1] == '{') {
+				// It's a substitution with modifiers.
+				i++;
+				if (i + 1 < str.length && str[i + 1] == '-') {
+					negative = true;
+					i++;
+				}
+				while (i + 1 < str.length) {
+					c = (char)(str[++i] & 0xFF);
+					if (c == ',' || c == '}')
+						break;
+					if (c < '0' || c > '9')
+						throw new TextParseException(
+							"invalid offset");
+					c -= '0';
+					offset *= 10;
+					offset += c;
+				}
+				if (negative)
+					offset = -offset;
+
+				if (c == ',') {
+					while (i + 1 < str.length) {
+						c = (char)(str[++i] & 0xFF);
+						if (c == ',' || c == '}')
+							break;
+						if (c < '0' || c > '9')
+							throw new
+							   TextParseException(
+							   "invalid width");
+						c -= '0';
+						width *= 10;
+						width += c;
+					}
+				}
+
+				if (c == ',') {
+					if  (i + 1 == str.length)
+						throw new TextParseException(
+							   "invalid base");
+					c = (char)(str[++i] & 0xFF);
+					if (c == 'o')
+						base = 8;
+					else if (c == 'x')
+						base = 16;
+					else if (c == 'X') {
+						base = 16;
+						wantUpperCase = true;
+					}
+					else if (c != 'd')
+						throw new TextParseException(
+							   "invalid base");
+				}
+
+				if (i + 1 == str.length || str[i + 1] != '}')
+					throw new TextParseException
+						("invalid modifiers");
+				i++;
+			}
+			long v = n + offset;
+			if (v < 0)
+				throw new TextParseException
+						("invalid offset expansion");
+			String number;
+			if (base == 8)
+				number = Long.toOctalString(v);
+			else if (base == 16)
+				number = Long.toHexString(v);
+			else
+				number = Long.toString(v);
+			if (wantUpperCase)
+				number = number.toUpperCase();
+			if (width != 0 && width > number.length()) {
+				int zeros = (int)width - number.length();
+				while (zeros-- > 0)
+					sb.append('0');
+			}
+			sb.append(number);
+		} else {
+			sb.append(c);
+		}
+	}
+	return sb.toString();
+}
+
+/**
+ * Constructs and returns the next record in the expansion.
+ * @throws IOException The name or rdata was invalid after substitutions were
+ * performed.
+ */
+public Record
+nextRecord() throws IOException {
+	if (current > end)
+		return null;
+	String namestr = substitute(namePattern, current);
+	Name name = Name.fromString(namestr, origin);
+	String rdata = substitute(rdataPattern, current);
+	current += step;
+	return Record.fromString(name, type, dclass, ttl, rdata, origin);
+}
+
+/**
+ * Constructs and returns all records in the expansion.
+ * @throws IOException The name or rdata of a record was invalid after
+ * substitutions were performed.
+ */
+public Record []
+expand() throws IOException {
+	List list = new ArrayList();
+	for (long i = start; i < end; i += step) {
+		String namestr = substitute(namePattern, current);
+		Name name = Name.fromString(namestr, origin);
+		String rdata = substitute(rdataPattern, current);
+		list.add(Record.fromString(name, type, dclass, ttl,
+					   rdata, origin));
+	}
+	return (Record []) list.toArray(new Record[list.size()]);
+}
+
+/**
+ * Converts the generate specification to a string containing the corresponding
+ * $GENERATE statement.
+ */
+public String
+toString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append("$GENERATE ");
+	sb.append(start + "-" + end);
+	if (step > 1)
+		sb.append("/" + step);
+	sb.append(" ");
+	sb.append(namePattern + " ");
+	sb.append(ttl + " ");
+	if (dclass != DClass.IN || !Options.check("noPrintIN"))
+		sb.append(DClass.string(dclass) + " ");
+	sb.append(Type.string(type) + " ");
+	sb.append(rdataPattern + " ");
+	return sb.toString();
+}
+
+}
diff --git a/src/org/xbill/DNS/GenericEDNSOption.java b/src/org/xbill/DNS/GenericEDNSOption.java
new file mode 100644
index 0000000..7c8b51b
--- /dev/null
+++ b/src/org/xbill/DNS/GenericEDNSOption.java
@@ -0,0 +1,47 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+package org.xbill.DNS;
+
+import java.io.*;
+
+import org.xbill.DNS.utils.base16;
+
+/**
+ * An EDNSOption with no internal structure.
+ * 
+ * @author Ming Zhou &lt;mizhou@bnivideo.com&gt;, Beaumaris Networks
+ * @author Brian Wellington
+ */
+public class GenericEDNSOption extends EDNSOption {
+
+private byte [] data;
+
+GenericEDNSOption(int code) {
+	super(code);
+}
+
+/**
+ * Construct a generic EDNS option.
+ * @param data The contents of the option.
+ */
+public 
+GenericEDNSOption(int code, byte [] data) {
+	super(code);
+	this.data = Record.checkByteArrayLength("option data", data, 0xFFFF);
+}
+
+void 
+optionFromWire(DNSInput in) throws IOException {
+	data = in.readByteArray();
+}
+
+void 
+optionToWire(DNSOutput out) {
+	out.writeByteArray(data);
+}
+
+String 
+optionToString() {
+	return "<" + base16.toString(data) + ">";
+}
+
+}
diff --git a/src/org/xbill/DNS/HINFORecord.java b/src/org/xbill/DNS/HINFORecord.java
new file mode 100644
index 0000000..18fed32
--- /dev/null
+++ b/src/org/xbill/DNS/HINFORecord.java
@@ -0,0 +1,95 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * Host Information - describes the CPU and OS of a host
+ *
+ * @author Brian Wellington
+ */
+
+public class HINFORecord extends Record {
+
+private static final long serialVersionUID = -4732870630947452112L;
+	
+private byte [] cpu, os;
+
+HINFORecord() {}
+
+Record
+getObject() {
+	return new HINFORecord();
+}
+
+/**
+ * Creates an HINFO Record from the given data
+ * @param cpu A string describing the host's CPU
+ * @param os A string describing the host's OS
+ * @throws IllegalArgumentException One of the strings has invalid escapes
+ */
+public
+HINFORecord(Name name, int dclass, long ttl, String cpu, String os) {
+	super(name, Type.HINFO, dclass, ttl);
+	try {
+		this.cpu = byteArrayFromString(cpu);
+		this.os = byteArrayFromString(os);
+	}
+	catch (TextParseException e) {
+		throw new IllegalArgumentException(e.getMessage());
+	}
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	cpu = in.readCountedString();
+	os = in.readCountedString();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	try {
+		cpu = byteArrayFromString(st.getString());
+		os = byteArrayFromString(st.getString());
+	}
+	catch (TextParseException e) {
+		throw st.exception(e.getMessage());
+	}
+}
+
+/**
+ * Returns the host's CPU
+ */
+public String
+getCPU() {
+	return byteArrayToString(cpu, false);
+}
+
+/**
+ * Returns the host's OS
+ */
+public String
+getOS() {
+	return byteArrayToString(os, false);
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeCountedString(cpu);
+	out.writeCountedString(os);
+}
+
+/**
+ * Converts to a string
+ */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(byteArrayToString(cpu, true));
+	sb.append(" ");
+	sb.append(byteArrayToString(os, true));
+	return sb.toString();
+}
+
+}
diff --git a/src/org/xbill/DNS/Header.java b/src/org/xbill/DNS/Header.java
new file mode 100644
index 0000000..2a44d08
--- /dev/null
+++ b/src/org/xbill/DNS/Header.java
@@ -0,0 +1,286 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * A DNS message header
+ * @see Message
+ *
+ * @author Brian Wellington
+ */
+
+public class Header implements Cloneable {
+
+private int id; 
+private int flags;
+private int [] counts;
+
+private static Random random = new Random();
+
+/** The length of a DNS Header in wire format. */
+public static final int LENGTH = 12;
+
+private void
+init() {
+	counts = new int[4];
+	flags = 0;
+	id = -1;
+}
+
+/**
+ * Create a new empty header.
+ * @param id The message id
+ */
+public
+Header(int id) {
+	init();
+	setID(id);
+}
+
+/**
+ * Create a new empty header with a random message id
+ */
+public
+Header() {
+	init();
+}
+
+/**
+ * Parses a Header from a stream containing DNS wire format.
+ */
+Header(DNSInput in) throws IOException {
+	this(in.readU16());
+	flags = in.readU16();
+	for (int i = 0; i < counts.length; i++)
+		counts[i] = in.readU16();
+}
+
+/**
+ * Creates a new Header from its DNS wire format representation
+ * @param b A byte array containing the DNS Header.
+ */
+public
+Header(byte [] b) throws IOException {
+	this(new DNSInput(b));
+}
+
+void
+toWire(DNSOutput out) {
+	out.writeU16(getID());
+	out.writeU16(flags);
+	for (int i = 0; i < counts.length; i++)
+		out.writeU16(counts[i]);
+}
+
+public byte []
+toWire() {
+	DNSOutput out = new DNSOutput();
+	toWire(out);
+	return out.toByteArray();
+}
+
+static private boolean
+validFlag(int bit) {
+	return (bit >= 0 && bit <= 0xF && Flags.isFlag(bit));
+}
+
+static private void
+checkFlag(int bit) {
+	if (!validFlag(bit))
+		throw new IllegalArgumentException("invalid flag bit " + bit);
+}
+
+/**
+ * Sets a flag to the supplied value
+ * @see Flags
+ */
+public void
+setFlag(int bit) {
+	checkFlag(bit);
+	// bits are indexed from left to right
+	flags |= (1 << (15 - bit));
+}
+
+/**
+ * Sets a flag to the supplied value
+ * @see Flags
+ */
+public void
+unsetFlag(int bit) {
+	checkFlag(bit);
+	// bits are indexed from left to right
+	flags &= ~(1 << (15 - bit));
+}
+
+/**
+ * Retrieves a flag
+ * @see Flags
+ */
+public boolean
+getFlag(int bit) {
+	checkFlag(bit);
+	// bits are indexed from left to right
+	return (flags & (1 << (15 - bit))) != 0;
+}
+
+boolean []
+getFlags() {
+	boolean [] array = new boolean[16];
+	for (int i = 0; i < array.length; i++)
+		if (validFlag(i))
+			array[i] = getFlag(i);
+	return array;
+}
+
+/**
+ * Retrieves the message ID
+ */
+public int
+getID() {
+	if (id >= 0)
+		return id;
+	synchronized (this) {
+		if (id < 0)
+			id = random.nextInt(0xffff);
+		return id;
+	}
+}
+
+/**
+ * Sets the message ID
+ */
+public void
+setID(int id) {
+	if (id < 0 || id > 0xffff)
+		throw new IllegalArgumentException("DNS message ID " + id +
+						   " is out of range");
+	this.id = id;
+}
+
+/**
+ * Sets the message's rcode
+ * @see Rcode
+ */
+public void
+setRcode(int value) {
+	if (value < 0 || value > 0xF)
+		throw new IllegalArgumentException("DNS Rcode " + value +
+						   " is out of range");
+	flags &= ~0xF;
+	flags |= value;
+}
+
+/**
+ * Retrieves the mesasge's rcode
+ * @see Rcode
+ */
+public int
+getRcode() {
+	return flags & 0xF;
+}
+
+/**
+ * Sets the message's opcode
+ * @see Opcode
+ */
+public void
+setOpcode(int value) {
+	if (value < 0 || value > 0xF)
+		throw new IllegalArgumentException("DNS Opcode " + value +
+						   "is out of range");
+	flags &= 0x87FF;
+	flags |= (value << 11);
+}
+
+/**
+ * Retrieves the mesasge's opcode
+ * @see Opcode
+ */
+public int
+getOpcode() {
+	return (flags >> 11) & 0xF;
+}
+
+void
+setCount(int field, int value) {
+	if (value < 0 || value > 0xFFFF)
+		throw new IllegalArgumentException("DNS section count " +
+						   value + " is out of range");
+	counts[field] = value;
+}
+
+void
+incCount(int field) {
+	if (counts[field] == 0xFFFF)
+		throw new IllegalStateException("DNS section count cannot " +
+						"be incremented");
+	counts[field]++;
+}
+
+void
+decCount(int field) {
+	if (counts[field] == 0)
+		throw new IllegalStateException("DNS section count cannot " +
+						"be decremented");
+	counts[field]--;
+}
+
+/**
+ * Retrieves the record count for the given section
+ * @see Section
+ */
+public int
+getCount(int field) {
+	return counts[field];
+}
+
+/** Converts the header's flags into a String */
+public String
+printFlags() {
+	StringBuffer sb = new StringBuffer();
+
+	for (int i = 0; i < 16; i++)
+		if (validFlag(i) && getFlag(i)) {
+			sb.append(Flags.string(i));
+			sb.append(" ");
+		}
+	return sb.toString();
+}
+
+String
+toStringWithRcode(int newrcode) {
+	StringBuffer sb = new StringBuffer();
+
+	sb.append(";; ->>HEADER<<- "); 
+	sb.append("opcode: " + Opcode.string(getOpcode()));
+	sb.append(", status: " + Rcode.string(newrcode));
+	sb.append(", id: " + getID());
+	sb.append("\n");
+
+	sb.append(";; flags: " + printFlags());
+	sb.append("; ");
+	for (int i = 0; i < 4; i++)
+		sb.append(Section.string(i) + ": " + getCount(i) + " ");
+	return sb.toString();
+}
+
+/** Converts the header into a String */
+public String
+toString() {
+	return toStringWithRcode(getRcode());
+}
+
+/* Creates a new Header identical to the current one */
+public Object
+clone() {
+	Header h = new Header();
+	h.id = id;
+	h.flags = flags;
+	System.arraycopy(counts, 0, h.counts, 0, counts.length);
+	return h;
+}
+
+}
diff --git a/src/org/xbill/DNS/IPSECKEYRecord.java b/src/org/xbill/DNS/IPSECKEYRecord.java
new file mode 100644
index 0000000..7eb2956
--- /dev/null
+++ b/src/org/xbill/DNS/IPSECKEYRecord.java
@@ -0,0 +1,231 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.net.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * IPsec Keying Material (RFC 4025)
+ *
+ * @author Brian Wellington
+ */
+
+public class IPSECKEYRecord extends Record {
+
+private static final long serialVersionUID = 3050449702765909687L;
+
+public static class Algorithm {
+	private Algorithm() {}
+
+	public static final int DSA = 1;
+	public static final int RSA = 2;
+}
+
+public static class Gateway {
+	private Gateway() {}
+
+	public static final int None = 0;
+	public static final int IPv4 = 1;
+	public static final int IPv6 = 2;
+	public static final int Name = 3;
+}
+
+private int precedence;
+private int gatewayType;
+private int algorithmType;
+private Object gateway;
+private byte [] key;
+
+IPSECKEYRecord() {} 
+
+Record
+getObject() {
+	return new IPSECKEYRecord();
+}
+
+/**
+ * Creates an IPSECKEY Record from the given data.
+ * @param precedence The record's precedence.
+ * @param gatewayType The record's gateway type.
+ * @param algorithmType The record's algorithm type.
+ * @param gateway The record's gateway.
+ * @param key The record's public key.
+ */
+public
+IPSECKEYRecord(Name name, int dclass, long ttl, int precedence,
+	       int gatewayType, int algorithmType, Object gateway,
+	       byte [] key)
+{
+	super(name, Type.IPSECKEY, dclass, ttl);
+	this.precedence = checkU8("precedence", precedence);
+	this.gatewayType = checkU8("gatewayType", gatewayType);
+	this.algorithmType = checkU8("algorithmType", algorithmType);
+	switch (gatewayType) {
+	case Gateway.None:
+		this.gateway = null;
+		break;
+	case Gateway.IPv4:
+		if (!(gateway instanceof InetAddress))
+			throw new IllegalArgumentException("\"gateway\" " +
+							   "must be an IPv4 " +
+							   "address");
+		this.gateway = gateway;
+		break;
+	case Gateway.IPv6:
+		if (!(gateway instanceof Inet6Address))
+			throw new IllegalArgumentException("\"gateway\" " +
+							   "must be an IPv6 " +
+							   "address");
+		this.gateway = gateway;
+		break;
+	case Gateway.Name:
+		if (!(gateway instanceof Name))
+			throw new IllegalArgumentException("\"gateway\" " +
+							   "must be a DNS " +
+							   "name");
+		this.gateway = checkName("gateway", (Name) gateway);
+		break;
+	default:
+		throw new IllegalArgumentException("\"gatewayType\" " +
+						   "must be between 0 and 3");
+	}
+
+	this.key = key;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	precedence = in.readU8();
+	gatewayType = in.readU8();
+	algorithmType = in.readU8();
+	switch (gatewayType) {
+	case Gateway.None:
+		gateway = null;
+		break;
+	case Gateway.IPv4:
+		gateway = InetAddress.getByAddress(in.readByteArray(4));
+		break;
+	case Gateway.IPv6:
+		gateway = InetAddress.getByAddress(in.readByteArray(16));
+		break;
+	case Gateway.Name:
+		gateway = new Name(in);
+		break;
+	default:
+		throw new WireParseException("invalid gateway type");
+	}
+	if (in.remaining() > 0)
+		key = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	precedence = st.getUInt8();
+	gatewayType = st.getUInt8();
+	algorithmType = st.getUInt8();
+	switch (gatewayType) {
+	case Gateway.None:
+		String s = st.getString();
+		if (!s.equals("."))
+			throw new TextParseException("invalid gateway format");
+		gateway = null;
+		break;
+	case Gateway.IPv4:
+		gateway = st.getAddress(Address.IPv4);
+		break;
+	case Gateway.IPv6:
+		gateway = st.getAddress(Address.IPv6);
+		break;
+	case Gateway.Name:
+		gateway = st.getName(origin);
+		break;
+	default:
+		throw new WireParseException("invalid gateway type");
+	}
+	key = st.getBase64(false);
+}
+
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(precedence);
+	sb.append(" ");
+	sb.append(gatewayType);
+	sb.append(" ");
+	sb.append(algorithmType);
+	sb.append(" ");
+	switch (gatewayType) {
+	case Gateway.None:
+		sb.append(".");
+		break;
+	case Gateway.IPv4:
+	case Gateway.IPv6:
+		InetAddress gatewayAddr = (InetAddress) gateway;
+		sb.append(gatewayAddr.getHostAddress());
+		break;
+	case Gateway.Name:
+		sb.append(gateway);
+		break;
+	}
+	if (key != null) {
+		sb.append(" ");
+		sb.append(base64.toString(key));
+	}
+	return sb.toString();
+}
+
+/** Returns the record's precedence. */
+public int
+getPrecedence() {
+	return precedence;
+}
+
+/** Returns the record's gateway type. */
+public int
+getGatewayType() {
+	return gatewayType;
+}
+
+/** Returns the record's algorithm type. */
+public int
+getAlgorithmType() {
+	return algorithmType;
+}
+
+/** Returns the record's gateway. */
+public Object
+getGateway() {
+	return gateway;
+}
+
+/** Returns the record's public key */
+public byte []
+getKey() {
+	return key;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU8(precedence);
+	out.writeU8(gatewayType);
+	out.writeU8(algorithmType);
+	switch (gatewayType) {
+	case Gateway.None:
+		break;
+	case Gateway.IPv4:
+	case Gateway.IPv6:
+		InetAddress gatewayAddr = (InetAddress) gateway;
+		out.writeByteArray(gatewayAddr.getAddress());
+		break;
+	case Gateway.Name:
+		Name gatewayName = (Name) gateway;
+		gatewayName.toWire(out, null, canonical);
+		break;
+	}
+	if (key != null)
+		out.writeByteArray(key);
+}
+
+}
diff --git a/src/org/xbill/DNS/ISDNRecord.java b/src/org/xbill/DNS/ISDNRecord.java
new file mode 100644
index 0000000..8f9b629
--- /dev/null
+++ b/src/org/xbill/DNS/ISDNRecord.java
@@ -0,0 +1,105 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * ISDN - identifies the ISDN number and subaddress associated with a name.
+ *
+ * @author Brian Wellington
+ */
+
+public class ISDNRecord extends Record {
+
+private static final long serialVersionUID = -8730801385178968798L;
+
+private byte [] address;
+private byte [] subAddress;
+
+ISDNRecord() {}
+
+Record
+getObject() {
+	return new ISDNRecord();
+}
+
+/**
+ * Creates an ISDN Record from the given data
+ * @param address The ISDN number associated with the domain.
+ * @param subAddress The subaddress, if any.
+ * @throws IllegalArgumentException One of the strings is invalid.
+ */
+public
+ISDNRecord(Name name, int dclass, long ttl, String address, String subAddress) {
+	super(name, Type.ISDN, dclass, ttl);
+	try {
+		this.address = byteArrayFromString(address);
+		if (subAddress != null)
+			this.subAddress = byteArrayFromString(subAddress);
+	}
+	catch (TextParseException e) {
+		throw new IllegalArgumentException(e.getMessage());
+	}
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	address = in.readCountedString();
+	if (in.remaining() > 0)
+		subAddress = in.readCountedString();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	try {
+		address = byteArrayFromString(st.getString());
+		Tokenizer.Token t = st.get();
+		if (t.isString()) {
+			subAddress = byteArrayFromString(t.value);
+		} else {
+			st.unget();
+		}
+	}
+	catch (TextParseException e) {
+		throw st.exception(e.getMessage());
+	}
+}
+
+/**
+ * Returns the ISDN number associated with the domain.
+ */
+public String
+getAddress() {
+	return byteArrayToString(address, false);
+}
+
+/**
+ * Returns the ISDN subaddress, or null if there is none.
+ */
+public String
+getSubAddress() {
+	if (subAddress == null)
+		return null;
+	return byteArrayToString(subAddress, false);
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeCountedString(address);
+	if (subAddress != null)
+		out.writeCountedString(subAddress);
+}
+
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(byteArrayToString(address, true));
+	if (subAddress != null) {
+		sb.append(" ");
+		sb.append(byteArrayToString(subAddress, true));
+	}
+	return sb.toString();
+}
+
+}
diff --git a/src/org/xbill/DNS/InvalidDClassException.java b/src/org/xbill/DNS/InvalidDClassException.java
new file mode 100644
index 0000000..6c95cd4
--- /dev/null
+++ b/src/org/xbill/DNS/InvalidDClassException.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * An exception thrown when an invalid dclass code is specified.
+ *
+ * @author Brian Wellington
+ */
+
+public class InvalidDClassException extends IllegalArgumentException {
+
+public
+InvalidDClassException(int dclass) {
+	super("Invalid DNS class: " + dclass);
+}
+
+}
diff --git a/src/org/xbill/DNS/InvalidTTLException.java b/src/org/xbill/DNS/InvalidTTLException.java
new file mode 100644
index 0000000..95776fe
--- /dev/null
+++ b/src/org/xbill/DNS/InvalidTTLException.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * An exception thrown when an invalid TTL is specified.
+ *
+ * @author Brian Wellington
+ */
+
+public class InvalidTTLException extends IllegalArgumentException {
+
+public
+InvalidTTLException(long ttl) {
+	super("Invalid DNS TTL: " + ttl);
+}
+
+}
diff --git a/src/org/xbill/DNS/InvalidTypeException.java b/src/org/xbill/DNS/InvalidTypeException.java
new file mode 100644
index 0000000..7c61276
--- /dev/null
+++ b/src/org/xbill/DNS/InvalidTypeException.java
@@ -0,0 +1,18 @@
+// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * An exception thrown when an invalid type code is specified.
+ *
+ * @author Brian Wellington
+ */
+
+public class InvalidTypeException extends IllegalArgumentException {
+
+public
+InvalidTypeException(int type) {
+	super("Invalid DNS type: " + type);
+}
+
+}
diff --git a/src/org/xbill/DNS/KEYBase.java b/src/org/xbill/DNS/KEYBase.java
new file mode 100644
index 0000000..59a2c6c
--- /dev/null
+++ b/src/org/xbill/DNS/KEYBase.java
@@ -0,0 +1,161 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.security.PublicKey;
+
+import org.xbill.DNS.utils.*;
+
+/**
+ * The base class for KEY/DNSKEY records, which have identical formats 
+ *
+ * @author Brian Wellington
+ */
+
+abstract class KEYBase extends Record {
+
+private static final long serialVersionUID = 3469321722693285454L;
+
+protected int flags, proto, alg;
+protected byte [] key;
+protected int footprint = -1;
+protected PublicKey publicKey = null;
+
+protected
+KEYBase() {}
+
+public
+KEYBase(Name name, int type, int dclass, long ttl, int flags, int proto,
+	int alg, byte [] key)
+{
+	super(name, type, dclass, ttl);
+	this.flags = checkU16("flags", flags);
+	this.proto = checkU8("proto", proto);
+	this.alg = checkU8("alg", alg);
+	this.key = key;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	flags = in.readU16();
+	proto = in.readU8();
+	alg = in.readU8();
+	if (in.remaining() > 0)
+		key = in.readByteArray();
+}
+
+/** Converts the DNSKEY/KEY Record to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(flags);
+	sb.append(" ");
+	sb.append(proto);
+	sb.append(" ");
+	sb.append(alg);
+	if (key != null) {
+		if (Options.check("multiline")) {
+			sb.append(" (\n");
+			sb.append(base64.formatString(key, 64, "\t", true));
+			sb.append(" ; key_tag = ");
+			sb.append(getFootprint());
+		} else {
+			sb.append(" ");
+			sb.append(base64.toString(key));
+		}
+	}
+	return sb.toString();
+}
+
+/**
+ * Returns the flags describing the key's properties
+ */
+public int
+getFlags() {
+	return flags;
+}
+
+/**
+ * Returns the protocol that the key was created for
+ */
+public int
+getProtocol() {
+	return proto;
+}
+
+/**
+ * Returns the key's algorithm
+ */
+public int
+getAlgorithm() {
+	return alg;
+}
+
+/**
+ * Returns the binary data representing the key
+ */
+public byte []
+getKey() {
+	return key;
+}
+
+/**
+ * Returns the key's footprint (after computing it)
+ */
+public int
+getFootprint() {
+	if (footprint >= 0)
+		return footprint;
+
+	int foot = 0;
+
+	DNSOutput out = new DNSOutput();
+	rrToWire(out, null, false);
+	byte [] rdata = out.toByteArray();
+
+	if (alg == DNSSEC.Algorithm.RSAMD5) {
+		int d1 = rdata[rdata.length - 3] & 0xFF;
+		int d2 = rdata[rdata.length - 2] & 0xFF;
+		foot = (d1 << 8) + d2;
+	}
+	else {
+		int i; 
+		for (i = 0; i < rdata.length - 1; i += 2) {
+			int d1 = rdata[i] & 0xFF;
+			int d2 = rdata[i + 1] & 0xFF;
+			foot += ((d1 << 8) + d2);
+		}
+		if (i < rdata.length) {
+			int d1 = rdata[i] & 0xFF;
+			foot += (d1 << 8);
+		}
+		foot += ((foot >> 16) & 0xFFFF);
+	}
+	footprint = (foot & 0xFFFF);
+	return footprint;
+}
+
+/**
+ * Returns a PublicKey corresponding to the data in this key.
+ * @throws DNSSEC.DNSSECException The key could not be converted.
+ */
+public PublicKey
+getPublicKey() throws DNSSEC.DNSSECException {
+	if (publicKey != null)
+		return publicKey;
+
+	publicKey = DNSSEC.toPublicKey(this);
+	return publicKey;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU16(flags);
+	out.writeU8(proto);
+	out.writeU8(alg);
+	if (key != null)
+		out.writeByteArray(key);
+}
+
+}
diff --git a/src/org/xbill/DNS/KEYRecord.java b/src/org/xbill/DNS/KEYRecord.java
new file mode 100644
index 0000000..3d2e01c
--- /dev/null
+++ b/src/org/xbill/DNS/KEYRecord.java
@@ -0,0 +1,352 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.security.PublicKey;
+import java.util.*;
+
+/**
+ * Key - contains a cryptographic public key.  The data can be converted
+ * to objects implementing java.security.interfaces.PublicKey
+ * @see DNSSEC
+ *
+ * @author Brian Wellington
+ */
+
+public class KEYRecord extends KEYBase {
+
+private static final long serialVersionUID = 6385613447571488906L;
+
+public static class Protocol {
+	/**
+	 * KEY protocol identifiers.
+	 */
+
+	private Protocol() {}
+
+	/** No defined protocol. */
+	public static final int NONE = 0;
+
+	/** Transaction Level Security */
+	public static final int TLS = 1;
+
+	/** Email */
+	public static final int EMAIL = 2;
+
+	/** DNSSEC */
+	public static final int DNSSEC = 3;
+
+	/** IPSEC Control */
+	public static final int IPSEC = 4;
+
+	/** Any protocol */
+	public static final int ANY = 255;
+
+	private static Mnemonic protocols = new Mnemonic("KEY protocol",
+							 Mnemonic.CASE_UPPER);
+
+	static {
+		protocols.setMaximum(0xFF);
+		protocols.setNumericAllowed(true);
+
+		protocols.add(NONE, "NONE");
+		protocols.add(TLS, "TLS");
+		protocols.add(EMAIL, "EMAIL");
+		protocols.add(DNSSEC, "DNSSEC");
+		protocols.add(IPSEC, "IPSEC");
+		protocols.add(ANY, "ANY");
+	}
+
+	/**
+	 * Converts an KEY protocol value into its textual representation
+	 */
+	public static String
+	string(int type) {
+		return protocols.getText(type);
+	}
+
+	/**
+	 * Converts a textual representation of a KEY protocol into its
+	 * numeric code.  Integers in the range 0..255 are also accepted.
+	 * @param s The textual representation of the protocol
+	 * @return The protocol code, or -1 on error.
+	 */
+	public static int
+	value(String s) {
+		return protocols.getValue(s);
+	}
+}
+	
+public static class Flags {
+	/**
+	 * KEY flags identifiers.
+	 */
+
+	private Flags() {}
+
+	/** KEY cannot be used for confidentiality */
+	public static final int NOCONF = 0x4000;
+
+	/** KEY cannot be used for authentication */
+	public static final int NOAUTH = 0x8000;
+
+	/** No key present */
+	public static final int NOKEY = 0xC000;
+
+	/** Bitmask of the use fields */
+	public static final int USE_MASK = 0xC000;
+
+	/** Flag 2 (unused) */
+	public static final int FLAG2 = 0x2000;
+
+	/** Flags extension */
+	public static final int EXTEND = 0x1000;
+
+	/** Flag 4 (unused) */
+	public static final int FLAG4 = 0x0800;
+
+	/** Flag 5 (unused) */
+	public static final int FLAG5 = 0x0400;
+
+	/** Key is owned by a user. */
+	public static final int USER = 0x0000;
+
+	/** Key is owned by a zone. */
+	public static final int ZONE = 0x0100;
+
+	/** Key is owned by a host. */
+	public static final int HOST = 0x0200;
+
+	/** Key owner type 3 (reserved). */
+	public static final int NTYP3 = 0x0300;
+
+	/** Key owner bitmask. */
+	public static final int OWNER_MASK = 0x0300;
+
+	/** Flag 8 (unused) */
+	public static final int FLAG8 = 0x0080;
+
+	/** Flag 9 (unused) */
+	public static final int FLAG9 = 0x0040;
+
+	/** Flag 10 (unused) */
+	public static final int FLAG10 = 0x0020;
+
+	/** Flag 11 (unused) */
+	public static final int FLAG11 = 0x0010;
+
+	/** Signatory value 0 */
+	public static final int SIG0 = 0;
+
+	/** Signatory value 1 */
+	public static final int SIG1 = 1;
+
+	/** Signatory value 2 */
+	public static final int SIG2 = 2;
+
+	/** Signatory value 3 */
+	public static final int SIG3 = 3;
+
+	/** Signatory value 4 */
+	public static final int SIG4 = 4;
+
+	/** Signatory value 5 */
+	public static final int SIG5 = 5;
+
+	/** Signatory value 6 */
+	public static final int SIG6 = 6;
+
+	/** Signatory value 7 */
+	public static final int SIG7 = 7;
+
+	/** Signatory value 8 */
+	public static final int SIG8 = 8;
+
+	/** Signatory value 9 */
+	public static final int SIG9 = 9;
+
+	/** Signatory value 10 */
+	public static final int SIG10 = 10;
+
+	/** Signatory value 11 */
+	public static final int SIG11 = 11;
+
+	/** Signatory value 12 */
+	public static final int SIG12 = 12;
+
+	/** Signatory value 13 */
+	public static final int SIG13 = 13;
+
+	/** Signatory value 14 */
+	public static final int SIG14 = 14;
+
+	/** Signatory value 15 */
+	public static final int SIG15 = 15;
+
+	private static Mnemonic flags = new Mnemonic("KEY flags",
+						      Mnemonic.CASE_UPPER);
+
+	static {
+		flags.setMaximum(0xFFFF);
+		flags.setNumericAllowed(false);
+
+		flags.add(NOCONF, "NOCONF");
+		flags.add(NOAUTH, "NOAUTH");
+		flags.add(NOKEY, "NOKEY");
+		flags.add(FLAG2, "FLAG2");
+		flags.add(EXTEND, "EXTEND");
+		flags.add(FLAG4, "FLAG4");
+		flags.add(FLAG5, "FLAG5");
+		flags.add(USER, "USER");
+		flags.add(ZONE, "ZONE");
+		flags.add(HOST, "HOST");
+		flags.add(NTYP3, "NTYP3");
+		flags.add(FLAG8, "FLAG8");
+		flags.add(FLAG9, "FLAG9");
+		flags.add(FLAG10, "FLAG10");
+		flags.add(FLAG11, "FLAG11");
+		flags.add(SIG0, "SIG0");
+		flags.add(SIG1, "SIG1");
+		flags.add(SIG2, "SIG2");
+		flags.add(SIG3, "SIG3");
+		flags.add(SIG4, "SIG4");
+		flags.add(SIG5, "SIG5");
+		flags.add(SIG6, "SIG6");
+		flags.add(SIG7, "SIG7");
+		flags.add(SIG8, "SIG8");
+		flags.add(SIG9, "SIG9");
+		flags.add(SIG10, "SIG10");
+		flags.add(SIG11, "SIG11");
+		flags.add(SIG12, "SIG12");
+		flags.add(SIG13, "SIG13");
+		flags.add(SIG14, "SIG14");
+		flags.add(SIG15, "SIG15");
+	}
+
+	/**
+	 * Converts a textual representation of KEY flags into its
+	 * numeric code.  Integers in the range 0..65535 are also accepted.
+	 * @param s The textual representation of the protocol
+	 * @return The protocol code, or -1 on error.
+	 */
+	public static int
+	value(String s) {
+		int value;
+		try {
+			value = Integer.parseInt(s);
+			if (value >= 0 && value <= 0xFFFF) {
+				return value;
+			}
+			return -1;
+		} catch (NumberFormatException e) {
+		}
+		StringTokenizer st = new StringTokenizer(s, "|");
+		value = 0;
+		while (st.hasMoreTokens()) {
+			int val = flags.getValue(st.nextToken());
+			if (val < 0) {
+				return -1;
+			}
+			value |= val;
+		}
+		return value;
+	}
+}
+
+/* flags */
+/** This key cannot be used for confidentiality (encryption) */
+public static final int FLAG_NOCONF = Flags.NOCONF;
+
+/** This key cannot be used for authentication */
+public static final int FLAG_NOAUTH = Flags.NOAUTH;
+
+/** This key cannot be used for authentication or confidentiality */
+public static final int FLAG_NOKEY = Flags.NOKEY;
+
+/** A zone key */
+public static final int OWNER_ZONE = Flags.ZONE;
+
+/** A host/end entity key */
+public static final int OWNER_HOST = Flags.HOST;
+
+/** A user key */
+public static final int OWNER_USER = Flags.USER;
+
+/* protocols */
+/** Key was created for use with transaction level security */
+public static final int PROTOCOL_TLS = Protocol.TLS;
+
+/** Key was created for use with email */
+public static final int PROTOCOL_EMAIL = Protocol.EMAIL;
+
+/** Key was created for use with DNSSEC */
+public static final int PROTOCOL_DNSSEC = Protocol.DNSSEC;
+
+/** Key was created for use with IPSEC */
+public static final int PROTOCOL_IPSEC = Protocol.IPSEC;
+
+/** Key was created for use with any protocol */
+public static final int PROTOCOL_ANY = Protocol.ANY;
+
+KEYRecord() {}
+
+Record
+getObject() {
+	return new KEYRecord();
+}
+
+/**
+ * Creates a KEY Record from the given data
+ * @param flags Flags describing the key's properties
+ * @param proto The protocol that the key was created for
+ * @param alg The key's algorithm
+ * @param key Binary data representing the key
+ */
+public
+KEYRecord(Name name, int dclass, long ttl, int flags, int proto, int alg,
+	  byte [] key)
+{
+	super(name, Type.KEY, dclass, ttl, flags, proto, alg, key);
+}
+
+/**
+ * Creates a KEY Record from the given data
+ * @param flags Flags describing the key's properties
+ * @param proto The protocol that the key was created for
+ * @param alg The key's algorithm
+ * @param key The key as a PublicKey
+ * @throws DNSSEC.DNSSECException The PublicKey could not be converted into DNS
+ * format.
+ */
+public
+KEYRecord(Name name, int dclass, long ttl, int flags, int proto, int alg,
+	  PublicKey key) throws DNSSEC.DNSSECException
+{
+	super(name, Type.KEY, dclass, ttl, flags, proto, alg,
+	      DNSSEC.fromPublicKey(key, alg));
+	publicKey = key;
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	String flagString = st.getIdentifier();
+	flags = Flags.value(flagString);
+	if (flags < 0)
+		throw st.exception("Invalid flags: " + flagString);
+	String protoString = st.getIdentifier();
+	proto = Protocol.value(protoString);
+	if (proto < 0)
+		throw st.exception("Invalid protocol: " + protoString);
+	String algString = st.getIdentifier();
+	alg = DNSSEC.Algorithm.value(algString);
+	if (alg < 0)
+		throw st.exception("Invalid algorithm: " + algString);
+	/* If this is a null KEY, there's no key data */
+	if ((flags & Flags.USE_MASK) == Flags.NOKEY)
+		key = null;
+	else
+		key = st.getBase64();
+}
+
+}
diff --git a/src/org/xbill/DNS/KXRecord.java b/src/org/xbill/DNS/KXRecord.java
new file mode 100644
index 0000000..481d21b
--- /dev/null
+++ b/src/org/xbill/DNS/KXRecord.java
@@ -0,0 +1,51 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Key Exchange - delegation of authority
+ *
+ * @author Brian Wellington
+ */
+
+public class KXRecord extends U16NameBase {
+
+private static final long serialVersionUID = 7448568832769757809L;
+
+KXRecord() {}
+
+Record
+getObject() {
+	return new KXRecord();
+}
+
+/**
+ * Creates a KX Record from the given data
+ * @param preference The preference of this KX.  Records with lower priority
+ * are preferred.
+ * @param target The host that authority is delegated to
+ */
+public
+KXRecord(Name name, int dclass, long ttl, int preference, Name target) {
+	super(name, Type.KX, dclass, ttl, preference, "preference",
+	      target, "target");
+}
+
+/** Returns the target of the KX record */
+public Name
+getTarget() {
+	return getNameField();
+}
+
+/** Returns the preference of this KX record */
+public int
+getPreference() {
+	return getU16Field();
+}
+
+public Name
+getAdditionalName() {
+	return getNameField();
+}
+
+}
diff --git a/src/org/xbill/DNS/LOCRecord.java b/src/org/xbill/DNS/LOCRecord.java
new file mode 100644
index 0000000..4eddc15
--- /dev/null
+++ b/src/org/xbill/DNS/LOCRecord.java
@@ -0,0 +1,314 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.text.*;
+
+/**
+ * Location - describes the physical location of hosts, networks, subnets.
+ *
+ * @author Brian Wellington
+ */
+
+public class LOCRecord extends Record {
+
+private static final long serialVersionUID = 9058224788126750409L;
+
+private static NumberFormat w2, w3;
+
+private long size, hPrecision, vPrecision;
+private long latitude, longitude, altitude;
+
+static {
+	w2 = new DecimalFormat();
+	w2.setMinimumIntegerDigits(2);
+
+	w3 = new DecimalFormat();
+	w3.setMinimumIntegerDigits(3);
+}
+
+LOCRecord() {}
+
+Record
+getObject() {
+	return new LOCRecord();
+}
+
+/**
+ * Creates an LOC Record from the given data
+ * @param latitude The latitude of the center of the sphere
+ * @param longitude The longitude of the center of the sphere
+ * @param altitude The altitude of the center of the sphere, in m
+ * @param size The diameter of a sphere enclosing the described entity, in m.
+ * @param hPrecision The horizontal precision of the data, in m.
+ * @param vPrecision The vertical precision of the data, in m.
+*/
+public
+LOCRecord(Name name, int dclass, long ttl, double latitude, double longitude,
+	  double altitude, double size, double hPrecision, double vPrecision)
+{
+	super(name, Type.LOC, dclass, ttl);
+	this.latitude = (long)(latitude * 3600 * 1000 + (1L << 31));
+	this.longitude = (long)(longitude * 3600 * 1000 + (1L << 31));
+	this.altitude = (long)((altitude + 100000) * 100);
+	this.size = (long)(size * 100);
+	this.hPrecision = (long)(hPrecision * 100);
+	this.vPrecision = (long)(vPrecision * 100);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	int version;
+
+	version = in.readU8();
+	if (version != 0)
+		throw new WireParseException("Invalid LOC version");
+
+	size = parseLOCformat(in.readU8());
+	hPrecision = parseLOCformat(in.readU8());
+	vPrecision = parseLOCformat(in.readU8());
+	latitude = in.readU32();
+	longitude = in.readU32();
+	altitude = in.readU32();
+}
+
+private double
+parseFixedPoint(String s)
+{
+	if (s.matches("^-?\\d+$"))
+		return Integer.parseInt(s);
+	else if (s.matches("^-?\\d+\\.\\d*$")) {
+		String [] parts = s.split("\\.");
+		double value = Integer.parseInt(parts[0]);
+		double fraction = Integer.parseInt(parts[1]);
+		if (value < 0)
+			fraction *= -1;
+		int digits = parts[1].length();
+		return value + (fraction / Math.pow(10, digits));
+	} else
+		throw new NumberFormatException();
+}
+
+private long
+parsePosition(Tokenizer st, String type) throws IOException {
+	boolean isLatitude = type.equals("latitude");
+	int deg = 0, min = 0;
+	double sec = 0;
+	long value;
+	String s;
+
+	deg = st.getUInt16();
+	if (deg > 180 || (deg > 90 && isLatitude))
+		throw st.exception("Invalid LOC " + type + " degrees");
+
+	s = st.getString();
+	try {
+		min = Integer.parseInt(s);
+		if (min < 0 || min > 59)
+			throw st.exception("Invalid LOC " + type + " minutes");
+		s = st.getString();
+		sec = parseFixedPoint(s);
+		if (sec < 0 || sec >= 60)
+			throw st.exception("Invalid LOC " + type + " seconds");
+		s = st.getString();
+	} catch (NumberFormatException e) {
+	}
+
+	if (s.length() != 1)
+		throw st.exception("Invalid LOC " + type);
+
+	value = (long) (1000 * (sec + 60L * (min + 60L * deg)));
+
+	char c = Character.toUpperCase(s.charAt(0));
+	if ((isLatitude && c == 'S') || (!isLatitude && c == 'W'))
+		value = -value;
+	else if ((isLatitude && c != 'N') || (!isLatitude && c != 'E'))
+		throw st.exception("Invalid LOC " + type);
+
+	value += (1L << 31);
+
+	return value;
+}
+
+private long
+parseDouble(Tokenizer st, String type, boolean required, long min, long max,
+	    long defaultValue)
+throws IOException
+{
+	Tokenizer.Token token = st.get();
+	if (token.isEOL()) {
+		if (required)
+			throw st.exception("Invalid LOC " + type);
+		st.unget();
+		return defaultValue;
+	}
+	String s = token.value;
+	if (s.length() > 1 && s.charAt(s.length() - 1) == 'm')
+		s = s.substring(0, s.length() - 1);
+	try {
+		long value = (long)(100 * parseFixedPoint(s));
+		if (value < min || value > max)
+			throw st.exception("Invalid LOC " + type);
+		return value;
+	}
+	catch (NumberFormatException e) {
+		throw st.exception("Invalid LOC " + type);
+	}
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	latitude = parsePosition(st, "latitude");
+	longitude = parsePosition(st, "longitude");
+	altitude = parseDouble(st, "altitude", true,
+			       -10000000, 4284967295L, 0) + 10000000;
+	size = parseDouble(st, "size", false, 0, 9000000000L, 100);
+	hPrecision = parseDouble(st, "horizontal precision", false,
+				 0, 9000000000L, 1000000);
+	vPrecision = parseDouble(st, "vertical precision", false,
+				 0, 9000000000L, 1000);
+}
+
+private void
+renderFixedPoint(StringBuffer sb, NumberFormat formatter, long value,
+		 long divisor)
+{
+	sb.append(value / divisor);
+	value %= divisor;
+	if (value != 0) {
+		sb.append(".");
+		sb.append(formatter.format(value));
+	}
+}
+
+private String
+positionToString(long value, char pos, char neg) {
+	StringBuffer sb = new StringBuffer();
+	char direction;
+
+	long temp = value - (1L << 31);
+	if (temp < 0) {
+		temp = -temp;
+		direction = neg;
+	} else
+		direction = pos;
+
+	sb.append(temp / (3600 * 1000)); /* degrees */
+	temp = temp % (3600 * 1000);
+	sb.append(" ");
+
+	sb.append(temp / (60 * 1000)); /* minutes */
+	temp = temp % (60 * 1000);
+	sb.append(" ");
+
+	renderFixedPoint(sb, w3, temp, 1000); /* seconds */
+	sb.append(" ");
+
+	sb.append(direction);
+
+	return sb.toString();
+}
+
+
+/** Convert to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+
+	/* Latitude */
+	sb.append(positionToString(latitude, 'N', 'S'));
+	sb.append(" ");
+
+	/* Latitude */
+	sb.append(positionToString(longitude, 'E', 'W'));
+	sb.append(" ");
+
+	/* Altitude */
+	renderFixedPoint(sb, w2, altitude - 10000000, 100);
+	sb.append("m ");
+
+	/* Size */
+	renderFixedPoint(sb, w2, size, 100);
+	sb.append("m ");
+
+	/* Horizontal precision */
+	renderFixedPoint(sb, w2, hPrecision, 100);
+	sb.append("m ");
+
+	/* Vertical precision */
+	renderFixedPoint(sb, w2, vPrecision, 100);
+	sb.append("m");
+
+	return sb.toString();
+}
+
+/** Returns the latitude */
+public double
+getLatitude() {  
+	return ((double)(latitude - (1L << 31))) / (3600 * 1000);
+}       
+
+/** Returns the longitude */
+public double
+getLongitude() {  
+	return ((double)(longitude - (1L << 31))) / (3600 * 1000);
+}       
+
+/** Returns the altitude */
+public double
+getAltitude() {  
+	return ((double)(altitude - 10000000)) / 100;
+}       
+
+/** Returns the diameter of the enclosing sphere */
+public double
+getSize() {  
+	return ((double)size) / 100;
+}       
+
+/** Returns the horizontal precision */
+public double
+getHPrecision() {  
+	return ((double)hPrecision) / 100;
+}       
+
+/** Returns the horizontal precision */
+public double
+getVPrecision() {  
+	return ((double)vPrecision) / 100;
+}       
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU8(0); /* version */
+	out.writeU8(toLOCformat(size));
+	out.writeU8(toLOCformat(hPrecision));
+	out.writeU8(toLOCformat(vPrecision));
+	out.writeU32(latitude);
+	out.writeU32(longitude);
+	out.writeU32(altitude);
+}
+
+private static long
+parseLOCformat(int b) throws WireParseException {
+	long out = b >> 4;
+	int exp = b & 0xF;
+	if (out > 9 || exp > 9)
+		throw new WireParseException("Invalid LOC Encoding");
+	while (exp-- > 0)
+		out *= 10;
+	return (out);
+}
+
+private int
+toLOCformat(long l) {
+	byte exp = 0;
+	while (l > 9) {
+		exp++;
+		l /= 10;
+	}
+	return (int)((l << 4) + exp);
+}
+
+}
diff --git a/src/org/xbill/DNS/Lookup.java b/src/org/xbill/DNS/Lookup.java
new file mode 100644
index 0000000..0beba59
--- /dev/null
+++ b/src/org/xbill/DNS/Lookup.java
@@ -0,0 +1,647 @@
+// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+import java.io.*;
+import java.net.*;
+
+/**
+ * The Lookup object issues queries to caching DNS servers.  The input consists
+ * of a name, an optional type, and an optional class.  Caching is enabled
+ * by default and used when possible to reduce the number of DNS requests.
+ * A Resolver, which defaults to an ExtendedResolver initialized with the
+ * resolvers located by the ResolverConfig class, performs the queries.  A
+ * search path of domain suffixes is used to resolve relative names, and is
+ * also determined by the ResolverConfig class.
+ *
+ * A Lookup object may be reused, but should not be used by multiple threads.
+ *
+ * @see Cache
+ * @see Resolver
+ * @see ResolverConfig
+ *
+ * @author Brian Wellington
+ */
+
+public final class Lookup {
+
+private static Resolver defaultResolver;
+private static Name [] defaultSearchPath;
+private static Map defaultCaches;
+private static int defaultNdots;
+
+private Resolver resolver;
+private Name [] searchPath;
+private Cache cache;
+private boolean temporary_cache;
+private int credibility;
+private Name name;
+private int type;
+private int dclass;
+private boolean verbose;
+private int iterations;
+private boolean foundAlias;
+private boolean done;
+private boolean doneCurrent;
+private List aliases;
+private Record [] answers;
+private int result;
+private String error;
+private boolean nxdomain;
+private boolean badresponse;
+private String badresponse_error;
+private boolean networkerror;
+private boolean timedout;
+private boolean nametoolong;
+private boolean referral;
+
+private static final Name [] noAliases = new Name[0];
+
+/** The lookup was successful. */
+public static final int SUCCESSFUL = 0;
+
+/**
+ * The lookup failed due to a data or server error. Repeating the lookup
+ * would not be helpful.
+ */
+public static final int UNRECOVERABLE = 1;
+
+/**
+ * The lookup failed due to a network error. Repeating the lookup may be
+ * helpful.
+ */
+public static final int TRY_AGAIN = 2;
+
+/** The host does not exist. */
+public static final int HOST_NOT_FOUND = 3;
+
+/** The host exists, but has no records associated with the queried type. */
+public static final int TYPE_NOT_FOUND = 4;
+
+public static synchronized void
+refreshDefault() {
+
+	try {
+		defaultResolver = new ExtendedResolver();
+	}
+	catch (UnknownHostException e) {
+		throw new RuntimeException("Failed to initialize resolver");
+	}
+	defaultSearchPath = ResolverConfig.getCurrentConfig().searchPath();
+	defaultCaches = new HashMap();
+	defaultNdots = ResolverConfig.getCurrentConfig().ndots();
+}
+
+static {
+	refreshDefault();
+}
+
+/**
+ * Gets the Resolver that will be used as the default by future Lookups.
+ * @return The default resolver.
+ */
+public static synchronized Resolver
+getDefaultResolver() {
+	return defaultResolver;
+}
+
+/**
+ * Sets the default Resolver to be used as the default by future Lookups.
+ * @param resolver The default resolver.
+ */
+public static synchronized void
+setDefaultResolver(Resolver resolver) {
+	defaultResolver = resolver;
+}
+
+/**
+ * Gets the Cache that will be used as the default for the specified
+ * class by future Lookups.
+ * @param dclass The class whose cache is being retrieved.
+ * @return The default cache for the specified class.
+ */
+public static synchronized Cache
+getDefaultCache(int dclass) {
+	DClass.check(dclass);
+	Cache c = (Cache) defaultCaches.get(Mnemonic.toInteger(dclass));
+	if (c == null) {
+		c = new Cache(dclass);
+		defaultCaches.put(Mnemonic.toInteger(dclass), c);
+	}
+	return c;
+}
+
+/**
+ * Sets the Cache to be used as the default for the specified class by future
+ * Lookups.
+ * @param cache The default cache for the specified class.
+ * @param dclass The class whose cache is being set.
+ */
+public static synchronized void
+setDefaultCache(Cache cache, int dclass) {
+	DClass.check(dclass);
+	defaultCaches.put(Mnemonic.toInteger(dclass), cache);
+}
+
+/**
+ * Gets the search path that will be used as the default by future Lookups.
+ * @return The default search path.
+ */
+public static synchronized Name []
+getDefaultSearchPath() {
+	return defaultSearchPath;
+}
+
+/**
+ * Sets the search path to be used as the default by future Lookups.
+ * @param domains The default search path.
+ */
+public static synchronized void
+setDefaultSearchPath(Name [] domains) {
+	defaultSearchPath = domains;
+}
+
+/**
+ * Sets the search path that will be used as the default by future Lookups.
+ * @param domains The default search path.
+ * @throws TextParseException A name in the array is not a valid DNS name.
+ */
+public static synchronized void
+setDefaultSearchPath(String [] domains) throws TextParseException {
+	if (domains == null) {
+		defaultSearchPath = null;
+		return;
+	}
+	Name [] newdomains = new Name[domains.length];
+	for (int i = 0; i < domains.length; i++)
+		newdomains[i] = Name.fromString(domains[i], Name.root);
+	defaultSearchPath = newdomains;
+}
+
+private final void
+reset() {
+	iterations = 0;
+	foundAlias = false;
+	done = false;
+	doneCurrent = false;
+	aliases = null;
+	answers = null;
+	result = -1;
+	error = null;
+	nxdomain = false;
+	badresponse = false;
+	badresponse_error = null;
+	networkerror = false;
+	timedout = false;
+	nametoolong = false;
+	referral = false;
+	if (temporary_cache)
+		cache.clearCache();
+}
+
+/**
+ * Create a Lookup object that will find records of the given name, type,
+ * and class.  The lookup will use the default cache, resolver, and search
+ * path, and look for records that are reasonably credible.
+ * @param name The name of the desired records
+ * @param type The type of the desired records
+ * @param dclass The class of the desired records
+ * @throws IllegalArgumentException The type is a meta type other than ANY.
+ * @see Cache
+ * @see Resolver
+ * @see Credibility
+ * @see Name
+ * @see Type
+ * @see DClass
+ */
+public
+Lookup(Name name, int type, int dclass) {
+	Type.check(type);
+	DClass.check(dclass);
+	if (!Type.isRR(type) && type != Type.ANY)
+		throw new IllegalArgumentException("Cannot query for " +
+						   "meta-types other than ANY");
+	this.name = name;
+	this.type = type;
+	this.dclass = dclass;
+	synchronized (Lookup.class) {
+		this.resolver = getDefaultResolver();
+		this.searchPath = getDefaultSearchPath();
+		this.cache = getDefaultCache(dclass);
+	}
+	this.credibility = Credibility.NORMAL;
+	this.verbose = Options.check("verbose");
+	this.result = -1;
+}
+
+/**
+ * Create a Lookup object that will find records of the given name and type
+ * in the IN class.
+ * @param name The name of the desired records
+ * @param type The type of the desired records
+ * @throws IllegalArgumentException The type is a meta type other than ANY.
+ * @see #Lookup(Name,int,int)
+ */
+public
+Lookup(Name name, int type) {
+	this(name, type, DClass.IN);
+}
+
+/**
+ * Create a Lookup object that will find records of type A at the given name
+ * in the IN class.
+ * @param name The name of the desired records
+ * @see #Lookup(Name,int,int)
+ */
+public
+Lookup(Name name) {
+	this(name, Type.A, DClass.IN);
+}
+
+/**
+ * Create a Lookup object that will find records of the given name, type,
+ * and class.
+ * @param name The name of the desired records
+ * @param type The type of the desired records
+ * @param dclass The class of the desired records
+ * @throws TextParseException The name is not a valid DNS name
+ * @throws IllegalArgumentException The type is a meta type other than ANY.
+ * @see #Lookup(Name,int,int)
+ */
+public
+Lookup(String name, int type, int dclass) throws TextParseException {
+	this(Name.fromString(name), type, dclass);
+}
+
+/**
+ * Create a Lookup object that will find records of the given name and type
+ * in the IN class.
+ * @param name The name of the desired records
+ * @param type The type of the desired records
+ * @throws TextParseException The name is not a valid DNS name
+ * @throws IllegalArgumentException The type is a meta type other than ANY.
+ * @see #Lookup(Name,int,int)
+ */
+public
+Lookup(String name, int type) throws TextParseException {
+	this(Name.fromString(name), type, DClass.IN);
+}
+
+/**
+ * Create a Lookup object that will find records of type A at the given name
+ * in the IN class.
+ * @param name The name of the desired records
+ * @throws TextParseException The name is not a valid DNS name
+ * @see #Lookup(Name,int,int)
+ */
+public
+Lookup(String name) throws TextParseException {
+	this(Name.fromString(name), Type.A, DClass.IN);
+}
+
+/**
+ * Sets the resolver to use when performing this lookup.  This overrides the
+ * default value.
+ * @param resolver The resolver to use.
+ */
+public void
+setResolver(Resolver resolver) {
+	this.resolver = resolver;
+}
+
+/**
+ * Sets the search path to use when performing this lookup.  This overrides the
+ * default value.
+ * @param domains An array of names containing the search path.
+ */
+public void
+setSearchPath(Name [] domains) {
+	this.searchPath = domains;
+}
+
+/**
+ * Sets the search path to use when performing this lookup. This overrides the
+ * default value.
+ * @param domains An array of names containing the search path.
+ * @throws TextParseException A name in the array is not a valid DNS name.
+ */
+public void
+setSearchPath(String [] domains) throws TextParseException {
+	if (domains == null) {
+		this.searchPath = null;
+		return;
+	}
+	Name [] newdomains = new Name[domains.length];
+	for (int i = 0; i < domains.length; i++)
+		newdomains[i] = Name.fromString(domains[i], Name.root);
+	this.searchPath = newdomains;
+}
+
+/**
+ * Sets the cache to use when performing this lookup.  This overrides the
+ * default value.  If the results of this lookup should not be permanently
+ * cached, null can be provided here.
+ * @param cache The cache to use.
+ */
+public void
+setCache(Cache cache) {
+	if (cache == null) {
+		this.cache = new Cache(dclass);
+		this.temporary_cache = true;
+	} else {
+		this.cache = cache;
+		this.temporary_cache = false;
+	}
+}
+
+/**
+ * Sets ndots to use when performing this lookup, overriding the default value.
+ * Specifically, this refers to the number of "dots" which, if present in a
+ * name, indicate that a lookup for the absolute name should be attempted
+ * before appending any search path elements.
+ * @param ndots The ndots value to use, which must be greater than or equal to
+ * 0.
+ */
+public void
+setNdots(int ndots) {
+	if (ndots < 0)
+		throw new IllegalArgumentException("Illegal ndots value: " +
+						   ndots);
+	defaultNdots = ndots;
+}
+
+/**
+ * Sets the minimum credibility level that will be accepted when performing
+ * the lookup.  This defaults to Credibility.NORMAL.
+ * @param credibility The minimum credibility level.
+ */
+public void
+setCredibility(int credibility) {
+	this.credibility = credibility;
+}
+
+private void
+follow(Name name, Name oldname) {
+	foundAlias = true;
+	badresponse = false;
+	networkerror = false;
+	timedout = false;
+	nxdomain = false;
+	referral = false;
+	iterations++;
+	if (iterations >= 6 || name.equals(oldname)) {
+		result = UNRECOVERABLE;
+		error = "CNAME loop";
+		done = true;
+		return;
+	}
+	if (aliases == null)
+		aliases = new ArrayList();
+	aliases.add(oldname);
+	lookup(name);
+}
+
+private void
+processResponse(Name name, SetResponse response) {
+	if (response.isSuccessful()) {
+		RRset [] rrsets = response.answers();
+		List l = new ArrayList();
+		Iterator it;
+		int i;
+
+		for (i = 0; i < rrsets.length; i++) {
+			it = rrsets[i].rrs();
+			while (it.hasNext())
+				l.add(it.next());
+		}
+
+		result = SUCCESSFUL;
+		answers = (Record []) l.toArray(new Record[l.size()]);
+		done = true;
+	} else if (response.isNXDOMAIN()) {
+		nxdomain = true;
+		doneCurrent = true;
+		if (iterations > 0) {
+			result = HOST_NOT_FOUND;
+			done = true;
+		}
+	} else if (response.isNXRRSET()) {
+		result = TYPE_NOT_FOUND;
+		answers = null;
+		done = true;
+	} else if (response.isCNAME()) {
+		CNAMERecord cname = response.getCNAME();
+		follow(cname.getTarget(), name);
+	} else if (response.isDNAME()) {
+		DNAMERecord dname = response.getDNAME();
+		try {
+			follow(name.fromDNAME(dname), name);
+		} catch (NameTooLongException e) {
+			result = UNRECOVERABLE;
+			error = "Invalid DNAME target";
+			done = true;
+		}
+	} else if (response.isDelegation()) {
+		// We shouldn't get a referral.  Ignore it.
+		referral = true;
+	}
+}
+
+private void
+lookup(Name current) {
+	SetResponse sr = cache.lookupRecords(current, type, credibility);
+	if (verbose) {
+		System.err.println("lookup " + current + " " +
+				   Type.string(type));
+		System.err.println(sr);
+	}
+	processResponse(current, sr);
+	if (done || doneCurrent)
+		return;
+
+	Record question = Record.newRecord(current, type, dclass);
+	Message query = Message.newQuery(question);
+	Message response = null;
+	try {
+		response = resolver.send(query);
+	}
+	catch (IOException e) {
+		// A network error occurred.  Press on.
+		if (e instanceof InterruptedIOException)
+			timedout = true;
+		else
+			networkerror = true;
+		return;
+	}
+	int rcode = response.getHeader().getRcode();
+	if (rcode != Rcode.NOERROR && rcode != Rcode.NXDOMAIN) {
+		// The server we contacted is broken or otherwise unhelpful.
+		// Press on.
+		badresponse = true;
+		badresponse_error = Rcode.string(rcode);
+		return;
+	}
+
+	if (!query.getQuestion().equals(response.getQuestion())) {
+		// The answer doesn't match the question.  That's not good.
+		badresponse = true;
+		badresponse_error = "response does not match query";
+		return;
+	}
+
+	sr = cache.addMessage(response);
+	if (sr == null)
+		sr = cache.lookupRecords(current, type, credibility);
+	if (verbose) {
+		System.err.println("queried " + current + " " +
+				   Type.string(type));
+		System.err.println(sr);
+	}
+	processResponse(current, sr);
+}
+
+private void
+resolve(Name current, Name suffix) {
+	doneCurrent = false;
+	Name tname = null;
+	if (suffix == null)
+		tname = current;
+	else {
+		try {
+			tname = Name.concatenate(current, suffix);
+		}
+		catch (NameTooLongException e) {
+			nametoolong = true;
+			return;
+		}
+	}
+	lookup(tname);
+}
+
+/**
+ * Performs the lookup, using the specified Cache, Resolver, and search path.
+ * @return The answers, or null if none are found.
+ */
+public Record []
+run() {
+	if (done)
+		reset();
+	if (name.isAbsolute())
+		resolve(name, null);
+	else if (searchPath == null)
+		resolve(name, Name.root);
+	else {
+		if (name.labels() > defaultNdots)
+			resolve(name, Name.root);
+		if (done)
+			return answers;
+
+		for (int i = 0; i < searchPath.length; i++) {
+			resolve(name, searchPath[i]);
+			if (done)
+				return answers;
+			else if (foundAlias)
+				break;
+		}
+	}
+	if (!done) {
+		if (badresponse) {
+			result = TRY_AGAIN;
+			error = badresponse_error;
+			done = true;
+		} else if (timedout) {
+			result = TRY_AGAIN;
+			error = "timed out";
+			done = true;
+		} else if (networkerror) {
+			result = TRY_AGAIN;
+			error = "network error";
+			done = true;
+		} else if (nxdomain) {
+			result = HOST_NOT_FOUND;
+			done = true;
+		} else if (referral) {
+			result = UNRECOVERABLE;
+			error = "referral";
+			done = true;
+		} else if (nametoolong) {
+			result = UNRECOVERABLE;
+			error = "name too long";
+			done = true;
+		}
+	}
+	return answers;
+}
+
+private void
+checkDone() {
+	if (done && result != -1)
+		return;
+	StringBuffer sb = new StringBuffer("Lookup of " + name + " ");
+	if (dclass != DClass.IN)
+		sb.append(DClass.string(dclass) + " ");
+	sb.append(Type.string(type) + " isn't done");
+	throw new IllegalStateException(sb.toString());
+}
+
+/**
+ * Returns the answers from the lookup.
+ * @return The answers, or null if none are found.
+ * @throws IllegalStateException The lookup has not completed.
+ */
+public Record []
+getAnswers() {
+	checkDone();
+	return answers;
+}
+
+/**
+ * Returns all known aliases for this name.  Whenever a CNAME/DNAME is
+ * followed, an alias is added to this array.  The last element in this
+ * array will be the owner name for records in the answer, if there are any.
+ * @return The aliases.
+ * @throws IllegalStateException The lookup has not completed.
+ */
+public Name []
+getAliases() {
+	checkDone();
+	if (aliases == null)
+		return noAliases;
+	return (Name []) aliases.toArray(new Name[aliases.size()]);
+}
+
+/**
+ * Returns the result code of the lookup.
+ * @return The result code, which can be SUCCESSFUL, UNRECOVERABLE, TRY_AGAIN,
+ * HOST_NOT_FOUND, or TYPE_NOT_FOUND.
+ * @throws IllegalStateException The lookup has not completed.
+ */
+public int
+getResult() {
+	checkDone();
+	return result;
+}
+
+/**
+ * Returns an error string describing the result code of this lookup.
+ * @return A string, which may either directly correspond the result code
+ * or be more specific.
+ * @throws IllegalStateException The lookup has not completed.
+ */
+public String
+getErrorString() {
+	checkDone();
+	if (error != null)
+		return error;
+	switch (result) {
+		case SUCCESSFUL:	return "successful";
+		case UNRECOVERABLE:	return "unrecoverable error";
+		case TRY_AGAIN:		return "try again";
+		case HOST_NOT_FOUND:	return "host not found";
+		case TYPE_NOT_FOUND:	return "type not found";
+	}
+	throw new IllegalStateException("unknown result");
+}
+
+}
diff --git a/src/org/xbill/DNS/MBRecord.java b/src/org/xbill/DNS/MBRecord.java
new file mode 100644
index 0000000..6b65edf
--- /dev/null
+++ b/src/org/xbill/DNS/MBRecord.java
@@ -0,0 +1,42 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Mailbox Record  - specifies a host containing a mailbox.
+ *
+ * @author Brian Wellington
+ */
+
+public class MBRecord extends SingleNameBase {
+
+private static final long serialVersionUID = 532349543479150419L;
+
+MBRecord() {}
+
+Record
+getObject() {
+	return new MBRecord();
+}
+
+/** 
+ * Creates a new MB Record with the given data
+ * @param mailbox The host containing the mailbox for the domain.
+ */
+public
+MBRecord(Name name, int dclass, long ttl, Name mailbox) {
+	super(name, Type.MB, dclass, ttl, mailbox, "mailbox");
+}
+
+/** Gets the mailbox for the domain */
+public Name
+getMailbox() {
+	return getSingleName();
+}
+
+public Name
+getAdditionalName() {
+	return getSingleName();
+}
+
+}
diff --git a/src/org/xbill/DNS/MDRecord.java b/src/org/xbill/DNS/MDRecord.java
new file mode 100644
index 0000000..dbf51af
--- /dev/null
+++ b/src/org/xbill/DNS/MDRecord.java
@@ -0,0 +1,43 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Mail Destination Record  - specifies a mail agent which delivers mail
+ * for a domain (obsolete)
+ *
+ * @author Brian Wellington
+ */
+
+public class MDRecord extends SingleNameBase {
+
+private static final long serialVersionUID = 5268878603762942202L;
+
+MDRecord() {}
+
+Record
+getObject() {
+	return new MDRecord();
+}
+
+/** 
+ * Creates a new MD Record with the given data
+ * @param mailAgent The mail agent that delivers mail for the domain.
+ */
+public
+MDRecord(Name name, int dclass, long ttl, Name mailAgent) {
+	super(name, Type.MD, dclass, ttl, mailAgent, "mail agent");
+}
+
+/** Gets the mail agent for the domain */
+public Name
+getMailAgent() {
+	return getSingleName();
+}
+
+public Name
+getAdditionalName() {
+	return getSingleName();
+}
+
+}
diff --git a/src/org/xbill/DNS/MFRecord.java b/src/org/xbill/DNS/MFRecord.java
new file mode 100644
index 0000000..ff293d7
--- /dev/null
+++ b/src/org/xbill/DNS/MFRecord.java
@@ -0,0 +1,43 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Mail Forwarder Record  - specifies a mail agent which forwards mail
+ * for a domain (obsolete)
+ *
+ * @author Brian Wellington
+ */
+
+public class MFRecord extends SingleNameBase {
+
+private static final long serialVersionUID = -6670449036843028169L;
+
+MFRecord() {}
+
+Record
+getObject() {
+	return new MFRecord();
+}
+
+/** 
+ * Creates a new MF Record with the given data
+ * @param mailAgent The mail agent that forwards mail for the domain.
+ */
+public
+MFRecord(Name name, int dclass, long ttl, Name mailAgent) {
+	super(name, Type.MF, dclass, ttl, mailAgent, "mail agent");
+}
+
+/** Gets the mail agent for the domain */
+public Name
+getMailAgent() {
+	return getSingleName();
+}
+
+public Name
+getAdditionalName() {
+	return getSingleName();
+}
+
+}
diff --git a/src/org/xbill/DNS/MGRecord.java b/src/org/xbill/DNS/MGRecord.java
new file mode 100644
index 0000000..5752f49
--- /dev/null
+++ b/src/org/xbill/DNS/MGRecord.java
@@ -0,0 +1,38 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Mail Group Record  - specifies a mailbox which is a member of a mail group.
+ *
+ * @author Brian Wellington
+ */
+
+public class MGRecord extends SingleNameBase {
+
+private static final long serialVersionUID = -3980055550863644582L;
+
+MGRecord() {}
+
+Record
+getObject() {
+	return new MGRecord();
+}
+
+/** 
+ * Creates a new MG Record with the given data
+ * @param mailbox The mailbox that is a member of the group specified by the
+ * domain.
+ */
+public
+MGRecord(Name name, int dclass, long ttl, Name mailbox) {
+	super(name, Type.MG, dclass, ttl, mailbox, "mailbox");
+}
+
+/** Gets the mailbox in the mail group specified by the domain */
+public Name
+getMailbox() {
+	return getSingleName();
+}
+
+}
diff --git a/src/org/xbill/DNS/MINFORecord.java b/src/org/xbill/DNS/MINFORecord.java
new file mode 100644
index 0000000..4324cda
--- /dev/null
+++ b/src/org/xbill/DNS/MINFORecord.java
@@ -0,0 +1,90 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * Mailbox information Record - lists the address responsible for a mailing
+ * list/mailbox and the address to receive error messages relating to the
+ * mailing list/mailbox.
+ *
+ * @author Brian Wellington
+ */
+
+public class MINFORecord extends Record {
+
+private static final long serialVersionUID = -3962147172340353796L;
+
+private Name responsibleAddress;
+private Name errorAddress;
+
+MINFORecord() {}
+
+Record
+getObject() {
+	return new MINFORecord();
+}
+
+/**
+ * Creates an MINFO Record from the given data
+ * @param responsibleAddress The address responsible for the
+ * mailing list/mailbox.
+ * @param errorAddress The address to receive error messages relating to the
+ * mailing list/mailbox.
+ */
+public
+MINFORecord(Name name, int dclass, long ttl,
+	    Name responsibleAddress, Name errorAddress)
+{
+	super(name, Type.MINFO, dclass, ttl);
+
+	this.responsibleAddress = checkName("responsibleAddress",
+					    responsibleAddress);
+	this.errorAddress = checkName("errorAddress", errorAddress);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	responsibleAddress = new Name(in);
+	errorAddress = new Name(in);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	responsibleAddress = st.getName(origin);
+	errorAddress = st.getName(origin);
+}
+
+/** Converts the MINFO Record to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(responsibleAddress);
+	sb.append(" ");
+	sb.append(errorAddress);
+	return sb.toString();
+}
+
+/** Gets the address responsible for the mailing list/mailbox. */
+public Name
+getResponsibleAddress() {
+	return responsibleAddress;
+}
+
+/**
+ * Gets the address to receive error messages relating to the mailing
+ * list/mailbox.
+ */
+public Name
+getErrorAddress() {
+	return errorAddress;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	responsibleAddress.toWire(out, null, canonical);
+	errorAddress.toWire(out, null, canonical);
+}
+
+}
diff --git a/src/org/xbill/DNS/MRRecord.java b/src/org/xbill/DNS/MRRecord.java
new file mode 100644
index 0000000..a7ff4fc
--- /dev/null
+++ b/src/org/xbill/DNS/MRRecord.java
@@ -0,0 +1,38 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Mailbox Rename Record  - specifies a rename of a mailbox.
+ *
+ * @author Brian Wellington
+ */
+
+public class MRRecord extends SingleNameBase {
+
+private static final long serialVersionUID = -5617939094209927533L;
+
+MRRecord() {}
+
+Record
+getObject() {
+	return new MRRecord();
+}
+
+/** 
+ * Creates a new MR Record with the given data
+ * @param newName The new name of the mailbox specified by the domain.
+ * domain.
+ */
+public
+MRRecord(Name name, int dclass, long ttl, Name newName) {
+	super(name, Type.MR, dclass, ttl, newName, "new name");
+}
+
+/** Gets the new name of the mailbox specified by the domain */
+public Name
+getNewName() {
+	return getSingleName();
+}
+
+}
diff --git a/src/org/xbill/DNS/MXRecord.java b/src/org/xbill/DNS/MXRecord.java
new file mode 100644
index 0000000..111977d
--- /dev/null
+++ b/src/org/xbill/DNS/MXRecord.java
@@ -0,0 +1,57 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Mail Exchange - specifies where mail to a domain is sent
+ *
+ * @author Brian Wellington
+ */
+
+public class MXRecord extends U16NameBase {
+
+private static final long serialVersionUID = 2914841027584208546L;
+
+MXRecord() {}
+
+Record
+getObject() {
+	return new MXRecord();
+}
+
+/**
+ * Creates an MX Record from the given data
+ * @param priority The priority of this MX.  Records with lower priority
+ * are preferred.
+ * @param target The host that mail is sent to
+ */
+public
+MXRecord(Name name, int dclass, long ttl, int priority, Name target) {
+	super(name, Type.MX, dclass, ttl, priority, "priority",
+	      target, "target");
+}
+
+/** Returns the target of the MX record */
+public Name
+getTarget() {
+	return getNameField();
+}
+
+/** Returns the priority of this MX record */
+public int
+getPriority() {
+	return getU16Field();
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU16(u16Field);
+	nameField.toWire(out, c, canonical);
+}
+
+public Name
+getAdditionalName() {
+	return getNameField();
+}
+
+}
diff --git a/src/org/xbill/DNS/Master.java b/src/org/xbill/DNS/Master.java
new file mode 100644
index 0000000..c795a9c
--- /dev/null
+++ b/src/org/xbill/DNS/Master.java
@@ -0,0 +1,427 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * A DNS master file parser.  This incrementally parses the file, returning
+ * one record at a time.  When directives are seen, they are added to the
+ * state and used when parsing future records.
+ *
+ * @author Brian Wellington
+ */
+
+public class Master {
+
+private Name origin;
+private File file;
+private Record last = null;
+private long defaultTTL;
+private Master included = null;
+private Tokenizer st;
+private int currentType;
+private int currentDClass;
+private long currentTTL;
+private boolean needSOATTL;
+
+private Generator generator;
+private List generators;
+private boolean noExpandGenerate;
+
+Master(File file, Name origin, long initialTTL) throws IOException {
+	if (origin != null && !origin.isAbsolute()) {
+		throw new RelativeNameException(origin);
+	}
+	this.file = file;
+	st = new Tokenizer(file);
+	this.origin = origin;
+	defaultTTL = initialTTL;
+}
+
+/**
+ * Initializes the master file reader and opens the specified master file.
+ * @param filename The master file.
+ * @param origin The initial origin to append to relative names.
+ * @param ttl The initial default TTL.
+ * @throws IOException The master file could not be opened.
+ */
+public
+Master(String filename, Name origin, long ttl) throws IOException {
+	this(new File(filename), origin, ttl);
+}
+
+/**
+ * Initializes the master file reader and opens the specified master file.
+ * @param filename The master file.
+ * @param origin The initial origin to append to relative names.
+ * @throws IOException The master file could not be opened.
+ */
+public
+Master(String filename, Name origin) throws IOException {
+	this(new File(filename), origin, -1);
+}
+
+/**
+ * Initializes the master file reader and opens the specified master file.
+ * @param filename The master file.
+ * @throws IOException The master file could not be opened.
+ */
+public
+Master(String filename) throws IOException {
+	this(new File(filename), null, -1);
+}
+
+/**
+ * Initializes the master file reader.
+ * @param in The input stream containing a master file.
+ * @param origin The initial origin to append to relative names.
+ * @param ttl The initial default TTL.
+ */
+public
+Master(InputStream in, Name origin, long ttl) {
+	if (origin != null && !origin.isAbsolute()) {
+		throw new RelativeNameException(origin);
+	}
+	st = new Tokenizer(in);
+	this.origin = origin;
+	defaultTTL = ttl;
+}
+
+/**
+ * Initializes the master file reader.
+ * @param in The input stream containing a master file.
+ * @param origin The initial origin to append to relative names.
+ */
+public
+Master(InputStream in, Name origin) {
+	this(in, origin, -1);
+}
+
+/**
+ * Initializes the master file reader.
+ * @param in The input stream containing a master file.
+ */
+public
+Master(InputStream in) {
+	this(in, null, -1);
+}
+
+private Name
+parseName(String s, Name origin) throws TextParseException {
+	try {
+		return Name.fromString(s, origin);
+	}
+	catch (TextParseException e) {
+		throw st.exception(e.getMessage());
+	}
+}
+
+private void
+parseTTLClassAndType() throws IOException {
+	String s;
+	boolean seen_class = false;
+
+
+	// This is a bit messy, since any of the following are legal:
+	//   class ttl type
+	//   ttl class type
+	//   class type
+	//   ttl type
+	//   type
+	seen_class = false;
+	s = st.getString();
+	if ((currentDClass = DClass.value(s)) >= 0) {
+		s = st.getString();
+		seen_class = true;
+	}
+
+	currentTTL = -1;
+	try {
+		currentTTL = TTL.parseTTL(s);
+		s = st.getString();
+	}
+	catch (NumberFormatException e) {
+		if (defaultTTL >= 0)
+			currentTTL = defaultTTL;
+		else if (last != null)
+			currentTTL = last.getTTL();
+	}
+
+	if (!seen_class) {
+		if ((currentDClass = DClass.value(s)) >= 0) {
+			s = st.getString();
+		} else {
+			currentDClass = DClass.IN;
+		}
+	}
+
+	if ((currentType = Type.value(s)) < 0)
+		throw st.exception("Invalid type '" + s + "'");
+
+	// BIND allows a missing TTL for the initial SOA record, and uses
+	// the SOA minimum value.  If the SOA is not the first record,
+	// this is an error.
+	if (currentTTL < 0) {
+		if (currentType != Type.SOA)
+			throw st.exception("missing TTL");
+		needSOATTL = true;
+		currentTTL = 0;
+	}
+}
+
+private long
+parseUInt32(String s) {
+	if (!Character.isDigit(s.charAt(0)))
+		return -1;
+	try {
+		long l = Long.parseLong(s);
+		if (l < 0 || l > 0xFFFFFFFFL)
+			return -1;
+		return l;
+	}
+	catch (NumberFormatException e) {
+		return -1;
+	}
+}
+
+private void
+startGenerate() throws IOException {
+	String s;
+	int n;
+
+	// The first field is of the form start-end[/step]
+	// Regexes would be useful here.
+	s = st.getIdentifier();
+	n = s.indexOf("-");
+	if (n < 0)
+		throw st.exception("Invalid $GENERATE range specifier: " + s);
+	String startstr = s.substring(0, n);
+	String endstr = s.substring(n + 1);
+	String stepstr = null;
+	n = endstr.indexOf("/");
+	if (n >= 0) {
+		stepstr = endstr.substring(n + 1);
+		endstr = endstr.substring(0, n);
+	}
+	long start = parseUInt32(startstr);
+	long end = parseUInt32(endstr);
+	long step;
+	if (stepstr != null)
+		step = parseUInt32(stepstr);
+	else
+		step = 1;
+	if (start < 0 || end < 0 || start > end || step <= 0)
+		throw st.exception("Invalid $GENERATE range specifier: " + s);
+
+	// The next field is the name specification.
+	String nameSpec = st.getIdentifier();
+
+	// Then the ttl/class/type, in the same form as a normal record.
+	// Only some types are supported.
+	parseTTLClassAndType();
+	if (!Generator.supportedType(currentType))
+		throw st.exception("$GENERATE does not support " +
+				   Type.string(currentType) + " records");
+
+	// Next comes the rdata specification.
+	String rdataSpec = st.getIdentifier();
+
+	// That should be the end.  However, we don't want to move past the
+	// line yet, so put back the EOL after reading it.
+	st.getEOL();
+	st.unget();
+
+	generator = new Generator(start, end, step, nameSpec,
+				  currentType, currentDClass, currentTTL,
+				  rdataSpec, origin);
+	if (generators == null)
+		generators = new ArrayList(1);
+	generators.add(generator);
+}
+
+private void
+endGenerate() throws IOException {
+	// Read the EOL that we put back before.
+	st.getEOL();
+
+	generator = null;
+}
+
+private Record
+nextGenerated() throws IOException {
+	try {
+		return generator.nextRecord();
+	}
+	catch (Tokenizer.TokenizerException e) {
+		throw st.exception("Parsing $GENERATE: " + e.getBaseMessage());
+	}
+	catch (TextParseException e) {
+		throw st.exception("Parsing $GENERATE: " + e.getMessage());
+	}
+}
+
+/**
+ * Returns the next record in the master file.  This will process any
+ * directives before the next record.
+ * @return The next record.
+ * @throws IOException The master file could not be read, or was syntactically
+ * invalid.
+ */
+public Record
+_nextRecord() throws IOException {
+	Tokenizer.Token token;
+	String s;
+
+	if (included != null) {
+		Record rec = included.nextRecord();
+		if (rec != null)
+			return rec;
+		included = null;
+	}
+	if (generator != null) {
+		Record rec = nextGenerated();
+		if (rec != null)
+			return rec;
+		endGenerate();
+	}
+	while (true) {
+		Name name;
+
+		token = st.get(true, false);
+		if (token.type == Tokenizer.WHITESPACE) {
+			Tokenizer.Token next = st.get();
+			if (next.type == Tokenizer.EOL)
+				continue;
+			else if (next.type == Tokenizer.EOF)
+				return null;
+			else
+				st.unget();
+			if (last == null)
+				throw st.exception("no owner");
+			name = last.getName();
+		}
+		else if (token.type == Tokenizer.EOL)
+			continue;
+		else if (token.type == Tokenizer.EOF)
+			return null;
+		else if (((String) token.value).charAt(0) == '$') {
+			s = token.value;
+
+			if (s.equalsIgnoreCase("$ORIGIN")) {
+				origin = st.getName(Name.root);
+				st.getEOL();
+				continue;
+			} else if (s.equalsIgnoreCase("$TTL")) {
+				defaultTTL = st.getTTL();
+				st.getEOL();
+				continue;
+			} else  if (s.equalsIgnoreCase("$INCLUDE")) {
+				String filename = st.getString();
+				File newfile;
+				if (file != null) {
+					String parent = file.getParent();
+					newfile = new File(parent, filename);
+				} else {
+					newfile = new File(filename);
+				}
+				Name incorigin = origin;
+				token = st.get();
+				if (token.isString()) {
+					incorigin = parseName(token.value,
+							      Name.root);
+					st.getEOL();
+				}
+				included = new Master(newfile, incorigin,
+						      defaultTTL);
+				/*
+				 * If we continued, we wouldn't be looking in
+				 * the new file.  Recursing works better.
+				 */
+				return nextRecord();
+			} else  if (s.equalsIgnoreCase("$GENERATE")) {
+				if (generator != null)
+					throw new IllegalStateException
+						("cannot nest $GENERATE");
+				startGenerate();
+				if (noExpandGenerate) {
+					endGenerate();
+					continue;
+				}
+				return nextGenerated();
+			} else {
+				throw st.exception("Invalid directive: " + s);
+			}
+		} else {
+			s = token.value;
+			name = parseName(s, origin);
+			if (last != null && name.equals(last.getName())) {
+				name = last.getName();
+			}
+		}
+
+		parseTTLClassAndType();
+		last = Record.fromString(name, currentType, currentDClass,
+					 currentTTL, st, origin);
+		if (needSOATTL) {
+			long ttl = ((SOARecord)last).getMinimum();
+			last.setTTL(ttl);
+			defaultTTL = ttl;
+			needSOATTL = false;
+		}
+		return last;
+	}
+}
+
+/**
+ * Returns the next record in the master file.  This will process any
+ * directives before the next record.
+ * @return The next record.
+ * @throws IOException The master file could not be read, or was syntactically
+ * invalid.
+ */
+public Record
+nextRecord() throws IOException {
+	Record rec = null;
+	try {
+		rec = _nextRecord();
+	}
+	finally {
+		if (rec == null) {
+			st.close();
+		}
+	}
+	return rec;
+}
+
+/**
+ * Specifies whether $GENERATE statements should be expanded.  Whether
+ * expanded or not, the specifications for generated records are available
+ * by calling {@link #generators}.  This must be called before a $GENERATE
+ * statement is seen during iteration to have an effect.
+ */
+public void
+expandGenerate(boolean wantExpand) {
+	noExpandGenerate = !wantExpand;
+}
+
+/**
+ * Returns an iterator over the generators specified in the master file; that
+ * is, the parsed contents of $GENERATE statements.
+ * @see Generator
+ */
+public Iterator
+generators() {
+	if (generators != null)
+		return Collections.unmodifiableList(generators).iterator();
+	else
+		return Collections.EMPTY_LIST.iterator();
+}
+
+protected void
+finalize() {
+	st.close();
+}
+
+}
diff --git a/src/org/xbill/DNS/Message.java b/src/org/xbill/DNS/Message.java
new file mode 100644
index 0000000..fe0c3c9
--- /dev/null
+++ b/src/org/xbill/DNS/Message.java
@@ -0,0 +1,611 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+import java.io.*;
+
+/**
+ * A DNS Message.  A message is the basic unit of communication between
+ * the client and server of a DNS operation.  A message consists of a Header
+ * and 4 message sections.
+ * @see Resolver
+ * @see Header
+ * @see Section
+ *
+ * @author Brian Wellington
+ */
+
+public class Message implements Cloneable {
+
+/** The maximum length of a message in wire format. */
+public static final int MAXLENGTH = 65535;
+
+private Header header;
+private List [] sections;
+private int size;
+private TSIG tsigkey;
+private TSIGRecord querytsig;
+private int tsigerror;
+
+int tsigstart;
+int tsigState;
+int sig0start;
+
+/* The message was not signed */
+static final int TSIG_UNSIGNED = 0;
+
+/* The message was signed and verification succeeded */
+static final int TSIG_VERIFIED = 1;
+
+/* The message was an unsigned message in multiple-message response */
+static final int TSIG_INTERMEDIATE = 2;
+
+/* The message was signed and no verification was attempted.  */
+static final int TSIG_SIGNED = 3;
+
+/*
+ * The message was signed and verification failed, or was not signed
+ * when it should have been.
+ */
+static final int TSIG_FAILED = 4;
+
+private static Record [] emptyRecordArray = new Record[0];
+private static RRset [] emptyRRsetArray = new RRset[0];
+
+private
+Message(Header header) {
+	sections = new List[4];
+	this.header = header;
+}
+
+/** Creates a new Message with the specified Message ID */
+public
+Message(int id) {
+	this(new Header(id));
+}
+
+/** Creates a new Message with a random Message ID */
+public
+Message() {
+	this(new Header());
+}
+
+/**
+ * Creates a new Message with a random Message ID suitable for sending as a
+ * query.
+ * @param r A record containing the question
+ */
+public static Message
+newQuery(Record r) {
+	Message m = new Message();
+	m.header.setOpcode(Opcode.QUERY);
+	m.header.setFlag(Flags.RD);
+	m.addRecord(r, Section.QUESTION);
+	return m;
+}
+
+/**
+ * Creates a new Message to contain a dynamic update.  A random Message ID
+ * and the zone are filled in.
+ * @param zone The zone to be updated
+ */
+public static Message
+newUpdate(Name zone) {
+	return new Update(zone);
+}
+
+Message(DNSInput in) throws IOException {
+	this(new Header(in));
+	boolean isUpdate = (header.getOpcode() == Opcode.UPDATE);
+	boolean truncated = header.getFlag(Flags.TC);
+	try {
+		for (int i = 0; i < 4; i++) {
+			int count = header.getCount(i);
+			if (count > 0)
+				sections[i] = new ArrayList(count);
+			for (int j = 0; j < count; j++) {
+				int pos = in.current();
+				Record rec = Record.fromWire(in, i, isUpdate);
+				sections[i].add(rec);
+				if (i == Section.ADDITIONAL) {
+					if (rec.getType() == Type.TSIG)
+						tsigstart = pos;
+					if (rec.getType() == Type.SIG) {
+						SIGRecord sig = (SIGRecord) rec;
+						if (sig.getTypeCovered() == 0)
+							sig0start = pos;
+					}
+				}
+			}
+		}
+	} catch (WireParseException e) {
+		if (!truncated)
+			throw e;
+	}
+	size = in.current();
+}
+
+/**
+ * Creates a new Message from its DNS wire format representation
+ * @param b A byte array containing the DNS Message.
+ */
+public
+Message(byte [] b) throws IOException {
+	this(new DNSInput(b));
+}
+
+/**
+ * Replaces the Header with a new one.
+ * @see Header
+ */
+public void
+setHeader(Header h) {
+	header = h;
+}
+
+/**
+ * Retrieves the Header.
+ * @see Header
+ */
+public Header
+getHeader() {
+	return header;
+}
+
+/**
+ * Adds a record to a section of the Message, and adjusts the header.
+ * @see Record
+ * @see Section
+ */
+public void
+addRecord(Record r, int section) {
+	if (sections[section] == null)
+		sections[section] = new LinkedList();
+	header.incCount(section);
+	sections[section].add(r);
+}
+
+/**
+ * Removes a record from a section of the Message, and adjusts the header.
+ * @see Record
+ * @see Section
+ */
+public boolean
+removeRecord(Record r, int section) {
+	if (sections[section] != null && sections[section].remove(r)) {
+		header.decCount(section);
+		return true;
+	}
+	else
+		return false;
+}
+
+/**
+ * Removes all records from a section of the Message, and adjusts the header.
+ * @see Record
+ * @see Section
+ */
+public void
+removeAllRecords(int section) {
+	sections[section] = null;
+	header.setCount(section, 0);
+}
+
+/**
+ * Determines if the given record is already present in the given section.
+ * @see Record
+ * @see Section
+ */
+public boolean
+findRecord(Record r, int section) {
+	return (sections[section] != null && sections[section].contains(r));
+}
+
+/**
+ * Determines if the given record is already present in any section.
+ * @see Record
+ * @see Section
+ */
+public boolean
+findRecord(Record r) {
+	for (int i = Section.ANSWER; i <= Section.ADDITIONAL; i++)
+		if (sections[i] != null && sections[i].contains(r))
+			return true;
+	return false;
+}
+
+/**
+ * Determines if an RRset with the given name and type is already
+ * present in the given section.
+ * @see RRset
+ * @see Section
+ */
+public boolean
+findRRset(Name name, int type, int section) {
+	if (sections[section] == null)
+		return false;
+	for (int i = 0; i < sections[section].size(); i++) {
+		Record r = (Record) sections[section].get(i);
+		if (r.getType() == type && name.equals(r.getName()))
+			return true;
+	}
+	return false;
+}
+
+/**
+ * Determines if an RRset with the given name and type is already
+ * present in any section.
+ * @see RRset
+ * @see Section
+ */
+public boolean
+findRRset(Name name, int type) {
+	return (findRRset(name, type, Section.ANSWER) ||
+		findRRset(name, type, Section.AUTHORITY) ||
+		findRRset(name, type, Section.ADDITIONAL));
+}
+
+/**
+ * Returns the first record in the QUESTION section.
+ * @see Record
+ * @see Section
+ */
+public Record
+getQuestion() {
+	List l = sections[Section.QUESTION];
+	if (l == null || l.size() == 0)
+		return null;
+	return (Record) l.get(0);
+}
+
+/**
+ * Returns the TSIG record from the ADDITIONAL section, if one is present.
+ * @see TSIGRecord
+ * @see TSIG
+ * @see Section
+ */
+public TSIGRecord
+getTSIG() {
+	int count = header.getCount(Section.ADDITIONAL);
+	if (count == 0)
+		return null;
+	List l = sections[Section.ADDITIONAL];
+	Record rec = (Record) l.get(count - 1);
+	if (rec.type !=  Type.TSIG)
+		return null;
+	return (TSIGRecord) rec;
+}
+
+/**
+ * Was this message signed by a TSIG?
+ * @see TSIG
+ */
+public boolean
+isSigned() {
+	return (tsigState == TSIG_SIGNED ||
+		tsigState == TSIG_VERIFIED ||
+		tsigState == TSIG_FAILED);
+}
+
+/**
+ * If this message was signed by a TSIG, was the TSIG verified?
+ * @see TSIG
+ */
+public boolean
+isVerified() {
+	return (tsigState == TSIG_VERIFIED);
+}
+
+/**
+ * Returns the OPT record from the ADDITIONAL section, if one is present.
+ * @see OPTRecord
+ * @see Section
+ */
+public OPTRecord
+getOPT() {
+	Record [] additional = getSectionArray(Section.ADDITIONAL);
+	for (int i = 0; i < additional.length; i++)
+		if (additional[i] instanceof OPTRecord)
+			return (OPTRecord) additional[i];
+	return null;
+}
+
+/**
+ * Returns the message's rcode (error code).  This incorporates the EDNS
+ * extended rcode.
+ */
+public int
+getRcode() {
+	int rcode = header.getRcode();
+	OPTRecord opt = getOPT();
+	if (opt != null)
+		rcode += (opt.getExtendedRcode() << 4);
+	return rcode;
+}
+
+/**
+ * Returns an array containing all records in the given section, or an
+ * empty array if the section is empty.
+ * @see Record
+ * @see Section
+ */
+public Record []
+getSectionArray(int section) {
+	if (sections[section] == null)
+		return emptyRecordArray;
+	List l = sections[section];
+	return (Record []) l.toArray(new Record[l.size()]);
+}
+
+private static boolean
+sameSet(Record r1, Record r2) {
+	return (r1.getRRsetType() == r2.getRRsetType() &&
+		r1.getDClass() == r2.getDClass() &&
+		r1.getName().equals(r2.getName()));
+}
+
+/**
+ * Returns an array containing all records in the given section grouped into
+ * RRsets.
+ * @see RRset
+ * @see Section
+ */
+public RRset []
+getSectionRRsets(int section) {
+	if (sections[section] == null)
+		return emptyRRsetArray;
+	List sets = new LinkedList();
+	Record [] recs = getSectionArray(section);
+	Set hash = new HashSet();
+	for (int i = 0; i < recs.length; i++) {
+		Name name = recs[i].getName();
+		boolean newset = true;
+		if (hash.contains(name)) {
+			for (int j = sets.size() - 1; j >= 0; j--) {
+				RRset set = (RRset) sets.get(j);
+				if (set.getType() == recs[i].getRRsetType() &&
+				    set.getDClass() == recs[i].getDClass() &&
+				    set.getName().equals(name))
+				{
+					set.addRR(recs[i]);
+					newset = false;
+					break;
+				}
+			}
+		}
+		if (newset) {
+			RRset set = new RRset(recs[i]);
+			sets.add(set);
+			hash.add(name);
+		}
+	}
+	return (RRset []) sets.toArray(new RRset[sets.size()]);
+}
+
+void
+toWire(DNSOutput out) {
+	header.toWire(out);
+	Compression c = new Compression();
+	for (int i = 0; i < 4; i++) {
+		if (sections[i] == null)
+			continue;
+		for (int j = 0; j < sections[i].size(); j++) {
+			Record rec = (Record)sections[i].get(j);
+			rec.toWire(out, i, c);
+		}
+	}
+}
+
+/* Returns the number of records not successfully rendered. */
+private int
+sectionToWire(DNSOutput out, int section, Compression c,
+	      int maxLength)
+{
+	int n = sections[section].size();
+	int pos = out.current();
+	int rendered = 0;
+	Record lastrec = null;
+
+	for (int i = 0; i < n; i++) {
+		Record rec = (Record)sections[section].get(i);
+		if (lastrec != null && !sameSet(rec, lastrec)) {
+			pos = out.current();
+			rendered = i;
+		}
+		lastrec = rec;
+		rec.toWire(out, section, c);
+		if (out.current() > maxLength) {
+			out.jump(pos);
+			return n - rendered;
+		}
+	}
+	return 0;
+}
+
+/* Returns true if the message could be rendered. */
+private boolean
+toWire(DNSOutput out, int maxLength) {
+	if (maxLength < Header.LENGTH)
+		return false;
+
+	Header newheader = null;
+
+	int tempMaxLength = maxLength;
+	if (tsigkey != null)
+		tempMaxLength -= tsigkey.recordLength();
+
+	int startpos = out.current();
+	header.toWire(out);
+	Compression c = new Compression();
+	for (int i = 0; i < 4; i++) {
+		int skipped;
+		if (sections[i] == null)
+			continue;
+		skipped = sectionToWire(out, i, c, tempMaxLength);
+		if (skipped != 0) {
+			if (newheader == null)
+				newheader = (Header) header.clone();
+			if (i != Section.ADDITIONAL)
+				newheader.setFlag(Flags.TC);
+			int count = newheader.getCount(i);
+			newheader.setCount(i, count - skipped);
+			for (int j = i + 1; j < 4; j++)
+				newheader.setCount(j, 0);
+
+			out.save();
+			out.jump(startpos);
+			newheader.toWire(out);
+			out.restore();
+			break;
+		}
+	}
+
+	if (tsigkey != null) {
+		TSIGRecord tsigrec = tsigkey.generate(this, out.toByteArray(),
+						      tsigerror, querytsig);
+
+		if (newheader == null)
+			newheader = (Header) header.clone();
+		tsigrec.toWire(out, Section.ADDITIONAL, c);
+		newheader.incCount(Section.ADDITIONAL);
+
+		out.save();
+		out.jump(startpos);
+		newheader.toWire(out);
+		out.restore();
+	}
+
+	return true;
+}
+
+/**
+ * Returns an array containing the wire format representation of the Message.
+ */
+public byte []
+toWire() {
+	DNSOutput out = new DNSOutput();
+	toWire(out);
+	size = out.current();
+	return out.toByteArray();
+}
+
+/**
+ * Returns an array containing the wire format representation of the Message
+ * with the specified maximum length.  This will generate a truncated
+ * message (with the TC bit) if the message doesn't fit, and will also
+ * sign the message with the TSIG key set by a call to setTSIG().  This
+ * method may return null if the message could not be rendered at all; this
+ * could happen if maxLength is smaller than a DNS header, for example.
+ * @param maxLength The maximum length of the message.
+ * @return The wire format of the message, or null if the message could not be
+ * rendered into the specified length.
+ * @see Flags
+ * @see TSIG
+ */
+public byte []
+toWire(int maxLength) {
+	DNSOutput out = new DNSOutput();
+	toWire(out, maxLength);
+	size = out.current();
+	return out.toByteArray();
+}
+
+/**
+ * Sets the TSIG key and other necessary information to sign a message.
+ * @param key The TSIG key.
+ * @param error The value of the TSIG error field.
+ * @param querytsig If this is a response, the TSIG from the request.
+ */
+public void
+setTSIG(TSIG key, int error, TSIGRecord querytsig) {
+	this.tsigkey = key;
+	this.tsigerror = error;
+	this.querytsig = querytsig;
+}
+
+/**
+ * Returns the size of the message.  Only valid if the message has been
+ * converted to or from wire format.
+ */
+public int
+numBytes() {
+	return size;
+}
+
+/**
+ * Converts the given section of the Message to a String.
+ * @see Section
+ */
+public String
+sectionToString(int i) {
+	if (i > 3)
+		return null;
+
+	StringBuffer sb = new StringBuffer();
+
+	Record [] records = getSectionArray(i);
+	for (int j = 0; j < records.length; j++) {
+		Record rec = records[j];
+		if (i == Section.QUESTION) {
+			sb.append(";;\t" + rec.name);
+			sb.append(", type = " + Type.string(rec.type));
+			sb.append(", class = " + DClass.string(rec.dclass));
+		}
+		else
+			sb.append(rec);
+		sb.append("\n");
+	}
+	return sb.toString();
+}
+
+/**
+ * Converts the Message to a String.
+ */
+public String
+toString() {
+	StringBuffer sb = new StringBuffer();
+	OPTRecord opt = getOPT();
+	if (opt != null)
+		sb.append(header.toStringWithRcode(getRcode()) + "\n");
+	else
+		sb.append(header + "\n");
+	if (isSigned()) {
+		sb.append(";; TSIG ");
+		if (isVerified())
+			sb.append("ok");
+		else
+			sb.append("invalid");
+		sb.append('\n');
+	}
+	for (int i = 0; i < 4; i++) {
+		if (header.getOpcode() != Opcode.UPDATE)
+			sb.append(";; " + Section.longString(i) + ":\n");
+		else
+			sb.append(";; " + Section.updString(i) + ":\n");
+		sb.append(sectionToString(i) + "\n");
+	}
+	sb.append(";; Message size: " + numBytes() + " bytes");
+	return sb.toString();
+}
+
+/**
+ * Creates a copy of this Message.  This is done by the Resolver before adding
+ * TSIG and OPT records, for example.
+ * @see Resolver
+ * @see TSIGRecord
+ * @see OPTRecord
+ */
+public Object
+clone() {
+	Message m = new Message();
+	for (int i = 0; i < sections.length; i++) {
+		if (sections[i] != null)
+			m.sections[i] = new LinkedList(sections[i]);
+	}
+	m.header = (Header) header.clone();
+	m.size = size;
+	return m;
+}
+
+}
diff --git a/src/org/xbill/DNS/Mnemonic.java b/src/org/xbill/DNS/Mnemonic.java
new file mode 100644
index 0000000..dd60f62
--- /dev/null
+++ b/src/org/xbill/DNS/Mnemonic.java
@@ -0,0 +1,210 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.HashMap;
+
+/**
+ * A utility class for converting between numeric codes and mnemonics
+ * for those codes.  Mnemonics are case insensitive.
+ *
+ * @author Brian Wellington
+ */
+
+class Mnemonic {
+
+private static Integer cachedInts[] = new Integer[64];
+
+static {
+	for (int i = 0; i < cachedInts.length; i++) {
+		cachedInts[i] = new Integer(i);
+	}
+}
+
+/* Strings are case-sensitive. */
+static final int CASE_SENSITIVE = 1;
+
+/* Strings will be stored/searched for in uppercase. */
+static final int CASE_UPPER = 2;
+
+/* Strings will be stored/searched for in lowercase. */
+static final int CASE_LOWER = 3;
+
+private HashMap strings;
+private HashMap values;
+private String description;
+private int wordcase;
+private String prefix;
+private int max;
+private boolean numericok;
+
+/**
+ * Creates a new Mnemonic table.
+ * @param description A short description of the mnemonic to use when
+ * @param wordcase Whether to convert strings into uppercase, lowercase,
+ * or leave them unchanged.
+ * throwing exceptions.
+ */
+public
+Mnemonic(String description, int wordcase) {
+	this.description = description;
+	this.wordcase = wordcase;
+	strings = new HashMap();
+	values = new HashMap();
+	max = Integer.MAX_VALUE;
+}
+
+/** Sets the maximum numeric value */
+public void
+setMaximum(int max) {
+	this.max = max;
+}
+
+/**
+ * Sets the prefix to use when converting to and from values that don't
+ * have mnemonics.
+ */
+public void
+setPrefix(String prefix) {
+	this.prefix = sanitize(prefix);
+}
+
+/**
+ * Sets whether numeric values stored in strings are acceptable.
+ */
+public void
+setNumericAllowed(boolean numeric) {
+	this.numericok = numeric;
+}
+
+/**
+ * Converts an int into a possibly cached Integer.
+ */
+public static Integer
+toInteger(int val) {
+	if (val >= 0 && val < cachedInts.length)
+		return (cachedInts[val]);
+	return new Integer(val);
+}       
+
+/**
+ * Checks that a numeric value is within the range [0..max]
+ */
+public void
+check(int val) {
+	if (val < 0 || val > max) {
+		throw new IllegalArgumentException(description + " " + val +
+						   "is out of range");
+	}
+}
+
+/* Converts a String to the correct case. */
+private String
+sanitize(String str) {
+	if (wordcase == CASE_UPPER)
+		return str.toUpperCase();
+	else if (wordcase == CASE_LOWER)
+		return str.toLowerCase();
+	return str;
+}
+
+private int
+parseNumeric(String s) {
+	try {
+		int val = Integer.parseInt(s);
+		if (val >= 0 && val <= max)
+			return val;
+	}
+	catch (NumberFormatException e) {
+	}
+	return -1;
+}
+
+/**
+ * Defines the text representation of a numeric value.
+ * @param val The numeric value
+ * @param string The text string
+ */
+public void
+add(int val, String str) {
+	check(val);
+	Integer value = toInteger(val);
+	str = sanitize(str);
+	strings.put(str, value);
+	values.put(value, str);
+}
+
+/**
+ * Defines an additional text representation of a numeric value.  This will
+ * be used by getValue(), but not getText().
+ * @param val The numeric value
+ * @param string The text string
+ */
+public void
+addAlias(int val, String str) {
+	check(val);
+	Integer value = toInteger(val);
+	str = sanitize(str);
+	strings.put(str, value);
+}
+
+/**
+ * Copies all mnemonics from one table into another.
+ * @param val The numeric value
+ * @param string The text string
+ * @throws IllegalArgumentException The wordcases of the Mnemonics do not
+ * match.
+ */
+public void
+addAll(Mnemonic source) {
+	if (wordcase != source.wordcase)
+		throw new IllegalArgumentException(source.description +
+						   ": wordcases do not match");
+	strings.putAll(source.strings);
+	values.putAll(source.values);
+}
+
+/**
+ * Gets the text mnemonic corresponding to a numeric value.
+ * @param val The numeric value
+ * @return The corresponding text mnemonic.
+ */
+public String
+getText(int val) {
+	check(val);
+	String str = (String) values.get(toInteger(val));
+	if (str != null)
+		return str;
+	str = Integer.toString(val);
+	if (prefix != null)
+		return prefix + str;
+	return str;
+}
+
+/**
+ * Gets the numeric value corresponding to a text mnemonic.
+ * @param str The text mnemonic
+ * @return The corresponding numeric value, or -1 if there is none
+ */
+public int
+getValue(String str) {
+	str = sanitize(str);
+	Integer value = (Integer) strings.get(str);
+	if (value != null) {
+		return value.intValue();
+	}
+	if (prefix != null) {
+		if (str.startsWith(prefix)) {
+			int val = parseNumeric(str.substring(prefix.length()));
+			if (val >= 0) {
+				return val;
+			}
+		}
+	}
+	if (numericok) {
+		return parseNumeric(str);
+	}
+	return -1;
+}
+
+}
diff --git a/src/org/xbill/DNS/NAPTRRecord.java b/src/org/xbill/DNS/NAPTRRecord.java
new file mode 100644
index 0000000..da2ec6d
--- /dev/null
+++ b/src/org/xbill/DNS/NAPTRRecord.java
@@ -0,0 +1,154 @@
+// Copyright (c) 2000-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * Name Authority Pointer Record  - specifies rewrite rule, that when applied
+ * to an existing string will produce a new domain.
+ *
+ * @author Chuck Santos
+ */
+
+public class NAPTRRecord extends Record {
+
+private static final long serialVersionUID = 5191232392044947002L;
+
+private int order, preference;
+private byte [] flags, service, regexp;
+private Name replacement;
+
+NAPTRRecord() {}
+
+Record
+getObject() {
+	return new NAPTRRecord();
+}
+
+/**
+ * Creates an NAPTR Record from the given data
+ * @param order The order of this NAPTR.  Records with lower order are
+ * preferred.
+ * @param preference The preference, used to select between records at the
+ * same order.
+ * @param flags The control aspects of the NAPTRRecord.
+ * @param service The service or protocol available down the rewrite path.
+ * @param regexp The regular/substitution expression.
+ * @param replacement The domain-name to query for the next DNS resource
+ * record, depending on the value of the flags field.
+ * @throws IllegalArgumentException One of the strings has invalid escapes
+ */
+public
+NAPTRRecord(Name name, int dclass, long ttl, int order, int preference,
+	    String flags, String service, String regexp, Name replacement)
+{
+	super(name, Type.NAPTR, dclass, ttl);
+	this.order = checkU16("order", order);
+	this.preference = checkU16("preference", preference);
+	try {
+		this.flags = byteArrayFromString(flags);
+		this.service = byteArrayFromString(service);
+		this.regexp = byteArrayFromString(regexp);
+	}
+	catch (TextParseException e) {
+		throw new IllegalArgumentException(e.getMessage());
+	}
+	this.replacement = checkName("replacement", replacement);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	order = in.readU16();
+	preference = in.readU16();
+	flags = in.readCountedString();
+	service = in.readCountedString();
+	regexp = in.readCountedString();
+	replacement = new Name(in);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	order = st.getUInt16();
+	preference = st.getUInt16();
+	try {
+		flags = byteArrayFromString(st.getString());
+		service = byteArrayFromString(st.getString());
+		regexp = byteArrayFromString(st.getString());
+	}
+	catch (TextParseException e) {
+		throw st.exception(e.getMessage());
+	}
+	replacement = st.getName(origin);
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(order);
+	sb.append(" ");
+	sb.append(preference);
+	sb.append(" ");
+	sb.append(byteArrayToString(flags, true));
+	sb.append(" ");
+	sb.append(byteArrayToString(service, true));
+	sb.append(" ");
+	sb.append(byteArrayToString(regexp, true));
+	sb.append(" ");
+	sb.append(replacement);
+	return sb.toString();
+}
+
+/** Returns the order */
+public int
+getOrder() {
+	return order;
+}
+
+/** Returns the preference */
+public int
+getPreference() {
+	return preference;
+}
+
+/** Returns flags */
+public String
+getFlags() {
+	return byteArrayToString(flags, false);
+}
+
+/** Returns service */
+public String
+getService() {
+	return byteArrayToString(service, false);
+}
+
+/** Returns regexp */
+public String
+getRegexp() {
+	return byteArrayToString(regexp, false);
+}
+
+/** Returns the replacement domain-name */
+public Name
+getReplacement() {
+	return replacement;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU16(order);
+	out.writeU16(preference);
+	out.writeCountedString(flags);
+	out.writeCountedString(service);
+	out.writeCountedString(regexp);
+	replacement.toWire(out, null, canonical);
+}
+
+public Name
+getAdditionalName() {
+	return replacement;
+}
+
+}
diff --git a/src/org/xbill/DNS/NSAPRecord.java b/src/org/xbill/DNS/NSAPRecord.java
new file mode 100644
index 0000000..a6b2031
--- /dev/null
+++ b/src/org/xbill/DNS/NSAPRecord.java
@@ -0,0 +1,106 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * NSAP Address Record.
+ *
+ * @author Brian Wellington
+ */
+
+public class NSAPRecord extends Record {
+
+private static final long serialVersionUID = -1037209403185658593L;
+
+private byte [] address;
+
+NSAPRecord() {}
+
+Record
+getObject() {
+	return new NSAPRecord();
+}
+
+private static final byte []
+checkAndConvertAddress(String address) {
+	if (!address.substring(0, 2).equalsIgnoreCase("0x")) {
+		return null;
+	}
+	ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+	boolean partial = false;
+	int current = 0;
+	for (int i = 2; i < address.length(); i++) {
+		char c = address.charAt(i);
+		if (c == '.') {
+			continue;
+		}
+		int value = Character.digit(c, 16);
+		if (value == -1) {
+			return null;
+		}
+		if (partial) {
+			current += value;
+			bytes.write(current);
+			partial = false;
+		} else {
+			current = value << 4;
+			partial = true;
+		}
+
+	}
+	if (partial) {
+		return null;
+	}
+	return bytes.toByteArray();
+}
+
+/**
+ * Creates an NSAP Record from the given data
+ * @param address The NSAP address.
+ * @throws IllegalArgumentException The address is not a valid NSAP address.
+ */
+public
+NSAPRecord(Name name, int dclass, long ttl, String address) {
+	super(name, Type.NSAP, dclass, ttl);
+	this.address = checkAndConvertAddress(address);
+	if (this.address == null) {
+		throw new IllegalArgumentException("invalid NSAP address " +
+						   address);
+	}
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	address = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	String addr = st.getString();
+	this.address = checkAndConvertAddress(addr);
+	if (this.address == null)
+		throw st.exception("invalid NSAP address " + addr);
+}
+
+/**
+ * Returns the NSAP address.
+ */
+public String
+getAddress() {
+	return byteArrayToString(address, false);
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeByteArray(address);
+}
+
+String
+rrToString() {
+	return "0x" + base16.toString(address);
+}
+
+}
diff --git a/src/org/xbill/DNS/NSAP_PTRRecord.java b/src/org/xbill/DNS/NSAP_PTRRecord.java
new file mode 100644
index 0000000..ecc609f
--- /dev/null
+++ b/src/org/xbill/DNS/NSAP_PTRRecord.java
@@ -0,0 +1,38 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * NSAP Pointer Record  - maps a domain name representing an NSAP Address to
+ * a hostname.
+ *
+ * @author Brian Wellington
+ */
+
+public class NSAP_PTRRecord extends SingleNameBase {
+
+private static final long serialVersionUID = 2386284746382064904L;
+
+NSAP_PTRRecord() {}
+
+Record
+getObject() {
+	return new NSAP_PTRRecord();
+}
+
+/** 
+ * Creates a new NSAP_PTR Record with the given data
+ * @param target The name of the host with this address
+ */
+public
+NSAP_PTRRecord(Name name, int dclass, long ttl, Name target) {
+	super(name, Type.NSAP_PTR, dclass, ttl, target, "target");
+}
+
+/** Gets the target of the NSAP_PTR Record */
+public Name
+getTarget() {
+	return getSingleName();
+}
+
+}
diff --git a/src/org/xbill/DNS/NSEC3PARAMRecord.java b/src/org/xbill/DNS/NSEC3PARAMRecord.java
new file mode 100644
index 0000000..d663a62
--- /dev/null
+++ b/src/org/xbill/DNS/NSEC3PARAMRecord.java
@@ -0,0 +1,165 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.IOException;
+import java.security.NoSuchAlgorithmException;
+
+import org.xbill.DNS.utils.base16;
+
+/**
+ * Next SECure name 3 Parameters - this record contains the parameters (hash
+ * algorithm, salt, iterations) used for a valid, complete NSEC3 chain present
+ * in a zone. Zones signed using NSEC3 must include this record at the zone apex
+ * to inform authoritative servers that NSEC3 is being used with the given
+ * parameters.
+ * 
+ * @author Brian Wellington
+ * @author David Blacka
+ */
+
+public class NSEC3PARAMRecord extends Record {
+
+private static final long serialVersionUID = -8689038598776316533L;
+
+private int hashAlg;
+private int flags;
+private int iterations;
+private byte salt[];
+
+NSEC3PARAMRecord() {}
+
+Record getObject() {
+	return new NSEC3PARAMRecord();
+}
+
+/**
+ * Creates an NSEC3PARAM record from the given data.
+ * 
+ * @param name The ownername of the NSEC3PARAM record (generally the zone name).
+ * @param dclass The class.
+ * @param ttl The TTL.
+ * @param hashAlg The hash algorithm.
+ * @param flags The value of the flags field.
+ * @param iterations The number of hash iterations.
+ * @param salt The salt to use (may be null).
+ */
+public NSEC3PARAMRecord(Name name, int dclass, long ttl, int hashAlg, 
+			int flags, int iterations, byte [] salt)
+{
+	super(name, Type.NSEC3PARAM, dclass, ttl);
+	this.hashAlg = checkU8("hashAlg", hashAlg);
+	this.flags = checkU8("flags", flags);
+	this.iterations = checkU16("iterations", iterations);
+
+	if (salt != null) {
+		if (salt.length > 255)
+			throw new IllegalArgumentException("Invalid salt " +
+							   "length");
+		if (salt.length > 0) {
+			this.salt = new byte[salt.length];
+			System.arraycopy(salt, 0, this.salt, 0, salt.length);
+		}
+	}
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	hashAlg = in.readU8();
+	flags = in.readU8();
+	iterations = in.readU16();
+
+	int salt_length = in.readU8();
+	if (salt_length > 0)
+		salt = in.readByteArray(salt_length);
+	else
+		salt = null;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU8(hashAlg);
+	out.writeU8(flags);
+	out.writeU16(iterations);
+
+	if (salt != null) {
+		out.writeU8(salt.length);
+		out.writeByteArray(salt);
+	} else
+		out.writeU8(0);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException
+{
+	hashAlg = st.getUInt8();
+	flags = st.getUInt8();
+	iterations = st.getUInt16();
+
+	String s = st.getString();
+	if (s.equals("-"))
+		salt = null;
+	else {
+		st.unget();
+		salt = st.getHexString();
+		if (salt.length > 255)
+			throw st.exception("salt value too long");
+	}
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(hashAlg);
+	sb.append(' ');
+	sb.append(flags);
+	sb.append(' ');
+	sb.append(iterations);
+	sb.append(' ');
+	if (salt == null)
+		sb.append('-');
+	else
+		sb.append(base16.toString(salt));
+
+	return sb.toString();
+}
+
+/** Returns the hash algorithm */
+public int
+getHashAlgorithm() {
+	return hashAlg;
+}
+
+/** Returns the flags */
+public int
+getFlags() {
+	return flags;
+}
+  
+/** Returns the number of iterations */
+public int
+getIterations() {
+	return iterations;
+}
+
+/** Returns the salt */
+public byte []
+getSalt()
+{
+	return salt;
+}
+
+/**
+ * Hashes a name with the parameters of this NSEC3PARAM record.
+ * @param name The name to hash
+ * @return The hashed version of the name
+ * @throws NoSuchAlgorithmException The hash algorithm is unknown.
+ */
+public byte []
+hashName(Name name) throws NoSuchAlgorithmException
+{
+	return NSEC3Record.hashName(name, hashAlg, iterations, salt);
+}
+
+}
diff --git a/src/org/xbill/DNS/NSEC3Record.java b/src/org/xbill/DNS/NSEC3Record.java
new file mode 100644
index 0000000..aa086b8
--- /dev/null
+++ b/src/org/xbill/DNS/NSEC3Record.java
@@ -0,0 +1,266 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.security.*;
+
+import org.xbill.DNS.utils.*;
+
+/**
+ * Next SECure name 3 - this record contains the next hashed name in an
+ * ordered list of hashed names in the zone, and a set of types for which
+ * records exist for this name. The presence of this record in a response
+ * signifies a negative response from a DNSSEC-signed zone.
+ * 
+ * This replaces the NSEC and NXT records, when used.
+ * 
+ * @author Brian Wellington
+ * @author David Blacka
+ */
+
+public class NSEC3Record extends Record {
+
+public static class Flags {
+	/**
+	 * NSEC3 flags identifiers.
+	 */
+
+	private Flags() {}
+
+	/** Unsigned delegation are not included in the NSEC3 chain.
+	 *
+	 */
+	public static final int OPT_OUT = 0x01;
+}
+
+public static class Digest {
+	private Digest() {}
+
+	/** SHA-1 */
+	public static final int SHA1 = 1;
+}
+
+public static final int SHA1_DIGEST_ID = Digest.SHA1;
+
+private static final long serialVersionUID = -7123504635968932855L;
+
+private int hashAlg;
+private int flags;
+private int iterations;
+private byte [] salt;
+private byte [] next;
+private TypeBitmap types;
+
+private static final base32 b32 = new base32(base32.Alphabet.BASE32HEX,
+					     false, false);
+
+NSEC3Record() {}
+
+Record getObject() {
+	return new NSEC3Record();
+}
+
+/**
+ * Creates an NSEC3 record from the given data.
+ * 
+ * @param name The ownername of the NSEC3 record (base32'd hash plus zonename).
+ * @param dclass The class.
+ * @param ttl The TTL.
+ * @param hashAlg The hash algorithm.
+ * @param flags The value of the flags field.
+ * @param iterations The number of hash iterations.
+ * @param salt The salt to use (may be null).
+ * @param next The next hash (may not be null).
+ * @param types The types present at the original ownername.
+ */
+public NSEC3Record(Name name, int dclass, long ttl, int hashAlg,
+		   int flags, int iterations, byte [] salt, byte [] next,
+		   int [] types)
+{
+	super(name, Type.NSEC3, dclass, ttl);
+	this.hashAlg = checkU8("hashAlg", hashAlg);
+	this.flags = checkU8("flags", flags);
+	this.iterations = checkU16("iterations", iterations);
+
+	if (salt != null) {
+		if (salt.length > 255)
+			throw new IllegalArgumentException("Invalid salt");
+		if (salt.length > 0) {
+			this.salt = new byte[salt.length];
+			System.arraycopy(salt, 0, this.salt, 0, salt.length);
+		}
+	}
+
+	if (next.length > 255) {
+		throw new IllegalArgumentException("Invalid next hash");
+	}
+	this.next = new byte[next.length];
+	System.arraycopy(next, 0, this.next, 0, next.length);
+	this.types = new TypeBitmap(types);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	hashAlg = in.readU8();
+	flags = in.readU8();
+	iterations = in.readU16();
+
+	int salt_length = in.readU8();
+	if (salt_length > 0)
+		salt = in.readByteArray(salt_length);
+	else
+		salt = null;
+
+	int next_length = in.readU8();
+	next = in.readByteArray(next_length);
+	types = new TypeBitmap(in);
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU8(hashAlg);
+	out.writeU8(flags);
+	out.writeU16(iterations);
+
+	if (salt != null) {
+		out.writeU8(salt.length);
+		out.writeByteArray(salt);
+	} else
+		out.writeU8(0);
+
+	out.writeU8(next.length);
+	out.writeByteArray(next);
+	types.toWire(out);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	hashAlg = st.getUInt8();
+	flags = st.getUInt8();
+	iterations = st.getUInt16();
+
+	String s = st.getString();
+	if (s.equals("-"))
+		salt = null;
+	else {
+		st.unget();
+		salt = st.getHexString();
+		if (salt.length > 255)
+			throw st.exception("salt value too long");
+	}
+
+	next = st.getBase32String(b32);
+	types = new TypeBitmap(st);
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(hashAlg);
+	sb.append(' ');
+	sb.append(flags);
+	sb.append(' ');
+	sb.append(iterations);
+	sb.append(' ');
+	if (salt == null)
+		sb.append('-');
+	else
+		sb.append(base16.toString(salt));
+	sb.append(' ');
+	sb.append(b32.toString(next));
+
+	if (!types.empty()) {
+		sb.append(' ');
+		sb.append(types.toString());
+	}
+
+	return sb.toString();
+}
+
+/** Returns the hash algorithm */
+public int
+getHashAlgorithm() {
+	return hashAlg;
+}
+
+/** Returns the flags */
+public int
+getFlags() {
+	return flags;
+}
+
+/** Returns the number of iterations */
+public int
+getIterations() {
+	return iterations;
+}
+
+/** Returns the salt */
+public byte []
+getSalt()
+{
+	return salt;
+}
+
+/** Returns the next hash */
+public byte []
+getNext() {
+	return next;
+}
+
+  /** Returns the set of types defined for this name */
+public int []
+getTypes() {
+	return types.toArray();
+}
+
+/** Returns whether a specific type is in the set of types. */
+public boolean
+hasType(int type)
+{
+	return types.contains(type);
+}
+
+static byte []
+hashName(Name name, int hashAlg, int iterations, byte [] salt)
+throws NoSuchAlgorithmException
+{
+	MessageDigest digest;
+	switch (hashAlg) {
+	case Digest.SHA1:
+		digest = MessageDigest.getInstance("sha-1");
+		break;
+	default:
+		throw new NoSuchAlgorithmException("Unknown NSEC3 algorithm" +
+						   "identifier: " +
+						   hashAlg);
+	}
+	byte [] hash = null;
+	for (int i = 0; i <= iterations; i++) {
+		digest.reset();
+		if (i == 0)
+			digest.update(name.toWireCanonical());
+		else
+			digest.update(hash);
+		if (salt != null)
+			digest.update(salt);
+		hash = digest.digest();
+	}
+	return hash;
+}
+
+/**
+ * Hashes a name with the parameters of this NSEC3 record.
+ * @param name The name to hash
+ * @return The hashed version of the name
+ * @throws NoSuchAlgorithmException The hash algorithm is unknown.
+ */
+public byte []
+hashName(Name name) throws NoSuchAlgorithmException
+{
+	return hashName(name, hashAlg, iterations, salt);
+}
+
+}
diff --git a/src/org/xbill/DNS/NSECRecord.java b/src/org/xbill/DNS/NSECRecord.java
new file mode 100644
index 0000000..e523e37
--- /dev/null
+++ b/src/org/xbill/DNS/NSECRecord.java
@@ -0,0 +1,98 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * Next SECure name - this record contains the following name in an
+ * ordered list of names in the zone, and a set of types for which
+ * records exist for this name.  The presence of this record in a response
+ * signifies a negative response from a DNSSEC-signed zone.
+ *
+ * This replaces the NXT record.
+ *
+ * @author Brian Wellington
+ * @author David Blacka
+ */
+
+public class NSECRecord extends Record {
+
+private static final long serialVersionUID = -5165065768816265385L;
+
+private Name next;
+private TypeBitmap types;
+
+NSECRecord() {}
+
+Record
+getObject() {
+	return new NSECRecord();
+}
+
+/**
+ * Creates an NSEC Record from the given data.
+ * @param next The following name in an ordered list of the zone
+ * @param types An array containing the types present.
+ */
+public
+NSECRecord(Name name, int dclass, long ttl, Name next, int [] types) {
+	super(name, Type.NSEC, dclass, ttl);
+	this.next = checkName("next", next);
+	for (int i = 0; i < types.length; i++) {
+		Type.check(types[i]);
+	}
+	this.types = new TypeBitmap(types);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	next = new Name(in);
+	types = new TypeBitmap(in);
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	// Note: The next name is not lowercased.
+	next.toWire(out, null, false);
+	types.toWire(out);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	next = st.getName(origin);
+	types = new TypeBitmap(st);
+}
+
+/** Converts rdata to a String */
+String
+rrToString()
+{
+	StringBuffer sb = new StringBuffer();
+	sb.append(next);
+	if (!types.empty()) {
+		sb.append(' ');
+		sb.append(types.toString());
+	}
+	return sb.toString();
+}
+
+/** Returns the next name */
+public Name
+getNext() {
+	return next;
+}
+
+/** Returns the set of types defined for this name */
+public int []
+getTypes() {
+	return types.toArray();
+}
+
+/** Returns whether a specific type is in the set of types. */
+public boolean
+hasType(int type) {
+	return types.contains(type);
+}
+
+}
diff --git a/src/org/xbill/DNS/NSIDOption.java b/src/org/xbill/DNS/NSIDOption.java
new file mode 100644
index 0000000..7bcbcd5
--- /dev/null
+++ b/src/org/xbill/DNS/NSIDOption.java
@@ -0,0 +1,29 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * The Name Server Identifier Option, define in RFC 5001.
+ *
+ * @see OPTRecord
+ * 
+ * @author Brian Wellington
+ */
+public class NSIDOption extends GenericEDNSOption {
+
+private static final long serialVersionUID = 74739759292589056L;
+
+NSIDOption() {
+	super(EDNSOption.Code.NSID);
+}
+
+/**
+ * Construct an NSID option.
+ * @param data The contents of the option.
+ */
+public 
+NSIDOption(byte [] data) {
+	super(EDNSOption.Code.NSID, data);
+}
+
+}
diff --git a/src/org/xbill/DNS/NSRecord.java b/src/org/xbill/DNS/NSRecord.java
new file mode 100644
index 0000000..2908da4
--- /dev/null
+++ b/src/org/xbill/DNS/NSRecord.java
@@ -0,0 +1,42 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Name Server Record  - contains the name server serving the named zone
+ *
+ * @author Brian Wellington
+ */
+
+public class NSRecord extends SingleCompressedNameBase {
+
+private static final long serialVersionUID = 487170758138268838L;
+
+NSRecord() {}
+
+Record
+getObject() {
+	return new NSRecord();
+}
+
+/** 
+ * Creates a new NS Record with the given data
+ * @param target The name server for the given domain
+ */
+public
+NSRecord(Name name, int dclass, long ttl, Name target) {
+	super(name, Type.NS, dclass, ttl, target, "target");
+}
+
+/** Gets the target of the NS Record */
+public Name
+getTarget() {
+	return getSingleName();
+}
+
+public Name
+getAdditionalName() {
+	return getSingleName();
+}
+
+}
diff --git a/src/org/xbill/DNS/NULLRecord.java b/src/org/xbill/DNS/NULLRecord.java
new file mode 100644
index 0000000..fa46d61
--- /dev/null
+++ b/src/org/xbill/DNS/NULLRecord.java
@@ -0,0 +1,67 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * The NULL Record.  This has no defined purpose, but can be used to
+ * hold arbitrary data.
+ *
+ * @author Brian Wellington
+ */
+
+public class NULLRecord extends Record {
+
+private static final long serialVersionUID = -5796493183235216538L;
+
+private byte [] data;
+
+NULLRecord() {}
+
+Record
+getObject() {
+	return new NULLRecord();
+}
+
+/**
+ * Creates a NULL record from the given data.
+ * @param data The contents of the record.
+ */
+public
+NULLRecord(Name name, int dclass, long ttl, byte [] data) {
+	super(name, Type.NULL, dclass, ttl);
+
+	if (data.length > 0xFFFF) {
+		throw new IllegalArgumentException("data must be <65536 bytes");
+	}
+	this.data = data;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	data = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	throw st.exception("no defined text format for NULL records");
+}
+
+String
+rrToString() {
+	return unknownToString(data);
+}
+
+/** Returns the contents of this record. */
+public byte []
+getData() {
+	return data;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeByteArray(data);
+}
+
+}
diff --git a/src/org/xbill/DNS/NXTRecord.java b/src/org/xbill/DNS/NXTRecord.java
new file mode 100644
index 0000000..ad04e01
--- /dev/null
+++ b/src/org/xbill/DNS/NXTRecord.java
@@ -0,0 +1,111 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Next name - this record contains the following name in an ordered list
+ * of names in the zone, and a set of types for which records exist for
+ * this name.  The presence of this record in a response signifies a
+ * failed query for data in a DNSSEC-signed zone. 
+ *
+ * @author Brian Wellington
+ */
+
+public class NXTRecord extends Record {
+
+private static final long serialVersionUID = -8851454400765507520L;
+
+private Name next;
+private BitSet bitmap;
+
+NXTRecord() {}
+
+Record
+getObject() {
+	return new NXTRecord();
+}
+
+/**
+ * Creates an NXT Record from the given data
+ * @param next The following name in an ordered list of the zone
+ * @param bitmap The set of type for which records exist at this name
+*/
+public
+NXTRecord(Name name, int dclass, long ttl, Name next, BitSet bitmap) {
+	super(name, Type.NXT, dclass, ttl);
+	this.next = checkName("next", next);
+	this.bitmap = bitmap;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	next = new Name(in);
+	bitmap = new BitSet();
+	int bitmapLength = in.remaining();
+	for (int i = 0; i < bitmapLength; i++) {
+		int t = in.readU8();
+		for (int j = 0; j < 8; j++)
+			if ((t & (1 << (7 - j))) != 0)
+				bitmap.set(i * 8 + j);
+	}
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	next = st.getName(origin);
+	bitmap = new BitSet();
+	while (true) {
+		Tokenizer.Token t = st.get();
+		if (!t.isString())
+			break;
+		int typecode = Type.value(t.value, true);
+		if (typecode <= 0 || typecode > 128)
+			throw st.exception("Invalid type: " + t.value);
+		bitmap.set(typecode);
+	}
+	st.unget();
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(next);
+	int length = bitmap.length();
+	for (short i = 0; i < length; i++)
+		if (bitmap.get(i)) {
+			sb.append(" ");
+			sb.append(Type.string(i));
+		}
+	return sb.toString();
+}
+
+/** Returns the next name */
+public Name
+getNext() {
+	return next;
+}
+
+/** Returns the set of types defined for this name */
+public BitSet
+getBitmap() {
+	return bitmap;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	next.toWire(out, null, canonical);
+	int length = bitmap.length();
+	for (int i = 0, t = 0; i < length; i++) {
+		t |= (bitmap.get(i) ? (1 << (7 - i % 8)) : 0);
+		if (i % 8 == 7 || i == length - 1) {
+			out.writeU8(t);
+			t = 0;
+		}
+	}
+}
+
+}
diff --git a/src/org/xbill/DNS/Name.java b/src/org/xbill/DNS/Name.java
new file mode 100644
index 0000000..1331ad9
--- /dev/null
+++ b/src/org/xbill/DNS/Name.java
@@ -0,0 +1,822 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.text.*;
+
+/**
+ * A representation of a domain name.  It may either be absolute (fully
+ * qualified) or relative.
+ *
+ * @author Brian Wellington
+ */
+
+public class Name implements Comparable, Serializable {
+
+private static final long serialVersionUID = -7257019940971525644L;
+
+private static final int LABEL_NORMAL = 0;
+private static final int LABEL_COMPRESSION = 0xC0;
+private static final int LABEL_MASK = 0xC0;
+
+/* The name data */
+private byte [] name;
+
+/*
+ * Effectively an 8 byte array, where the low order byte stores the number
+ * of labels and the 7 higher order bytes store per-label offsets.
+ */
+private long offsets;
+
+/* Precomputed hashcode. */
+private int hashcode;
+
+private static final byte [] emptyLabel = new byte[] {(byte)0};
+private static final byte [] wildLabel = new byte[] {(byte)1, (byte)'*'};
+
+/** The root name */
+public static final Name root;
+
+/** The root name */
+public static final Name empty;
+
+/** The maximum length of a Name */
+private static final int MAXNAME = 255;
+
+/** The maximum length of a label a Name */
+private static final int MAXLABEL = 63;
+
+/** The maximum number of labels in a Name */
+private static final int MAXLABELS = 128;
+
+/** The maximum number of cached offsets */
+private static final int MAXOFFSETS = 7;
+
+/* Used for printing non-printable characters */
+private static final DecimalFormat byteFormat = new DecimalFormat();
+
+/* Used to efficiently convert bytes to lowercase */
+private static final byte lowercase[] = new byte[256];
+
+/* Used in wildcard names. */
+private static final Name wild;
+
+static {
+	byteFormat.setMinimumIntegerDigits(3);
+	for (int i = 0; i < lowercase.length; i++) {
+		if (i < 'A' || i > 'Z')
+			lowercase[i] = (byte)i;
+		else
+			lowercase[i] = (byte)(i - 'A' + 'a');
+	}
+	root = new Name();
+	root.appendSafe(emptyLabel, 0, 1);
+	empty = new Name();
+	empty.name = new byte[0];
+	wild = new Name();
+	wild.appendSafe(wildLabel, 0, 1);
+}
+
+private
+Name() {
+}
+
+private final void
+setoffset(int n, int offset) {
+	if (n >= MAXOFFSETS)
+		return;
+	int shift = 8 * (7 - n);
+	offsets &= (~(0xFFL << shift));
+	offsets |= ((long)offset << shift);
+}
+
+private final int
+offset(int n) {
+	if (n == 0 && getlabels() == 0)
+		return 0;
+	if (n < 0 || n >= getlabels())
+		throw new IllegalArgumentException("label out of range");
+	if (n < MAXOFFSETS) {
+		int shift = 8 * (7 - n);
+		return ((int)(offsets >>> shift) & 0xFF);
+	} else {
+		int pos = offset(MAXOFFSETS - 1);
+		for (int i = MAXOFFSETS - 1; i < n; i++)
+			pos += (name[pos] + 1);
+		return (pos);
+	}
+}
+
+private final void
+setlabels(int labels) {
+	offsets &= ~(0xFF);
+	offsets |= labels;
+}
+
+private final int
+getlabels() {
+	return (int)(offsets & 0xFF);
+}
+
+private static final void
+copy(Name src, Name dst) {
+	if (src.offset(0) == 0) {
+		dst.name = src.name;
+		dst.offsets = src.offsets;
+	} else {
+		int offset0 = src.offset(0);
+		int namelen = src.name.length - offset0;
+		int labels = src.labels();
+		dst.name = new byte[namelen];
+		System.arraycopy(src.name, offset0, dst.name, 0, namelen);
+		for (int i = 0; i < labels && i < MAXOFFSETS; i++)
+			dst.setoffset(i, src.offset(i) - offset0);
+		dst.setlabels(labels);
+	}
+}
+
+private final void
+append(byte [] array, int start, int n) throws NameTooLongException {
+	int length = (name == null ? 0 : (name.length - offset(0)));
+	int alength = 0;
+	for (int i = 0, pos = start; i < n; i++) {
+		int len = array[pos];
+		if (len > MAXLABEL)
+			throw new IllegalStateException("invalid label");
+		len++;
+		pos += len;
+		alength += len;
+	}
+	int newlength = length + alength;
+	if (newlength > MAXNAME)
+		throw new NameTooLongException();
+	int labels = getlabels();
+	int newlabels = labels + n;
+	if (newlabels > MAXLABELS)
+		throw new IllegalStateException("too many labels");
+	byte [] newname = new byte[newlength];
+	if (length != 0)
+		System.arraycopy(name, offset(0), newname, 0, length);
+	System.arraycopy(array, start, newname, length, alength);
+	name = newname;
+	for (int i = 0, pos = length; i < n; i++) {
+		setoffset(labels + i, pos);
+		pos += (newname[pos] + 1);
+	}
+	setlabels(newlabels);
+}
+
+private static TextParseException
+parseException(String str, String message) {
+	return new TextParseException("'" + str + "': " + message);
+}
+
+private final void
+appendFromString(String fullName, byte [] array, int start, int n)
+throws TextParseException
+{
+	try {
+		append(array, start, n);
+	}
+	catch (NameTooLongException e) {
+		throw parseException(fullName, "Name too long");
+	}
+}
+
+private final void
+appendSafe(byte [] array, int start, int n) {
+	try {
+		append(array, start, n);
+	}
+	catch (NameTooLongException e) {
+	}
+}
+
+/**
+ * Create a new name from a string and an origin.  This does not automatically
+ * make the name absolute; it will be absolute if it has a trailing dot or an
+ * absolute origin is appended.
+ * @param s The string to be converted
+ * @param origin If the name is not absolute, the origin to be appended.
+ * @throws TextParseException The name is invalid.
+ */
+public
+Name(String s, Name origin) throws TextParseException {
+	if (s.equals(""))
+		throw parseException(s, "empty name");
+	else if (s.equals("@")) {
+		if (origin == null)
+			copy(empty, this);
+		else
+			copy(origin, this);
+		return;
+	} else if (s.equals(".")) {
+		copy(root, this);
+		return;
+	}
+	int labelstart = -1;
+	int pos = 1;
+	byte [] label = new byte[MAXLABEL + 1];
+	boolean escaped = false;
+	int digits = 0;
+	int intval = 0;
+	boolean absolute = false;
+	for (int i = 0; i < s.length(); i++) {
+		byte b = (byte) s.charAt(i);
+		if (escaped) {
+			if (b >= '0' && b <= '9' && digits < 3) {
+				digits++;
+				intval *= 10;
+				intval += (b - '0');
+				if (intval > 255)
+					throw parseException(s, "bad escape");
+				if (digits < 3)
+					continue;
+				b = (byte) intval;
+			}
+			else if (digits > 0 && digits < 3)
+				throw parseException(s, "bad escape");
+			if (pos > MAXLABEL)
+				throw parseException(s, "label too long");
+			labelstart = pos;
+			label[pos++] = b;
+			escaped = false;
+		} else if (b == '\\') {
+			escaped = true;
+			digits = 0;
+			intval = 0;
+		} else if (b == '.') {
+			if (labelstart == -1)
+				throw parseException(s, "invalid empty label");
+			label[0] = (byte)(pos - 1);
+			appendFromString(s, label, 0, 1);
+			labelstart = -1;
+			pos = 1;
+		} else {
+			if (labelstart == -1)
+				labelstart = i;
+			if (pos > MAXLABEL)
+				throw parseException(s, "label too long");
+			label[pos++] = b;
+		}
+	}
+	if (digits > 0 && digits < 3)
+		throw parseException(s, "bad escape");
+	if (escaped)
+		throw parseException(s, "bad escape");
+	if (labelstart == -1) {
+		appendFromString(s, emptyLabel, 0, 1);
+		absolute = true;
+	} else {
+		label[0] = (byte)(pos - 1);
+		appendFromString(s, label, 0, 1);
+	}
+	if (origin != null && !absolute)
+		appendFromString(s, origin.name, 0, origin.getlabels());
+}
+
+/**
+ * Create a new name from a string.  This does not automatically make the name
+ * absolute; it will be absolute if it has a trailing dot.
+ * @param s The string to be converted
+ * @throws TextParseException The name is invalid.
+ */
+public
+Name(String s) throws TextParseException {
+	this(s, null);
+}
+
+/**
+ * Create a new name from a string and an origin.  This does not automatically
+ * make the name absolute; it will be absolute if it has a trailing dot or an
+ * absolute origin is appended.  This is identical to the constructor, except
+ * that it will avoid creating new objects in some cases.
+ * @param s The string to be converted
+ * @param origin If the name is not absolute, the origin to be appended.
+ * @throws TextParseException The name is invalid.
+ */
+public static Name
+fromString(String s, Name origin) throws TextParseException {
+	if (s.equals("@") && origin != null)
+		return origin;
+	else if (s.equals("."))
+		return (root);
+
+	return new Name(s, origin);
+}
+
+/**
+ * Create a new name from a string.  This does not automatically make the name
+ * absolute; it will be absolute if it has a trailing dot.  This is identical
+ * to the constructor, except that it will avoid creating new objects in some
+ * cases.
+ * @param s The string to be converted
+ * @throws TextParseException The name is invalid.
+ */
+public static Name
+fromString(String s) throws TextParseException {
+	return fromString(s, null);
+}
+
+/**
+ * Create a new name from a constant string.  This should only be used when
+ the name is known to be good - that is, when it is constant.
+ * @param s The string to be converted
+ * @throws IllegalArgumentException The name is invalid.
+ */
+public static Name
+fromConstantString(String s) {
+	try {
+		return fromString(s, null);
+	}
+	catch (TextParseException e) {
+		throw new IllegalArgumentException("Invalid name '" + s + "'");
+	}
+}
+
+/**
+ * Create a new name from DNS a wire format message
+ * @param in A stream containing the DNS message which is currently
+ * positioned at the start of the name to be read.
+ */
+public
+Name(DNSInput in) throws WireParseException {
+	int len, pos;
+	boolean done = false;
+	byte [] label = new byte[MAXLABEL + 1];
+	boolean savedState = false;
+
+	while (!done) {
+		len = in.readU8();
+		switch (len & LABEL_MASK) {
+		case LABEL_NORMAL:
+			if (getlabels() >= MAXLABELS)
+				throw new WireParseException("too many labels");
+			if (len == 0) {
+				append(emptyLabel, 0, 1);
+				done = true;
+			} else {
+				label[0] = (byte)len;
+				in.readByteArray(label, 1, len);
+				append(label, 0, 1);
+			}
+			break;
+		case LABEL_COMPRESSION:
+			pos = in.readU8();
+			pos += ((len & ~LABEL_MASK) << 8);
+			if (Options.check("verbosecompression"))
+				System.err.println("currently " + in.current() +
+						   ", pointer to " + pos);
+
+			if (pos >= in.current() - 2)
+				throw new WireParseException("bad compression");
+			if (!savedState) {
+				in.save();
+				savedState = true;
+			}
+			in.jump(pos);
+			if (Options.check("verbosecompression"))
+				System.err.println("current name '" + this +
+						   "', seeking to " + pos);
+			break;
+		default:
+			throw new WireParseException("bad label type");
+		}
+	}
+	if (savedState) {
+		in.restore();
+	}
+}
+
+/**
+ * Create a new name from DNS wire format
+ * @param b A byte array containing the wire format of the name.
+ */
+public
+Name(byte [] b) throws IOException {
+	this(new DNSInput(b));
+}
+
+/**
+ * Create a new name by removing labels from the beginning of an existing Name
+ * @param src An existing Name
+ * @param n The number of labels to remove from the beginning in the copy
+ */
+public
+Name(Name src, int n) {
+	int slabels = src.labels();
+	if (n > slabels)
+		throw new IllegalArgumentException("attempted to remove too " +
+						   "many labels");
+	name = src.name;
+	setlabels(slabels - n);
+	for (int i = 0; i < MAXOFFSETS && i < slabels - n; i++)
+		setoffset(i, src.offset(i + n));
+}
+
+/**
+ * Creates a new name by concatenating two existing names.
+ * @param prefix The prefix name.
+ * @param suffix The suffix name.
+ * @return The concatenated name.
+ * @throws NameTooLongException The name is too long.
+ */
+public static Name
+concatenate(Name prefix, Name suffix) throws NameTooLongException {
+	if (prefix.isAbsolute())
+		return (prefix);
+	Name newname = new Name();
+	copy(prefix, newname);
+	newname.append(suffix.name, suffix.offset(0), suffix.getlabels());
+	return newname;
+}
+
+/**
+ * If this name is a subdomain of origin, return a new name relative to
+ * origin with the same value. Otherwise, return the existing name.
+ * @param origin The origin to remove.
+ * @return The possibly relativized name.
+ */
+public Name
+relativize(Name origin) {
+	if (origin == null || !subdomain(origin))
+		return this;
+	Name newname = new Name();
+	copy(this, newname);
+	int length = length() - origin.length();
+	int labels = newname.labels() - origin.labels();
+	newname.setlabels(labels);
+	newname.name = new byte[length];
+	System.arraycopy(name, offset(0), newname.name, 0, length);
+	return newname;
+}
+
+/**
+ * Generates a new Name with the first n labels replaced by a wildcard 
+ * @return The wildcard name
+ */
+public Name
+wild(int n) {
+	if (n < 1)
+		throw new IllegalArgumentException("must replace 1 or more " +
+						   "labels");
+	try {
+		Name newname = new Name();
+		copy(wild, newname);
+		newname.append(name, offset(n), getlabels() - n);
+		return newname;
+	}
+	catch (NameTooLongException e) {
+		throw new IllegalStateException
+					("Name.wild: concatenate failed");
+	}
+}
+
+/**
+ * Generates a new Name to be used when following a DNAME.
+ * @param dname The DNAME record to follow.
+ * @return The constructed name.
+ * @throws NameTooLongException The resulting name is too long.
+ */
+public Name
+fromDNAME(DNAMERecord dname) throws NameTooLongException {
+	Name dnameowner = dname.getName();
+	Name dnametarget = dname.getTarget();
+	if (!subdomain(dnameowner))
+		return null;
+
+	int plabels = labels() - dnameowner.labels();
+	int plength = length() - dnameowner.length();
+	int pstart = offset(0);
+
+	int dlabels = dnametarget.labels();
+	int dlength = dnametarget.length();
+
+	if (plength + dlength > MAXNAME)
+		throw new NameTooLongException();
+
+	Name newname = new Name();
+	newname.setlabels(plabels + dlabels);
+	newname.name = new byte[plength + dlength];
+	System.arraycopy(name, pstart, newname.name, 0, plength);
+	System.arraycopy(dnametarget.name, 0, newname.name, plength, dlength);
+
+	for (int i = 0, pos = 0; i < MAXOFFSETS && i < plabels + dlabels; i++) {
+		newname.setoffset(i, pos);
+		pos += (newname.name[pos] + 1);
+	}
+	return newname;
+}
+
+/**
+ * Is this name a wildcard?
+ */
+public boolean
+isWild() {
+	if (labels() == 0)
+		return false;
+	return (name[0] == (byte)1 && name[1] == (byte)'*');
+}
+
+/**
+ * Is this name absolute?
+ */
+public boolean
+isAbsolute() {
+	if (labels() == 0)
+		return false;
+	return (name[name.length - 1] == 0);
+}
+
+/**
+ * The length of the name.
+ */
+public short
+length() {
+	if (getlabels() == 0)
+		return 0;
+	return (short)(name.length - offset(0));
+}
+
+/**
+ * The number of labels in the name.
+ */
+public int
+labels() {
+	return getlabels();
+}
+
+/**
+ * Is the current Name a subdomain of the specified name?
+ */
+public boolean
+subdomain(Name domain) {
+	int labels = labels();
+	int dlabels = domain.labels();
+	if (dlabels > labels)
+		return false;
+	if (dlabels == labels)
+		return equals(domain);
+	return domain.equals(name, offset(labels - dlabels));
+}
+
+private String
+byteString(byte [] array, int pos) {
+	StringBuffer sb = new StringBuffer();
+	int len = array[pos++];
+	for (int i = pos; i < pos + len; i++) {
+		int b = array[i] & 0xFF;
+		if (b <= 0x20 || b >= 0x7f) {
+			sb.append('\\');
+			sb.append(byteFormat.format(b));
+		}
+		else if (b == '"' || b == '(' || b == ')' || b == '.' ||
+			 b == ';' || b == '\\' || b == '@' || b == '$')
+		{
+			sb.append('\\');
+			sb.append((char)b);
+		}
+		else
+			sb.append((char)b);
+	}
+	return sb.toString();
+}
+
+/**
+ * Convert a Name to a String
+ * @return The representation of this name as a (printable) String.
+ */
+public String
+toString() {
+	int labels = labels();
+	if (labels == 0)
+		return "@";
+	else if (labels == 1 && name[offset(0)] == 0)
+		return ".";
+	StringBuffer sb = new StringBuffer();
+	for (int i = 0, pos = offset(0); i < labels; i++) {
+		int len = name[pos];
+		if (len > MAXLABEL)
+			throw new IllegalStateException("invalid label");
+		if (len == 0)
+			break;
+		sb.append(byteString(name, pos));
+		sb.append('.');
+		pos += (1 + len);
+	}
+	if (!isAbsolute())
+		sb.deleteCharAt(sb.length() - 1);
+	return sb.toString();
+}
+
+/**
+ * Retrieve the nth label of a Name.  This makes a copy of the label; changing
+ * this does not change the Name.
+ * @param n The label to be retrieved.  The first label is 0.
+ */
+public byte []
+getLabel(int n) {
+	int pos = offset(n);
+	byte len = (byte)(name[pos] + 1);
+	byte [] label = new byte[len];
+	System.arraycopy(name, pos, label, 0, len);
+	return label;
+}
+
+/**
+ * Convert the nth label in a Name to a String
+ * @param n The label to be converted to a (printable) String.  The first
+ * label is 0.
+ */
+public String
+getLabelString(int n) {
+	int pos = offset(n);
+	return byteString(name, pos);
+}
+
+/**
+ * Emit a Name in DNS wire format
+ * @param out The output stream containing the DNS message.
+ * @param c The compression context, or null of no compression is desired.
+ * @throws IllegalArgumentException The name is not absolute.
+ */
+public void
+toWire(DNSOutput out, Compression c) {
+	if (!isAbsolute())
+		throw new IllegalArgumentException("toWire() called on " +
+						   "non-absolute name");
+	
+	int labels = labels();
+	for (int i = 0; i < labels - 1; i++) {
+		Name tname;
+		if (i == 0)
+			tname = this;
+		else
+			tname = new Name(this, i);
+		int pos = -1;
+		if (c != null)
+			pos = c.get(tname);
+		if (pos >= 0) {
+			pos |= (LABEL_MASK << 8);
+			out.writeU16(pos);
+			return;
+		} else {
+			if (c != null)
+				c.add(out.current(), tname);
+			int off = offset(i);
+			out.writeByteArray(name, off, name[off] + 1);
+		}
+	}
+	out.writeU8(0);
+}
+
+/**
+ * Emit a Name in DNS wire format
+ * @throws IllegalArgumentException The name is not absolute.
+ */
+public byte []
+toWire() {
+	DNSOutput out = new DNSOutput();
+	toWire(out, null);
+	return out.toByteArray();
+}
+
+/**
+ * Emit a Name in canonical DNS wire format (all lowercase)
+ * @param out The output stream to which the message is written.
+ */
+public void
+toWireCanonical(DNSOutput out) {
+	byte [] b = toWireCanonical();
+	out.writeByteArray(b);
+}
+
+/**
+ * Emit a Name in canonical DNS wire format (all lowercase)
+ * @return The canonical form of the name.
+ */
+public byte []
+toWireCanonical() {
+	int labels = labels();
+	if (labels == 0)
+		return (new byte[0]);
+	byte [] b = new byte[name.length - offset(0)];
+	for (int i = 0, spos = offset(0), dpos = 0; i < labels; i++) {
+		int len = name[spos];
+		if (len > MAXLABEL)
+			throw new IllegalStateException("invalid label");
+		b[dpos++] = name[spos++];
+		for (int j = 0; j < len; j++)
+			b[dpos++] = lowercase[(name[spos++] & 0xFF)];
+	}
+	return b;
+}
+
+/**
+ * Emit a Name in DNS wire format
+ * @param out The output stream containing the DNS message.
+ * @param c The compression context, or null of no compression is desired.
+ * @param canonical If true, emit the name in canonicalized form
+ * (all lowercase).
+ * @throws IllegalArgumentException The name is not absolute.
+ */
+public void
+toWire(DNSOutput out, Compression c, boolean canonical) {
+	if (canonical)
+		toWireCanonical(out);
+	else
+		toWire(out, c);
+}
+
+private final boolean
+equals(byte [] b, int bpos) {
+	int labels = labels();
+	for (int i = 0, pos = offset(0); i < labels; i++) {
+		if (name[pos] != b[bpos])
+			return false;
+		int len = name[pos++];
+		bpos++;
+		if (len > MAXLABEL)
+			throw new IllegalStateException("invalid label");
+		for (int j = 0; j < len; j++)
+			if (lowercase[(name[pos++] & 0xFF)] !=
+			    lowercase[(b[bpos++] & 0xFF)])
+				return false;
+	}
+	return true;
+}
+
+/**
+ * Are these two Names equivalent?
+ */
+public boolean
+equals(Object arg) {
+	if (arg == this)
+		return true;
+	if (arg == null || !(arg instanceof Name))
+		return false;
+	Name d = (Name) arg;
+	if (d.hashcode == 0)
+		d.hashCode();
+	if (hashcode == 0)
+		hashCode();
+	if (d.hashcode != hashcode)
+		return false;
+	if (d.labels() != labels())
+		return false;
+	return equals(d.name, d.offset(0));
+}
+
+/**
+ * Computes a hashcode based on the value
+ */
+public int
+hashCode() {
+	if (hashcode != 0)
+		return (hashcode);
+	int code = 0;
+	for (int i = offset(0); i < name.length; i++)
+		code += ((code << 3) + lowercase[(name[i] & 0xFF)]);
+	hashcode = code;
+	return hashcode;
+}
+
+/**
+ * Compares this Name to another Object.
+ * @param o The Object to be compared.
+ * @return The value 0 if the argument is a name equivalent to this name;
+ * a value less than 0 if the argument is less than this name in the canonical 
+ * ordering, and a value greater than 0 if the argument is greater than this
+ * name in the canonical ordering.
+ * @throws ClassCastException if the argument is not a Name.
+ */
+public int
+compareTo(Object o) {
+	Name arg = (Name) o;
+
+	if (this == arg)
+		return (0);
+
+	int labels = labels();
+	int alabels = arg.labels();
+	int compares = labels > alabels ? alabels : labels;
+
+	for (int i = 1; i <= compares; i++) {
+		int start = offset(labels - i);
+		int astart = arg.offset(alabels - i);
+		int length = name[start];
+		int alength = arg.name[astart];
+		for (int j = 0; j < length && j < alength; j++) {
+			int n = lowercase[(name[j + start + 1]) & 0xFF] -
+				lowercase[(arg.name[j + astart + 1]) & 0xFF];
+			if (n != 0)
+				return (n);
+		}
+		if (length != alength)
+			return (length - alength);
+	}
+	return (labels - alabels);
+}
+
+}
diff --git a/src/org/xbill/DNS/NameTooLongException.java b/src/org/xbill/DNS/NameTooLongException.java
new file mode 100644
index 0000000..114be39
--- /dev/null
+++ b/src/org/xbill/DNS/NameTooLongException.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * An exception thrown when a name is longer than the maximum length of a DNS
+ * name.
+ *
+ * @author Brian Wellington
+ */
+
+public class NameTooLongException extends WireParseException {
+
+public
+NameTooLongException() {
+	super();
+}
+
+public
+NameTooLongException(String s) {
+	super(s);
+}
+
+}
diff --git a/src/org/xbill/DNS/OPTRecord.java b/src/org/xbill/DNS/OPTRecord.java
new file mode 100644
index 0000000..47fef2f
--- /dev/null
+++ b/src/org/xbill/DNS/OPTRecord.java
@@ -0,0 +1,191 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Options - describes Extended DNS (EDNS) properties of a Message.
+ * No specific options are defined other than those specified in the
+ * header.  An OPT should be generated by Resolver.
+ *
+ * EDNS is a method to extend the DNS protocol while providing backwards
+ * compatibility and not significantly changing the protocol.  This
+ * implementation of EDNS is mostly complete at level 0.
+ *
+ * @see Message
+ * @see Resolver 
+ *
+ * @author Brian Wellington
+ */
+
+public class OPTRecord extends Record {
+
+private static final long serialVersionUID = -6254521894809367938L;
+
+private List options;
+
+OPTRecord() {}
+
+Record
+getObject() {
+	return new OPTRecord();
+}
+
+/**
+ * Creates an OPT Record.  This is normally called by SimpleResolver, but can
+ * also be called by a server.
+ * @param payloadSize The size of a packet that can be reassembled on the 
+ * sending host.
+ * @param xrcode The value of the extended rcode field.  This is the upper
+ * 16 bits of the full rcode.
+ * @param flags Additional message flags.
+ * @param version The EDNS version that this DNS implementation supports.
+ * This should be 0 for dnsjava.
+ * @param options The list of options that comprise the data field.  There
+ * are currently no defined options.
+ * @see ExtendedFlags
+ */
+public
+OPTRecord(int payloadSize, int xrcode, int version, int flags, List options) {
+	super(Name.root, Type.OPT, payloadSize, 0);
+	checkU16("payloadSize", payloadSize);
+	checkU8("xrcode", xrcode);
+	checkU8("version", version);
+	checkU16("flags", flags);
+	ttl = ((long)xrcode << 24) + ((long)version << 16) + flags;
+	if (options != null) {
+		this.options = new ArrayList(options);
+	}
+}
+
+/**
+ * Creates an OPT Record with no data.  This is normally called by
+ * SimpleResolver, but can also be called by a server.
+ * @param payloadSize The size of a packet that can be reassembled on the 
+ * sending host.
+ * @param xrcode The value of the extended rcode field.  This is the upper
+ * 16 bits of the full rcode.
+ * @param flags Additional message flags.
+ * @param version The EDNS version that this DNS implementation supports.
+ * This should be 0 for dnsjava.
+ * @see ExtendedFlags
+ */
+public
+OPTRecord(int payloadSize, int xrcode, int version, int flags) {
+	this(payloadSize, xrcode, version, flags, null);
+}
+
+/**
+ * Creates an OPT Record with no data.  This is normally called by
+ * SimpleResolver, but can also be called by a server.
+ */
+public
+OPTRecord(int payloadSize, int xrcode, int version) {
+	this(payloadSize, xrcode, version, 0, null);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	if (in.remaining() > 0)
+		options = new ArrayList();
+	while (in.remaining() > 0) {
+		EDNSOption option = EDNSOption.fromWire(in);
+		options.add(option);
+	}
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	throw st.exception("no text format defined for OPT");
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	if (options != null) {
+		sb.append(options);
+		sb.append(" ");
+	}
+	sb.append(" ; payload ");
+	sb.append(getPayloadSize());
+	sb.append(", xrcode ");
+	sb.append(getExtendedRcode());
+	sb.append(", version ");
+	sb.append(getVersion());
+	sb.append(", flags ");
+	sb.append(getFlags());
+	return sb.toString();
+}
+
+/** Returns the maximum allowed payload size. */
+public int
+getPayloadSize() {
+	return dclass;
+}
+
+/**
+ * Returns the extended Rcode
+ * @see Rcode
+ */
+public int
+getExtendedRcode() {
+	return (int)(ttl >>> 24);
+}
+
+/** Returns the highest supported EDNS version */
+public int
+getVersion() {
+	return (int)((ttl >>> 16) & 0xFF);
+}
+
+/** Returns the EDNS flags */
+public int
+getFlags() {
+	return (int)(ttl & 0xFFFF);
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	if (options == null)
+		return;
+	Iterator it = options.iterator();
+	while (it.hasNext()) {
+		EDNSOption option = (EDNSOption) it.next();
+		option.toWire(out);
+	}
+}
+
+/**
+ * Gets all options in the OPTRecord.  This returns a list of EDNSOptions.
+ */
+public List
+getOptions() {
+	if (options == null)
+		return Collections.EMPTY_LIST;
+	return Collections.unmodifiableList(options);
+}
+
+/**
+ * Gets all options in the OPTRecord with a specific code.  This returns a list
+ * of EDNSOptions.
+ */
+public List
+getOptions(int code) {
+	if (options == null)
+		return Collections.EMPTY_LIST;
+	List list = Collections.EMPTY_LIST;
+	for (Iterator it = options.iterator(); it.hasNext(); ) {
+		EDNSOption opt = (EDNSOption) it.next();
+		if (opt.getCode() == code) {
+			if (list == Collections.EMPTY_LIST)
+				list = new ArrayList();
+			list.add(opt);
+		}
+	}
+	return list;
+}
+
+}
diff --git a/src/org/xbill/DNS/Opcode.java b/src/org/xbill/DNS/Opcode.java
new file mode 100644
index 0000000..dadbca1
--- /dev/null
+++ b/src/org/xbill/DNS/Opcode.java
@@ -0,0 +1,60 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Constants and functions relating to DNS opcodes
+ *
+ * @author Brian Wellington
+ */
+
+public final class Opcode {
+
+/** A standard query */
+public static final int QUERY		= 0;
+
+/** An inverse query (deprecated) */
+public static final int IQUERY		= 1;
+
+/** A server status request (not used) */
+public static final int STATUS		= 2;
+
+/**
+ * A message from a primary to a secondary server to initiate a zone transfer
+ */
+public static final int NOTIFY		= 4;
+
+/** A dynamic update message */
+public static final int UPDATE		= 5;
+
+private static Mnemonic opcodes = new Mnemonic("DNS Opcode",
+					       Mnemonic.CASE_UPPER);
+
+static {
+	opcodes.setMaximum(0xF);
+	opcodes.setPrefix("RESERVED");
+	opcodes.setNumericAllowed(true);
+
+	opcodes.add(QUERY, "QUERY");
+	opcodes.add(IQUERY, "IQUERY");
+	opcodes.add(STATUS, "STATUS");
+	opcodes.add(NOTIFY, "NOTIFY");
+	opcodes.add(UPDATE, "UPDATE");
+}
+
+private
+Opcode() {}
+
+/** Converts a numeric Opcode into a String */
+public static String
+string(int i) {
+	return opcodes.getText(i);
+}
+
+/** Converts a String representation of an Opcode into its numeric value */
+public static int
+value(String s) {
+	return opcodes.getValue(s);
+}
+
+}
diff --git a/src/org/xbill/DNS/Options.java b/src/org/xbill/DNS/Options.java
new file mode 100644
index 0000000..2f1dae3
--- /dev/null
+++ b/src/org/xbill/DNS/Options.java
@@ -0,0 +1,123 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+
+/**
+ * Boolean options:<BR>
+ * bindttl - Print TTLs in BIND format<BR>
+ * multiline - Print records in multiline format<BR>
+ * noprintin - Don't print the class of a record if it's IN<BR>
+ * verbose - Turn on general debugging statements<BR>
+ * verbosemsg - Print all messages sent or received by SimpleResolver<BR>
+ * verbosecompression - Print messages related to name compression<BR>
+ * verbosesec - Print messages related to signature verification<BR>
+ * verbosecache - Print messages related to cache lookups<BR>
+ * <BR>
+ * Valued options:<BR>
+ * tsigfudge=n - Sets the default TSIG fudge value (in seconds)<BR>
+ * sig0validity=n - Sets the default SIG(0) validity period (in seconds)<BR>
+ *
+ * @author Brian Wellington
+ */
+
+public final class Options {
+
+private static Map table;
+
+static {
+	try {
+		refresh();
+	}
+	catch (SecurityException e) {
+	}
+}
+
+private
+Options() {}
+
+public static void
+refresh() {
+	String s = System.getProperty("dnsjava.options");
+	if (s != null) {
+		StringTokenizer st = new StringTokenizer(s, ",");
+		while (st.hasMoreTokens()) {
+			String token = st.nextToken();
+			int index = token.indexOf('=');
+			if (index == -1)
+				set(token);
+			else {
+				String option = token.substring(0, index);
+				String value = token.substring(index + 1);
+				set(option, value);
+			}
+		}
+	}
+}
+
+/** Clears all defined options */
+public static void
+clear() {
+	table = null;
+}
+
+/** Sets an option to "true" */
+public static void
+set(String option) {
+	if (table == null)
+		table = new HashMap();
+	table.put(option.toLowerCase(), "true");
+}
+
+/** Sets an option to the the supplied value */
+public static void
+set(String option, String value) {
+	if (table == null)
+		table = new HashMap();
+	table.put(option.toLowerCase(), value.toLowerCase());
+}
+
+/** Removes an option */
+public static void
+unset(String option) {
+	if (table == null)
+		return;
+	table.remove(option.toLowerCase());
+}
+
+/** Checks if an option is defined */
+public static boolean
+check(String option) {
+	if (table == null)
+		return false;
+	return (table.get(option.toLowerCase()) != null);
+}
+
+/** Returns the value of an option */
+public static String
+value(String option) {
+	if (table == null)
+		return null;
+	return ((String)table.get(option.toLowerCase()));
+}
+
+/**
+ * Returns the value of an option as an integer, or -1 if not defined.
+ */
+public static int
+intValue(String option) {
+	String s = value(option);
+	if (s != null) {
+		try {
+			int val = Integer.parseInt(s);
+			if (val > 0)
+				return (val);
+		}
+		catch (NumberFormatException e) {
+		}
+	}
+	return (-1);
+}
+
+}
diff --git a/src/org/xbill/DNS/PTRRecord.java b/src/org/xbill/DNS/PTRRecord.java
new file mode 100644
index 0000000..89be578
--- /dev/null
+++ b/src/org/xbill/DNS/PTRRecord.java
@@ -0,0 +1,38 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Pointer Record  - maps a domain name representing an Internet Address to
+ * a hostname.
+ *
+ * @author Brian Wellington
+ */
+
+public class PTRRecord extends SingleCompressedNameBase {
+
+private static final long serialVersionUID = -8321636610425434192L;
+
+PTRRecord() {}
+
+Record
+getObject() {
+	return new PTRRecord();
+}
+
+/** 
+ * Creates a new PTR Record with the given data
+ * @param target The name of the machine with this address
+ */
+public
+PTRRecord(Name name, int dclass, long ttl, Name target) {
+	super(name, Type.PTR, dclass, ttl, target, "target");
+}
+
+/** Gets the target of the PTR Record */
+public Name
+getTarget() {
+	return getSingleName();
+}
+
+}
diff --git a/src/org/xbill/DNS/PXRecord.java b/src/org/xbill/DNS/PXRecord.java
new file mode 100644
index 0000000..a407241
--- /dev/null
+++ b/src/org/xbill/DNS/PXRecord.java
@@ -0,0 +1,96 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * X.400 mail mapping record.
+ *
+ * @author Brian Wellington
+ */
+
+public class PXRecord extends Record {
+
+private static final long serialVersionUID = 1811540008806660667L;
+
+private int preference;
+private Name map822;
+private Name mapX400;
+
+PXRecord() {}
+
+Record
+getObject() {
+	return new PXRecord();
+}
+
+/**
+ * Creates an PX Record from the given data
+ * @param preference The preference of this mail address.
+ * @param map822 The RFC 822 component of the mail address.
+ * @param mapX400 The X.400 component of the mail address.
+ */
+public
+PXRecord(Name name, int dclass, long ttl, int preference,
+	 Name map822, Name mapX400)
+{
+	super(name, Type.PX, dclass, ttl);
+
+	this.preference = checkU16("preference", preference);
+	this.map822 = checkName("map822", map822);
+	this.mapX400 = checkName("mapX400", mapX400);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	preference = in.readU16();
+	map822 = new Name(in);
+	mapX400 = new Name(in);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	preference = st.getUInt16();
+	map822 = st.getName(origin);
+	mapX400 = st.getName(origin);
+}
+
+/** Converts the PX Record to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(preference);
+	sb.append(" ");
+	sb.append(map822);
+	sb.append(" ");
+	sb.append(mapX400);
+	return sb.toString();
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU16(preference);
+	map822.toWire(out, null, canonical);
+	mapX400.toWire(out, null, canonical);
+}
+
+/** Gets the preference of the route. */
+public int
+getPreference() {
+	return preference;
+}
+
+/** Gets the RFC 822 component of the mail address. */
+public Name
+getMap822() {
+	return map822;
+}
+
+/** Gets the X.400 component of the mail address. */
+public Name
+getMapX400() {
+	return mapX400;
+}
+
+}
diff --git a/src/org/xbill/DNS/RPRecord.java b/src/org/xbill/DNS/RPRecord.java
new file mode 100644
index 0000000..7aa066c
--- /dev/null
+++ b/src/org/xbill/DNS/RPRecord.java
@@ -0,0 +1,82 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * Responsible Person Record - lists the mail address of a responsible person
+ * and a domain where TXT records are available.
+ *
+ * @author Tom Scola <tscola@research.att.com>
+ * @author Brian Wellington
+ */
+
+public class RPRecord extends Record {
+
+private static final long serialVersionUID = 8124584364211337460L;
+
+private Name mailbox;
+private Name textDomain;
+
+RPRecord() {}
+
+Record
+getObject() {
+	return new RPRecord();
+}
+
+/**
+ * Creates an RP Record from the given data
+ * @param mailbox The responsible person
+ * @param textDomain The address where TXT records can be found
+ */
+public
+RPRecord(Name name, int dclass, long ttl, Name mailbox, Name textDomain) {
+	super(name, Type.RP, dclass, ttl);
+
+	this.mailbox = checkName("mailbox", mailbox);
+	this.textDomain = checkName("textDomain", textDomain);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	mailbox = new Name(in);
+	textDomain = new Name(in);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	mailbox = st.getName(origin);
+	textDomain = st.getName(origin);
+}
+
+/** Converts the RP Record to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(mailbox);
+	sb.append(" ");
+	sb.append(textDomain);
+	return sb.toString();
+}
+
+/** Gets the mailbox address of the RP Record */
+public Name
+getMailbox() {
+	return mailbox;
+}
+
+/** Gets the text domain info of the RP Record */
+public Name
+getTextDomain() {
+	return textDomain;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	mailbox.toWire(out, null, canonical);
+	textDomain.toWire(out, null, canonical);
+}
+
+}
diff --git a/src/org/xbill/DNS/RRSIGRecord.java b/src/org/xbill/DNS/RRSIGRecord.java
new file mode 100644
index 0000000..c092839
--- /dev/null
+++ b/src/org/xbill/DNS/RRSIGRecord.java
@@ -0,0 +1,50 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+
+/**
+ * Recource Record Signature - An RRSIG provides the digital signature of an
+ * RRset, so that the data can be authenticated by a DNSSEC-capable resolver.
+ * The signature is generated by a key contained in a DNSKEY Record.
+ * @see RRset
+ * @see DNSSEC
+ * @see KEYRecord
+ *
+ * @author Brian Wellington
+ */
+
+public class RRSIGRecord extends SIGBase {
+
+private static final long serialVersionUID = -2609150673537226317L;
+
+RRSIGRecord() {}
+
+Record
+getObject() {
+	return new RRSIGRecord();
+}
+
+/**
+ * Creates an RRSIG Record from the given data
+ * @param covered The RRset type covered by this signature
+ * @param alg The cryptographic algorithm of the key that generated the
+ * signature
+ * @param origttl The original TTL of the RRset
+ * @param expire The time at which the signature expires
+ * @param timeSigned The time at which this signature was generated
+ * @param footprint The footprint/key id of the signing key.
+ * @param signer The owner of the signing key
+ * @param signature Binary data representing the signature
+ */
+public
+RRSIGRecord(Name name, int dclass, long ttl, int covered, int alg, long origttl,
+	    Date expire, Date timeSigned, int footprint, Name signer,
+	    byte [] signature)
+{
+	super(name, Type.RRSIG, dclass, ttl, covered, alg, origttl, expire,
+	      timeSigned, footprint, signer, signature);
+}
+
+}
diff --git a/src/org/xbill/DNS/RRset.java b/src/org/xbill/DNS/RRset.java
new file mode 100644
index 0000000..fa1a6ad
--- /dev/null
+++ b/src/org/xbill/DNS/RRset.java
@@ -0,0 +1,258 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.Serializable;
+import java.util.*;
+
+/**
+ * A set of Records with the same name, type, and class.  Also included
+ * are all RRSIG records signing the data records.
+ * @see Record
+ * @see RRSIGRecord 
+ *
+ * @author Brian Wellington
+ */
+
+public class RRset implements Serializable {
+
+private static final long serialVersionUID = -3270249290171239695L;
+
+/*
+ * rrs contains both normal and RRSIG records, with the RRSIG records
+ * at the end.
+ */
+private List rrs;
+private short nsigs;
+private short position;
+
+/** Creates an empty RRset */
+public
+RRset() {
+	rrs = new ArrayList(1);
+	nsigs = 0;
+	position = 0;
+}
+
+/** Creates an RRset and sets its contents to the specified record */
+public
+RRset(Record record) {
+	this();
+	safeAddRR(record);
+}
+
+/** Creates an RRset with the contents of an existing RRset */
+public
+RRset(RRset rrset) {
+	synchronized (rrset) {
+		rrs = (List) ((ArrayList)rrset.rrs).clone();
+		nsigs = rrset.nsigs;
+		position = rrset.position;
+	}
+}
+
+private void
+safeAddRR(Record r) {
+	if (!(r instanceof RRSIGRecord)) {
+		if (nsigs == 0)
+			rrs.add(r);
+		else
+			rrs.add(rrs.size() - nsigs, r);
+	} else {
+		rrs.add(r);
+		nsigs++;
+	}
+}
+
+/** Adds a Record to an RRset */
+public synchronized void
+addRR(Record r) {
+	if (rrs.size() == 0) {
+		safeAddRR(r);
+		return;
+	}
+	Record first = first();
+	if (!r.sameRRset(first))
+		throw new IllegalArgumentException("record does not match " +
+						   "rrset");
+
+	if (r.getTTL() != first.getTTL()) {
+		if (r.getTTL() > first.getTTL()) {
+			r = r.cloneRecord();
+			r.setTTL(first.getTTL());
+		} else {
+			for (int i = 0; i < rrs.size(); i++) {
+				Record tmp = (Record) rrs.get(i);
+				tmp = tmp.cloneRecord();
+				tmp.setTTL(r.getTTL());
+				rrs.set(i, tmp);
+			}
+		}
+	}
+
+	if (!rrs.contains(r))
+		safeAddRR(r);
+}
+
+/** Deletes a Record from an RRset */
+public synchronized void
+deleteRR(Record r) {
+	if (rrs.remove(r) && (r instanceof RRSIGRecord))
+		nsigs--;
+}
+
+/** Deletes all Records from an RRset */
+public synchronized void
+clear() {
+	rrs.clear();
+	position = 0;
+	nsigs = 0;
+}
+
+private synchronized Iterator
+iterator(boolean data, boolean cycle) {
+	int size, start, total;
+
+	total = rrs.size();
+
+	if (data)
+		size = total - nsigs;
+	else
+		size = nsigs;
+	if (size == 0)
+		return Collections.EMPTY_LIST.iterator();
+
+	if (data) {
+		if (!cycle)
+			start = 0;
+		else {
+			if (position >= size)
+				position = 0;
+			start = position++;
+		}
+	} else {
+		start = total - nsigs;
+	}
+
+	List list = new ArrayList(size);
+	if (data) {
+		list.addAll(rrs.subList(start, size));
+		if (start != 0)
+			list.addAll(rrs.subList(0, start));
+	} else {
+		list.addAll(rrs.subList(start, total));
+	}
+
+	return list.iterator();
+}
+
+/**
+ * Returns an Iterator listing all (data) records.
+ * @param cycle If true, cycle through the records so that each Iterator will
+ * start with a different record.
+ */
+public synchronized Iterator
+rrs(boolean cycle) {
+	return iterator(true, cycle);
+}
+
+/**
+ * Returns an Iterator listing all (data) records.  This cycles through
+ * the records, so each Iterator will start with a different record.
+ */
+public synchronized Iterator
+rrs() {
+	return iterator(true, true);
+}
+
+/** Returns an Iterator listing all signature records */
+public synchronized Iterator
+sigs() {
+	return iterator(false, false);
+}
+
+/** Returns the number of (data) records */
+public synchronized int
+size() {
+	return rrs.size() - nsigs;
+}
+
+/**
+ * Returns the name of the records
+ * @see Name
+ */
+public Name
+getName() {
+	return first().getName();
+}
+
+/**
+ * Returns the type of the records
+ * @see Type
+ */
+public int
+getType() {
+	return first().getRRsetType();
+}
+
+/**
+ * Returns the class of the records
+ * @see DClass
+ */
+public int
+getDClass() {
+	return first().getDClass();
+}
+
+/** Returns the ttl of the records */
+public synchronized long
+getTTL() {
+	return first().getTTL();
+}
+
+/**
+ * Returns the first record
+ * @throws IllegalStateException if the rrset is empty
+ */
+public synchronized Record
+first() {
+	if (rrs.size() == 0)
+		throw new IllegalStateException("rrset is empty");
+	return (Record) rrs.get(0);
+}
+
+private String
+iteratorToString(Iterator it) {
+	StringBuffer sb = new StringBuffer();
+	while (it.hasNext()) {
+		Record rr = (Record) it.next();
+		sb.append("[");
+		sb.append(rr.rdataToString());
+		sb.append("]");
+		if (it.hasNext())
+			sb.append(" ");
+	}
+	return sb.toString();
+}
+
+/** Converts the RRset to a String */
+public String
+toString() {
+	if (rrs == null)
+		return ("{empty}");
+	StringBuffer sb = new StringBuffer();
+	sb.append("{ ");
+	sb.append(getName() + " ");
+	sb.append(getTTL() + " ");
+	sb.append(DClass.string(getDClass()) + " ");
+	sb.append(Type.string(getType()) + " ");
+	sb.append(iteratorToString(iterator(true, false)));
+	if (nsigs > 0) {
+		sb.append(" sigs: ");
+		sb.append(iteratorToString(iterator(false, false)));
+	}
+	sb.append(" }");
+	return sb.toString();
+}
+
+}
diff --git a/src/org/xbill/DNS/RTRecord.java b/src/org/xbill/DNS/RTRecord.java
new file mode 100644
index 0000000..549731e
--- /dev/null
+++ b/src/org/xbill/DNS/RTRecord.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Route Through Record - lists a route preference and intermediate host.
+ *
+ * @author Brian Wellington
+ */
+
+public class RTRecord extends U16NameBase {
+
+private static final long serialVersionUID = -3206215651648278098L;
+
+RTRecord() {}
+
+Record
+getObject() {
+	return new RTRecord();
+}
+
+/**
+ * Creates an RT Record from the given data
+ * @param preference The preference of the route.  Smaller numbers indicate
+ * more preferred routes.
+ * @param intermediateHost The domain name of the host to use as a router.
+ */
+public
+RTRecord(Name name, int dclass, long ttl, int preference,
+	 Name intermediateHost)
+{
+	super(name, Type.RT, dclass, ttl, preference, "preference",
+	      intermediateHost, "intermediateHost");
+}
+
+/** Gets the preference of the route. */
+public int
+getPreference() {
+	return getU16Field();
+}
+
+/** Gets the host to use as a router. */
+public Name
+getIntermediateHost() {
+	return getNameField();
+}
+
+}
diff --git a/src/org/xbill/DNS/Rcode.java b/src/org/xbill/DNS/Rcode.java
new file mode 100644
index 0000000..7f0dd1f
--- /dev/null
+++ b/src/org/xbill/DNS/Rcode.java
@@ -0,0 +1,123 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Constants and functions relating to DNS rcodes (error values)
+ *
+ * @author Brian Wellington
+ */
+
+public final class Rcode {
+
+private static Mnemonic rcodes = new Mnemonic("DNS Rcode",
+					      Mnemonic.CASE_UPPER);
+
+private static Mnemonic tsigrcodes = new Mnemonic("TSIG rcode",
+						  Mnemonic.CASE_UPPER);
+
+/** No error */
+public static final int NOERROR		= 0;
+
+/** Format error */
+public static final int FORMERR		= 1;
+
+/** Server failure */
+public static final int SERVFAIL	= 2;
+
+/** The name does not exist */
+public static final int NXDOMAIN	= 3;
+
+/** The operation requested is not implemented */
+public static final int NOTIMP		= 4;
+
+/** Deprecated synonym for NOTIMP. */
+public static final int NOTIMPL		= 4;
+
+/** The operation was refused by the server */
+public static final int REFUSED		= 5;
+
+/** The name exists */
+public static final int YXDOMAIN	= 6;
+
+/** The RRset (name, type) exists */
+public static final int YXRRSET		= 7;
+
+/** The RRset (name, type) does not exist */
+public static final int NXRRSET		= 8;
+
+/** The requestor is not authorized to perform this operation */
+public static final int NOTAUTH		= 9;
+
+/** The zone specified is not a zone */
+public static final int NOTZONE		= 10;
+
+/* EDNS extended rcodes */
+/** Unsupported EDNS level */
+public static final int BADVERS		= 16;
+
+/* TSIG/TKEY only rcodes */
+/** The signature is invalid (TSIG/TKEY extended error) */
+public static final int BADSIG		= 16;
+
+/** The key is invalid (TSIG/TKEY extended error) */
+public static final int BADKEY		= 17;
+
+/** The time is out of range (TSIG/TKEY extended error) */
+public static final int BADTIME		= 18;
+
+/** The mode is invalid (TKEY extended error) */
+public static final int BADMODE		= 19;
+
+static {
+	rcodes.setMaximum(0xFFF);
+	rcodes.setPrefix("RESERVED");
+	rcodes.setNumericAllowed(true);
+
+	rcodes.add(NOERROR, "NOERROR");
+	rcodes.add(FORMERR, "FORMERR");
+	rcodes.add(SERVFAIL, "SERVFAIL");
+	rcodes.add(NXDOMAIN, "NXDOMAIN");
+	rcodes.add(NOTIMP, "NOTIMP");
+	rcodes.addAlias(NOTIMP, "NOTIMPL");
+	rcodes.add(REFUSED, "REFUSED");
+	rcodes.add(YXDOMAIN, "YXDOMAIN");
+	rcodes.add(YXRRSET, "YXRRSET");
+	rcodes.add(NXRRSET, "NXRRSET");
+	rcodes.add(NOTAUTH, "NOTAUTH");
+	rcodes.add(NOTZONE, "NOTZONE");
+	rcodes.add(BADVERS, "BADVERS");
+
+	tsigrcodes.setMaximum(0xFFFF);
+	tsigrcodes.setPrefix("RESERVED");
+	tsigrcodes.setNumericAllowed(true);
+	tsigrcodes.addAll(rcodes);
+
+	tsigrcodes.add(BADSIG, "BADSIG");
+	tsigrcodes.add(BADKEY, "BADKEY");
+	tsigrcodes.add(BADTIME, "BADTIME");
+	tsigrcodes.add(BADMODE, "BADMODE");
+}
+
+private
+Rcode() {}
+
+/** Converts a numeric Rcode into a String */
+public static String
+string(int i) {
+	return rcodes.getText(i);
+}
+
+/** Converts a numeric TSIG extended Rcode into a String */
+public static String
+TSIGstring(int i) {
+	return tsigrcodes.getText(i);
+}
+
+/** Converts a String representation of an Rcode into its numeric value */
+public static int
+value(String s) {
+	return rcodes.getValue(s);
+}
+
+}
diff --git a/src/org/xbill/DNS/Record.java b/src/org/xbill/DNS/Record.java
new file mode 100644
index 0000000..8da7015
--- /dev/null
+++ b/src/org/xbill/DNS/Record.java
@@ -0,0 +1,736 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.text.*;
+import java.util.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * A generic DNS resource record.  The specific record types extend this class.
+ * A record contains a name, type, class, ttl, and rdata.
+ *
+ * @author Brian Wellington
+ */
+
+public abstract class Record implements Cloneable, Comparable, Serializable {
+
+private static final long serialVersionUID = 2694906050116005466L;
+
+protected Name name;
+protected int type, dclass;
+protected long ttl;
+
+private static final DecimalFormat byteFormat = new DecimalFormat();
+
+static {
+	byteFormat.setMinimumIntegerDigits(3);
+}
+
+protected
+Record() {}
+
+Record(Name name, int type, int dclass, long ttl) {
+	if (!name.isAbsolute())
+		throw new RelativeNameException(name);
+	Type.check(type);
+	DClass.check(dclass);
+	TTL.check(ttl);
+	this.name = name;
+	this.type = type;
+	this.dclass = dclass;
+	this.ttl = ttl;
+}
+
+/**
+ * Creates an empty record of the correct type; must be overriden
+ */
+abstract Record
+getObject();
+
+private static final Record
+getEmptyRecord(Name name, int type, int dclass, long ttl, boolean hasData) {
+	Record proto, rec;
+
+	if (hasData) {
+		proto = Type.getProto(type);
+		if (proto != null)
+			rec = proto.getObject();
+		else
+			rec = new UNKRecord();
+	} else
+		rec = new EmptyRecord();
+	rec.name = name;
+	rec.type = type;
+	rec.dclass = dclass;
+	rec.ttl = ttl;
+	return rec;
+}
+
+/**
+ * Converts the type-specific RR to wire format - must be overriden
+ */
+abstract void
+rrFromWire(DNSInput in) throws IOException;
+
+private static Record
+newRecord(Name name, int type, int dclass, long ttl, int length, DNSInput in)
+throws IOException
+{
+	Record rec;
+	rec = getEmptyRecord(name, type, dclass, ttl, in != null);
+	if (in != null) {
+		if (in.remaining() < length)
+			throw new WireParseException("truncated record");
+		in.setActive(length);
+
+		rec.rrFromWire(in);
+
+		if (in.remaining() > 0)
+			throw new WireParseException("invalid record length");
+		in.clearActive();
+	}
+	return rec;
+}
+
+/**
+ * Creates a new record, with the given parameters.
+ * @param name The owner name of the record.
+ * @param type The record's type.
+ * @param dclass The record's class.
+ * @param ttl The record's time to live.
+ * @param length The length of the record's data.
+ * @param data The rdata of the record, in uncompressed DNS wire format.  Only
+ * the first length bytes are used.
+ */
+public static Record
+newRecord(Name name, int type, int dclass, long ttl, int length, byte [] data) {
+	if (!name.isAbsolute())
+		throw new RelativeNameException(name);
+	Type.check(type);
+	DClass.check(dclass);
+	TTL.check(ttl);
+
+	DNSInput in;
+	if (data != null)
+		in = new DNSInput(data);
+	else
+		in = null;
+	try {
+		return newRecord(name, type, dclass, ttl, length, in);
+	}
+	catch (IOException e) {
+		return null;
+	}
+}
+
+/**
+ * Creates a new record, with the given parameters.
+ * @param name The owner name of the record.
+ * @param type The record's type.
+ * @param dclass The record's class.
+ * @param ttl The record's time to live.
+ * @param data The complete rdata of the record, in uncompressed DNS wire
+ * format.
+ */
+public static Record
+newRecord(Name name, int type, int dclass, long ttl, byte [] data) {
+	return newRecord(name, type, dclass, ttl, data.length, data);
+}
+
+/**
+ * Creates a new empty record, with the given parameters.
+ * @param name The owner name of the record.
+ * @param type The record's type.
+ * @param dclass The record's class.
+ * @param ttl The record's time to live.
+ * @return An object of a subclass of Record
+ */
+public static Record
+newRecord(Name name, int type, int dclass, long ttl) {
+	if (!name.isAbsolute())
+		throw new RelativeNameException(name);
+	Type.check(type);
+	DClass.check(dclass);
+	TTL.check(ttl);
+
+	return getEmptyRecord(name, type, dclass, ttl, false);
+}
+
+/**
+ * Creates a new empty record, with the given parameters.  This method is
+ * designed to create records that will be added to the QUERY section
+ * of a message.
+ * @param name The owner name of the record.
+ * @param type The record's type.
+ * @param dclass The record's class.
+ * @return An object of a subclass of Record
+ */
+public static Record
+newRecord(Name name, int type, int dclass) {
+	return newRecord(name, type, dclass, 0);
+}
+
+static Record
+fromWire(DNSInput in, int section, boolean isUpdate) throws IOException {
+	int type, dclass;
+	long ttl;
+	int length;
+	Name name;
+	Record rec;
+
+	name = new Name(in);
+	type = in.readU16();
+	dclass = in.readU16();
+
+	if (section == Section.QUESTION)
+		return newRecord(name, type, dclass);
+
+	ttl = in.readU32();
+	length = in.readU16();
+	if (length == 0 && isUpdate &&
+	    (section == Section.PREREQ || section == Section.UPDATE))
+		return newRecord(name, type, dclass, ttl);
+	rec = newRecord(name, type, dclass, ttl, length, in);
+	return rec;
+}
+
+static Record
+fromWire(DNSInput in, int section) throws IOException {
+	return fromWire(in, section, false);
+}
+
+/**
+ * Builds a Record from DNS uncompressed wire format.
+ */
+public static Record
+fromWire(byte [] b, int section) throws IOException {
+	return fromWire(new DNSInput(b), section, false);
+}
+
+void
+toWire(DNSOutput out, int section, Compression c) {
+	name.toWire(out, c);
+	out.writeU16(type);
+	out.writeU16(dclass);
+	if (section == Section.QUESTION)
+		return;
+	out.writeU32(ttl);
+	int lengthPosition = out.current();
+	out.writeU16(0); /* until we know better */
+	rrToWire(out, c, false);
+	int rrlength = out.current() - lengthPosition - 2;
+	out.writeU16At(rrlength, lengthPosition);
+}
+
+/**
+ * Converts a Record into DNS uncompressed wire format.
+ */
+public byte []
+toWire(int section) {
+	DNSOutput out = new DNSOutput();
+	toWire(out, section, null);
+	return out.toByteArray();
+}
+
+private void
+toWireCanonical(DNSOutput out, boolean noTTL) {
+	name.toWireCanonical(out);
+	out.writeU16(type);
+	out.writeU16(dclass);
+	if (noTTL) {
+		out.writeU32(0);
+	} else {
+		out.writeU32(ttl);
+	}
+	int lengthPosition = out.current();
+	out.writeU16(0); /* until we know better */
+	rrToWire(out, null, true);
+	int rrlength = out.current() - lengthPosition - 2;
+	out.writeU16At(rrlength, lengthPosition);
+}
+
+/*
+ * Converts a Record into canonical DNS uncompressed wire format (all names are
+ * converted to lowercase), optionally ignoring the TTL.
+ */
+private byte []
+toWireCanonical(boolean noTTL) {
+	DNSOutput out = new DNSOutput();
+	toWireCanonical(out, noTTL);
+	return out.toByteArray();
+}
+
+/**
+ * Converts a Record into canonical DNS uncompressed wire format (all names are
+ * converted to lowercase).
+ */
+public byte []
+toWireCanonical() {
+	return toWireCanonical(false);
+}
+
+/**
+ * Converts the rdata in a Record into canonical DNS uncompressed wire format
+ * (all names are converted to lowercase).
+ */
+public byte []
+rdataToWireCanonical() {
+	DNSOutput out = new DNSOutput();
+	rrToWire(out, null, true);
+	return out.toByteArray();
+}
+
+/**
+ * Converts the type-specific RR to text format - must be overriden
+ */
+abstract String rrToString();
+
+/**
+ * Converts the rdata portion of a Record into a String representation
+ */
+public String
+rdataToString() {
+	return rrToString();
+}
+
+/**
+ * Converts a Record into a String representation
+ */
+public String
+toString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(name);
+	if (sb.length() < 8)
+		sb.append("\t");
+	if (sb.length() < 16)
+		sb.append("\t");
+	sb.append("\t");
+	if (Options.check("BINDTTL"))
+		sb.append(TTL.format(ttl));
+	else
+		sb.append(ttl);
+	sb.append("\t");
+	if (dclass != DClass.IN || !Options.check("noPrintIN")) {
+		sb.append(DClass.string(dclass));
+		sb.append("\t");
+	}
+	sb.append(Type.string(type));
+	String rdata = rrToString();
+	if (!rdata.equals("")) {
+		sb.append("\t");
+		sb.append(rdata);
+	}
+	return sb.toString();
+}
+
+/**
+ * Converts the text format of an RR to the internal format - must be overriden
+ */
+abstract void
+rdataFromString(Tokenizer st, Name origin) throws IOException;
+
+/**
+ * Converts a String into a byte array.
+ */
+protected static byte []
+byteArrayFromString(String s) throws TextParseException {
+	byte [] array = s.getBytes();
+	boolean escaped = false;
+	boolean hasEscapes = false;
+
+	for (int i = 0; i < array.length; i++) {
+		if (array[i] == '\\') {
+			hasEscapes = true;
+			break;
+		}
+	}
+	if (!hasEscapes) {
+		if (array.length > 255) {
+			throw new TextParseException("text string too long");
+		}
+		return array;
+	}
+
+	ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+	int digits = 0;
+	int intval = 0;
+	for (int i = 0; i < array.length; i++) {
+		byte b = array[i];
+		if (escaped) {
+			if (b >= '0' && b <= '9' && digits < 3) {
+				digits++; 
+				intval *= 10;
+				intval += (b - '0');
+				if (intval > 255)
+					throw new TextParseException
+								("bad escape");
+				if (digits < 3)
+					continue;
+				b = (byte) intval;
+			}
+			else if (digits > 0 && digits < 3)
+				throw new TextParseException("bad escape");
+			os.write(b);
+			escaped = false;
+		}
+		else if (array[i] == '\\') {
+			escaped = true;
+			digits = 0;
+			intval = 0;
+		}
+		else
+			os.write(array[i]);
+	}
+	if (digits > 0 && digits < 3)
+		throw new TextParseException("bad escape");
+	array = os.toByteArray();
+	if (array.length > 255) {
+		throw new TextParseException("text string too long");
+	}
+
+	return os.toByteArray();
+}
+
+/**
+ * Converts a byte array into a String.
+ */
+protected static String
+byteArrayToString(byte [] array, boolean quote) {
+	StringBuffer sb = new StringBuffer();
+	if (quote)
+		sb.append('"');
+	for (int i = 0; i < array.length; i++) {
+		int b = array[i] & 0xFF;
+		if (b < 0x20 || b >= 0x7f) {
+			sb.append('\\');
+			sb.append(byteFormat.format(b));
+		} else if (b == '"' || b == '\\') {
+			sb.append('\\');
+			sb.append((char)b);
+		} else
+			sb.append((char)b);
+	}
+	if (quote)
+		sb.append('"');
+	return sb.toString();
+}
+
+/**
+ * Converts a byte array into the unknown RR format.
+ */
+protected static String
+unknownToString(byte [] data) {
+	StringBuffer sb = new StringBuffer();
+	sb.append("\\# ");
+	sb.append(data.length);
+	sb.append(" ");
+	sb.append(base16.toString(data));
+	return sb.toString();
+}
+
+/**
+ * Builds a new Record from its textual representation
+ * @param name The owner name of the record.
+ * @param type The record's type.
+ * @param dclass The record's class.
+ * @param ttl The record's time to live.
+ * @param st A tokenizer containing the textual representation of the rdata.
+ * @param origin The default origin to be appended to relative domain names.
+ * @return The new record
+ * @throws IOException The text format was invalid.
+ */
+public static Record
+fromString(Name name, int type, int dclass, long ttl, Tokenizer st, Name origin)
+throws IOException
+{
+	Record rec;
+
+	if (!name.isAbsolute())
+		throw new RelativeNameException(name);
+	Type.check(type);
+	DClass.check(dclass);
+	TTL.check(ttl);
+
+	Tokenizer.Token t = st.get();
+	if (t.type == Tokenizer.IDENTIFIER && t.value.equals("\\#")) {
+		int length = st.getUInt16();
+		byte [] data = st.getHex();
+		if (data == null) {
+			data = new byte[0];
+		}
+		if (length != data.length)
+			throw st.exception("invalid unknown RR encoding: " +
+					   "length mismatch");
+		DNSInput in = new DNSInput(data);
+		return newRecord(name, type, dclass, ttl, length, in);
+	}
+	st.unget();
+	rec = getEmptyRecord(name, type, dclass, ttl, true);
+	rec.rdataFromString(st, origin);
+	t = st.get();
+	if (t.type != Tokenizer.EOL && t.type != Tokenizer.EOF) {
+		throw st.exception("unexpected tokens at end of record");
+	}
+	return rec;
+}
+
+/**
+ * Builds a new Record from its textual representation
+ * @param name The owner name of the record.
+ * @param type The record's type.
+ * @param dclass The record's class.
+ * @param ttl The record's time to live.
+ * @param s The textual representation of the rdata.
+ * @param origin The default origin to be appended to relative domain names.
+ * @return The new record
+ * @throws IOException The text format was invalid.
+ */
+public static Record
+fromString(Name name, int type, int dclass, long ttl, String s, Name origin)
+throws IOException
+{
+	return fromString(name, type, dclass, ttl, new Tokenizer(s), origin);
+}
+
+/**
+ * Returns the record's name
+ * @see Name
+ */
+public Name
+getName() {
+	return name;
+}
+
+/**
+ * Returns the record's type
+ * @see Type
+ */
+public int
+getType() {
+	return type;
+}
+
+/**
+ * Returns the type of RRset that this record would belong to.  For all types
+ * except RRSIG, this is equivalent to getType().
+ * @return The type of record, if not RRSIG.  If the type is RRSIG,
+ * the type covered is returned.
+ * @see Type
+ * @see RRset
+ * @see SIGRecord
+ */
+public int
+getRRsetType() {
+	if (type == Type.RRSIG) {
+		RRSIGRecord sig = (RRSIGRecord) this;
+		return sig.getTypeCovered();
+	}
+	return type;
+}
+
+/**
+ * Returns the record's class
+ */
+public int
+getDClass() {
+	return dclass;
+}
+
+/**
+ * Returns the record's TTL
+ */
+public long
+getTTL() {
+	return ttl;
+}
+
+/**
+ * Converts the type-specific RR to wire format - must be overriden
+ */
+abstract void
+rrToWire(DNSOutput out, Compression c, boolean canonical);
+
+/**
+ * Determines if two Records could be part of the same RRset.
+ * This compares the name, type, and class of the Records; the ttl and
+ * rdata are not compared.
+ */
+public boolean
+sameRRset(Record rec) {
+	return (getRRsetType() == rec.getRRsetType() &&
+		dclass == rec.dclass &&
+		name.equals(rec.name));
+}
+
+/**
+ * Determines if two Records are identical.  This compares the name, type,
+ * class, and rdata (with names canonicalized).  The TTLs are not compared.
+ * @param arg The record to compare to
+ * @return true if the records are equal, false otherwise.
+ */
+public boolean
+equals(Object arg) {
+	if (arg == null || !(arg instanceof Record))
+		return false;
+	Record r = (Record) arg;
+	if (type != r.type || dclass != r.dclass || !name.equals(r.name))
+		return false;
+	byte [] array1 = rdataToWireCanonical();
+	byte [] array2 = r.rdataToWireCanonical();
+	return Arrays.equals(array1, array2);
+}
+
+/**
+ * Generates a hash code based on the Record's data.
+ */
+public int
+hashCode() {
+	byte [] array = toWireCanonical(true);
+	int code = 0;
+	for (int i = 0; i < array.length; i++)
+		code += ((code << 3) + (array[i] & 0xFF));
+	return code;
+}
+
+Record
+cloneRecord() {
+	try {
+		return (Record) clone();
+	}
+	catch (CloneNotSupportedException e) {
+		throw new IllegalStateException();
+	}
+}
+
+/**
+ * Creates a new record identical to the current record, but with a different
+ * name.  This is most useful for replacing the name of a wildcard record.
+ */
+public Record
+withName(Name name) {
+	if (!name.isAbsolute())
+		throw new RelativeNameException(name);
+	Record rec = cloneRecord();
+	rec.name = name;
+	return rec;
+}
+
+/**
+ * Creates a new record identical to the current record, but with a different
+ * class and ttl.  This is most useful for dynamic update.
+ */
+Record
+withDClass(int dclass, long ttl) {
+	Record rec = cloneRecord();
+	rec.dclass = dclass;
+	rec.ttl = ttl;
+	return rec;
+}
+
+/* Sets the TTL to the specified value.  This is intentionally not public. */
+void
+setTTL(long ttl) {
+	this.ttl = ttl;
+}
+
+/**
+ * Compares this Record to another Object.
+ * @param o The Object to be compared.
+ * @return The value 0 if the argument is a record equivalent to this record;
+ * a value less than 0 if the argument is less than this record in the
+ * canonical ordering, and a value greater than 0 if the argument is greater
+ * than this record in the canonical ordering.  The canonical ordering
+ * is defined to compare by name, class, type, and rdata.
+ * @throws ClassCastException if the argument is not a Record.
+ */
+public int
+compareTo(Object o) {
+	Record arg = (Record) o;
+
+	if (this == arg)
+		return (0);
+
+	int n = name.compareTo(arg.name);
+	if (n != 0)
+		return (n);
+	n = dclass - arg.dclass;
+	if (n != 0)
+		return (n);
+	n = type - arg.type;
+	if (n != 0)
+		return (n);
+	byte [] rdata1 = rdataToWireCanonical();
+	byte [] rdata2 = arg.rdataToWireCanonical();
+	for (int i = 0; i < rdata1.length && i < rdata2.length; i++) {
+		n = (rdata1[i] & 0xFF) - (rdata2[i] & 0xFF);
+		if (n != 0)
+			return (n);
+	}
+	return (rdata1.length - rdata2.length);
+}
+
+/**
+ * Returns the name for which additional data processing should be done
+ * for this record.  This can be used both for building responses and
+ * parsing responses.
+ * @return The name to used for additional data processing, or null if this
+ * record type does not require additional data processing.
+ */
+public Name
+getAdditionalName() {
+	return null;
+}
+
+/* Checks that an int contains an unsigned 8 bit value */
+static int
+checkU8(String field, int val) {
+	if (val < 0 || val > 0xFF)
+		throw new IllegalArgumentException("\"" + field + "\" " + val + 
+						   " must be an unsigned 8 " +
+						   "bit value");
+	return val;
+}
+
+/* Checks that an int contains an unsigned 16 bit value */
+static int
+checkU16(String field, int val) {
+	if (val < 0 || val > 0xFFFF)
+		throw new IllegalArgumentException("\"" + field + "\" " + val + 
+						   " must be an unsigned 16 " +
+						   "bit value");
+	return val;
+}
+
+/* Checks that a long contains an unsigned 32 bit value */
+static long
+checkU32(String field, long val) {
+	if (val < 0 || val > 0xFFFFFFFFL)
+		throw new IllegalArgumentException("\"" + field + "\" " + val + 
+						   " must be an unsigned 32 " +
+						   "bit value");
+	return val;
+}
+
+/* Checks that a name is absolute */
+static Name
+checkName(String field, Name name) {
+	if (!name.isAbsolute())
+		throw new RelativeNameException(name);
+	return name;
+}
+
+static byte []
+checkByteArrayLength(String field, byte [] array, int maxLength) {
+	if (array.length > 0xFFFF)
+		throw new IllegalArgumentException("\"" + field + "\" array " +
+						   "must have no more than " +
+						   maxLength + " elements");
+	byte [] out = new byte[array.length];
+	System.arraycopy(array, 0, out, 0, array.length);
+	return out;
+}
+
+}
diff --git a/src/org/xbill/DNS/RelativeNameException.java b/src/org/xbill/DNS/RelativeNameException.java
new file mode 100644
index 0000000..869fd39
--- /dev/null
+++ b/src/org/xbill/DNS/RelativeNameException.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * An exception thrown when a relative name is passed as an argument to
+ * a method requiring an absolute name.
+ *
+ * @author Brian Wellington
+ */
+
+public class RelativeNameException extends IllegalArgumentException {
+
+public
+RelativeNameException(Name name) {
+	super("'" + name + "' is not an absolute name");
+}
+
+public
+RelativeNameException(String s) {
+	super(s);
+}
+
+}
diff --git a/src/org/xbill/DNS/ResolveThread.java b/src/org/xbill/DNS/ResolveThread.java
new file mode 100644
index 0000000..3087cdb
--- /dev/null
+++ b/src/org/xbill/DNS/ResolveThread.java
@@ -0,0 +1,45 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * A special-purpose thread used by Resolvers (both SimpleResolver and
+ * ExtendedResolver) to perform asynchronous queries.
+ *
+ * @author Brian Wellington
+ */
+
+class ResolveThread extends Thread {
+
+private Message query;
+private Object id;
+private ResolverListener listener;
+private Resolver res;
+
+/** Creates a new ResolveThread */
+public
+ResolveThread(Resolver res, Message query, Object id,
+	      ResolverListener listener)
+{
+	this.res = res;
+	this.query = query;
+	this.id = id;
+	this.listener = listener;
+}
+
+
+/**
+ * Performs the query, and executes the callback.
+ */
+public void
+run() {
+	try {
+		Message response = res.send(query);
+		listener.receiveMessage(id, response);
+	}
+	catch (Exception e) {
+		listener.handleException(id, e);
+	}
+}
+
+}
diff --git a/src/org/xbill/DNS/Resolver.java b/src/org/xbill/DNS/Resolver.java
new file mode 100644
index 0000000..7d28d40
--- /dev/null
+++ b/src/org/xbill/DNS/Resolver.java
@@ -0,0 +1,95 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Interface describing a resolver.
+ *
+ * @author Brian Wellington
+ */
+
+public interface Resolver {
+
+/**
+ * Sets the port to communicate with on the server
+ * @param port The port to send messages to
+ */
+void setPort(int port);
+
+/**
+ * Sets whether TCP connections will be sent by default
+ * @param flag Indicates whether TCP connections are made
+ */
+void setTCP(boolean flag);
+
+/**
+ * Sets whether truncated responses will be ignored.  If not, a truncated
+ * response over UDP will cause a retransmission over TCP.
+ * @param flag Indicates whether truncated responses should be ignored.
+ */
+void setIgnoreTruncation(boolean flag);
+
+/**
+ * Sets the EDNS version used on outgoing messages.
+ * @param level The EDNS level to use.  0 indicates EDNS0 and -1 indicates no
+ * EDNS.
+ * @throws IllegalArgumentException An invalid level was indicated.
+ */
+void setEDNS(int level);
+
+/**
+ * Sets the EDNS information on outgoing messages.
+ * @param level The EDNS level to use.  0 indicates EDNS0 and -1 indicates no
+ * EDNS.
+ * @param payloadSize The maximum DNS packet size that this host is capable
+ * of receiving over UDP.  If 0 is specified, the default (1280) is used.
+ * @param flags EDNS extended flags to be set in the OPT record.
+ * @param options EDNS options to be set in the OPT record, specified as a
+ * List of OPTRecord.Option elements.
+ * @throws IllegalArgumentException An invalid field was specified.
+ * @see OPTRecord
+ */
+void setEDNS(int level, int payloadSize, int flags, List options);
+
+/**
+ * Specifies the TSIG key that messages will be signed with
+ * @param key The key
+ */
+void setTSIGKey(TSIG key);
+
+/**
+ * Sets the amount of time to wait for a response before giving up.
+ * @param secs The number of seconds to wait.
+ * @param msecs The number of milliseconds to wait.
+ */
+void setTimeout(int secs, int msecs);
+
+/**
+ * Sets the amount of time to wait for a response before giving up.
+ * @param secs The number of seconds to wait.
+ */
+void setTimeout(int secs);
+
+/**
+ * Sends a message and waits for a response.
+ * @param query The query to send.
+ * @return The response
+ * @throws IOException An error occurred while sending or receiving.
+ */
+Message send(Message query) throws IOException;
+
+/**
+ * Asynchronously sends a message registering a listener to receive a callback
+ * on success or exception.  Multiple asynchronous lookups can be performed
+ * in parallel.  Since the callback may be invoked before the function returns,
+ * external synchronization is necessary.
+ * @param query The query to send
+ * @param listener The object containing the callbacks.
+ * @return An identifier, which is also a parameter in the callback
+ */
+Object sendAsync(final Message query, final ResolverListener listener);
+
+}
diff --git a/src/org/xbill/DNS/ResolverConfig.java b/src/org/xbill/DNS/ResolverConfig.java
new file mode 100644
index 0000000..7b09daf
--- /dev/null
+++ b/src/org/xbill/DNS/ResolverConfig.java
@@ -0,0 +1,509 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.lang.reflect.*;
+import java.util.*;
+
+/**
+ * A class that tries to locate name servers and the search path to
+ * be appended to unqualified names.
+ *
+ * The following are attempted, in order, until one succeeds.
+ * <UL>
+ *   <LI>The properties 'dns.server' and 'dns.search' (comma delimited lists)
+ *       are checked.  The servers can either be IP addresses or hostnames
+ *       (which are resolved using Java's built in DNS support).
+ *   <LI>The sun.net.dns.ResolverConfiguration class is queried.
+ *   <LI>On Unix, /etc/resolv.conf is parsed.
+ *   <LI>On Windows, ipconfig/winipcfg is called and its output parsed.  This
+ *       may fail for non-English versions on Windows.
+ *   <LI>"localhost" is used as the nameserver, and the search path is empty.
+ * </UL>
+ *
+ * These routines will be called internally when creating Resolvers/Lookups
+ * without explicitly specifying server names, and can also be called
+ * directly if desired.
+ *
+ * @author Brian Wellington
+ * @author <a href="mailto:yannick@meudal.net">Yannick Meudal</a>
+ * @author <a href="mailto:arnt@gulbrandsen.priv.no">Arnt Gulbrandsen</a>
+ */
+
+public class ResolverConfig {
+
+private String [] servers = null;
+private Name [] searchlist = null;
+private int ndots = -1;
+
+private static ResolverConfig currentConfig;
+
+static {
+	refresh();
+}
+
+public
+ResolverConfig() {
+	if (findProperty())
+		return;
+	if (findSunJVM())
+		return;
+	if (servers == null || searchlist == null) {
+		String OS = System.getProperty("os.name");
+		String vendor = System.getProperty("java.vendor");
+		if (OS.indexOf("Windows") != -1) {
+			if (OS.indexOf("95") != -1 ||
+			    OS.indexOf("98") != -1 ||
+			    OS.indexOf("ME") != -1)
+				find95();
+			else
+				findNT();
+		} else if (OS.indexOf("NetWare") != -1) {
+			findNetware();
+		} else if (vendor.indexOf("Android") != -1) {
+			findAndroid();
+		} else {
+			findUnix();
+		}
+	}
+}
+
+private void
+addServer(String server, List list) {
+	if (list.contains(server))
+		return;
+	if (Options.check("verbose"))
+		System.out.println("adding server " + server);
+	list.add(server);
+}
+
+private void
+addSearch(String search, List list) {
+	Name name;
+	if (Options.check("verbose"))
+		System.out.println("adding search " + search);
+	try {
+		name = Name.fromString(search, Name.root);
+	}
+	catch (TextParseException e) {
+		return;
+	}
+	if (list.contains(name))
+		return;
+	list.add(name);
+}
+
+private int
+parseNdots(String token) {
+	token = token.substring(6);
+	try {
+		int ndots = Integer.parseInt(token);
+		if (ndots >= 0) {
+			if (Options.check("verbose"))
+				System.out.println("setting ndots " + token);
+			return ndots;
+		}
+	}
+	catch (NumberFormatException e) {
+	}
+	return -1;
+}
+
+private void
+configureFromLists(List lserver, List lsearch) {
+	if (servers == null && lserver.size() > 0)
+		servers = (String []) lserver.toArray(new String[0]);
+	if (searchlist == null && lsearch.size() > 0)
+		searchlist = (Name []) lsearch.toArray(new Name[0]);
+}
+
+private void
+configureNdots(int lndots) {
+	if (ndots < 0 && lndots > 0)
+		ndots = lndots;
+}
+
+/**
+ * Looks in the system properties to find servers and a search path.
+ * Servers are defined by dns.server=server1,server2...
+ * The search path is defined by dns.search=domain1,domain2...
+ */
+private boolean
+findProperty() {
+	String prop;
+	List lserver = new ArrayList(0);
+	List lsearch = new ArrayList(0);
+	StringTokenizer st;
+
+	prop = System.getProperty("dns.server");
+	if (prop != null) {
+		st = new StringTokenizer(prop, ",");
+		while (st.hasMoreTokens())
+			addServer(st.nextToken(), lserver);
+	}
+
+	prop = System.getProperty("dns.search");
+	if (prop != null) {
+		st = new StringTokenizer(prop, ",");
+		while (st.hasMoreTokens())
+			addSearch(st.nextToken(), lsearch);
+	}
+	configureFromLists(lserver, lsearch);
+	return (servers != null && searchlist != null);
+}
+
+/**
+ * Uses the undocumented Sun DNS implementation to determine the configuration.
+ * This doesn't work or even compile with all JVMs (gcj, for example).
+ */
+private boolean
+findSunJVM() {
+	List lserver = new ArrayList(0);
+	List lserver_tmp;
+	List lsearch = new ArrayList(0);
+	List lsearch_tmp;
+
+	try {
+		Class [] noClasses = new Class[0];
+		Object [] noObjects = new Object[0];
+		String resConfName = "sun.net.dns.ResolverConfiguration";
+		Class resConfClass = Class.forName(resConfName);
+		Object resConf;
+
+		// ResolverConfiguration resConf = ResolverConfiguration.open();
+		Method open = resConfClass.getDeclaredMethod("open", noClasses);
+		resConf = open.invoke(null, noObjects);
+
+		// lserver_tmp = resConf.nameservers();
+		Method nameservers = resConfClass.getMethod("nameservers",
+							    noClasses);
+		lserver_tmp = (List) nameservers.invoke(resConf, noObjects);
+
+		// lsearch_tmp = resConf.searchlist();
+		Method searchlist = resConfClass.getMethod("searchlist",
+							    noClasses);
+		lsearch_tmp = (List) searchlist.invoke(resConf, noObjects);
+	}
+	catch (Exception e) {
+		return false;
+	}
+
+	if (lserver_tmp.size() == 0)
+		return false;
+
+	if (lserver_tmp.size() > 0) {
+		Iterator it = lserver_tmp.iterator();
+		while (it.hasNext())
+			addServer((String) it.next(), lserver);
+	}
+
+	if (lsearch_tmp.size() > 0) {
+		Iterator it = lsearch_tmp.iterator();
+		while (it.hasNext())
+			addSearch((String) it.next(), lsearch);
+	}
+	configureFromLists(lserver, lsearch);
+	return true;
+}
+
+/**
+ * Looks in /etc/resolv.conf to find servers and a search path.
+ * "nameserver" lines specify servers.  "domain" and "search" lines
+ * define the search path.
+ */
+private void
+findResolvConf(String file) {
+	InputStream in = null;
+	try {
+		in = new FileInputStream(file);
+	}
+	catch (FileNotFoundException e) {
+		return;
+	}
+	InputStreamReader isr = new InputStreamReader(in);
+	BufferedReader br = new BufferedReader(isr);
+	List lserver = new ArrayList(0);
+	List lsearch = new ArrayList(0);
+	int lndots = -1;
+	try {
+		String line;
+		while ((line = br.readLine()) != null) {
+			if (line.startsWith("nameserver")) {
+				StringTokenizer st = new StringTokenizer(line);
+				st.nextToken(); /* skip nameserver */
+				addServer(st.nextToken(), lserver);
+			}
+			else if (line.startsWith("domain")) {
+				StringTokenizer st = new StringTokenizer(line);
+				st.nextToken(); /* skip domain */
+				if (!st.hasMoreTokens())
+					continue;
+				if (lsearch.isEmpty())
+					addSearch(st.nextToken(), lsearch);
+			}
+			else if (line.startsWith("search")) {
+				if (!lsearch.isEmpty())
+					lsearch.clear();
+				StringTokenizer st = new StringTokenizer(line);
+				st.nextToken(); /* skip search */
+				while (st.hasMoreTokens())
+					addSearch(st.nextToken(), lsearch);
+			}
+			else if(line.startsWith("options")) {
+				StringTokenizer st = new StringTokenizer(line);
+				st.nextToken(); /* skip options */
+				while (st.hasMoreTokens()) {
+					String token = st.nextToken();
+					if (token.startsWith("ndots:")) {
+						lndots = parseNdots(token);
+					}
+				}
+			}
+		}
+		br.close();
+	}
+	catch (IOException e) {
+	}
+
+	configureFromLists(lserver, lsearch);
+	configureNdots(lndots);
+}
+
+private void
+findUnix() {
+	findResolvConf("/etc/resolv.conf");
+}
+
+private void
+findNetware() {
+	findResolvConf("sys:/etc/resolv.cfg");
+}
+
+/**
+ * Parses the output of winipcfg or ipconfig.
+ */
+private void
+findWin(InputStream in, Locale locale) {
+	String packageName = ResolverConfig.class.getPackage().getName();
+	String resPackageName = packageName + ".windows.DNSServer";
+	ResourceBundle res;
+	if (locale != null)
+		res = ResourceBundle.getBundle(resPackageName, locale);
+	else
+		res = ResourceBundle.getBundle(resPackageName);
+
+	String host_name = res.getString("host_name");
+	String primary_dns_suffix = res.getString("primary_dns_suffix");
+	String dns_suffix = res.getString("dns_suffix");
+	String dns_servers = res.getString("dns_servers");
+
+	BufferedReader br = new BufferedReader(new InputStreamReader(in));
+	try {
+		List lserver = new ArrayList();
+		List lsearch = new ArrayList();
+		String line = null;
+		boolean readingServers = false;
+		boolean readingSearches = false;
+		while ((line = br.readLine()) != null) {
+			StringTokenizer st = new StringTokenizer(line);
+			if (!st.hasMoreTokens()) {
+				readingServers = false;
+				readingSearches = false;
+				continue;
+			}
+			String s = st.nextToken();
+			if (line.indexOf(":") != -1) {
+				readingServers = false;
+				readingSearches = false;
+			}
+			
+			if (line.indexOf(host_name) != -1) {
+				while (st.hasMoreTokens())
+					s = st.nextToken();
+				Name name;
+				try {
+					name = Name.fromString(s, null);
+				}
+				catch (TextParseException e) {
+					continue;
+				}
+				if (name.labels() == 1)
+					continue;
+				addSearch(s, lsearch);
+			} else if (line.indexOf(primary_dns_suffix) != -1) {
+				while (st.hasMoreTokens())
+					s = st.nextToken();
+				if (s.equals(":"))
+					continue;
+				addSearch(s, lsearch);
+				readingSearches = true;
+			} else if (readingSearches ||
+				   line.indexOf(dns_suffix) != -1)
+			{
+				while (st.hasMoreTokens())
+					s = st.nextToken();
+				if (s.equals(":"))
+					continue;
+				addSearch(s, lsearch);
+				readingSearches = true;
+			} else if (readingServers ||
+				   line.indexOf(dns_servers) != -1)
+			{
+				while (st.hasMoreTokens())
+					s = st.nextToken();
+				if (s.equals(":"))
+					continue;
+				addServer(s, lserver);
+				readingServers = true;
+			}
+		}
+		
+		configureFromLists(lserver, lsearch);
+	}
+	catch (IOException e) {
+	}
+	return;
+}
+
+private void
+findWin(InputStream in) {
+	String property = "org.xbill.DNS.windows.parse.buffer";
+	final int defaultBufSize = 8 * 1024;
+	int bufSize = Integer.getInteger(property, defaultBufSize).intValue();
+	BufferedInputStream b = new BufferedInputStream(in, bufSize);
+	b.mark(bufSize);
+	findWin(b, null);
+	if (servers == null) {
+		try {
+			b.reset();
+		} 
+		catch (IOException e) {
+			return;
+		}
+		findWin(b, new Locale("", ""));
+	}
+}
+
+/**
+ * Calls winipcfg and parses the result to find servers and a search path.
+ */
+private void
+find95() {
+	String s = "winipcfg.out";
+	try {
+		Process p;
+		p = Runtime.getRuntime().exec("winipcfg /all /batch " + s);
+		p.waitFor();
+		File f = new File(s);
+		findWin(new FileInputStream(f));
+		new File(s).delete();
+	}
+	catch (Exception e) {
+		return;
+	}
+}
+
+/**
+ * Calls ipconfig and parses the result to find servers and a search path.
+ */
+private void
+findNT() {
+	try {
+		Process p;
+		p = Runtime.getRuntime().exec("ipconfig /all");
+		findWin(p.getInputStream());
+		p.destroy();
+	}
+	catch (Exception e) {
+		return;
+	}
+}
+
+/**
+ * Parses the output of getprop, which is the only way to get DNS
+ * info on Android. getprop might disappear in future releases, so
+ * this code comes with a use-by date.
+ */
+private void
+findAndroid() {
+	// This originally looked for all lines containing .dns; but
+	// http://code.google.com/p/android/issues/detail?id=2207#c73
+	// indicates that net.dns* should always be the active nameservers, so
+	// we use those.
+	String re1 = "^\\d+(\\.\\d+){3}$";
+	String re2 = "^[0-9a-f]+(:[0-9a-f]*)+:[0-9a-f]+$";
+	try { 
+		ArrayList lserver = new ArrayList(); 
+		ArrayList lsearch = new ArrayList(); 
+		String line; 
+		Process p = Runtime.getRuntime().exec("getprop"); 
+		InputStream in = p.getInputStream();
+		InputStreamReader isr = new InputStreamReader(in);
+		BufferedReader br = new BufferedReader(isr);
+		while ((line = br.readLine()) != null ) { 
+			StringTokenizer t = new StringTokenizer(line, ":");
+			String name = t.nextToken();
+			if (name.indexOf( "net.dns" ) > -1) {
+				String v = t.nextToken();
+				v = v.replaceAll("[ \\[\\]]", "");
+				if ((v.matches(re1) || v.matches(re2)) &&
+				    !lserver.contains(v))
+					lserver.add(v);
+			}
+		}
+		configureFromLists(lserver, lsearch);
+	} catch ( Exception e ) { 
+		// ignore resolutely
+	}
+}
+
+/** Returns all located servers */
+public String []
+servers() {
+	return servers;
+}
+
+/** Returns the first located server */
+public String
+server() {
+	if (servers == null)
+		return null;
+	return servers[0];
+}
+
+/** Returns all entries in the located search path */
+public Name []
+searchPath() {
+	return searchlist;
+}
+
+/**
+ * Returns the located ndots value, or the default (1) if not configured.
+ * Note that ndots can only be configured in a resolv.conf file, and will only
+ * take effect if ResolverConfig uses resolv.conf directly (that is, if the
+ * JVM does not include the sun.net.dns.ResolverConfiguration class).
+ */
+public int
+ndots() {
+	if (ndots < 0)
+		return 1;
+	return ndots;
+}
+
+/** Gets the current configuration */
+public static synchronized ResolverConfig
+getCurrentConfig() {
+	return currentConfig;
+}
+
+/** Gets the current configuration */
+public static void
+refresh() {
+	ResolverConfig newConfig = new ResolverConfig();
+	synchronized (ResolverConfig.class) {
+		currentConfig = newConfig;
+	}
+}
+
+}
diff --git a/src/org/xbill/DNS/ResolverListener.java b/src/org/xbill/DNS/ResolverListener.java
new file mode 100644
index 0000000..accf82c
--- /dev/null
+++ b/src/org/xbill/DNS/ResolverListener.java
@@ -0,0 +1,30 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.EventListener;
+
+/**
+ * An interface to the asynchronous resolver.
+ * @see Resolver
+ *
+ * @author Brian Wellington
+ */
+
+public interface ResolverListener extends EventListener {
+
+/**
+ * The callback used by an asynchronous resolver
+ * @param id The identifier returned by Resolver.sendAsync()
+ * @param m The response message as returned by the Resolver
+ */
+void receiveMessage(Object id, Message m);
+
+/**
+ * The callback used by an asynchronous resolver when an exception is thrown
+ * @param id The identifier returned by Resolver.sendAsync()
+ * @param e The thrown exception
+ */
+void handleException(Object id, Exception e);
+
+}
diff --git a/src/org/xbill/DNS/ReverseMap.java b/src/org/xbill/DNS/ReverseMap.java
new file mode 100644
index 0000000..f4293de
--- /dev/null
+++ b/src/org/xbill/DNS/ReverseMap.java
@@ -0,0 +1,130 @@
+// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.net.*;
+
+/**
+ * A set functions designed to deal with DNS names used in reverse mappings.
+ * For the IPv4 address a.b.c.d, the reverse map name is d.c.b.a.in-addr.arpa.
+ * For an IPv6 address, the reverse map name is ...ip6.arpa.
+ *
+ * @author Brian Wellington
+ */
+
+public final class ReverseMap {
+
+private static Name inaddr4 = Name.fromConstantString("in-addr.arpa.");
+private static Name inaddr6 = Name.fromConstantString("ip6.arpa.");
+
+/* Otherwise the class could be instantiated */
+private
+ReverseMap() {}
+
+/**
+ * Creates a reverse map name corresponding to an address contained in
+ * an array of 4 bytes (for an IPv4 address) or 16 bytes (for an IPv6 address).
+ * @param addr The address from which to build a name.
+ * @return The name corresponding to the address in the reverse map.
+ */
+public static Name
+fromAddress(byte [] addr) {
+	if (addr.length != 4 && addr.length != 16)
+		throw new IllegalArgumentException("array must contain " +
+						   "4 or 16 elements");
+
+	StringBuffer sb = new StringBuffer();
+	if (addr.length == 4) {
+		for (int i = addr.length - 1; i >= 0; i--) {
+			sb.append(addr[i] & 0xFF);
+			if (i > 0)
+				sb.append(".");
+		}
+	} else {
+		int [] nibbles = new int[2];
+		for (int i = addr.length - 1; i >= 0; i--) {
+			nibbles[0] = (addr[i] & 0xFF) >> 4;
+			nibbles[1] = (addr[i] & 0xFF) & 0xF;
+			for (int j = nibbles.length - 1; j >= 0; j--) {
+				sb.append(Integer.toHexString(nibbles[j]));
+				if (i > 0 || j > 0)
+					sb.append(".");
+			}
+		}
+	}
+
+	try {
+		if (addr.length == 4)
+			return Name.fromString(sb.toString(), inaddr4);
+		else
+			return Name.fromString(sb.toString(), inaddr6);
+	}
+	catch (TextParseException e) {
+		throw new IllegalStateException("name cannot be invalid");
+	}
+}
+
+/**
+ * Creates a reverse map name corresponding to an address contained in
+ * an array of 4 integers between 0 and 255 (for an IPv4 address) or 16
+ * integers between 0 and 255 (for an IPv6 address).
+ * @param addr The address from which to build a name.
+ * @return The name corresponding to the address in the reverse map.
+ */
+public static Name
+fromAddress(int [] addr) {
+	byte [] bytes = new byte[addr.length];
+	for (int i = 0; i < addr.length; i++) {
+		if (addr[i] < 0 || addr[i] > 0xFF)
+			throw new IllegalArgumentException("array must " +
+							   "contain values " +
+							   "between 0 and 255");
+		bytes[i] = (byte) addr[i];
+	}
+	return fromAddress(bytes);
+}
+
+/**
+ * Creates a reverse map name corresponding to an address contained in
+ * an InetAddress.
+ * @param addr The address from which to build a name.
+ * @return The name corresponding to the address in the reverse map.
+ */
+public static Name
+fromAddress(InetAddress addr) {
+	return fromAddress(addr.getAddress());
+}
+
+/**
+ * Creates a reverse map name corresponding to an address contained in
+ * a String.
+ * @param addr The address from which to build a name.
+ * @return The name corresponding to the address in the reverse map.
+ * @throws UnknownHostException The string does not contain a valid address.
+ */
+public static Name
+fromAddress(String addr, int family) throws UnknownHostException {
+	byte [] array = Address.toByteArray(addr, family);
+	if (array == null)
+		throw new UnknownHostException("Invalid IP address");
+	return fromAddress(array);
+}
+
+/**
+ * Creates a reverse map name corresponding to an address contained in
+ * a String.
+ * @param addr The address from which to build a name.
+ * @return The name corresponding to the address in the reverse map.
+ * @throws UnknownHostException The string does not contain a valid address.
+ */
+public static Name
+fromAddress(String addr) throws UnknownHostException {
+	byte [] array = Address.toByteArray(addr, Address.IPv4);
+	if (array == null)
+		array = Address.toByteArray(addr, Address.IPv6);
+	if (array == null)
+		throw new UnknownHostException("Invalid IP address");
+	return fromAddress(array);
+}
+
+}
diff --git a/src/org/xbill/DNS/SIG0.java b/src/org/xbill/DNS/SIG0.java
new file mode 100644
index 0000000..5a00e72
--- /dev/null
+++ b/src/org/xbill/DNS/SIG0.java
@@ -0,0 +1,79 @@
+// Copyright (c) 2001-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.security.PrivateKey;
+import java.util.Date;
+
+/**
+ * Creates SIG(0) transaction signatures.
+ *
+ * @author Pasi Eronen
+ * @author Brian Wellington
+ */
+
+public class SIG0 {
+
+/**
+ * The default validity period for outgoing SIG(0) signed messages.
+ * Can be overriden by the sig0validity option.
+ */
+private static final short VALIDITY = 300;
+    
+private
+SIG0() { }
+
+/**
+ * Sign a message with SIG(0). The DNS key and private key must refer to the
+ * same underlying cryptographic key.
+ * @param message The message to be signed
+ * @param key The DNSKEY record to use as part of signing
+ * @param privkey The PrivateKey to use when signing
+ * @param previous If this message is a response, the SIG(0) from the query
+ */
+public static void
+signMessage(Message message, KEYRecord key, PrivateKey privkey,
+	    SIGRecord previous) throws DNSSEC.DNSSECException
+{
+	
+	int validity = Options.intValue("sig0validity");
+	if (validity < 0)
+		validity = VALIDITY;
+
+	long now = System.currentTimeMillis();
+	Date timeSigned = new Date(now);
+	Date timeExpires = new Date(now + validity * 1000);
+
+	SIGRecord sig =  DNSSEC.signMessage(message, previous, key, privkey,
+					    timeSigned, timeExpires);
+	
+	message.addRecord(sig, Section.ADDITIONAL);
+}
+
+/**
+ * Verify a message using SIG(0).
+ * @param message The message to be signed
+ * @param b An array containing the message in unparsed form.  This is
+ * necessary since SIG(0) signs the message in wire format, and we can't
+ * recreate the exact wire format (with the same name compression).
+ * @param key The KEY record to verify the signature with.
+ * @param previous If this message is a response, the SIG(0) from the query
+ */
+public static void
+verifyMessage(Message message, byte [] b, KEYRecord key, SIGRecord previous)
+	throws DNSSEC.DNSSECException
+{
+	SIGRecord sig = null;
+	Record [] additional = message.getSectionArray(Section.ADDITIONAL);
+	for (int i = 0; i < additional.length; i++) {
+		if (additional[i].getType() != Type.SIG)
+			continue;
+		if (((SIGRecord) additional[i]).getTypeCovered() != 0)
+			continue;
+		sig = (SIGRecord) additional[i];
+		break;
+	}
+	DNSSEC.verifyMessage(message, b, sig, previous, key);
+}
+
+}
diff --git a/src/org/xbill/DNS/SIGBase.java b/src/org/xbill/DNS/SIGBase.java
new file mode 100644
index 0000000..6e5f12d
--- /dev/null
+++ b/src/org/xbill/DNS/SIGBase.java
@@ -0,0 +1,193 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * The base class for SIG/RRSIG records, which have identical formats 
+ *
+ * @author Brian Wellington
+ */
+
+abstract class SIGBase extends Record {
+
+private static final long serialVersionUID = -3738444391533812369L;
+
+protected int covered;
+protected int alg, labels;
+protected long origttl;
+protected Date expire, timeSigned;
+protected int footprint;
+protected Name signer;
+protected byte [] signature;
+
+protected
+SIGBase() {}
+
+public
+SIGBase(Name name, int type, int dclass, long ttl, int covered, int alg,
+	long origttl, Date expire, Date timeSigned, int footprint, Name signer,
+	byte [] signature)
+{
+	super(name, type, dclass, ttl);
+	Type.check(covered);
+	TTL.check(origttl);
+	this.covered = covered;
+	this.alg = checkU8("alg", alg);
+	this.labels = name.labels() - 1;
+	if (name.isWild())
+		this.labels--;
+	this.origttl = origttl;
+	this.expire = expire;
+	this.timeSigned = timeSigned;
+	this.footprint = checkU16("footprint", footprint);
+	this.signer = checkName("signer", signer);
+	this.signature = signature;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	covered = in.readU16();
+	alg = in.readU8();
+	labels = in.readU8();
+	origttl = in.readU32();
+	expire = new Date(1000 * in.readU32());
+	timeSigned = new Date(1000 * in.readU32());
+	footprint = in.readU16();
+	signer = new Name(in);
+	signature = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	String typeString = st.getString();
+	covered = Type.value(typeString);
+	if (covered < 0)
+		throw st.exception("Invalid type: " + typeString);
+	String algString = st.getString();
+	alg = DNSSEC.Algorithm.value(algString);
+	if (alg < 0)
+		throw st.exception("Invalid algorithm: " + algString);
+	labels = st.getUInt8();
+	origttl = st.getTTL();
+	expire = FormattedTime.parse(st.getString());
+	timeSigned = FormattedTime.parse(st.getString());
+	footprint = st.getUInt16();
+	signer = st.getName(origin);
+	signature = st.getBase64();
+}
+
+/** Converts the RRSIG/SIG Record to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append (Type.string(covered));
+	sb.append (" ");
+	sb.append (alg);
+	sb.append (" ");
+	sb.append (labels);
+	sb.append (" ");
+	sb.append (origttl);
+	sb.append (" ");
+	if (Options.check("multiline"))
+		sb.append ("(\n\t");
+	sb.append (FormattedTime.format(expire));
+	sb.append (" ");
+	sb.append (FormattedTime.format(timeSigned));
+	sb.append (" ");
+	sb.append (footprint);
+	sb.append (" ");
+	sb.append (signer);
+	if (Options.check("multiline")) {
+		sb.append("\n");
+		sb.append(base64.formatString(signature, 64, "\t",
+					      true));
+	} else {
+		sb.append (" ");
+		sb.append(base64.toString(signature));
+	}
+	return sb.toString();
+}
+
+/** Returns the RRset type covered by this signature */
+public int
+getTypeCovered() {
+	return covered;
+}
+
+/**
+ * Returns the cryptographic algorithm of the key that generated the signature
+ */
+public int
+getAlgorithm() {
+	return alg;
+}
+
+/**
+ * Returns the number of labels in the signed domain name.  This may be
+ * different than the record's domain name if the record is a wildcard
+ * record.
+ */
+public int
+getLabels() {
+	return labels;
+}
+
+/** Returns the original TTL of the RRset */
+public long
+getOrigTTL() {
+	return origttl;
+}
+
+/** Returns the time at which the signature expires */
+public Date
+getExpire() {
+	return expire;
+}
+
+/** Returns the time at which this signature was generated */
+public Date
+getTimeSigned() {
+	return timeSigned;
+}
+
+/** Returns The footprint/key id of the signing key.  */
+public int
+getFootprint() {
+	return footprint;
+}
+
+/** Returns the owner of the signing key */
+public Name
+getSigner() {
+	return signer;
+}
+
+/** Returns the binary data representing the signature */
+public byte []
+getSignature() {
+	return signature;
+}
+
+void
+setSignature(byte [] signature) {
+	this.signature = signature;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU16(covered);
+	out.writeU8(alg);
+	out.writeU8(labels);
+	out.writeU32(origttl);
+	out.writeU32(expire.getTime() / 1000);
+	out.writeU32(timeSigned.getTime() / 1000);
+	out.writeU16(footprint);
+	signer.toWire(out, null, canonical);
+	out.writeByteArray(signature);
+}
+
+}
diff --git a/src/org/xbill/DNS/SIGRecord.java b/src/org/xbill/DNS/SIGRecord.java
new file mode 100644
index 0000000..8b6f58d
--- /dev/null
+++ b/src/org/xbill/DNS/SIGRecord.java
@@ -0,0 +1,50 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+
+/**
+ * Signature - A SIG provides the digital signature of an RRset, so that
+ * the data can be authenticated by a DNSSEC-capable resolver.  The
+ * signature is usually generated by a key contained in a KEYRecord
+ * @see RRset
+ * @see DNSSEC
+ * @see KEYRecord
+ *
+ * @author Brian Wellington
+ */
+
+public class SIGRecord extends SIGBase {
+
+private static final long serialVersionUID = 4963556060953589058L;
+
+SIGRecord() {}
+
+Record
+getObject() {
+	return new SIGRecord();
+}
+
+/**
+ * Creates an SIG Record from the given data
+ * @param covered The RRset type covered by this signature
+ * @param alg The cryptographic algorithm of the key that generated the
+ * signature
+ * @param origttl The original TTL of the RRset
+ * @param expire The time at which the signature expires
+ * @param timeSigned The time at which this signature was generated
+ * @param footprint The footprint/key id of the signing key.
+ * @param signer The owner of the signing key
+ * @param signature Binary data representing the signature
+ */
+public
+SIGRecord(Name name, int dclass, long ttl, int covered, int alg, long origttl,
+	  Date expire, Date timeSigned, int footprint, Name signer,
+	  byte [] signature)
+{
+	super(name, Type.SIG, dclass, ttl, covered, alg, origttl, expire,
+	      timeSigned, footprint, signer, signature);
+}
+
+}
diff --git a/src/org/xbill/DNS/SOARecord.java b/src/org/xbill/DNS/SOARecord.java
new file mode 100644
index 0000000..7f27077
--- /dev/null
+++ b/src/org/xbill/DNS/SOARecord.java
@@ -0,0 +1,162 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * Start of Authority - describes properties of a zone.
+ *
+ * @author Brian Wellington
+ */
+
+public class SOARecord extends Record {
+
+private static final long serialVersionUID = 1049740098229303931L;
+
+private Name host, admin;
+private long serial, refresh, retry, expire, minimum;
+
+SOARecord() {}
+
+Record
+getObject() {
+	return new SOARecord();
+}
+
+/**
+ * Creates an SOA Record from the given data
+ * @param host The primary name server for the zone
+ * @param admin The zone administrator's address
+ * @param serial The zone's serial number
+ * @param refresh The amount of time until a secondary checks for a new serial
+ * number
+ * @param retry The amount of time between a secondary's checks for a new
+ * serial number
+ * @param expire The amount of time until a secondary expires a zone
+ * @param minimum The minimum TTL for records in the zone
+*/
+public
+SOARecord(Name name, int dclass, long ttl, Name host, Name admin,
+	  long serial, long refresh, long retry, long expire, long minimum)
+{
+	super(name, Type.SOA, dclass, ttl);
+	this.host = checkName("host", host);
+	this.admin = checkName("admin", admin);
+	this.serial = checkU32("serial", serial);
+	this.refresh = checkU32("refresh", refresh);
+	this.retry = checkU32("retry", retry);
+	this.expire = checkU32("expire", expire);
+	this.minimum = checkU32("minimum", minimum);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	host = new Name(in);
+	admin = new Name(in);
+	serial = in.readU32();
+	refresh = in.readU32();
+	retry = in.readU32();
+	expire = in.readU32();
+	minimum = in.readU32();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	host = st.getName(origin);
+	admin = st.getName(origin);
+	serial = st.getUInt32();
+	refresh = st.getTTLLike();
+	retry = st.getTTLLike();
+	expire = st.getTTLLike();
+	minimum = st.getTTLLike();
+}
+
+/** Convert to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(host);
+	sb.append(" ");
+	sb.append(admin);
+	if (Options.check("multiline")) {
+		sb.append(" (\n\t\t\t\t\t");
+		sb.append(serial);
+		sb.append("\t; serial\n\t\t\t\t\t");
+		sb.append(refresh);
+		sb.append("\t; refresh\n\t\t\t\t\t");
+		sb.append(retry);
+		sb.append("\t; retry\n\t\t\t\t\t");
+		sb.append(expire);
+		sb.append("\t; expire\n\t\t\t\t\t");
+		sb.append(minimum);
+		sb.append(" )\t; minimum");
+	} else {
+		sb.append(" ");
+		sb.append(serial);
+		sb.append(" ");
+		sb.append(refresh);
+		sb.append(" ");
+		sb.append(retry);
+		sb.append(" ");
+		sb.append(expire);
+		sb.append(" ");
+		sb.append(minimum);
+	}
+	return sb.toString();
+}
+
+/** Returns the primary name server */
+public Name
+getHost() {  
+	return host;
+}       
+
+/** Returns the zone administrator's address */
+public Name
+getAdmin() {  
+	return admin;
+}       
+
+/** Returns the zone's serial number */
+public long
+getSerial() {  
+	return serial;
+}       
+
+/** Returns the zone refresh interval */
+public long
+getRefresh() {  
+	return refresh;
+}       
+
+/** Returns the zone retry interval */
+public long
+getRetry() {  
+	return retry;
+}       
+
+/** Returns the time until a secondary expires a zone */
+public long
+getExpire() {  
+	return expire;
+}       
+
+/** Returns the minimum TTL for records in the zone */
+public long
+getMinimum() {  
+	return minimum;
+}       
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	host.toWire(out, c, canonical);
+	admin.toWire(out, c, canonical);
+	out.writeU32(serial);
+	out.writeU32(refresh);
+	out.writeU32(retry);
+	out.writeU32(expire);
+	out.writeU32(minimum);
+}
+
+}
diff --git a/src/org/xbill/DNS/SPFRecord.java b/src/org/xbill/DNS/SPFRecord.java
new file mode 100644
index 0000000..a286220
--- /dev/null
+++ b/src/org/xbill/DNS/SPFRecord.java
@@ -0,0 +1,44 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+
+/**
+ * Sender Policy Framework (RFC 4408, experimental)
+ *
+ * @author Brian Wellington
+ */
+
+public class SPFRecord extends TXTBase {
+
+private static final long serialVersionUID = -2100754352801658722L;
+
+SPFRecord() {}
+
+Record
+getObject() {
+	return new SPFRecord();
+}
+
+/**
+ * Creates a SPF Record from the given data
+ * @param strings The text strings
+ * @throws IllegalArgumentException One of the strings has invalid escapes
+ */
+public
+SPFRecord(Name name, int dclass, long ttl, List strings) {
+	super(name, Type.SPF, dclass, ttl, strings);
+}
+
+/**
+ * Creates a SPF Record from the given data
+ * @param string One text string
+ * @throws IllegalArgumentException The string has invalid escapes
+ */
+public
+SPFRecord(Name name, int dclass, long ttl, String string) {
+	super(name, Type.SPF, dclass, ttl, string);
+}
+
+}
diff --git a/src/org/xbill/DNS/SRVRecord.java b/src/org/xbill/DNS/SRVRecord.java
new file mode 100644
index 0000000..c0635fb
--- /dev/null
+++ b/src/org/xbill/DNS/SRVRecord.java
@@ -0,0 +1,114 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * Server Selection Record  - finds hosts running services in a domain.  An
+ * SRV record will normally be named _&lt;service&gt;._&lt;protocol&gt;.domain
+ * - examples would be _sips._tcp.example.org (for the secure SIP protocol) and
+ * _http._tcp.example.com (if HTTP used SRV records)
+ *
+ * @author Brian Wellington
+ */
+
+public class SRVRecord extends Record {
+
+private static final long serialVersionUID = -3886460132387522052L;
+
+private int priority, weight, port;
+private Name target;
+
+SRVRecord() {}
+
+Record
+getObject() {
+	return new SRVRecord();
+}
+
+/**
+ * Creates an SRV Record from the given data
+ * @param priority The priority of this SRV.  Records with lower priority
+ * are preferred.
+ * @param weight The weight, used to select between records at the same
+ * priority.
+ * @param port The TCP/UDP port that the service uses
+ * @param target The host running the service
+ */
+public
+SRVRecord(Name name, int dclass, long ttl, int priority,
+	  int weight, int port, Name target)
+{
+	super(name, Type.SRV, dclass, ttl);
+	this.priority = checkU16("priority", priority);
+	this.weight = checkU16("weight", weight);
+	this.port = checkU16("port", port);
+	this.target = checkName("target", target);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	priority = in.readU16();
+	weight = in.readU16();
+	port = in.readU16();
+	target = new Name(in);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	priority = st.getUInt16();
+	weight = st.getUInt16();
+	port = st.getUInt16();
+	target = st.getName(origin);
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(priority + " ");
+	sb.append(weight + " ");
+	sb.append(port + " ");
+	sb.append(target);
+	return sb.toString();
+}
+
+/** Returns the priority */
+public int
+getPriority() {
+	return priority;
+}
+
+/** Returns the weight */
+public int
+getWeight() {
+	return weight;
+}
+
+/** Returns the port that the service runs on */
+public int
+getPort() {
+	return port;
+}
+
+/** Returns the host running that the service */
+public Name
+getTarget() {
+	return target;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU16(priority);
+	out.writeU16(weight);
+	out.writeU16(port);
+	target.toWire(out, null, canonical);
+}
+
+public Name
+getAdditionalName() {
+	return target;
+}
+
+}
diff --git a/src/org/xbill/DNS/SSHFPRecord.java b/src/org/xbill/DNS/SSHFPRecord.java
new file mode 100644
index 0000000..079741e
--- /dev/null
+++ b/src/org/xbill/DNS/SSHFPRecord.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * SSH Fingerprint - stores the fingerprint of an SSH host key.
+ *
+ * @author Brian Wellington
+ */
+
+public class SSHFPRecord extends Record {
+
+private static final long serialVersionUID = -8104701402654687025L;
+
+public static class Algorithm {
+	private Algorithm() {}
+
+	public static final int RSA = 1;
+	public static final int DSS = 2;
+}
+
+public static class Digest {
+	private Digest() {}
+
+	public static final int SHA1 = 1;
+}
+
+private int alg;
+private int digestType;
+private byte [] fingerprint;
+
+SSHFPRecord() {} 
+
+Record
+getObject() {
+	return new SSHFPRecord();
+}
+
+/**
+ * Creates an SSHFP Record from the given data.
+ * @param alg The public key's algorithm.
+ * @param digestType The public key's digest type.
+ * @param fingerprint The public key's fingerprint.
+ */
+public
+SSHFPRecord(Name name, int dclass, long ttl, int alg, int digestType,
+	    byte [] fingerprint)
+{
+	super(name, Type.SSHFP, dclass, ttl);
+	this.alg = checkU8("alg", alg);
+	this.digestType = checkU8("digestType", digestType);
+	this.fingerprint = fingerprint;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	alg = in.readU8();
+	digestType = in.readU8();
+	fingerprint = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	alg = st.getUInt8();
+	digestType = st.getUInt8();
+	fingerprint = st.getHex(true);
+}
+
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(alg);
+	sb.append(" ");
+	sb.append(digestType);
+	sb.append(" ");
+	sb.append(base16.toString(fingerprint));
+	return sb.toString();
+}
+
+/** Returns the public key's algorithm. */
+public int
+getAlgorithm() {
+	return alg;
+}
+
+/** Returns the public key's digest type. */
+public int
+getDigestType() {
+	return digestType;
+}
+
+/** Returns the fingerprint */
+public byte []
+getFingerPrint() {
+	return fingerprint;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU8(alg);
+	out.writeU8(digestType);
+	out.writeByteArray(fingerprint);
+}
+
+}
diff --git a/src/org/xbill/DNS/Section.java b/src/org/xbill/DNS/Section.java
new file mode 100644
index 0000000..e0c8caa
--- /dev/null
+++ b/src/org/xbill/DNS/Section.java
@@ -0,0 +1,92 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Constants and functions relating to DNS message sections
+ *
+ * @author Brian Wellington
+ */
+
+public final class Section {
+
+/** The question (first) section */
+public static final int QUESTION	= 0;
+
+/** The answer (second) section */
+public static final int ANSWER		= 1;
+
+/** The authority (third) section */
+public static final int AUTHORITY	= 2;
+
+/** The additional (fourth) section */
+public static final int ADDITIONAL	= 3;
+
+/* Aliases for dynamic update */
+/** The zone (first) section of a dynamic update message */
+public static final int ZONE		= 0;
+
+/** The prerequisite (second) section of a dynamic update message */
+public static final int PREREQ		= 1;
+
+/** The update (third) section of a dynamic update message */
+public static final int UPDATE		= 2;
+
+private static Mnemonic sections = new Mnemonic("Message Section",
+						Mnemonic.CASE_LOWER);
+private static String [] longSections = new String[4];
+private static String [] updateSections = new String[4];
+
+static {
+	sections.setMaximum(3);
+	sections.setNumericAllowed(true);
+
+	sections.add(QUESTION, "qd");
+	sections.add(ANSWER, "an");
+	sections.add(AUTHORITY, "au");
+	sections.add(ADDITIONAL, "ad");
+
+	longSections[QUESTION]		= "QUESTIONS";
+	longSections[ANSWER]		= "ANSWERS";
+	longSections[AUTHORITY]		= "AUTHORITY RECORDS";
+	longSections[ADDITIONAL]	= "ADDITIONAL RECORDS";
+
+	updateSections[ZONE]		= "ZONE";
+	updateSections[PREREQ]		= "PREREQUISITES";
+	updateSections[UPDATE]		= "UPDATE RECORDS";
+	updateSections[ADDITIONAL]	= "ADDITIONAL RECORDS";
+}
+
+private
+Section() {}
+
+/** Converts a numeric Section into an abbreviation String */
+public static String
+string(int i) {
+	return sections.getText(i);
+}
+
+/** Converts a numeric Section into a full description String */
+public static String
+longString(int i) {
+	sections.check(i);
+	return longSections[i];
+}
+
+/**
+ * Converts a numeric Section into a full description String for an update
+ * Message.
+ */
+public static String
+updString(int i) {
+	sections.check(i);
+	return updateSections[i];
+}
+
+/** Converts a String representation of a Section into its numeric value */
+public static int
+value(String s) {
+	return sections.getValue(s);
+}
+
+}
diff --git a/src/org/xbill/DNS/Serial.java b/src/org/xbill/DNS/Serial.java
new file mode 100644
index 0000000..3a146c6
--- /dev/null
+++ b/src/org/xbill/DNS/Serial.java
@@ -0,0 +1,61 @@
+// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Helper functions for doing serial arithmetic.  These should be used when
+ * setting/checking SOA serial numbers.  SOA serial number arithmetic is
+ * defined in RFC 1982.
+ *
+ * @author Brian Wellington
+ */
+
+public final class Serial {
+
+private static final long MAX32 = 0xFFFFFFFFL;
+
+private
+Serial() {
+}
+
+/**
+ * Compares two numbers using serial arithmetic.  The numbers are assumed
+ * to be 32 bit unsigned integers stored in longs.
+ * @param serial1 The first integer
+ * @param serial2 The second integer
+ * @return 0 if the 2 numbers are equal, a positive number if serial1 is greater
+ * than serial2, and a negative number if serial2 is greater than serial1.
+ * @throws IllegalArgumentException serial1 or serial2 is out of range
+ */
+public static int
+compare(long serial1, long serial2) {
+	if (serial1 < 0 || serial1 > MAX32)
+		throw new IllegalArgumentException(serial1 + " out of range");
+	if (serial2 < 0 || serial2 > MAX32)
+		throw new IllegalArgumentException(serial2 + " out of range");
+	long diff = serial1 - serial2;
+	if (diff >= MAX32)
+		diff -= (MAX32 + 1);
+	else if (diff < -MAX32)
+		diff += (MAX32 + 1);
+	return (int)diff;
+}
+
+/**
+ * Increments a serial number.  The number is assumed to be a 32 bit unsigned
+ * integer stored in a long.  This basically adds 1 and resets the value to
+ * 0 if it is 2^32.
+ * @param serial The serial number
+ * @return The incremented serial number
+ * @throws IllegalArgumentException serial is out of range
+ */
+public static long
+increment(long serial) {
+	if (serial < 0 || serial > MAX32)
+		throw new IllegalArgumentException(serial + " out of range");
+	if (serial == MAX32)
+		return 0;
+	return serial + 1;
+}
+
+}
diff --git a/src/org/xbill/DNS/SetResponse.java b/src/org/xbill/DNS/SetResponse.java
new file mode 100644
index 0000000..05d9f32
--- /dev/null
+++ b/src/org/xbill/DNS/SetResponse.java
@@ -0,0 +1,202 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+
+/**
+ * The Response from a query to Cache.lookupRecords() or Zone.findRecords()
+ * @see Cache
+ * @see Zone
+ *
+ * @author Brian Wellington
+ */
+
+public class SetResponse {
+
+/**
+ * The Cache contains no information about the requested name/type
+ */
+static final int UNKNOWN	= 0;
+
+/**
+ * The Zone does not contain the requested name, or the Cache has
+ * determined that the name does not exist.
+ */
+static final int NXDOMAIN	= 1;
+
+/**
+ * The Zone contains the name, but no data of the requested type,
+ * or the Cache has determined that the name exists and has no data
+ * of the requested type.
+ */
+static final int NXRRSET	= 2;
+
+/**
+ * A delegation enclosing the requested name was found.
+ */
+static final int DELEGATION	= 3;
+
+/**
+ * The Cache/Zone found a CNAME when looking for the name.
+ * @see CNAMERecord
+ */
+static final int CNAME		= 4;
+
+/**
+ * The Cache/Zone found a DNAME when looking for the name.
+ * @see DNAMERecord
+ */
+static final int DNAME		= 5;
+
+/**
+ * The Cache/Zone has successfully answered the question for the
+ * requested name/type/class.
+ */
+static final int SUCCESSFUL	= 6;
+
+private static final SetResponse unknown = new SetResponse(UNKNOWN);
+private static final SetResponse nxdomain = new SetResponse(NXDOMAIN);
+private static final SetResponse nxrrset = new SetResponse(NXRRSET);
+
+private int type;
+private Object data;
+
+private
+SetResponse() {}
+
+SetResponse(int type, RRset rrset) {
+	if (type < 0 || type > 6)
+		throw new IllegalArgumentException("invalid type");
+	this.type = type;
+	this.data = rrset;
+}
+
+SetResponse(int type) {
+	if (type < 0 || type > 6)
+		throw new IllegalArgumentException("invalid type");
+	this.type = type;
+	this.data = null;
+}
+
+static SetResponse
+ofType(int type) {
+	switch (type) {
+		case UNKNOWN:
+			return unknown;
+		case NXDOMAIN:
+			return nxdomain;
+		case NXRRSET:
+			return nxrrset;
+		case DELEGATION:
+		case CNAME:
+		case DNAME:
+		case SUCCESSFUL:
+			SetResponse sr = new SetResponse();
+			sr.type = type;
+			sr.data = null;
+			return sr;
+		default:
+			throw new IllegalArgumentException("invalid type");
+	}
+}
+
+void
+addRRset(RRset rrset) {
+	if (data == null)
+		data = new ArrayList();
+	List l = (List) data;
+	l.add(rrset);
+}
+
+/** Is the answer to the query unknown? */
+public boolean
+isUnknown() {
+	return (type == UNKNOWN);
+}
+
+/** Is the answer to the query that the name does not exist? */
+public boolean
+isNXDOMAIN() {
+	return (type == NXDOMAIN);
+}
+
+/** Is the answer to the query that the name exists, but the type does not? */
+public boolean
+isNXRRSET() {
+	return (type == NXRRSET);
+}
+
+/** Is the result of the lookup that the name is below a delegation? */
+public boolean
+isDelegation() {
+	return (type == DELEGATION);
+}
+
+/** Is the result of the lookup a CNAME? */
+public boolean
+isCNAME() {
+	return (type == CNAME);
+}
+
+/** Is the result of the lookup a DNAME? */
+public boolean
+isDNAME() {
+	return (type == DNAME);
+}
+
+/** Was the query successful? */
+public boolean
+isSuccessful() {
+	return (type == SUCCESSFUL);
+}
+
+/** If the query was successful, return the answers */
+public RRset []
+answers() {
+	if (type != SUCCESSFUL)
+		return null;
+	List l = (List) data;
+	return (RRset []) l.toArray(new RRset[l.size()]);
+}
+
+/**
+ * If the query encountered a CNAME, return it.
+ */
+public CNAMERecord
+getCNAME() {
+	return (CNAMERecord)((RRset)data).first();
+}
+
+/**
+ * If the query encountered a DNAME, return it.
+ */
+public DNAMERecord
+getDNAME() {
+	return (DNAMERecord)((RRset)data).first();
+}
+
+/**
+ * If the query hit a delegation point, return the NS set.
+ */
+public RRset
+getNS() {
+	return (RRset)data;
+}
+
+/** Prints the value of the SetResponse */
+public String
+toString() {
+	switch (type) {
+		case UNKNOWN:		return "unknown";
+		case NXDOMAIN:		return "NXDOMAIN";
+		case NXRRSET:		return "NXRRSET";
+		case DELEGATION:	return "delegation: " + data;
+		case CNAME:		return "CNAME: " + data;
+		case DNAME:		return "DNAME: " + data;
+		case SUCCESSFUL:	return "successful";
+		default:		throw new IllegalStateException();
+	}
+}
+
+}
diff --git a/src/org/xbill/DNS/SimpleResolver.java b/src/org/xbill/DNS/SimpleResolver.java
new file mode 100644
index 0000000..7436133
--- /dev/null
+++ b/src/org/xbill/DNS/SimpleResolver.java
@@ -0,0 +1,351 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+import java.io.*;
+import java.net.*;
+
+/**
+ * An implementation of Resolver that sends one query to one server.
+ * SimpleResolver handles TCP retries, transaction security (TSIG), and
+ * EDNS 0.
+ * @see Resolver
+ * @see TSIG
+ * @see OPTRecord
+ *
+ * @author Brian Wellington
+ */
+
+
+public class SimpleResolver implements Resolver {
+
+/** The default port to send queries to */
+public static final int DEFAULT_PORT = 53;
+
+/** The default EDNS payload size */
+public static final int DEFAULT_EDNS_PAYLOADSIZE = 1280;
+
+private InetSocketAddress address;
+private InetSocketAddress localAddress;
+private boolean useTCP, ignoreTruncation;
+private OPTRecord queryOPT;
+private TSIG tsig;
+private long timeoutValue = 10 * 1000;
+
+private static final short DEFAULT_UDPSIZE = 512;
+
+private static String defaultResolver = "localhost";
+private static int uniqueID = 0;
+
+/**
+ * Creates a SimpleResolver that will query the specified host 
+ * @exception UnknownHostException Failure occurred while finding the host
+ */
+public
+SimpleResolver(String hostname) throws UnknownHostException {
+	if (hostname == null) {
+		hostname = ResolverConfig.getCurrentConfig().server();
+		if (hostname == null)
+			hostname = defaultResolver;
+	}
+	InetAddress addr;
+	if (hostname.equals("0"))
+		addr = InetAddress.getLocalHost();
+	else
+		addr = InetAddress.getByName(hostname);
+	address = new InetSocketAddress(addr, DEFAULT_PORT);
+}
+
+/**
+ * Creates a SimpleResolver.  The host to query is either found by using
+ * ResolverConfig, or the default host is used.
+ * @see ResolverConfig
+ * @exception UnknownHostException Failure occurred while finding the host
+ */
+public
+SimpleResolver() throws UnknownHostException {
+	this(null);
+}
+
+/**
+ * Gets the destination address associated with this SimpleResolver.
+ * Messages sent using this SimpleResolver will be sent to this address.
+ * @return The destination address associated with this SimpleResolver.
+ */
+InetSocketAddress
+getAddress() {
+	return address;
+}
+
+/** Sets the default host (initially localhost) to query */
+public static void
+setDefaultResolver(String hostname) {
+	defaultResolver = hostname;
+}
+
+public void
+setPort(int port) {
+	address = new InetSocketAddress(address.getAddress(), port);
+}
+
+/**
+ * Sets the address of the server to communicate with.
+ * @param addr The address of the DNS server
+ */
+public void
+setAddress(InetSocketAddress addr) {
+	address = addr;
+}
+
+/**
+ * Sets the address of the server to communicate with (on the default
+ * DNS port)
+ * @param addr The address of the DNS server
+ */
+public void
+setAddress(InetAddress addr) {
+	address = new InetSocketAddress(addr, address.getPort());
+}
+
+/**
+ * Sets the local address to bind to when sending messages.
+ * @param addr The local address to send messages from.
+ */
+public void
+setLocalAddress(InetSocketAddress addr) {
+	localAddress = addr;
+}
+
+/**
+ * Sets the local address to bind to when sending messages.  A random port
+ * will be used.
+ * @param addr The local address to send messages from.
+ */
+public void
+setLocalAddress(InetAddress addr) {
+	localAddress = new InetSocketAddress(addr, 0);
+}
+
+public void
+setTCP(boolean flag) {
+	this.useTCP = flag;
+}
+
+public void
+setIgnoreTruncation(boolean flag) {
+	this.ignoreTruncation = flag;
+}
+
+public void
+setEDNS(int level, int payloadSize, int flags, List options) {
+	if (level != 0 && level != -1)
+		throw new IllegalArgumentException("invalid EDNS level - " +
+						   "must be 0 or -1");
+	if (payloadSize == 0)
+		payloadSize = DEFAULT_EDNS_PAYLOADSIZE;
+	queryOPT = new OPTRecord(payloadSize, 0, level, flags, options);
+}
+
+public void
+setEDNS(int level) {
+	setEDNS(level, 0, 0, null);
+}
+
+public void
+setTSIGKey(TSIG key) {
+	tsig = key;
+}
+
+TSIG
+getTSIGKey() {
+	return tsig;
+}
+
+public void
+setTimeout(int secs, int msecs) {
+	timeoutValue = (long)secs * 1000 + msecs;
+}
+
+public void
+setTimeout(int secs) {
+	setTimeout(secs, 0);
+}
+
+long
+getTimeout() {
+	return timeoutValue;
+}
+
+private Message
+parseMessage(byte [] b) throws WireParseException {
+	try {
+		return (new Message(b));
+	}
+	catch (IOException e) {
+		if (Options.check("verbose"))
+			e.printStackTrace();
+		if (!(e instanceof WireParseException))
+			e = new WireParseException("Error parsing message");
+		throw (WireParseException) e;
+	}
+}
+
+private void
+verifyTSIG(Message query, Message response, byte [] b, TSIG tsig) {
+	if (tsig == null)
+		return;
+	int error = tsig.verify(response, b, query.getTSIG());
+	if (Options.check("verbose"))
+		System.err.println("TSIG verify: " + Rcode.TSIGstring(error));
+}
+
+private void
+applyEDNS(Message query) {
+	if (queryOPT == null || query.getOPT() != null)
+		return;
+	query.addRecord(queryOPT, Section.ADDITIONAL);
+}
+
+private int
+maxUDPSize(Message query) {
+	OPTRecord opt = query.getOPT();
+	if (opt == null)
+		return DEFAULT_UDPSIZE;
+	else
+		return opt.getPayloadSize();
+}
+
+/**
+ * Sends a message to a single server and waits for a response.  No checking
+ * is done to ensure that the response is associated with the query.
+ * @param query The query to send.
+ * @return The response.
+ * @throws IOException An error occurred while sending or receiving.
+ */
+public Message
+send(Message query) throws IOException {
+	if (Options.check("verbose"))
+		System.err.println("Sending to " +
+				   address.getAddress().getHostAddress() +
+				   ":" + address.getPort());
+
+	if (query.getHeader().getOpcode() == Opcode.QUERY) {
+		Record question = query.getQuestion();
+		if (question != null && question.getType() == Type.AXFR)
+			return sendAXFR(query);
+	}
+
+	query = (Message) query.clone();
+	applyEDNS(query);
+	if (tsig != null)
+		tsig.apply(query, null);
+
+	byte [] out = query.toWire(Message.MAXLENGTH);
+	int udpSize = maxUDPSize(query);
+	boolean tcp = false;
+	long endTime = System.currentTimeMillis() + timeoutValue;
+	do {
+		byte [] in;
+
+		if (useTCP || out.length > udpSize)
+			tcp = true;
+		if (tcp)
+			in = TCPClient.sendrecv(localAddress, address, out,
+						endTime);
+		else
+			in = UDPClient.sendrecv(localAddress, address, out,
+						udpSize, endTime);
+
+		/*
+		 * Check that the response is long enough.
+		 */
+		if (in.length < Header.LENGTH) {
+			throw new WireParseException("invalid DNS header - " +
+						     "too short");
+		}
+		/*
+		 * Check that the response ID matches the query ID.  We want
+		 * to check this before actually parsing the message, so that
+		 * if there's a malformed response that's not ours, it
+		 * doesn't confuse us.
+		 */
+		int id = ((in[0] & 0xFF) << 8) + (in[1] & 0xFF);
+		int qid = query.getHeader().getID();
+		if (id != qid) {
+			String error = "invalid message id: expected " + qid +
+				       "; got id " + id;
+			if (tcp) {
+				throw new WireParseException(error);
+			} else {
+				if (Options.check("verbose")) {
+					System.err.println(error);
+				}
+				continue;
+			}
+		}
+		Message response = parseMessage(in);
+		verifyTSIG(query, response, in, tsig);
+		if (!tcp && !ignoreTruncation &&
+		    response.getHeader().getFlag(Flags.TC))
+		{
+			tcp = true;
+			continue;
+		}
+		return response;
+	} while (true);
+}
+
+/**
+ * Asynchronously sends a message to a single server, registering a listener
+ * to receive a callback on success or exception.  Multiple asynchronous
+ * lookups can be performed in parallel.  Since the callback may be invoked
+ * before the function returns, external synchronization is necessary.
+ * @param query The query to send
+ * @param listener The object containing the callbacks.
+ * @return An identifier, which is also a parameter in the callback
+ */
+public Object
+sendAsync(final Message query, final ResolverListener listener) {
+	final Object id;
+	synchronized (this) {
+		id = new Integer(uniqueID++);
+	}
+	Record question = query.getQuestion();
+	String qname;
+	if (question != null)
+		qname = question.getName().toString();
+	else
+		qname = "(none)";
+	String name = this.getClass() + ": " + qname;
+	Thread thread = new ResolveThread(this, query, id, listener);
+	thread.setName(name);
+	thread.setDaemon(true);
+	thread.start();
+	return id;
+}
+
+private Message
+sendAXFR(Message query) throws IOException {
+	Name qname = query.getQuestion().getName();
+	ZoneTransferIn xfrin = ZoneTransferIn.newAXFR(qname, address, tsig);
+	xfrin.setTimeout((int)(getTimeout() / 1000));
+	xfrin.setLocalAddress(localAddress);
+	try {
+		xfrin.run();
+	}
+	catch (ZoneTransferException e) {
+		throw new WireParseException(e.getMessage());
+	}
+	List records = xfrin.getAXFR();
+	Message response = new Message(query.getHeader().getID());
+	response.getHeader().setFlag(Flags.AA);
+	response.getHeader().setFlag(Flags.QR);
+	response.addRecord(query.getQuestion(), Section.QUESTION);
+	Iterator it = records.iterator();
+	while (it.hasNext())
+		response.addRecord((Record)it.next(), Section.ANSWER);
+	return response;
+}
+
+}
diff --git a/src/org/xbill/DNS/SingleCompressedNameBase.java b/src/org/xbill/DNS/SingleCompressedNameBase.java
new file mode 100644
index 0000000..790ca1f
--- /dev/null
+++ b/src/org/xbill/DNS/SingleCompressedNameBase.java
@@ -0,0 +1,31 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Implements common functionality for the many record types whose format
+ * is a single compressed name.
+ *
+ * @author Brian Wellington
+ */
+
+abstract class SingleCompressedNameBase extends SingleNameBase {
+
+private static final long serialVersionUID = -236435396815460677L;
+
+protected
+SingleCompressedNameBase() {}
+
+protected
+SingleCompressedNameBase(Name name, int type, int dclass, long ttl,
+			 Name singleName, String description)
+{
+        super(name, type, dclass, ttl, singleName, description);
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	singleName.toWire(out, c, canonical);
+}
+
+}
diff --git a/src/org/xbill/DNS/SingleNameBase.java b/src/org/xbill/DNS/SingleNameBase.java
new file mode 100644
index 0000000..6d6c041
--- /dev/null
+++ b/src/org/xbill/DNS/SingleNameBase.java
@@ -0,0 +1,61 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * Implements common functionality for the many record types whose format
+ * is a single name.
+ *
+ * @author Brian Wellington
+ */
+
+abstract class SingleNameBase extends Record {
+
+private static final long serialVersionUID = -18595042501413L;
+
+protected Name singleName;
+
+protected
+SingleNameBase() {}
+
+protected
+SingleNameBase(Name name, int type, int dclass, long ttl) {
+	super(name, type, dclass, ttl);
+}
+
+protected
+SingleNameBase(Name name, int type, int dclass, long ttl, Name singleName,
+	    String description)
+{
+	super(name, type, dclass, ttl);
+	this.singleName = checkName(description, singleName);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	singleName = new Name(in);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	singleName = st.getName(origin);
+}
+
+String
+rrToString() {
+	return singleName.toString();
+}
+
+protected Name
+getSingleName() {
+	return singleName;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	singleName.toWire(out, null, canonical);
+}
+
+}
diff --git a/src/org/xbill/DNS/TCPClient.java b/src/org/xbill/DNS/TCPClient.java
new file mode 100644
index 0000000..1f17d72
--- /dev/null
+++ b/src/org/xbill/DNS/TCPClient.java
@@ -0,0 +1,132 @@
+// Copyright (c) 2005 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.net.*;
+import java.nio.*;
+import java.nio.channels.*;
+
+final class TCPClient extends Client {
+
+public
+TCPClient(long endTime) throws IOException {
+	super(SocketChannel.open(), endTime);
+}
+
+void
+bind(SocketAddress addr) throws IOException {
+	SocketChannel channel = (SocketChannel) key.channel();
+	channel.socket().bind(addr);
+}
+
+void
+connect(SocketAddress addr) throws IOException {
+	SocketChannel channel = (SocketChannel) key.channel();
+	if (channel.connect(addr))
+		return;
+	key.interestOps(SelectionKey.OP_CONNECT);
+	try {
+		while (!channel.finishConnect()) {
+			if (!key.isConnectable())
+				blockUntil(key, endTime);
+		}
+	}
+	finally {
+		if (key.isValid())
+			key.interestOps(0);
+	}
+}
+
+void
+send(byte [] data) throws IOException {
+	SocketChannel channel = (SocketChannel) key.channel();
+	verboseLog("TCP write", data);
+	byte [] lengthArray = new byte[2];
+	lengthArray[0] = (byte)(data.length >>> 8);
+	lengthArray[1] = (byte)(data.length & 0xFF);
+	ByteBuffer [] buffers = new ByteBuffer[2];
+	buffers[0] = ByteBuffer.wrap(lengthArray);
+	buffers[1] = ByteBuffer.wrap(data);
+	int nsent = 0;
+	key.interestOps(SelectionKey.OP_WRITE);
+	try {
+		while (nsent < data.length + 2) {
+			if (key.isWritable()) {
+				long n = channel.write(buffers);
+				if (n < 0)
+					throw new EOFException();
+				nsent += (int) n;
+				if (nsent < data.length + 2 &&
+				    System.currentTimeMillis() > endTime)
+					throw new SocketTimeoutException();
+			} else
+				blockUntil(key, endTime);
+		}
+	}
+	finally {
+		if (key.isValid())
+			key.interestOps(0);
+	}
+}
+
+private byte []
+_recv(int length) throws IOException {
+	SocketChannel channel = (SocketChannel) key.channel();
+	int nrecvd = 0;
+	byte [] data = new byte[length];
+	ByteBuffer buffer = ByteBuffer.wrap(data);
+	key.interestOps(SelectionKey.OP_READ);
+	try {
+		while (nrecvd < length) {
+			if (key.isReadable()) {
+				long n = channel.read(buffer);
+				if (n < 0)
+					throw new EOFException();
+				nrecvd += (int) n;
+				if (nrecvd < length &&
+				    System.currentTimeMillis() > endTime)
+					throw new SocketTimeoutException();
+			} else
+				blockUntil(key, endTime);
+		}
+	}
+	finally {
+		if (key.isValid())
+			key.interestOps(0);
+	}
+	return data;
+}
+
+byte []
+recv() throws IOException {
+	byte [] buf = _recv(2);
+	int length = ((buf[0] & 0xFF) << 8) + (buf[1] & 0xFF);
+	byte [] data = _recv(length);
+	verboseLog("TCP read", data);
+	return data;
+}
+
+static byte []
+sendrecv(SocketAddress local, SocketAddress remote, byte [] data, long endTime)
+throws IOException
+{
+	TCPClient client = new TCPClient(endTime);
+	try {
+		if (local != null)
+			client.bind(local);
+		client.connect(remote);
+		client.send(data);
+		return client.recv();
+	}
+	finally {
+		client.cleanup();
+	}
+}
+
+static byte []
+sendrecv(SocketAddress addr, byte [] data, long endTime) throws IOException {
+	return sendrecv(null, addr, data, endTime);
+}
+
+}
diff --git a/src/org/xbill/DNS/TKEYRecord.java b/src/org/xbill/DNS/TKEYRecord.java
new file mode 100644
index 0000000..4dcbb5c
--- /dev/null
+++ b/src/org/xbill/DNS/TKEYRecord.java
@@ -0,0 +1,225 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * Transaction Key - used to compute and/or securely transport a shared
+ * secret to be used with TSIG.
+ * @see TSIG
+ *
+ * @author Brian Wellington
+ */
+
+public class TKEYRecord extends Record {
+
+private static final long serialVersionUID = 8828458121926391756L;
+
+private Name alg;
+private Date timeInception;
+private Date timeExpire;
+private int mode, error;
+private byte [] key;
+private byte [] other;
+
+/** The key is assigned by the server (unimplemented) */
+public static final int SERVERASSIGNED		= 1;
+
+/** The key is computed using a Diffie-Hellman key exchange */
+public static final int DIFFIEHELLMAN		= 2;
+
+/** The key is computed using GSS_API (unimplemented) */
+public static final int GSSAPI			= 3;
+
+/** The key is assigned by the resolver (unimplemented) */
+public static final int RESOLVERASSIGNED	= 4;
+
+/** The key should be deleted */
+public static final int DELETE			= 5;
+
+TKEYRecord() {}
+
+Record
+getObject() {
+	return new TKEYRecord();
+}
+
+/**
+ * Creates a TKEY Record from the given data.
+ * @param alg The shared key's algorithm
+ * @param timeInception The beginning of the validity period of the shared
+ * secret or keying material
+ * @param timeExpire The end of the validity period of the shared
+ * secret or keying material
+ * @param mode The mode of key agreement
+ * @param error The extended error field.  Should be 0 in queries
+ * @param key The shared secret
+ * @param other The other data field.  Currently unused
+ * responses.
+ */
+public
+TKEYRecord(Name name, int dclass, long ttl, Name alg,
+	   Date timeInception, Date timeExpire, int mode, int error,
+	   byte [] key, byte other[])
+{
+	super(name, Type.TKEY, dclass, ttl);
+	this.alg = checkName("alg", alg);
+	this.timeInception = timeInception;
+	this.timeExpire = timeExpire;
+	this.mode = checkU16("mode", mode);
+	this.error = checkU16("error", error);
+	this.key = key;
+	this.other = other;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	alg = new Name(in);
+	timeInception = new Date(1000 * in.readU32());
+	timeExpire = new Date(1000 * in.readU32());
+	mode = in.readU16();
+	error = in.readU16();
+
+	int keylen = in.readU16();
+	if (keylen > 0)
+		key = in.readByteArray(keylen);
+	else
+		key = null;
+
+	int otherlen = in.readU16();
+	if (otherlen > 0)
+		other = in.readByteArray(otherlen);
+	else
+		other = null;
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	throw st.exception("no text format defined for TKEY");
+}
+
+protected String
+modeString() {
+	switch (mode) {
+		case SERVERASSIGNED:	return "SERVERASSIGNED";
+		case DIFFIEHELLMAN:	return "DIFFIEHELLMAN";
+		case GSSAPI:		return "GSSAPI";
+		case RESOLVERASSIGNED:	return "RESOLVERASSIGNED";
+		case DELETE:		return "DELETE";
+		default:		return Integer.toString(mode);
+	}
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(alg);
+	sb.append(" ");
+	if (Options.check("multiline"))
+		sb.append("(\n\t");
+	sb.append(FormattedTime.format(timeInception));
+	sb.append(" ");
+	sb.append(FormattedTime.format(timeExpire));
+	sb.append(" ");
+	sb.append(modeString());
+	sb.append(" ");
+	sb.append(Rcode.TSIGstring(error));
+	if (Options.check("multiline")) {
+		sb.append("\n");
+		if (key != null) {
+			sb.append(base64.formatString(key, 64, "\t", false));
+			sb.append("\n");
+		}
+		if (other != null)
+			sb.append(base64.formatString(other, 64, "\t", false));
+		sb.append(" )");
+	} else {
+		sb.append(" ");
+		if (key != null) {
+			sb.append(base64.toString(key));
+			sb.append(" ");
+		}
+		if (other != null)
+			sb.append(base64.toString(other));
+	}
+	return sb.toString();
+}
+
+/** Returns the shared key's algorithm */
+public Name
+getAlgorithm() {
+	return alg;
+}
+
+/**
+ * Returns the beginning of the validity period of the shared secret or
+ * keying material
+ */
+public Date
+getTimeInception() {
+	return timeInception;
+}
+
+/**
+ * Returns the end of the validity period of the shared secret or
+ * keying material
+ */
+public Date
+getTimeExpire() {
+	return timeExpire;
+}
+
+/** Returns the key agreement mode */
+public int
+getMode() {
+	return mode;
+}
+
+/** Returns the extended error */
+public int
+getError() {
+	return error;
+}
+
+/** Returns the shared secret or keying material */
+public byte []
+getKey() {
+	return key;
+}
+
+/** Returns the other data */
+public byte []
+getOther() {
+	return other;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	alg.toWire(out, null, canonical);
+
+	out.writeU32(timeInception.getTime() / 1000);
+	out.writeU32(timeExpire.getTime() / 1000);
+
+	out.writeU16(mode);
+	out.writeU16(error);
+
+	if (key != null) {
+		out.writeU16(key.length);
+		out.writeByteArray(key);
+	}
+	else
+		out.writeU16(0);
+
+	if (other != null) {
+		out.writeU16(other.length);
+		out.writeByteArray(other);
+	}
+	else
+		out.writeU16(0);
+}
+
+}
diff --git a/src/org/xbill/DNS/TLSARecord.java b/src/org/xbill/DNS/TLSARecord.java
new file mode 100644
index 0000000..48e2e80
--- /dev/null
+++ b/src/org/xbill/DNS/TLSARecord.java
@@ -0,0 +1,156 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * Transport Layer Security Authentication
+ *
+ * @author Brian Wellington
+ */
+
+public class TLSARecord extends Record {
+
+private static final long serialVersionUID = 356494267028580169L;
+
+public static class CertificateUsage {
+	private CertificateUsage() {}
+
+	public static final int CA_CONSTRAINT = 0;
+	public static final int SERVICE_CERTIFICATE_CONSTRAINT = 1;
+	public static final int TRUST_ANCHOR_ASSERTION = 2;
+	public static final int DOMAIN_ISSUED_CERTIFICATE = 3;
+}
+
+public static class Selector {
+	private Selector() {}
+
+	/**
+	 * Full certificate; the Certificate binary structure defined in
+	 * [RFC5280]
+	 */
+	public static final int FULL_CERTIFICATE = 0;
+
+	/**
+	 * SubjectPublicKeyInfo; DER-encoded binary structure defined in
+	 * [RFC5280]
+	 */
+	public static final int SUBJECT_PUBLIC_KEY_INFO = 1;
+}
+
+public static class MatchingType {
+	private MatchingType() {}
+
+	/** Exact match on selected content */
+	public static final int EXACT = 0;
+
+	/** SHA-256 hash of selected content [RFC6234] */
+	public static final int SHA256 = 1;
+
+	/** SHA-512 hash of selected content [RFC6234] */
+	public static final int SHA512 = 2;
+}
+
+private int certificateUsage;
+private int selector;
+private int matchingType;
+private byte [] certificateAssociationData;
+
+TLSARecord() {}
+
+Record
+getObject() {
+	return new TLSARecord();
+}
+
+/**
+ * Creates an TLSA Record from the given data
+ * @param certificateUsage The provided association that will be used to
+ * match the certificate presented in the TLS handshake. 
+ * @param selector The part of the TLS certificate presented by the server
+ * that will be matched against the association data. 
+ * @param matchingType How the certificate association is presented.
+ * @param certificateAssociationData The "certificate association data" to be
+ * matched.
+ */
+public
+TLSARecord(Name name, int dclass, long ttl, 
+	   int certificateUsage, int selector, int matchingType,
+	   byte [] certificateAssociationData)
+{
+	super(name, Type.TLSA, dclass, ttl);
+	this.certificateUsage = checkU8("certificateUsage", certificateUsage);
+	this.selector = checkU8("selector", selector);
+	this.matchingType = checkU8("matchingType", matchingType);
+	this.certificateAssociationData = checkByteArrayLength(
+						"certificateAssociationData",
+						certificateAssociationData,
+						0xFFFF);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	certificateUsage = in.readU8();
+	selector = in.readU8();
+	matchingType = in.readU8();
+	certificateAssociationData = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	certificateUsage = st.getUInt8();
+	selector = st.getUInt8();
+	matchingType = st.getUInt8();
+	certificateAssociationData = st.getHex();
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(certificateUsage);
+	sb.append(" ");
+	sb.append(selector);
+	sb.append(" ");
+	sb.append(matchingType);
+	sb.append(" ");
+	sb.append(base16.toString(certificateAssociationData));
+
+	return sb.toString();
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU8(certificateUsage);
+	out.writeU8(selector);
+	out.writeU8(matchingType);
+	out.writeByteArray(certificateAssociationData);
+}
+
+/** Returns the certificate usage of the TLSA record */
+public int
+getCertificateUsage() {
+	return certificateUsage;
+}
+
+/** Returns the selector of the TLSA record */
+public int
+getSelector() {
+	return selector;
+}
+
+/** Returns the matching type of the TLSA record */
+public int
+getMatchingType() {
+	return matchingType;
+}
+
+/** Returns the certificate associate data of this TLSA record */
+public final byte []
+getCertificateAssociationData() {
+	return certificateAssociationData;
+}
+
+}
diff --git a/src/org/xbill/DNS/TSIG.java b/src/org/xbill/DNS/TSIG.java
new file mode 100644
index 0000000..d9e6972
--- /dev/null
+++ b/src/org/xbill/DNS/TSIG.java
@@ -0,0 +1,592 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * Transaction signature handling.  This class generates and verifies
+ * TSIG records on messages, which provide transaction security.
+ * @see TSIGRecord
+ *
+ * @author Brian Wellington
+ */
+
+public class TSIG {
+
+private static final String HMAC_MD5_STR = "HMAC-MD5.SIG-ALG.REG.INT.";
+private static final String HMAC_SHA1_STR = "hmac-sha1.";
+private static final String HMAC_SHA224_STR = "hmac-sha224.";
+private static final String HMAC_SHA256_STR = "hmac-sha256.";
+private static final String HMAC_SHA384_STR = "hmac-sha384.";
+private static final String HMAC_SHA512_STR = "hmac-sha512.";
+
+/** The domain name representing the HMAC-MD5 algorithm. */
+public static final Name HMAC_MD5 = Name.fromConstantString(HMAC_MD5_STR);
+
+/** The domain name representing the HMAC-MD5 algorithm (deprecated). */
+public static final Name HMAC = HMAC_MD5;
+
+/** The domain name representing the HMAC-SHA1 algorithm. */
+public static final Name HMAC_SHA1 = Name.fromConstantString(HMAC_SHA1_STR);
+
+/**
+ * The domain name representing the HMAC-SHA224 algorithm.
+ * Note that SHA224 is not supported by Java out-of-the-box, this requires use
+ * of a third party provider like BouncyCastle.org.
+ */
+public static final Name HMAC_SHA224 = Name.fromConstantString(HMAC_SHA224_STR);
+
+/** The domain name representing the HMAC-SHA256 algorithm. */
+public static final Name HMAC_SHA256 = Name.fromConstantString(HMAC_SHA256_STR);
+
+/** The domain name representing the HMAC-SHA384 algorithm. */
+public static final Name HMAC_SHA384 = Name.fromConstantString(HMAC_SHA384_STR);
+
+/** The domain name representing the HMAC-SHA512 algorithm. */
+public static final Name HMAC_SHA512 = Name.fromConstantString(HMAC_SHA512_STR);
+
+/**
+ * The default fudge value for outgoing packets.  Can be overriden by the
+ * tsigfudge option.
+ */
+public static final short FUDGE		= 300;
+
+private Name name, alg;
+private String digest;
+private int digestBlockLength;
+private byte [] key;
+
+private void
+getDigest() {
+	if (alg.equals(HMAC_MD5)) {
+		digest = "md5";
+		digestBlockLength = 64;
+	} else if (alg.equals(HMAC_SHA1)) {
+		digest = "sha-1";
+		digestBlockLength = 64;
+	} else if (alg.equals(HMAC_SHA224)) {
+		digest = "sha-224";
+		digestBlockLength = 64;
+	} else if (alg.equals(HMAC_SHA256)) {
+		digest = "sha-256";
+		digestBlockLength = 64;
+	} else if (alg.equals(HMAC_SHA512)) {
+		digest = "sha-512";
+		digestBlockLength = 128;
+	} else if (alg.equals(HMAC_SHA384)) {
+		digest = "sha-384";
+		digestBlockLength = 128;
+	} else
+		throw new IllegalArgumentException("Invalid algorithm");
+}
+
+/**
+ * Creates a new TSIG key, which can be used to sign or verify a message.
+ * @param algorithm The algorithm of the shared key.
+ * @param name The name of the shared key.
+ * @param key The shared key's data.
+ */
+public
+TSIG(Name algorithm, Name name, byte [] key) {
+	this.name = name;
+	this.alg = algorithm;
+	this.key = key;
+	getDigest();
+}
+
+/**
+ * Creates a new TSIG key with the hmac-md5 algorithm, which can be used to
+ * sign or verify a message.
+ * @param name The name of the shared key.
+ * @param key The shared key's data.
+ */
+public
+TSIG(Name name, byte [] key) {
+	this(HMAC_MD5, name, key);
+}
+
+/**
+ * Creates a new TSIG object, which can be used to sign or verify a message.
+ * @param name The name of the shared key.
+ * @param key The shared key's data represented as a base64 encoded string.
+ * @throws IllegalArgumentException The key name is an invalid name
+ * @throws IllegalArgumentException The key data is improperly encoded
+ */
+public
+TSIG(Name algorithm, String name, String key) {
+	this.key = base64.fromString(key);
+	if (this.key == null)
+		throw new IllegalArgumentException("Invalid TSIG key string");
+	try {
+		this.name = Name.fromString(name, Name.root);
+	}
+	catch (TextParseException e) {
+		throw new IllegalArgumentException("Invalid TSIG key name");
+	}
+	this.alg = algorithm;
+	getDigest();
+}
+
+/**
+ * Creates a new TSIG object, which can be used to sign or verify a message.
+ * @param name The name of the shared key.
+ * @param algorithm The algorithm of the shared key.  The legal values are
+ * "hmac-md5", "hmac-sha1", "hmac-sha224", "hmac-sha256", "hmac-sha384", and
+ * "hmac-sha512".
+ * @param key The shared key's data represented as a base64 encoded string.
+ * @throws IllegalArgumentException The key name is an invalid name
+ * @throws IllegalArgumentException The key data is improperly encoded
+ */
+public
+TSIG(String algorithm, String name, String key) {
+	this(HMAC_MD5, name, key);
+	if (algorithm.equalsIgnoreCase("hmac-md5"))
+		this.alg = HMAC_MD5;
+	else if (algorithm.equalsIgnoreCase("hmac-sha1"))
+		this.alg = HMAC_SHA1;
+	else if (algorithm.equalsIgnoreCase("hmac-sha224"))
+		this.alg = HMAC_SHA224;
+	else if (algorithm.equalsIgnoreCase("hmac-sha256"))
+		this.alg = HMAC_SHA256;
+	else if (algorithm.equalsIgnoreCase("hmac-sha384"))
+		this.alg = HMAC_SHA384;
+	else if (algorithm.equalsIgnoreCase("hmac-sha512"))
+		this.alg = HMAC_SHA512;
+	else
+		throw new IllegalArgumentException("Invalid TSIG algorithm");
+	getDigest();
+}
+
+/**
+ * Creates a new TSIG object with the hmac-md5 algorithm, which can be used to
+ * sign or verify a message.
+ * @param name The name of the shared key
+ * @param key The shared key's data, represented as a base64 encoded string.
+ * @throws IllegalArgumentException The key name is an invalid name
+ * @throws IllegalArgumentException The key data is improperly encoded
+ */
+public
+TSIG(String name, String key) {
+	this(HMAC_MD5, name, key);
+}
+
+/**
+ * Creates a new TSIG object, which can be used to sign or verify a message.
+ * @param str The TSIG key, in the form name:secret, name/secret,
+ * alg:name:secret, or alg/name/secret.  If an algorithm is specified, it must
+ * be "hmac-md5", "hmac-sha1", or "hmac-sha256".
+ * @throws IllegalArgumentException The string does not contain both a name
+ * and secret.
+ * @throws IllegalArgumentException The key name is an invalid name
+ * @throws IllegalArgumentException The key data is improperly encoded
+ */
+static public TSIG
+fromString(String str) {
+	String [] parts = str.split("[:/]", 3);
+	if (parts.length < 2)
+		throw new IllegalArgumentException("Invalid TSIG key " +
+						   "specification");
+	if (parts.length == 3) {
+		try {
+			return new TSIG(parts[0], parts[1], parts[2]);
+		} catch (IllegalArgumentException e) {
+			parts = str.split("[:/]", 2);
+		}
+	}
+	return new TSIG(HMAC_MD5, parts[0], parts[1]);
+}
+
+/**
+ * Generates a TSIG record with a specific error for a message that has
+ * been rendered.
+ * @param m The message
+ * @param b The rendered message
+ * @param error The error
+ * @param old If this message is a response, the TSIG from the request
+ * @return The TSIG record to be added to the message
+ */
+public TSIGRecord
+generate(Message m, byte [] b, int error, TSIGRecord old) {
+	Date timeSigned;
+	if (error != Rcode.BADTIME)
+		timeSigned = new Date();
+	else
+		timeSigned = old.getTimeSigned();
+	int fudge;
+	HMAC hmac = null;
+	if (error == Rcode.NOERROR || error == Rcode.BADTIME)
+		hmac = new HMAC(digest, digestBlockLength, key);
+
+	fudge = Options.intValue("tsigfudge");
+	if (fudge < 0 || fudge > 0x7FFF)
+		fudge = FUDGE;
+
+	if (old != null) {
+		DNSOutput out = new DNSOutput();
+		out.writeU16(old.getSignature().length);
+		if (hmac != null) {
+			hmac.update(out.toByteArray());
+			hmac.update(old.getSignature());
+		}
+	}
+
+	/* Digest the message */
+	if (hmac != null)
+		hmac.update(b);
+
+	DNSOutput out = new DNSOutput();
+	name.toWireCanonical(out);
+	out.writeU16(DClass.ANY);	/* class */
+	out.writeU32(0);		/* ttl */
+	alg.toWireCanonical(out);
+	long time = timeSigned.getTime() / 1000;
+	int timeHigh = (int) (time >> 32);
+	long timeLow = (time & 0xFFFFFFFFL);
+	out.writeU16(timeHigh);
+	out.writeU32(timeLow);
+	out.writeU16(fudge);
+
+	out.writeU16(error);
+	out.writeU16(0); /* No other data */
+
+	if (hmac != null)
+		hmac.update(out.toByteArray());
+
+	byte [] signature;
+	if (hmac != null)
+		signature = hmac.sign();
+	else
+		signature = new byte[0];
+
+	byte [] other = null;
+	if (error == Rcode.BADTIME) {
+		out = new DNSOutput();
+		time = new Date().getTime() / 1000;
+		timeHigh = (int) (time >> 32);
+		timeLow = (time & 0xFFFFFFFFL);
+		out.writeU16(timeHigh);
+		out.writeU32(timeLow);
+		other = out.toByteArray();
+	}
+
+	return (new TSIGRecord(name, DClass.ANY, 0, alg, timeSigned, fudge,
+			       signature, m.getHeader().getID(), error, other));
+}
+
+/**
+ * Generates a TSIG record with a specific error for a message and adds it
+ * to the message.
+ * @param m The message
+ * @param error The error
+ * @param old If this message is a response, the TSIG from the request
+ */
+public void
+apply(Message m, int error, TSIGRecord old) {
+	Record r = generate(m, m.toWire(), error, old);
+	m.addRecord(r, Section.ADDITIONAL);
+	m.tsigState = Message.TSIG_SIGNED;
+}
+
+/**
+ * Generates a TSIG record for a message and adds it to the message
+ * @param m The message
+ * @param old If this message is a response, the TSIG from the request
+ */
+public void
+apply(Message m, TSIGRecord old) {
+	apply(m, Rcode.NOERROR, old);
+}
+
+/**
+ * Generates a TSIG record for a message and adds it to the message
+ * @param m The message
+ * @param old If this message is a response, the TSIG from the request
+ */
+public void
+applyStream(Message m, TSIGRecord old, boolean first) {
+	if (first) {
+		apply(m, old);
+		return;
+	}
+	Date timeSigned = new Date();
+	int fudge;
+	HMAC hmac = new HMAC(digest, digestBlockLength, key);
+
+	fudge = Options.intValue("tsigfudge");
+	if (fudge < 0 || fudge > 0x7FFF)
+		fudge = FUDGE;
+
+	DNSOutput out = new DNSOutput();
+	out.writeU16(old.getSignature().length);
+	hmac.update(out.toByteArray());
+	hmac.update(old.getSignature());
+
+	/* Digest the message */
+	hmac.update(m.toWire());
+
+	out = new DNSOutput();
+	long time = timeSigned.getTime() / 1000;
+	int timeHigh = (int) (time >> 32);
+	long timeLow = (time & 0xFFFFFFFFL);
+	out.writeU16(timeHigh);
+	out.writeU32(timeLow);
+	out.writeU16(fudge);
+
+	hmac.update(out.toByteArray());
+
+	byte [] signature = hmac.sign();
+	byte [] other = null;
+
+	Record r = new TSIGRecord(name, DClass.ANY, 0, alg, timeSigned, fudge,
+				  signature, m.getHeader().getID(),
+				  Rcode.NOERROR, other);
+	m.addRecord(r, Section.ADDITIONAL);
+	m.tsigState = Message.TSIG_SIGNED;
+}
+
+/**
+ * Verifies a TSIG record on an incoming message.  Since this is only called
+ * in the context where a TSIG is expected to be present, it is an error
+ * if one is not present.  After calling this routine, Message.isVerified() may
+ * be called on this message.
+ * @param m The message
+ * @param b An array containing the message in unparsed form.  This is
+ * necessary since TSIG signs the message in wire format, and we can't
+ * recreate the exact wire format (with the same name compression).
+ * @param length The length of the message in the array.
+ * @param old If this message is a response, the TSIG from the request
+ * @return The result of the verification (as an Rcode)
+ * @see Rcode
+ */
+public byte
+verify(Message m, byte [] b, int length, TSIGRecord old) {
+	m.tsigState = Message.TSIG_FAILED;
+	TSIGRecord tsig = m.getTSIG();
+	HMAC hmac = new HMAC(digest, digestBlockLength, key);
+	if (tsig == null)
+		return Rcode.FORMERR;
+
+	if (!tsig.getName().equals(name) || !tsig.getAlgorithm().equals(alg)) {
+		if (Options.check("verbose"))
+			System.err.println("BADKEY failure");
+		return Rcode.BADKEY;
+	}
+	long now = System.currentTimeMillis();
+	long then = tsig.getTimeSigned().getTime();
+	long fudge = tsig.getFudge();
+	if (Math.abs(now - then) > fudge * 1000) {
+		if (Options.check("verbose"))
+			System.err.println("BADTIME failure");
+		return Rcode.BADTIME;
+	}
+
+	if (old != null && tsig.getError() != Rcode.BADKEY &&
+	    tsig.getError() != Rcode.BADSIG)
+	{
+		DNSOutput out = new DNSOutput();
+		out.writeU16(old.getSignature().length);
+		hmac.update(out.toByteArray());
+		hmac.update(old.getSignature());
+	}
+	m.getHeader().decCount(Section.ADDITIONAL);
+	byte [] header = m.getHeader().toWire();
+	m.getHeader().incCount(Section.ADDITIONAL);
+	hmac.update(header);
+
+	int len = m.tsigstart - header.length;	
+	hmac.update(b, header.length, len);
+
+	DNSOutput out = new DNSOutput();
+	tsig.getName().toWireCanonical(out);
+	out.writeU16(tsig.dclass);
+	out.writeU32(tsig.ttl);
+	tsig.getAlgorithm().toWireCanonical(out);
+	long time = tsig.getTimeSigned().getTime() / 1000;
+	int timeHigh = (int) (time >> 32);
+	long timeLow = (time & 0xFFFFFFFFL);
+	out.writeU16(timeHigh);
+	out.writeU32(timeLow);
+	out.writeU16(tsig.getFudge());
+	out.writeU16(tsig.getError());
+	if (tsig.getOther() != null) {
+		out.writeU16(tsig.getOther().length);
+		out.writeByteArray(tsig.getOther());
+	} else {
+		out.writeU16(0);
+	}
+
+	hmac.update(out.toByteArray());
+
+	byte [] signature = tsig.getSignature();
+	int digestLength = hmac.digestLength();
+	int minDigestLength = digest.equals("md5") ? 10 : digestLength / 2;
+
+	if (signature.length > digestLength) {
+		if (Options.check("verbose"))
+			System.err.println("BADSIG: signature too long");
+		return Rcode.BADSIG;
+	} else if (signature.length < minDigestLength) {
+		if (Options.check("verbose"))
+			System.err.println("BADSIG: signature too short");
+		return Rcode.BADSIG;
+	} else if (!hmac.verify(signature, true)) {
+		if (Options.check("verbose"))
+			System.err.println("BADSIG: signature verification");
+		return Rcode.BADSIG;
+	}
+
+	m.tsigState = Message.TSIG_VERIFIED;
+	return Rcode.NOERROR;
+}
+
+/**
+ * Verifies a TSIG record on an incoming message.  Since this is only called
+ * in the context where a TSIG is expected to be present, it is an error
+ * if one is not present.  After calling this routine, Message.isVerified() may
+ * be called on this message.
+ * @param m The message
+ * @param b The message in unparsed form.  This is necessary since TSIG
+ * signs the message in wire format, and we can't recreate the exact wire
+ * format (with the same name compression).
+ * @param old If this message is a response, the TSIG from the request
+ * @return The result of the verification (as an Rcode)
+ * @see Rcode
+ */
+public int
+verify(Message m, byte [] b, TSIGRecord old) {
+	return verify(m, b, b.length, old);
+}
+
+/**
+ * Returns the maximum length of a TSIG record generated by this key.
+ * @see TSIGRecord
+ */
+public int
+recordLength() {
+	return (name.length() + 10 +
+		alg.length() +
+		8 +	// time signed, fudge
+		18 +	// 2 byte MAC length, 16 byte MAC
+		4 +	// original id, error
+		8);	// 2 byte error length, 6 byte max error field.
+}
+
+public static class StreamVerifier {
+	/**
+	 * A helper class for verifying multiple message responses.
+	 */
+
+	private TSIG key;
+	private HMAC verifier;
+	private int nresponses;
+	private int lastsigned;
+	private TSIGRecord lastTSIG;
+
+	/** Creates an object to verify a multiple message response */
+	public
+	StreamVerifier(TSIG tsig, TSIGRecord old) {
+		key = tsig;
+		verifier = new HMAC(key.digest, key.digestBlockLength, key.key);
+		nresponses = 0;
+		lastTSIG = old;
+	}
+
+	/**
+	 * Verifies a TSIG record on an incoming message that is part of a
+	 * multiple message response.
+	 * TSIG records must be present on the first and last messages, and
+	 * at least every 100 records in between.
+	 * After calling this routine, Message.isVerified() may be called on
+	 * this message.
+	 * @param m The message
+	 * @param b The message in unparsed form
+	 * @return The result of the verification (as an Rcode)
+	 * @see Rcode
+	 */
+	public int
+	verify(Message m, byte [] b) {
+		TSIGRecord tsig = m.getTSIG();
+	
+		nresponses++;
+
+		if (nresponses == 1) {
+			int result = key.verify(m, b, lastTSIG);
+			if (result == Rcode.NOERROR) {
+				byte [] signature = tsig.getSignature();
+				DNSOutput out = new DNSOutput();
+				out.writeU16(signature.length);
+				verifier.update(out.toByteArray());
+				verifier.update(signature);
+			}
+			lastTSIG = tsig;
+			return result;
+		}
+
+		if (tsig != null)
+			m.getHeader().decCount(Section.ADDITIONAL);
+		byte [] header = m.getHeader().toWire();
+		if (tsig != null)
+			m.getHeader().incCount(Section.ADDITIONAL);
+		verifier.update(header);
+
+		int len;
+		if (tsig == null)
+			len = b.length - header.length;
+		else
+			len = m.tsigstart - header.length;
+		verifier.update(b, header.length, len);
+
+		if (tsig != null) {
+			lastsigned = nresponses;
+			lastTSIG = tsig;
+		}
+		else {
+			boolean required = (nresponses - lastsigned >= 100);
+			if (required) {
+				m.tsigState = Message.TSIG_FAILED;
+				return Rcode.FORMERR;
+			} else {
+				m.tsigState = Message.TSIG_INTERMEDIATE;
+				return Rcode.NOERROR;
+			}
+		}
+
+		if (!tsig.getName().equals(key.name) ||
+		    !tsig.getAlgorithm().equals(key.alg))
+		{
+			if (Options.check("verbose"))
+				System.err.println("BADKEY failure");
+			m.tsigState = Message.TSIG_FAILED;
+			return Rcode.BADKEY;
+		}
+
+		DNSOutput out = new DNSOutput();
+		long time = tsig.getTimeSigned().getTime() / 1000;
+		int timeHigh = (int) (time >> 32);
+		long timeLow = (time & 0xFFFFFFFFL);
+		out.writeU16(timeHigh);
+		out.writeU32(timeLow);
+		out.writeU16(tsig.getFudge());
+		verifier.update(out.toByteArray());
+
+		if (verifier.verify(tsig.getSignature()) == false) {
+			if (Options.check("verbose"))
+				System.err.println("BADSIG failure");
+			m.tsigState = Message.TSIG_FAILED;
+			return Rcode.BADSIG;
+		}
+
+		verifier.clear();
+		out = new DNSOutput();
+		out.writeU16(tsig.getSignature().length);
+		verifier.update(out.toByteArray());
+		verifier.update(tsig.getSignature());
+
+		m.tsigState = Message.TSIG_VERIFIED;
+		return Rcode.NOERROR;
+	}
+}
+
+}
diff --git a/src/org/xbill/DNS/TSIGRecord.java b/src/org/xbill/DNS/TSIGRecord.java
new file mode 100644
index 0000000..c7ce9ed
--- /dev/null
+++ b/src/org/xbill/DNS/TSIGRecord.java
@@ -0,0 +1,220 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+import org.xbill.DNS.utils.*;
+
+/**
+ * Transaction Signature - this record is automatically generated by the
+ * resolver.  TSIG records provide transaction security between the
+ * sender and receiver of a message, using a shared key.
+ * @see Resolver
+ * @see TSIG
+ *
+ * @author Brian Wellington
+ */
+
+public class TSIGRecord extends Record {
+
+private static final long serialVersionUID = -88820909016649306L;
+
+private Name alg;
+private Date timeSigned;
+private int fudge;
+private byte [] signature;
+private int originalID;
+private int error;
+private byte [] other;
+
+TSIGRecord() {} 
+
+Record
+getObject() {
+	return new TSIGRecord();
+}
+
+/**
+ * Creates a TSIG Record from the given data.  This is normally called by
+ * the TSIG class
+ * @param alg The shared key's algorithm
+ * @param timeSigned The time that this record was generated
+ * @param fudge The fudge factor for time - if the time that the message is
+ * received is not in the range [now - fudge, now + fudge], the signature
+ * fails
+ * @param signature The signature
+ * @param originalID The message ID at the time of its generation
+ * @param error The extended error field.  Should be 0 in queries.
+ * @param other The other data field.  Currently used only in BADTIME
+ * responses.
+ * @see TSIG
+ */
+public
+TSIGRecord(Name name, int dclass, long ttl, Name alg, Date timeSigned,
+	   int fudge, byte [] signature, int originalID, int error,
+	   byte other[])
+{
+	super(name, Type.TSIG, dclass, ttl);
+	this.alg = checkName("alg", alg);
+	this.timeSigned = timeSigned;
+	this.fudge = checkU16("fudge", fudge);
+	this.signature = signature;
+	this.originalID = checkU16("originalID", originalID);
+	this.error = checkU16("error", error);
+	this.other = other;
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	alg = new Name(in);
+
+	long timeHigh = in.readU16();
+	long timeLow = in.readU32();
+	long time = (timeHigh << 32) + timeLow;
+	timeSigned = new Date(time * 1000);
+	fudge = in.readU16();
+
+	int sigLen = in.readU16();
+	signature = in.readByteArray(sigLen);
+
+	originalID = in.readU16();
+	error = in.readU16();
+
+	int otherLen = in.readU16();
+	if (otherLen > 0)
+		other = in.readByteArray(otherLen);
+	else
+		other = null;
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	throw st.exception("no text format defined for TSIG");
+}
+
+/** Converts rdata to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(alg);
+	sb.append(" ");
+	if (Options.check("multiline"))
+		sb.append("(\n\t");
+
+	sb.append (timeSigned.getTime() / 1000);
+	sb.append (" ");
+	sb.append (fudge);
+	sb.append (" ");
+	sb.append (signature.length);
+	if (Options.check("multiline")) {
+		sb.append ("\n");
+		sb.append (base64.formatString(signature, 64, "\t", false));
+	} else {
+		sb.append (" ");
+		sb.append (base64.toString(signature));
+	}
+	sb.append (" ");
+	sb.append (Rcode.TSIGstring(error));
+	sb.append (" ");
+	if (other == null)
+		sb.append (0);
+	else {
+		sb.append (other.length);
+		if (Options.check("multiline"))
+			sb.append("\n\n\n\t");
+		else
+			sb.append(" ");
+		if (error == Rcode.BADTIME) {
+			if (other.length != 6) {
+				sb.append("<invalid BADTIME other data>");
+			} else {
+				long time = ((long)(other[0] & 0xFF) << 40) +
+					    ((long)(other[1] & 0xFF) << 32) +
+					    ((other[2] & 0xFF) << 24) +
+					    ((other[3] & 0xFF) << 16) +
+					    ((other[4] & 0xFF) << 8) +
+					    ((other[5] & 0xFF)     );
+				sb.append("<server time: ");
+				sb.append(new Date(time * 1000));
+				sb.append(">");
+			}
+		} else {
+			sb.append("<");
+			sb.append(base64.toString(other));
+			sb.append(">");
+		}
+	}
+	if (Options.check("multiline"))
+		sb.append(" )");
+	return sb.toString();
+}
+
+/** Returns the shared key's algorithm */
+public Name
+getAlgorithm() {
+	return alg;
+}
+
+/** Returns the time that this record was generated */
+public Date
+getTimeSigned() {
+	return timeSigned;
+}
+
+/** Returns the time fudge factor */
+public int
+getFudge() {
+	return fudge;
+}
+
+/** Returns the signature */
+public byte []
+getSignature() {
+	return signature;
+}
+
+/** Returns the original message ID */
+public int
+getOriginalID() {
+	return originalID;
+}
+
+/** Returns the extended error */
+public int
+getError() {
+	return error;
+}
+
+/** Returns the other data */
+public byte []
+getOther() {
+	return other;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	alg.toWire(out, null, canonical);
+
+	long time = timeSigned.getTime() / 1000;
+	int timeHigh = (int) (time >> 32);
+	long timeLow = (time & 0xFFFFFFFFL);
+	out.writeU16(timeHigh);
+	out.writeU32(timeLow);
+	out.writeU16(fudge);
+
+	out.writeU16(signature.length);
+	out.writeByteArray(signature);
+
+	out.writeU16(originalID);
+	out.writeU16(error);
+
+	if (other != null) {
+		out.writeU16(other.length);
+		out.writeByteArray(other);
+	}
+	else
+		out.writeU16(0);
+}
+
+}
diff --git a/src/org/xbill/DNS/TTL.java b/src/org/xbill/DNS/TTL.java
new file mode 100644
index 0000000..01bf416
--- /dev/null
+++ b/src/org/xbill/DNS/TTL.java
@@ -0,0 +1,113 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Routines for parsing BIND-style TTL values.  These values consist of
+ * numbers followed by 1 letter units of time (W - week, D - day, H - hour,
+ * M - minute, S - second).
+ *
+ * @author Brian Wellington
+ */
+
+public final class TTL {
+
+public static final long MAX_VALUE = 0x7FFFFFFFL;
+
+private
+TTL() {}
+
+static void
+check(long i) {
+	if (i < 0 || i > MAX_VALUE)
+		throw new InvalidTTLException(i);
+}
+
+/**
+ * Parses a TTL-like value, which can either be expressed as a number or a
+ * BIND-style string with numbers and units.
+ * @param s The string representing the numeric value.
+ * @param clamp Whether to clamp values in the range [MAX_VALUE + 1, 2^32 -1]
+ * to MAX_VALUE.  This should be donw for TTLs, but not other values which
+ * can be expressed in this format.
+ * @return The value as a number of seconds
+ * @throws NumberFormatException The string was not in a valid TTL format.
+ */
+public static long
+parse(String s, boolean clamp) {
+	if (s == null || s.length() == 0 || !Character.isDigit(s.charAt(0)))
+		throw new NumberFormatException();
+	long value = 0;
+	long ttl = 0;
+	for (int i = 0; i < s.length(); i++) {
+		char c = s.charAt(i);
+		long oldvalue = value;
+		if (Character.isDigit(c)) {
+			value = (value * 10) + Character.getNumericValue(c);
+			if (value < oldvalue)
+				throw new NumberFormatException();
+		} else {
+			switch (Character.toUpperCase(c)) {
+				case 'W': value *= 7;
+				case 'D': value *= 24;
+				case 'H': value *= 60;
+				case 'M': value *= 60;
+				case 'S': break;
+				default:  throw new NumberFormatException();
+			}
+			ttl += value;
+			value = 0;
+			if (ttl > 0xFFFFFFFFL)
+				throw new NumberFormatException();
+		}
+	}
+	if (ttl == 0)
+		ttl = value;
+
+	if (ttl > 0xFFFFFFFFL)
+		throw new NumberFormatException();
+	else if (ttl > MAX_VALUE && clamp)
+		ttl = MAX_VALUE;
+	return ttl;
+}
+
+/**
+ * Parses a TTL, which can either be expressed as a number or a BIND-style
+ * string with numbers and units.
+ * @param s The string representing the TTL
+ * @return The TTL as a number of seconds
+ * @throws NumberFormatException The string was not in a valid TTL format.
+ */
+public static long
+parseTTL(String s) {
+	return parse(s, true);
+}
+
+public static String
+format(long ttl) {
+	TTL.check(ttl);
+	StringBuffer sb = new StringBuffer();
+	long secs, mins, hours, days, weeks;
+	secs = ttl % 60;
+	ttl /= 60;
+	mins = ttl % 60;
+	ttl /= 60;
+	hours = ttl % 24;
+	ttl /= 24;
+	days = ttl % 7;
+	ttl /= 7;
+	weeks = ttl;
+	if (weeks > 0)
+		sb.append(weeks + "W");
+	if (days > 0)
+		sb.append(days + "D");
+	if (hours > 0)
+		sb.append(hours + "H");
+	if (mins > 0)
+		sb.append(mins + "M");
+	if (secs > 0 || (weeks == 0 && days == 0 && hours == 0 && mins == 0))
+		sb.append(secs + "S");
+	return sb.toString();
+}
+
+}
diff --git a/src/org/xbill/DNS/TXTBase.java b/src/org/xbill/DNS/TXTBase.java
new file mode 100644
index 0000000..fd99bfb
--- /dev/null
+++ b/src/org/xbill/DNS/TXTBase.java
@@ -0,0 +1,123 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * Implements common functionality for the many record types whose format
+ * is a list of strings.
+ *
+ * @author Brian Wellington
+ */
+
+abstract class TXTBase extends Record {
+
+private static final long serialVersionUID = -4319510507246305931L;
+
+protected List strings;
+
+protected
+TXTBase() {}
+
+protected
+TXTBase(Name name, int type, int dclass, long ttl) {
+	super(name, type, dclass, ttl);
+}
+
+protected
+TXTBase(Name name, int type, int dclass, long ttl, List strings) {
+	super(name, type, dclass, ttl);
+	if (strings == null)
+		throw new IllegalArgumentException("strings must not be null");
+	this.strings = new ArrayList(strings.size());
+	Iterator it = strings.iterator();
+	try {
+		while (it.hasNext()) {
+			String s = (String) it.next();
+			this.strings.add(byteArrayFromString(s));
+		}
+	}
+	catch (TextParseException e) {
+		throw new IllegalArgumentException(e.getMessage());
+	}
+}
+
+protected
+TXTBase(Name name, int type, int dclass, long ttl, String string) {
+	this(name, type, dclass, ttl, Collections.singletonList(string));
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	strings = new ArrayList(2);
+	while (in.remaining() > 0) {
+		byte [] b = in.readCountedString();
+		strings.add(b);
+	}
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	strings = new ArrayList(2);
+	while (true) {
+		Tokenizer.Token t = st.get();
+		if (!t.isString())
+			break;
+		try {
+			strings.add(byteArrayFromString(t.value));
+		}
+		catch (TextParseException e) { 
+			throw st.exception(e.getMessage());
+		}
+
+	}
+	st.unget();
+}
+
+/** converts to a String */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	Iterator it = strings.iterator();
+	while (it.hasNext()) {
+		byte [] array = (byte []) it.next();
+		sb.append(byteArrayToString(array, true));
+		if (it.hasNext())
+			sb.append(" ");
+	}
+	return sb.toString();
+}
+
+/**
+ * Returns the text strings
+ * @return A list of Strings corresponding to the text strings.
+ */
+public List
+getStrings() {
+	List list = new ArrayList(strings.size());
+	for (int i = 0; i < strings.size(); i++)
+		list.add(byteArrayToString((byte []) strings.get(i), false));
+	return list;
+}
+
+/**
+ * Returns the text strings
+ * @return A list of byte arrays corresponding to the text strings.
+ */
+public List
+getStringsAsByteArrays() {
+	return strings;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	Iterator it = strings.iterator();
+	while (it.hasNext()) {
+		byte [] b = (byte []) it.next();
+		out.writeCountedString(b);
+	}
+}
+
+}
diff --git a/src/org/xbill/DNS/TXTRecord.java b/src/org/xbill/DNS/TXTRecord.java
new file mode 100644
index 0000000..ea5de04
--- /dev/null
+++ b/src/org/xbill/DNS/TXTRecord.java
@@ -0,0 +1,44 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.*;
+
+/**
+ * Text - stores text strings
+ *
+ * @author Brian Wellington
+ */
+
+public class TXTRecord extends TXTBase {
+
+private static final long serialVersionUID = -5780785764284221342L;
+
+TXTRecord() {}
+
+Record
+getObject() {
+	return new TXTRecord();
+}
+
+/**
+ * Creates a TXT Record from the given data
+ * @param strings The text strings
+ * @throws IllegalArgumentException One of the strings has invalid escapes
+ */
+public
+TXTRecord(Name name, int dclass, long ttl, List strings) {
+	super(name, Type.TXT, dclass, ttl, strings);
+}
+
+/**
+ * Creates a TXT Record from the given data
+ * @param string One text string
+ * @throws IllegalArgumentException The string has invalid escapes
+ */
+public
+TXTRecord(Name name, int dclass, long ttl, String string) {
+	super(name, Type.TXT, dclass, ttl, string);
+}
+
+}
diff --git a/src/org/xbill/DNS/TextParseException.java b/src/org/xbill/DNS/TextParseException.java
new file mode 100644
index 0000000..3b9a425
--- /dev/null
+++ b/src/org/xbill/DNS/TextParseException.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2002-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * An exception thrown when unable to parse text.
+ *
+ * @author Brian Wellington
+ */
+
+public class TextParseException extends IOException {
+
+public
+TextParseException() {
+	super();
+}
+
+public
+TextParseException(String s) {
+	super(s);
+}
+
+}
diff --git a/src/org/xbill/DNS/Tokenizer.java b/src/org/xbill/DNS/Tokenizer.java
new file mode 100644
index 0000000..bc637ab
--- /dev/null
+++ b/src/org/xbill/DNS/Tokenizer.java
@@ -0,0 +1,713 @@
+// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
+//
+// Copyright (C) 2003-2004 Nominum, Inc.
+// 
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+// OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.net.*;
+
+import org.xbill.DNS.utils.*;
+
+/**
+ * Tokenizer is used to parse DNS records and zones from text format,
+ *
+ * @author Brian Wellington
+ * @author Bob Halley
+ */
+
+public class Tokenizer {
+
+private static String delim = " \t\n;()\"";
+private static String quotes = "\"";
+
+/** End of file */
+public static final int EOF		= 0;
+
+/** End of line */
+public static final int EOL		= 1;
+
+/** Whitespace; only returned when wantWhitespace is set */
+public static final int WHITESPACE	= 2;
+
+/** An identifier (unquoted string) */
+public static final int IDENTIFIER	= 3;
+
+/** A quoted string */
+public static final int QUOTED_STRING	= 4;
+
+/** A comment; only returned when wantComment is set */
+public static final int COMMENT		= 5;
+
+private PushbackInputStream is;
+private boolean ungottenToken;
+private int multiline;
+private boolean quoting;
+private String delimiters;
+private Token current;
+private StringBuffer sb;
+private boolean wantClose;
+
+private String filename;
+private int line;
+
+public static class Token {
+	/** The type of token. */
+	public int type;
+
+	/** The value of the token, or null for tokens without values. */
+	public String value;
+
+	private
+	Token() {
+		type = -1;
+		value = null;
+	}
+
+	private Token
+	set(int type, StringBuffer value) {
+		if (type < 0)
+			throw new IllegalArgumentException();
+		this.type = type;
+		this.value = value == null ? null : value.toString();
+		return this;
+	}
+
+	/**
+	 * Converts the token to a string containing a representation useful
+	 * for debugging.
+	 */
+	public String
+	toString() {
+		switch (type) {
+		case EOF:
+			return "<eof>";
+		case EOL:
+			return "<eol>";
+		case WHITESPACE:
+			return "<whitespace>";
+		case IDENTIFIER:
+			return "<identifier: " + value + ">";
+		case QUOTED_STRING:
+			return "<quoted_string: " + value + ">";
+		case COMMENT:
+			return "<comment: " + value + ">";
+		default:
+			return "<unknown>";
+		}
+	}
+
+	/** Indicates whether this token contains a string. */
+	public boolean
+	isString() {
+		return (type == IDENTIFIER || type == QUOTED_STRING);
+	}
+
+	/** Indicates whether this token contains an EOL or EOF. */
+	public boolean
+	isEOL() {
+		return (type == EOL || type == EOF);
+	}
+}
+
+static class TokenizerException extends TextParseException {
+	String message;
+
+	public
+	TokenizerException(String filename, int line, String message) {
+		super(filename + ":" + line + ": " + message);
+		this.message = message;
+	}
+
+	public String
+	getBaseMessage() {
+		return message;
+	}
+}
+
+/**
+ * Creates a Tokenizer from an arbitrary input stream.
+ * @param is The InputStream to tokenize.
+ */
+public
+Tokenizer(InputStream is) {
+	if (!(is instanceof BufferedInputStream))
+		is = new BufferedInputStream(is);
+	this.is = new PushbackInputStream(is, 2);
+	ungottenToken = false;
+	multiline = 0;
+	quoting = false;
+	delimiters = delim;
+	current = new Token();
+	sb = new StringBuffer();
+	filename = "<none>";
+	line = 1;
+}
+
+/**
+ * Creates a Tokenizer from a string.
+ * @param s The String to tokenize.
+ */
+public
+Tokenizer(String s) {
+	this(new ByteArrayInputStream(s.getBytes()));
+}
+
+/**
+ * Creates a Tokenizer from a file.
+ * @param f The File to tokenize.
+ */
+public
+Tokenizer(File f) throws FileNotFoundException {
+	this(new FileInputStream(f));
+	wantClose = true;
+	filename = f.getName();
+}
+
+private int
+getChar() throws IOException {
+	int c = is.read();
+	if (c == '\r') {
+		int next = is.read();
+		if (next != '\n')
+			is.unread(next);
+		c = '\n';
+	}
+	if (c == '\n')
+		line++;
+	return c;
+}
+
+private void
+ungetChar(int c) throws IOException {
+	if (c == -1)
+		return;
+	is.unread(c);
+	if (c == '\n')
+		line--;
+}
+
+private int
+skipWhitespace() throws IOException {
+	int skipped = 0;
+	while (true) {
+		int c = getChar();
+		if (c != ' ' && c != '\t') {
+	                if (!(c == '\n' && multiline > 0)) {
+				ungetChar(c);
+				return skipped;
+			}
+		}
+		skipped++;
+	}
+}
+
+private void
+checkUnbalancedParens() throws TextParseException {
+	if (multiline > 0)
+		throw exception("unbalanced parentheses");
+}
+
+/**
+ * Gets the next token from a tokenizer.
+ * @param wantWhitespace If true, leading whitespace will be returned as a
+ * token.
+ * @param wantComment If true, comments are returned as tokens.
+ * @return The next token in the stream.
+ * @throws TextParseException The input was invalid.
+ * @throws IOException An I/O error occurred.
+ */
+public Token
+get(boolean wantWhitespace, boolean wantComment) throws IOException {
+	int type;
+	int c;
+
+	if (ungottenToken) {
+		ungottenToken = false;
+		if (current.type == WHITESPACE) {
+			if (wantWhitespace)
+				return current;
+		} else if (current.type == COMMENT) {
+			if (wantComment)
+				return current;
+		} else {
+			if (current.type == EOL)
+				line++;
+			return current;
+		}
+	}
+	int skipped = skipWhitespace();
+	if (skipped > 0 && wantWhitespace)
+		return current.set(WHITESPACE, null);
+	type = IDENTIFIER;
+	sb.setLength(0);
+	while (true) {
+		c = getChar();
+		if (c == -1 || delimiters.indexOf(c) != -1) {
+			if (c == -1) {
+				if (quoting)
+					throw exception("EOF in " +
+							"quoted string");
+				else if (sb.length() == 0)
+					return current.set(EOF, null);
+				else
+					return current.set(type, sb);
+			}
+			if (sb.length() == 0 && type != QUOTED_STRING) {
+				if (c == '(') {
+					multiline++;
+					skipWhitespace();
+					continue;
+				} else if (c == ')') {
+					if (multiline <= 0)
+						throw exception("invalid " +
+								"close " +
+								"parenthesis");
+					multiline--;
+					skipWhitespace();
+					continue;
+				} else if (c == '"') {
+					if (!quoting) {
+						quoting = true;
+						delimiters = quotes;
+						type = QUOTED_STRING;
+					} else {
+						quoting = false;
+						delimiters = delim;
+						skipWhitespace();
+					}
+					continue;
+				} else if (c == '\n') {
+					return current.set(EOL, null);
+				} else if (c == ';') {
+					while (true) {
+						c = getChar();
+						if (c == '\n' || c == -1)
+							break;
+						sb.append((char)c);
+					}
+					if (wantComment) {
+						ungetChar(c);
+						return current.set(COMMENT, sb);
+					} else if (c == -1 &&
+						   type != QUOTED_STRING)
+					{
+						checkUnbalancedParens();
+						return current.set(EOF, null);
+					} else if (multiline > 0) {
+						skipWhitespace();
+						sb.setLength(0);
+						continue;
+					} else
+						return current.set(EOL, null);
+				} else
+					throw new IllegalStateException();
+			} else
+				ungetChar(c);
+			break;
+		} else if (c == '\\') {
+			c = getChar();
+			if (c == -1)
+				throw exception("unterminated escape sequence");
+			sb.append('\\');
+		} else if (quoting && c == '\n') {
+			throw exception("newline in quoted string");
+		}
+		sb.append((char)c);
+	}
+	if (sb.length() == 0 && type != QUOTED_STRING) {
+		checkUnbalancedParens();
+		return current.set(EOF, null);
+	}
+	return current.set(type, sb);
+}
+
+/**
+ * Gets the next token from a tokenizer, ignoring whitespace and comments.
+ * @return The next token in the stream.
+ * @throws TextParseException The input was invalid.
+ * @throws IOException An I/O error occurred.
+ */
+public Token
+get() throws IOException {
+	return get(false, false);
+}
+
+/**
+ * Returns a token to the stream, so that it will be returned by the next call
+ * to get().
+ * @throws IllegalStateException There are already ungotten tokens.
+ */
+public void
+unget() {
+	if (ungottenToken)
+		throw new IllegalStateException
+				("Cannot unget multiple tokens");
+	if (current.type == EOL)
+		line--;
+	ungottenToken = true;
+}
+
+/**
+ * Gets the next token from a tokenizer and converts it to a string.
+ * @return The next token in the stream, as a string.
+ * @throws TextParseException The input was invalid or not a string.
+ * @throws IOException An I/O error occurred.
+ */
+public String
+getString() throws IOException {
+	Token next = get();
+	if (!next.isString()) {
+		throw exception("expected a string");
+	}
+	return next.value;
+}
+
+private String
+_getIdentifier(String expected) throws IOException {
+	Token next = get();
+	if (next.type != IDENTIFIER)
+		throw exception("expected " + expected);
+	return next.value;
+}
+
+/**
+ * Gets the next token from a tokenizer, ensures it is an unquoted string,
+ * and converts it to a string.
+ * @return The next token in the stream, as a string.
+ * @throws TextParseException The input was invalid or not an unquoted string.
+ * @throws IOException An I/O error occurred.
+ */
+public String
+getIdentifier() throws IOException {
+	return _getIdentifier("an identifier");
+}
+
+/**
+ * Gets the next token from a tokenizer and converts it to a long.
+ * @return The next token in the stream, as a long.
+ * @throws TextParseException The input was invalid or not a long.
+ * @throws IOException An I/O error occurred.
+ */
+public long
+getLong() throws IOException {
+	String next = _getIdentifier("an integer");
+	if (!Character.isDigit(next.charAt(0)))
+		throw exception("expected an integer");
+	try {
+		return Long.parseLong(next);
+	} catch (NumberFormatException e) {
+		throw exception("expected an integer");
+	}
+}
+
+/**
+ * Gets the next token from a tokenizer and converts it to an unsigned 32 bit
+ * integer.
+ * @return The next token in the stream, as an unsigned 32 bit integer.
+ * @throws TextParseException The input was invalid or not an unsigned 32
+ * bit integer.
+ * @throws IOException An I/O error occurred.
+ */
+public long
+getUInt32() throws IOException {
+	long l = getLong();
+	if (l < 0 || l > 0xFFFFFFFFL)
+		throw exception("expected an 32 bit unsigned integer");
+	return l;
+}
+
+/**
+ * Gets the next token from a tokenizer and converts it to an unsigned 16 bit
+ * integer.
+ * @return The next token in the stream, as an unsigned 16 bit integer.
+ * @throws TextParseException The input was invalid or not an unsigned 16
+ * bit integer.
+ * @throws IOException An I/O error occurred.
+ */
+public int
+getUInt16() throws IOException {
+	long l = getLong();
+	if (l < 0 || l > 0xFFFFL)
+		throw exception("expected an 16 bit unsigned integer");
+	return (int) l;
+}
+
+/**
+ * Gets the next token from a tokenizer and converts it to an unsigned 8 bit
+ * integer.
+ * @return The next token in the stream, as an unsigned 8 bit integer.
+ * @throws TextParseException The input was invalid or not an unsigned 8
+ * bit integer.
+ * @throws IOException An I/O error occurred.
+ */
+public int
+getUInt8() throws IOException {
+	long l = getLong();
+	if (l < 0 || l > 0xFFL)
+		throw exception("expected an 8 bit unsigned integer");
+	return (int) l;
+}
+
+/**
+ * Gets the next token from a tokenizer and parses it as a TTL.
+ * @return The next token in the stream, as an unsigned 32 bit integer.
+ * @throws TextParseException The input was not valid.
+ * @throws IOException An I/O error occurred.
+ * @see TTL
+ */
+public long
+getTTL() throws IOException {
+	String next = _getIdentifier("a TTL value");
+	try {
+		return TTL.parseTTL(next);
+	}
+	catch (NumberFormatException e) {
+		throw exception("expected a TTL value");
+	}
+}
+
+/**
+ * Gets the next token from a tokenizer and parses it as if it were a TTL.
+ * @return The next token in the stream, as an unsigned 32 bit integer.
+ * @throws TextParseException The input was not valid.
+ * @throws IOException An I/O error occurred.
+ * @see TTL
+ */
+public long
+getTTLLike() throws IOException {
+	String next = _getIdentifier("a TTL-like value");
+	try {
+		return TTL.parse(next, false);
+	}
+	catch (NumberFormatException e) {
+		throw exception("expected a TTL-like value");
+	}
+}
+
+/**
+ * Gets the next token from a tokenizer and converts it to a name.
+ * @param origin The origin to append to relative names.
+ * @return The next token in the stream, as a name.
+ * @throws TextParseException The input was invalid or not a valid name.
+ * @throws IOException An I/O error occurred.
+ * @throws RelativeNameException The parsed name was relative, even with the
+ * origin.
+ * @see Name
+ */
+public Name
+getName(Name origin) throws IOException {
+	String next = _getIdentifier("a name");
+	try {
+		Name name = Name.fromString(next, origin);
+		if (!name.isAbsolute())
+			throw new RelativeNameException(name);
+		return name;
+	}
+	catch (TextParseException e) {
+		throw exception(e.getMessage());
+	}
+}
+
+/**
+ * Gets the next token from a tokenizer and converts it to an IP Address.
+ * @param family The address family.
+ * @return The next token in the stream, as an InetAddress
+ * @throws TextParseException The input was invalid or not a valid address.
+ * @throws IOException An I/O error occurred.
+ * @see Address
+ */
+public InetAddress
+getAddress(int family) throws IOException {
+	String next = _getIdentifier("an address");
+	try {
+		return Address.getByAddress(next, family);
+	}
+	catch (UnknownHostException e) {
+		throw exception(e.getMessage());
+	}
+}
+
+/**
+ * Gets the next token from a tokenizer, which must be an EOL or EOF.
+ * @throws TextParseException The input was invalid or not an EOL or EOF token.
+ * @throws IOException An I/O error occurred.
+ */
+public void
+getEOL() throws IOException {
+	Token next = get();
+	if (next.type != EOL && next.type != EOF) {
+		throw exception("expected EOL or EOF");
+	}
+}
+
+/**
+ * Returns a concatenation of the remaining strings from a Tokenizer.
+ */
+private String
+remainingStrings() throws IOException {
+        StringBuffer buffer = null;
+        while (true) {
+                Tokenizer.Token t = get();
+                if (!t.isString())
+                        break;
+                if (buffer == null)
+                        buffer = new StringBuffer();
+                buffer.append(t.value);
+        }
+        unget();
+        if (buffer == null)
+                return null;
+        return buffer.toString();
+}
+
+/**
+ * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
+ * them together, and converts the base64 encoded data to a byte array.
+ * @param required If true, an exception will be thrown if no strings remain;
+ * otherwise null be be returned.
+ * @return The byte array containing the decoded strings, or null if there
+ * were no strings to decode.
+ * @throws TextParseException The input was invalid.
+ * @throws IOException An I/O error occurred.
+ */
+public byte []
+getBase64(boolean required) throws IOException {
+	String s = remainingStrings();
+	if (s == null) {
+		if (required)
+			throw exception("expected base64 encoded string");
+		else
+			return null;
+	}
+	byte [] array = base64.fromString(s);
+	if (array == null)
+		throw exception("invalid base64 encoding");
+	return array;
+}
+
+/**
+ * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
+ * them together, and converts the base64 encoded data to a byte array.
+ * @return The byte array containing the decoded strings, or null if there
+ * were no strings to decode.
+ * @throws TextParseException The input was invalid.
+ * @throws IOException An I/O error occurred.
+ */
+public byte []
+getBase64() throws IOException {
+	return getBase64(false);
+}
+
+/**
+ * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
+ * them together, and converts the hex encoded data to a byte array.
+ * @param required If true, an exception will be thrown if no strings remain;
+ * otherwise null be be returned.
+ * @return The byte array containing the decoded strings, or null if there
+ * were no strings to decode.
+ * @throws TextParseException The input was invalid.
+ * @throws IOException An I/O error occurred.
+ */
+public byte []
+getHex(boolean required) throws IOException {
+	String s = remainingStrings();
+	if (s == null) {
+		if (required)
+			throw exception("expected hex encoded string");
+		else
+			return null;
+	}
+	byte [] array = base16.fromString(s);
+	if (array == null)
+		throw exception("invalid hex encoding");
+	return array;
+}
+
+/**
+ * Gets the remaining string tokens until an EOL/EOF is seen, concatenates
+ * them together, and converts the hex encoded data to a byte array.
+ * @return The byte array containing the decoded strings, or null if there
+ * were no strings to decode.
+ * @throws TextParseException The input was invalid.
+ * @throws IOException An I/O error occurred.
+ */
+public byte []
+getHex() throws IOException {
+	return getHex(false);
+}
+
+/**
+ * Gets the next token from a tokenizer and decodes it as hex.
+ * @return The byte array containing the decoded string.
+ * @throws TextParseException The input was invalid.
+ * @throws IOException An I/O error occurred.
+ */
+public byte []
+getHexString() throws IOException {
+	String next = _getIdentifier("a hex string");
+	byte [] array = base16.fromString(next);
+	if (array == null)
+		throw exception("invalid hex encoding");
+	return array;
+}
+
+/**
+ * Gets the next token from a tokenizer and decodes it as base32.
+ * @param b32 The base32 context to decode with.
+ * @return The byte array containing the decoded string.
+ * @throws TextParseException The input was invalid.
+ * @throws IOException An I/O error occurred.
+ */
+public byte []
+getBase32String(base32 b32) throws IOException {
+	String next = _getIdentifier("a base32 string");
+	byte [] array = b32.fromString(next);
+	if (array == null)
+		throw exception("invalid base32 encoding");
+	return array;
+}
+
+/**
+ * Creates an exception which includes the current state in the error message
+ * @param s The error message to include.
+ * @return The exception to be thrown
+ */
+public TextParseException
+exception(String s) {
+	return new TokenizerException(filename, line, s);
+}
+
+/**
+ * Closes any files opened by this tokenizer.
+ */
+public void
+close() {
+	if (wantClose) {
+		try {
+			is.close();
+		}
+		catch (IOException e) {
+		}
+	}
+}
+
+protected void
+finalize() {
+	close();
+}
+
+}
diff --git a/src/org/xbill/DNS/Type.java b/src/org/xbill/DNS/Type.java
new file mode 100644
index 0000000..df84e83
--- /dev/null
+++ b/src/org/xbill/DNS/Type.java
@@ -0,0 +1,361 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.util.HashMap;
+
+/**
+ * Constants and functions relating to DNS Types
+ *
+ * @author Brian Wellington
+ */
+
+public final class Type {
+
+/** Address */
+public static final int A		= 1;
+
+/** Name server */
+public static final int NS		= 2;
+
+/** Mail destination */
+public static final int MD		= 3;
+
+/** Mail forwarder */
+public static final int MF		= 4;
+
+/** Canonical name (alias) */
+public static final int CNAME		= 5;
+
+/** Start of authority */
+public static final int SOA		= 6;
+
+/** Mailbox domain name */
+public static final int MB		= 7;
+
+/** Mail group member */
+public static final int MG		= 8;
+
+/** Mail rename name */
+public static final int MR		= 9;
+
+/** Null record */
+public static final int NULL		= 10;
+
+/** Well known services */
+public static final int WKS		= 11;
+
+/** Domain name pointer */
+public static final int PTR		= 12;
+
+/** Host information */
+public static final int HINFO		= 13;
+
+/** Mailbox information */
+public static final int MINFO		= 14;
+
+/** Mail routing information */
+public static final int MX		= 15;
+
+/** Text strings */
+public static final int TXT		= 16;
+
+/** Responsible person */
+public static final int RP		= 17;
+
+/** AFS cell database */
+public static final int AFSDB		= 18;
+
+/** X.25 calling address */
+public static final int X25		= 19;
+
+/** ISDN calling address */
+public static final int ISDN		= 20;
+
+/** Router */
+public static final int RT		= 21;
+
+/** NSAP address */
+public static final int NSAP		= 22;
+
+/** Reverse NSAP address (deprecated) */
+public static final int NSAP_PTR	= 23;
+
+/** Signature */
+public static final int SIG		= 24;
+
+/** Key */
+public static final int KEY		= 25;
+
+/** X.400 mail mapping */
+public static final int PX		= 26;
+
+/** Geographical position (withdrawn) */
+public static final int GPOS		= 27;
+
+/** IPv6 address */
+public static final int AAAA		= 28;
+
+/** Location */
+public static final int LOC		= 29;
+
+/** Next valid name in zone */
+public static final int NXT		= 30;
+
+/** Endpoint identifier */
+public static final int EID		= 31;
+
+/** Nimrod locator */
+public static final int NIMLOC		= 32;
+
+/** Server selection */
+public static final int SRV		= 33;
+
+/** ATM address */
+public static final int ATMA		= 34;
+
+/** Naming authority pointer */
+public static final int NAPTR		= 35;
+
+/** Key exchange */
+public static final int KX		= 36;
+
+/** Certificate */
+public static final int CERT		= 37;
+
+/** IPv6 address (experimental) */
+public static final int A6		= 38;
+
+/** Non-terminal name redirection */
+public static final int DNAME		= 39;
+
+/** Options - contains EDNS metadata */
+public static final int OPT		= 41;
+
+/** Address Prefix List */
+public static final int APL		= 42;
+
+/** Delegation Signer */
+public static final int DS		= 43;
+
+/** SSH Key Fingerprint */
+public static final int SSHFP		= 44;
+
+/** IPSEC key */
+public static final int IPSECKEY	= 45;
+
+/** Resource Record Signature */
+public static final int RRSIG		= 46;
+
+/** Next Secure Name */
+public static final int NSEC		= 47;
+
+/** DNSSEC Key */
+public static final int DNSKEY		= 48;
+
+/** Dynamic Host Configuration Protocol (DHCP) ID */
+public static final int DHCID		= 49;
+
+/** Next SECure, 3rd edition, RFC 5155 */
+public static final int NSEC3		= 50;
+
+/** Next SECure PARAMeter, RFC 5155 */
+public static final int NSEC3PARAM	= 51;
+
+/** Transport Layer Security Authentication, draft-ietf-dane-protocol-23 */
+public static final int TLSA		= 52;
+
+/** Sender Policy Framework (experimental) */
+public static final int SPF		= 99;
+
+/** Transaction key - used to compute a shared secret or exchange a key */
+public static final int TKEY		= 249;
+
+/** Transaction signature */
+public static final int TSIG		= 250;
+
+/** Incremental zone transfer */
+public static final int IXFR		= 251;
+
+/** Zone transfer */
+public static final int AXFR		= 252;
+
+/** Transfer mailbox records */
+public static final int MAILB		= 253;
+
+/** Transfer mail agent records */
+public static final int MAILA		= 254;
+
+/** Matches any type */
+public static final int ANY		= 255;
+
+/** DNSSEC Lookaside Validation, RFC 4431 . */
+public static final int DLV		= 32769;
+
+
+private static class TypeMnemonic extends Mnemonic {
+	private HashMap objects;
+
+	public
+	TypeMnemonic() {
+		super("Type", CASE_UPPER);
+		setPrefix("TYPE");
+		objects = new HashMap();
+	}
+
+	public void
+	add(int val, String str, Record proto) {
+		super.add(val, str);
+		objects.put(Mnemonic.toInteger(val), proto);
+	}
+	
+	public void
+	check(int val) {
+		Type.check(val);
+	}
+
+	public Record
+	getProto(int val) {
+		check(val);
+		return (Record) objects.get(toInteger(val));
+	}
+}
+
+private static TypeMnemonic types = new TypeMnemonic();
+
+static {
+	types.add(A, "A", new ARecord());
+	types.add(NS, "NS", new NSRecord());
+	types.add(MD, "MD", new MDRecord());
+	types.add(MF, "MF", new MFRecord());
+	types.add(CNAME, "CNAME", new CNAMERecord());
+	types.add(SOA, "SOA", new SOARecord());
+	types.add(MB, "MB", new MBRecord());
+	types.add(MG, "MG", new MGRecord());
+	types.add(MR, "MR", new MRRecord());
+	types.add(NULL, "NULL", new NULLRecord());
+	types.add(WKS, "WKS", new WKSRecord());
+	types.add(PTR, "PTR", new PTRRecord());
+	types.add(HINFO, "HINFO", new HINFORecord());
+	types.add(MINFO, "MINFO", new MINFORecord());
+	types.add(MX, "MX", new MXRecord());
+	types.add(TXT, "TXT", new TXTRecord());
+	types.add(RP, "RP", new RPRecord());
+	types.add(AFSDB, "AFSDB", new AFSDBRecord());
+	types.add(X25, "X25", new X25Record());
+	types.add(ISDN, "ISDN", new ISDNRecord());
+	types.add(RT, "RT", new RTRecord());
+	types.add(NSAP, "NSAP", new NSAPRecord());
+	types.add(NSAP_PTR, "NSAP-PTR", new NSAP_PTRRecord());
+	types.add(SIG, "SIG", new SIGRecord());
+	types.add(KEY, "KEY", new KEYRecord());
+	types.add(PX, "PX", new PXRecord());
+	types.add(GPOS, "GPOS", new GPOSRecord());
+	types.add(AAAA, "AAAA", new AAAARecord());
+	types.add(LOC, "LOC", new LOCRecord());
+	types.add(NXT, "NXT", new NXTRecord());
+	types.add(EID, "EID");
+	types.add(NIMLOC, "NIMLOC");
+	types.add(SRV, "SRV", new SRVRecord());
+	types.add(ATMA, "ATMA");
+	types.add(NAPTR, "NAPTR", new NAPTRRecord());
+	types.add(KX, "KX", new KXRecord());
+	types.add(CERT, "CERT", new CERTRecord());
+	types.add(A6, "A6", new A6Record());
+	types.add(DNAME, "DNAME", new DNAMERecord());
+	types.add(OPT, "OPT", new OPTRecord());
+	types.add(APL, "APL", new APLRecord());
+	types.add(DS, "DS", new DSRecord());
+	types.add(SSHFP, "SSHFP", new SSHFPRecord());
+	types.add(IPSECKEY, "IPSECKEY", new IPSECKEYRecord());
+	types.add(RRSIG, "RRSIG", new RRSIGRecord());
+	types.add(NSEC, "NSEC", new NSECRecord());
+	types.add(DNSKEY, "DNSKEY", new DNSKEYRecord());
+	types.add(DHCID, "DHCID", new DHCIDRecord());
+	types.add(NSEC3, "NSEC3", new NSEC3Record());
+	types.add(NSEC3PARAM, "NSEC3PARAM", new NSEC3PARAMRecord());
+	types.add(TLSA, "TLSA", new TLSARecord());
+	types.add(SPF, "SPF", new SPFRecord());
+	types.add(TKEY, "TKEY", new TKEYRecord());
+	types.add(TSIG, "TSIG", new TSIGRecord());
+	types.add(IXFR, "IXFR");
+	types.add(AXFR, "AXFR");
+	types.add(MAILB, "MAILB");
+	types.add(MAILA, "MAILA");
+	types.add(ANY, "ANY");
+	types.add(DLV, "DLV", new DLVRecord());
+}
+
+private
+Type() {
+}
+
+/**
+ * Checks that a numeric Type is valid.
+ * @throws InvalidTypeException The type is out of range.
+ */
+public static void
+check(int val) {
+	if (val < 0 || val > 0xFFFF)
+		throw new InvalidTypeException(val);
+}
+
+/**
+ * Converts a numeric Type into a String
+ * @param val The type value.
+ * @return The canonical string representation of the type
+ * @throws InvalidTypeException The type is out of range.
+ */
+public static String
+string(int val) {
+	return types.getText(val);
+}
+
+/**
+ * Converts a String representation of an Type into its numeric value.
+ * @param s The string representation of the type
+ * @param numberok Whether a number will be accepted or not.
+ * @return The type code, or -1 on error.
+ */
+public static int
+value(String s, boolean numberok) {
+	int val = types.getValue(s);
+	if (val == -1 && numberok) {
+		val = types.getValue("TYPE" + s);
+	}
+	return val;
+}
+
+/**
+ * Converts a String representation of an Type into its numeric value
+ * @return The type code, or -1 on error.
+ */
+public static int
+value(String s) {
+	return value(s, false);
+}
+
+static Record
+getProto(int val) {
+	return types.getProto(val);
+}
+
+/** Is this type valid for a record (a non-meta type)? */
+public static boolean
+isRR(int type) {
+	switch (type) {
+		case OPT:
+		case TKEY:
+		case TSIG:
+		case IXFR:
+		case AXFR:
+		case MAILB:
+		case MAILA:
+		case ANY:
+			return false;
+		default:
+			return true;
+	}
+}
+
+}
diff --git a/src/org/xbill/DNS/TypeBitmap.java b/src/org/xbill/DNS/TypeBitmap.java
new file mode 100644
index 0000000..628cc35
--- /dev/null
+++ b/src/org/xbill/DNS/TypeBitmap.java
@@ -0,0 +1,147 @@
+// Copyright (c) 2004-2009 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * Routines for deal with the lists of types found in NSEC/NSEC3 records.
+ *
+ * @author Brian Wellington
+ */
+
+import java.io.*;
+import java.util.*;
+
+final class TypeBitmap implements Serializable {
+
+private static final long serialVersionUID = -125354057735389003L;
+
+private TreeSet types;
+
+private
+TypeBitmap() {
+	types = new TreeSet();
+}
+
+public
+TypeBitmap(int [] array) {
+	this();
+	for (int i = 0; i < array.length; i++) {
+		Type.check(array[i]);
+		types.add(new Integer(array[i]));
+	}
+}
+
+public
+TypeBitmap(DNSInput in) throws WireParseException {
+	this();
+	int lastbase = -1;
+	while (in.remaining() > 0) {
+		if (in.remaining() < 2)
+			throw new WireParseException
+				("invalid bitmap descriptor");
+		int mapbase = in.readU8();
+		if (mapbase < lastbase)
+			throw new WireParseException("invalid ordering");
+		int maplength = in.readU8();
+		if (maplength > in.remaining())
+			throw new WireParseException("invalid bitmap");
+		for (int i = 0; i < maplength; i++) {
+			int current = in.readU8();
+			if (current == 0)
+				continue;
+			for (int j = 0; j < 8; j++) {
+				if ((current & (1 << (7 - j))) == 0)
+					continue;
+				int typecode = mapbase * 256 + + i * 8 + j;
+				types.add(Mnemonic.toInteger(typecode));
+			}
+		}
+	}
+}
+
+public
+TypeBitmap(Tokenizer st) throws IOException {
+	this();
+	while (true) {
+		Tokenizer.Token t = st.get();
+		if (!t.isString())
+			break;
+		int typecode = Type.value(t.value);
+		if (typecode < 0) {
+			throw st.exception("Invalid type: " + t.value);
+		}
+		types.add(Mnemonic.toInteger(typecode));
+	}
+	st.unget();
+}
+
+public int []
+toArray() {
+	int [] array = new int[types.size()];
+	int n = 0;
+	for (Iterator it = types.iterator(); it.hasNext(); )
+		array[n++] = ((Integer)it.next()).intValue();
+	return array;
+}
+
+public String
+toString() {
+	StringBuffer sb = new StringBuffer();
+	for (Iterator it = types.iterator(); it.hasNext(); ) {
+		int t = ((Integer)it.next()).intValue();
+		sb.append(Type.string(t));
+		if (it.hasNext())
+			sb.append(' ');
+	}
+	return sb.toString();
+}
+
+private static void
+mapToWire(DNSOutput out, TreeSet map, int mapbase) {
+	int arraymax = (((Integer)map.last()).intValue()) & 0xFF;
+	int arraylength = (arraymax / 8) + 1;
+	int [] array = new int[arraylength];
+	out.writeU8(mapbase);
+	out.writeU8(arraylength);
+	for (Iterator it = map.iterator(); it.hasNext(); ) {
+		int typecode = ((Integer)it.next()).intValue();
+		array[(typecode & 0xFF) / 8] |= (1 << ( 7 - typecode % 8));
+	}
+	for (int j = 0; j < arraylength; j++)
+		out.writeU8(array[j]);
+}
+
+public void
+toWire(DNSOutput out) {
+	if (types.size() == 0)
+		return;
+
+	int mapbase = -1;
+	TreeSet map = new TreeSet();
+
+	for (Iterator it = types.iterator(); it.hasNext(); ) {
+		int t = ((Integer)it.next()).intValue();
+		int base = t >> 8;
+		if (base != mapbase) {
+			if (map.size() > 0) {
+				mapToWire(out, map, mapbase);
+				map.clear();
+			}
+			mapbase = base;
+		}
+			map.add(new Integer(t));
+	}
+	mapToWire(out, map, mapbase);
+}
+
+public boolean
+empty() {
+	return types.isEmpty();
+}
+
+public boolean
+contains(int typecode) {
+	return types.contains(Mnemonic.toInteger(typecode));
+}
+
+}
diff --git a/src/org/xbill/DNS/U16NameBase.java b/src/org/xbill/DNS/U16NameBase.java
new file mode 100644
index 0000000..df3c836
--- /dev/null
+++ b/src/org/xbill/DNS/U16NameBase.java
@@ -0,0 +1,75 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * Implements common functionality for the many record types whose format
+ * is an unsigned 16 bit integer followed by a name.
+ *
+ * @author Brian Wellington
+ */
+
+abstract class U16NameBase extends Record {
+
+private static final long serialVersionUID = -8315884183112502995L;
+
+protected int u16Field;
+protected Name nameField;
+
+protected
+U16NameBase() {}
+
+protected
+U16NameBase(Name name, int type, int dclass, long ttl) {
+	super(name, type, dclass, ttl);
+}
+
+protected
+U16NameBase(Name name, int type, int dclass, long ttl, int u16Field,
+	    String u16Description, Name nameField, String nameDescription)
+{
+	super(name, type, dclass, ttl);
+	this.u16Field = checkU16(u16Description, u16Field);
+	this.nameField = checkName(nameDescription, nameField);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	u16Field = in.readU16();
+	nameField = new Name(in);
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	u16Field = st.getUInt16();
+	nameField = st.getName(origin);
+}
+
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(u16Field);
+	sb.append(" ");
+	sb.append(nameField);
+	return sb.toString();
+}
+
+protected int
+getU16Field() {
+	return u16Field;
+}
+
+protected Name
+getNameField() {
+	return nameField;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeU16(u16Field);
+	nameField.toWire(out, null, canonical);
+}
+
+}
diff --git a/src/org/xbill/DNS/UDPClient.java b/src/org/xbill/DNS/UDPClient.java
new file mode 100644
index 0000000..e752ce4
--- /dev/null
+++ b/src/org/xbill/DNS/UDPClient.java
@@ -0,0 +1,164 @@
+// Copyright (c) 2005 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.net.*;
+import java.security.SecureRandom;
+import java.nio.*;
+import java.nio.channels.*;
+
+final class UDPClient extends Client {
+
+private static final int EPHEMERAL_START = 1024;
+private static final int EPHEMERAL_STOP  = 65535;
+private static final int EPHEMERAL_RANGE  = EPHEMERAL_STOP - EPHEMERAL_START;
+
+private static SecureRandom prng = new SecureRandom();
+private static volatile boolean prng_initializing = true;
+
+/*
+ * On some platforms (Windows), the SecureRandom module initialization involves
+ * a call to InetAddress.getLocalHost(), which can end up here if using a
+ * dnsjava name service provider.
+ *
+ * This can cause problems in multiple ways.
+ *   - If the SecureRandom seed generation process calls into here, and this
+ *     module attempts to seed the local SecureRandom object, the thread hangs.
+ *   - If something else calls InetAddress.getLocalHost(), and that causes this
+ *     module to seed the local SecureRandom object, the thread hangs.
+ *
+ * To avoid both of these, check at initialization time to see if InetAddress
+ * is in the call chain.  If so, initialize the SecureRandom object in a new
+ * thread, and disable port randomization until it completes.
+ */
+static {
+	new Thread(new Runnable() {
+			   public void run() {
+			   int n = prng.nextInt();
+			   prng_initializing = false;
+		   }}).start();
+}
+
+private boolean bound = false;
+
+public
+UDPClient(long endTime) throws IOException {
+	super(DatagramChannel.open(), endTime);
+}
+
+private void
+bind_random(InetSocketAddress addr) throws IOException
+{
+	if (prng_initializing) {
+		try {
+			Thread.sleep(2);
+		}
+		catch (InterruptedException e) {
+		}
+		if (prng_initializing)
+			return;
+	}
+
+	DatagramChannel channel = (DatagramChannel) key.channel();
+	InetSocketAddress temp;
+
+	for (int i = 0; i < 1024; i++) {
+		try {
+			int port = prng.nextInt(EPHEMERAL_RANGE) +
+				   EPHEMERAL_START;
+			if (addr != null)
+				temp = new InetSocketAddress(addr.getAddress(),
+							     port);
+			else
+				temp = new InetSocketAddress(port);
+			channel.socket().bind(temp);
+			bound = true;
+			return;
+		}
+		catch (SocketException e) {
+		}
+	}
+}
+
+void
+bind(SocketAddress addr) throws IOException {
+	if (addr == null ||
+	    (addr instanceof InetSocketAddress &&
+	     ((InetSocketAddress)addr).getPort() == 0))
+	{
+		bind_random((InetSocketAddress) addr);
+		if (bound)
+			return;
+	}
+
+	if (addr != null) {
+		DatagramChannel channel = (DatagramChannel) key.channel();
+		channel.socket().bind(addr);
+		bound = true;
+	}
+}
+
+void
+connect(SocketAddress addr) throws IOException {
+	if (!bound)
+		bind(null);
+	DatagramChannel channel = (DatagramChannel) key.channel();
+	channel.connect(addr);
+}
+
+void
+send(byte [] data) throws IOException {
+	DatagramChannel channel = (DatagramChannel) key.channel();
+	verboseLog("UDP write", data);
+	channel.write(ByteBuffer.wrap(data));
+}
+
+byte []
+recv(int max) throws IOException {
+	DatagramChannel channel = (DatagramChannel) key.channel();
+	byte [] temp = new byte[max];
+	key.interestOps(SelectionKey.OP_READ);
+	try {
+		while (!key.isReadable())
+			blockUntil(key, endTime);
+	}
+	finally {
+		if (key.isValid())
+			key.interestOps(0);
+	}
+	long ret = channel.read(ByteBuffer.wrap(temp));
+	if (ret <= 0)
+		throw new EOFException();
+	int len = (int) ret;
+	byte [] data = new byte[len];
+	System.arraycopy(temp, 0, data, 0, len);
+	verboseLog("UDP read", data);
+	return data;
+}
+
+static byte []
+sendrecv(SocketAddress local, SocketAddress remote, byte [] data, int max,
+	 long endTime)
+throws IOException
+{
+	UDPClient client = new UDPClient(endTime);
+	try {
+		client.bind(local);
+		client.connect(remote);
+		client.send(data);
+		return client.recv(max);
+	}
+	finally {
+		client.cleanup();
+	}
+}
+
+static byte []
+sendrecv(SocketAddress addr, byte [] data, int max, long endTime)
+throws IOException
+{
+	return sendrecv(null, addr, data, max, endTime);
+}
+
+}
diff --git a/src/org/xbill/DNS/UNKRecord.java b/src/org/xbill/DNS/UNKRecord.java
new file mode 100644
index 0000000..91c9697
--- /dev/null
+++ b/src/org/xbill/DNS/UNKRecord.java
@@ -0,0 +1,54 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * A class implementing Records of unknown and/or unimplemented types.  This
+ * class can only be initialized using static Record initializers.
+ *
+ * @author Brian Wellington
+ */
+
+public class UNKRecord extends Record {
+
+private static final long serialVersionUID = -4193583311594626915L;
+
+private byte [] data;
+
+UNKRecord() {}
+
+Record
+getObject() {
+	return new UNKRecord();
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	data = in.readByteArray();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	throw st.exception("invalid unknown RR encoding");
+}
+
+/** Converts this Record to the String "unknown format" */
+String
+rrToString() {
+	return unknownToString(data);
+}
+
+/** Returns the contents of this record. */
+public byte []
+getData() { 
+	return data;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeByteArray(data);
+}
+
+}
diff --git a/src/org/xbill/DNS/Update.java b/src/org/xbill/DNS/Update.java
new file mode 100644
index 0000000..02a920b
--- /dev/null
+++ b/src/org/xbill/DNS/Update.java
@@ -0,0 +1,300 @@
+// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * A helper class for constructing dynamic DNS (DDNS) update messages.
+ *
+ * @author Brian Wellington
+ */
+
+public class Update extends Message {
+
+private Name origin;
+private int dclass;
+
+/**
+ * Creates an update message.
+ * @param zone The name of the zone being updated.
+ * @param dclass The class of the zone being updated.
+ */
+public
+Update(Name zone, int dclass) {
+	super();
+	if (!zone.isAbsolute())
+		throw new RelativeNameException(zone);
+	DClass.check(dclass);
+        getHeader().setOpcode(Opcode.UPDATE);
+	Record soa = Record.newRecord(zone, Type.SOA, DClass.IN);
+	addRecord(soa, Section.QUESTION);
+	this.origin = zone;
+	this.dclass = dclass;
+}
+
+/**
+ * Creates an update message.  The class is assumed to be IN.
+ * @param zone The name of the zone being updated.
+ */
+public
+Update(Name zone) {
+	this(zone, DClass.IN);
+}
+
+private void
+newPrereq(Record rec) {
+	addRecord(rec, Section.PREREQ);
+}
+
+private void
+newUpdate(Record rec) {
+	addRecord(rec, Section.UPDATE);
+}
+
+/**
+ * Inserts a prerequisite that the specified name exists; that is, there
+ * exist records with the given name in the zone.
+ */
+public void
+present(Name name) {
+	newPrereq(Record.newRecord(name, Type.ANY, DClass.ANY, 0));
+}
+
+/**
+ * Inserts a prerequisite that the specified rrset exists; that is, there
+ * exist records with the given name and type in the zone.
+ */
+public void
+present(Name name, int type) {
+	newPrereq(Record.newRecord(name, type, DClass.ANY, 0));
+}
+
+/**
+ * Parses a record from the string, and inserts a prerequisite that the
+ * record exists.  Due to the way value-dependent prequisites work, the
+ * condition that must be met is that the set of all records with the same 
+ * and type in the update message must be identical to the set of all records
+ * with that name and type on the server.
+ * @throws IOException The record could not be parsed.
+ */
+public void
+present(Name name, int type, String record) throws IOException {
+	newPrereq(Record.fromString(name, type, dclass, 0, record, origin));
+}
+
+/**
+ * Parses a record from the tokenizer, and inserts a prerequisite that the
+ * record exists.  Due to the way value-dependent prequisites work, the
+ * condition that must be met is that the set of all records with the same 
+ * and type in the update message must be identical to the set of all records
+ * with that name and type on the server.
+ * @throws IOException The record could not be parsed.
+ */
+public void
+present(Name name, int type, Tokenizer tokenizer) throws IOException {
+	newPrereq(Record.fromString(name, type, dclass, 0, tokenizer, origin));
+}
+
+/**
+ * Inserts a prerequisite that the specified record exists.  Due to the way
+ * value-dependent prequisites work, the condition that must be met is that
+ * the set of all records with the same and type in the update message must
+ * be identical to the set of all records with that name and type on the server.
+ */
+public void
+present(Record record) {
+	newPrereq(record);
+}
+
+/**
+ * Inserts a prerequisite that the specified name does not exist; that is,
+ * there are no records with the given name in the zone.
+ */
+public void
+absent(Name name) {
+	newPrereq(Record.newRecord(name, Type.ANY, DClass.NONE, 0));
+}
+
+/**
+ * Inserts a prerequisite that the specified rrset does not exist; that is,
+ * there are no records with the given name and type in the zone.
+ */
+public void
+absent(Name name, int type) {
+	newPrereq(Record.newRecord(name, type, DClass.NONE, 0));
+}
+
+/**
+ * Parses a record from the string, and indicates that the record
+ * should be inserted into the zone.
+ * @throws IOException The record could not be parsed.
+ */
+public void
+add(Name name, int type, long ttl, String record) throws IOException {
+	newUpdate(Record.fromString(name, type, dclass, ttl, record, origin));
+}
+
+/**
+ * Parses a record from the tokenizer, and indicates that the record
+ * should be inserted into the zone.
+ * @throws IOException The record could not be parsed.
+ */
+public void
+add(Name name, int type, long ttl, Tokenizer tokenizer) throws IOException {
+	newUpdate(Record.fromString(name, type, dclass, ttl, tokenizer,
+				    origin));
+}
+
+/**
+ * Indicates that the record should be inserted into the zone.
+ */
+public void
+add(Record record) {
+	newUpdate(record);
+}
+
+/**
+ * Indicates that the records should be inserted into the zone.
+ */
+public void
+add(Record [] records) {
+	for (int i = 0; i < records.length; i++)
+		add(records[i]);
+}
+
+/**
+ * Indicates that all of the records in the rrset should be inserted into the
+ * zone.
+ */
+public void
+add(RRset rrset) {
+	for (Iterator it = rrset.rrs(); it.hasNext(); )
+		add((Record) it.next());
+}
+
+/**
+ * Indicates that all records with the given name should be deleted from
+ * the zone.
+ */
+public void
+delete(Name name) {
+	newUpdate(Record.newRecord(name, Type.ANY, DClass.ANY, 0));
+}
+
+/**
+ * Indicates that all records with the given name and type should be deleted
+ * from the zone.
+ */
+public void
+delete(Name name, int type) {
+	newUpdate(Record.newRecord(name, type, DClass.ANY, 0));
+}
+
+/**
+ * Parses a record from the string, and indicates that the record
+ * should be deleted from the zone.
+ * @throws IOException The record could not be parsed.
+ */
+public void
+delete(Name name, int type, String record) throws IOException {
+	newUpdate(Record.fromString(name, type, DClass.NONE, 0, record,
+				    origin));
+}
+
+/**
+ * Parses a record from the tokenizer, and indicates that the record
+ * should be deleted from the zone.
+ * @throws IOException The record could not be parsed.
+ */
+public void
+delete(Name name, int type, Tokenizer tokenizer) throws IOException {
+	newUpdate(Record.fromString(name, type, DClass.NONE, 0, tokenizer,
+				    origin));
+}
+
+/**
+ * Indicates that the specified record should be deleted from the zone.
+ */
+public void
+delete(Record record) {
+	newUpdate(record.withDClass(DClass.NONE, 0));
+}
+
+/**
+ * Indicates that the records should be deleted from the zone.
+ */
+public void
+delete(Record [] records) {
+	for (int i = 0; i < records.length; i++)
+		delete(records[i]);
+}
+
+/**
+ * Indicates that all of the records in the rrset should be deleted from the
+ * zone.
+ */
+public void
+delete(RRset rrset) {
+	for (Iterator it = rrset.rrs(); it.hasNext(); )
+		delete((Record) it.next());
+}
+
+/**
+ * Parses a record from the string, and indicates that the record
+ * should be inserted into the zone replacing any other records with the
+ * same name and type.
+ * @throws IOException The record could not be parsed.
+ */
+public void
+replace(Name name, int type, long ttl, String record) throws IOException {
+	delete(name, type);
+	add(name, type, ttl, record);
+}
+
+/**
+ * Parses a record from the tokenizer, and indicates that the record
+ * should be inserted into the zone replacing any other records with the
+ * same name and type.
+ * @throws IOException The record could not be parsed.
+ */
+public void
+replace(Name name, int type, long ttl, Tokenizer tokenizer) throws IOException
+{
+	delete(name, type);
+	add(name, type, ttl, tokenizer);
+}
+
+/**
+ * Indicates that the record should be inserted into the zone replacing any
+ * other records with the same name and type.
+ */
+public void
+replace(Record record) {
+	delete(record.getName(), record.getType());
+	add(record);
+}
+
+/**
+ * Indicates that the records should be inserted into the zone replacing any
+ * other records with the same name and type as each one.
+ */
+public void
+replace(Record [] records) {
+	for (int i = 0; i < records.length; i++)
+		replace(records[i]);
+}
+
+/**
+ * Indicates that all of the records in the rrset should be inserted into the
+ * zone replacing any other records with the same name and type.
+ */
+public void
+replace(RRset rrset) {
+	delete(rrset.getName(), rrset.getType());
+	for (Iterator it = rrset.rrs(); it.hasNext(); )
+		add((Record) it.next());
+}
+
+}
diff --git a/src/org/xbill/DNS/WKSRecord.java b/src/org/xbill/DNS/WKSRecord.java
new file mode 100644
index 0000000..10b61be
--- /dev/null
+++ b/src/org/xbill/DNS/WKSRecord.java
@@ -0,0 +1,719 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.net.*;
+import java.io.*;
+import java.util.*;
+
+/**
+ * Well Known Services - Lists services offered by this host.
+ *
+ * @author Brian Wellington
+ */
+
+public class WKSRecord extends Record {
+
+private static final long serialVersionUID = -9104259763909119805L;
+
+public static class Protocol {
+	/**
+	 * IP protocol identifiers.  This is basically copied out of RFC 1010.
+	 */
+
+	private Protocol() {}
+
+	/** Internet Control Message */
+	public static final int ICMP = 1;
+
+	/** Internet Group Management */
+	public static final int IGMP = 2;
+
+	/** Gateway-to-Gateway */
+	public static final int GGP = 3;
+
+	/** Stream */
+	public static final int ST = 5;
+
+	/** Transmission Control */
+	public static final int TCP = 6;
+
+	/** UCL */
+	public static final int UCL = 7;
+
+	/** Exterior Gateway Protocol */
+	public static final int EGP = 8;
+
+	/** any private interior gateway */
+	public static final int IGP = 9;
+
+	/** BBN RCC Monitoring */
+	public static final int BBN_RCC_MON = 10;
+
+	/** Network Voice Protocol */
+	public static final int NVP_II = 11;
+
+	/** PUP */
+	public static final int PUP = 12;
+
+	/** ARGUS */
+	public static final int ARGUS = 13;
+
+	/** EMCON */
+	public static final int EMCON = 14;
+
+	/** Cross Net Debugger */
+	public static final int XNET = 15;
+
+	/** Chaos */
+	public static final int CHAOS = 16;
+
+	/** User Datagram */
+	public static final int UDP = 17;
+
+	/** Multiplexing */
+	public static final int MUX = 18;
+
+	/** DCN Measurement Subsystems */
+	public static final int DCN_MEAS = 19;
+
+	/** Host Monitoring */
+	public static final int HMP = 20;
+
+	/** Packet Radio Measurement */
+	public static final int PRM = 21;
+
+	/** XEROX NS IDP */
+	public static final int XNS_IDP = 22;
+
+	/** Trunk-1 */
+	public static final int TRUNK_1 = 23;
+
+	/** Trunk-2 */
+	public static final int TRUNK_2 = 24;
+
+	/** Leaf-1 */
+	public static final int LEAF_1 = 25;
+
+	/** Leaf-2 */
+	public static final int LEAF_2 = 26;
+
+	/** Reliable Data Protocol */
+	public static final int RDP = 27;
+
+	/** Internet Reliable Transaction */
+	public static final int IRTP = 28;
+
+	/** ISO Transport Protocol Class 4 */
+	public static final int ISO_TP4 = 29;
+
+	/** Bulk Data Transfer Protocol */
+	public static final int NETBLT = 30;
+
+	/** MFE Network Services Protocol */
+	public static final int MFE_NSP = 31;
+
+	/** MERIT Internodal Protocol */
+	public static final int MERIT_INP = 32;
+
+	/** Sequential Exchange Protocol */
+	public static final int SEP = 33;
+
+	/** CFTP */
+	public static final int CFTP = 62;
+
+	/** SATNET and Backroom EXPAK */
+	public static final int SAT_EXPAK = 64;
+
+	/** MIT Subnet Support */
+	public static final int MIT_SUBNET = 65;
+
+	/** MIT Remote Virtual Disk Protocol */
+	public static final int RVD = 66;
+
+	/** Internet Pluribus Packet Core */
+	public static final int IPPC = 67;
+
+	/** SATNET Monitoring */
+	public static final int SAT_MON = 69;
+
+	/** Internet Packet Core Utility */
+	public static final int IPCV = 71;
+
+	/** Backroom SATNET Monitoring */
+	public static final int BR_SAT_MON = 76;
+
+	/** WIDEBAND Monitoring */
+	public static final int WB_MON = 78;
+
+	/** WIDEBAND EXPAK */
+	public static final int WB_EXPAK = 79;
+
+	private static Mnemonic protocols = new Mnemonic("IP protocol",
+							 Mnemonic.CASE_LOWER);
+
+	static {
+		protocols.setMaximum(0xFF);
+		protocols.setNumericAllowed(true);
+
+		protocols.add(ICMP, "icmp");
+		protocols.add(IGMP, "igmp");
+		protocols.add(GGP, "ggp");
+		protocols.add(ST, "st");
+		protocols.add(TCP, "tcp");
+		protocols.add(UCL, "ucl");
+		protocols.add(EGP, "egp");
+		protocols.add(IGP, "igp");
+		protocols.add(BBN_RCC_MON, "bbn-rcc-mon");
+		protocols.add(NVP_II, "nvp-ii");
+		protocols.add(PUP, "pup");
+		protocols.add(ARGUS, "argus");
+		protocols.add(EMCON, "emcon");
+		protocols.add(XNET, "xnet");
+		protocols.add(CHAOS, "chaos");
+		protocols.add(UDP, "udp");
+		protocols.add(MUX, "mux");
+		protocols.add(DCN_MEAS, "dcn-meas");
+		protocols.add(HMP, "hmp");
+		protocols.add(PRM, "prm");
+		protocols.add(XNS_IDP, "xns-idp");
+		protocols.add(TRUNK_1, "trunk-1");
+		protocols.add(TRUNK_2, "trunk-2");
+		protocols.add(LEAF_1, "leaf-1");
+		protocols.add(LEAF_2, "leaf-2");
+		protocols.add(RDP, "rdp");
+		protocols.add(IRTP, "irtp");
+		protocols.add(ISO_TP4, "iso-tp4");
+		protocols.add(NETBLT, "netblt");
+		protocols.add(MFE_NSP, "mfe-nsp");
+		protocols.add(MERIT_INP, "merit-inp");
+		protocols.add(SEP, "sep");
+		protocols.add(CFTP, "cftp");
+		protocols.add(SAT_EXPAK, "sat-expak");
+		protocols.add(MIT_SUBNET, "mit-subnet");
+		protocols.add(RVD, "rvd");
+		protocols.add(IPPC, "ippc");
+		protocols.add(SAT_MON, "sat-mon");
+		protocols.add(IPCV, "ipcv");
+		protocols.add(BR_SAT_MON, "br-sat-mon");
+		protocols.add(WB_MON, "wb-mon");
+		protocols.add(WB_EXPAK, "wb-expak");
+	}
+
+	/**
+	 * Converts an IP protocol value into its textual representation
+	 */
+	public static String
+	string(int type) {
+		return protocols.getText(type);
+	}
+
+	/**
+	 * Converts a textual representation of an IP protocol into its
+	 * numeric code.  Integers in the range 0..255 are also accepted.
+	 * @param s The textual representation of the protocol
+	 * @return The protocol code, or -1 on error.
+	 */
+	public static int
+	value(String s) {
+		return protocols.getValue(s);
+	}
+}
+
+public static class Service {
+	/**
+	 * TCP/UDP services.  This is basically copied out of RFC 1010,
+	 * with MIT-ML-DEV removed, as it is not unique, and the description
+	 * of SWIFT-RVF fixed.
+	 */
+
+	private Service() {}
+
+	/** Remote Job Entry */
+	public static final int RJE = 5;
+
+	/** Echo */
+	public static final int ECHO = 7;
+
+	/** Discard */
+	public static final int DISCARD = 9;
+
+	/** Active Users */
+	public static final int USERS = 11;
+
+	/** Daytime */
+	public static final int DAYTIME = 13;
+
+	/** Quote of the Day */
+	public static final int QUOTE = 17;
+
+	/** Character Generator */
+	public static final int CHARGEN = 19;
+
+	/** File Transfer [Default Data] */
+	public static final int FTP_DATA = 20;
+
+	/** File Transfer [Control] */
+	public static final int FTP = 21;
+
+	/** Telnet */
+	public static final int TELNET = 23;
+
+	/** Simple Mail Transfer */
+	public static final int SMTP = 25;
+
+	/** NSW User System FE */
+	public static final int NSW_FE = 27;
+
+	/** MSG ICP */
+	public static final int MSG_ICP = 29;
+
+	/** MSG Authentication */
+	public static final int MSG_AUTH = 31;
+
+	/** Display Support Protocol */
+	public static final int DSP = 33;
+
+	/** Time */
+	public static final int TIME = 37;
+
+	/** Resource Location Protocol */
+	public static final int RLP = 39;
+
+	/** Graphics */
+	public static final int GRAPHICS = 41;
+
+	/** Host Name Server */
+	public static final int NAMESERVER = 42;
+
+	/** Who Is */
+	public static final int NICNAME = 43;
+
+	/** MPM FLAGS Protocol */
+	public static final int MPM_FLAGS = 44;
+
+	/** Message Processing Module [recv] */
+	public static final int MPM = 45;
+
+	/** MPM [default send] */
+	public static final int MPM_SND = 46;
+
+	/** NI FTP */
+	public static final int NI_FTP = 47;
+
+	/** Login Host Protocol */
+	public static final int LOGIN = 49;
+
+	/** IMP Logical Address Maintenance */
+	public static final int LA_MAINT = 51;
+
+	/** Domain Name Server */
+	public static final int DOMAIN = 53;
+
+	/** ISI Graphics Language */
+	public static final int ISI_GL = 55;
+
+	/** NI MAIL */
+	public static final int NI_MAIL = 61;
+
+	/** VIA Systems - FTP */
+	public static final int VIA_FTP = 63;
+
+	/** TACACS-Database Service */
+	public static final int TACACS_DS = 65;
+
+	/** Bootstrap Protocol Server */
+	public static final int BOOTPS = 67;
+
+	/** Bootstrap Protocol Client */
+	public static final int BOOTPC = 68;
+
+	/** Trivial File Transfer */
+	public static final int TFTP = 69;
+
+	/** Remote Job Service */
+	public static final int NETRJS_1 = 71;
+
+	/** Remote Job Service */
+	public static final int NETRJS_2 = 72;
+
+	/** Remote Job Service */
+	public static final int NETRJS_3 = 73;
+
+	/** Remote Job Service */
+	public static final int NETRJS_4 = 74;
+
+	/** Finger */
+	public static final int FINGER = 79;
+
+	/** HOSTS2 Name Server */
+	public static final int HOSTS2_NS = 81;
+
+	/** SU/MIT Telnet Gateway */
+	public static final int SU_MIT_TG = 89;
+
+	/** MIT Dover Spooler */
+	public static final int MIT_DOV = 91;
+
+	/** Device Control Protocol */
+	public static final int DCP = 93;
+
+	/** SUPDUP */
+	public static final int SUPDUP = 95;
+
+	/** Swift Remote Virtual File Protocol */
+	public static final int SWIFT_RVF = 97;
+
+	/** TAC News */
+	public static final int TACNEWS = 98;
+
+	/** Metagram Relay */
+	public static final int METAGRAM = 99;
+
+	/** NIC Host Name Server */
+	public static final int HOSTNAME = 101;
+
+	/** ISO-TSAP */
+	public static final int ISO_TSAP = 102;
+
+	/** X400 */
+	public static final int X400 = 103;
+
+	/** X400-SND */
+	public static final int X400_SND = 104;
+
+	/** Mailbox Name Nameserver */
+	public static final int CSNET_NS = 105;
+
+	/** Remote Telnet Service */
+	public static final int RTELNET = 107;
+
+	/** Post Office Protocol - Version 2 */
+	public static final int POP_2 = 109;
+
+	/** SUN Remote Procedure Call */
+	public static final int SUNRPC = 111;
+
+	/** Authentication Service */
+	public static final int AUTH = 113;
+
+	/** Simple File Transfer Protocol */
+	public static final int SFTP = 115;
+
+	/** UUCP Path Service */
+	public static final int UUCP_PATH = 117;
+
+	/** Network News Transfer Protocol */
+	public static final int NNTP = 119;
+
+	/** HYDRA Expedited Remote Procedure */
+	public static final int ERPC = 121;
+
+	/** Network Time Protocol */
+	public static final int NTP = 123;
+
+	/** Locus PC-Interface Net Map Server */
+	public static final int LOCUS_MAP = 125;
+
+	/** Locus PC-Interface Conn Server */
+	public static final int LOCUS_CON = 127;
+
+	/** Password Generator Protocol */
+	public static final int PWDGEN = 129;
+
+	/** CISCO FNATIVE */
+	public static final int CISCO_FNA = 130;
+
+	/** CISCO TNATIVE */
+	public static final int CISCO_TNA = 131;
+
+	/** CISCO SYSMAINT */
+	public static final int CISCO_SYS = 132;
+
+	/** Statistics Service */
+	public static final int STATSRV = 133;
+
+	/** INGRES-NET Service */
+	public static final int INGRES_NET = 134;
+
+	/** Location Service */
+	public static final int LOC_SRV = 135;
+
+	/** PROFILE Naming System */
+	public static final int PROFILE = 136;
+
+	/** NETBIOS Name Service */
+	public static final int NETBIOS_NS = 137;
+
+	/** NETBIOS Datagram Service */
+	public static final int NETBIOS_DGM = 138;
+
+	/** NETBIOS Session Service */
+	public static final int NETBIOS_SSN = 139;
+
+	/** EMFIS Data Service */
+	public static final int EMFIS_DATA = 140;
+
+	/** EMFIS Control Service */
+	public static final int EMFIS_CNTL = 141;
+
+	/** Britton-Lee IDM */
+	public static final int BL_IDM = 142;
+
+	/** Survey Measurement */
+	public static final int SUR_MEAS = 243;
+
+	/** LINK */
+	public static final int LINK = 245;
+
+	private static Mnemonic services = new Mnemonic("TCP/UDP service",
+							Mnemonic.CASE_LOWER);
+
+	static {
+		services.setMaximum(0xFFFF);
+		services.setNumericAllowed(true);
+
+		services.add(RJE, "rje");
+		services.add(ECHO, "echo");
+		services.add(DISCARD, "discard");
+		services.add(USERS, "users");
+		services.add(DAYTIME, "daytime");
+		services.add(QUOTE, "quote");
+		services.add(CHARGEN, "chargen");
+		services.add(FTP_DATA, "ftp-data");
+		services.add(FTP, "ftp");
+		services.add(TELNET, "telnet");
+		services.add(SMTP, "smtp");
+		services.add(NSW_FE, "nsw-fe");
+		services.add(MSG_ICP, "msg-icp");
+		services.add(MSG_AUTH, "msg-auth");
+		services.add(DSP, "dsp");
+		services.add(TIME, "time");
+		services.add(RLP, "rlp");
+		services.add(GRAPHICS, "graphics");
+		services.add(NAMESERVER, "nameserver");
+		services.add(NICNAME, "nicname");
+		services.add(MPM_FLAGS, "mpm-flags");
+		services.add(MPM, "mpm");
+		services.add(MPM_SND, "mpm-snd");
+		services.add(NI_FTP, "ni-ftp");
+		services.add(LOGIN, "login");
+		services.add(LA_MAINT, "la-maint");
+		services.add(DOMAIN, "domain");
+		services.add(ISI_GL, "isi-gl");
+		services.add(NI_MAIL, "ni-mail");
+		services.add(VIA_FTP, "via-ftp");
+		services.add(TACACS_DS, "tacacs-ds");
+		services.add(BOOTPS, "bootps");
+		services.add(BOOTPC, "bootpc");
+		services.add(TFTP, "tftp");
+		services.add(NETRJS_1, "netrjs-1");
+		services.add(NETRJS_2, "netrjs-2");
+		services.add(NETRJS_3, "netrjs-3");
+		services.add(NETRJS_4, "netrjs-4");
+		services.add(FINGER, "finger");
+		services.add(HOSTS2_NS, "hosts2-ns");
+		services.add(SU_MIT_TG, "su-mit-tg");
+		services.add(MIT_DOV, "mit-dov");
+		services.add(DCP, "dcp");
+		services.add(SUPDUP, "supdup");
+		services.add(SWIFT_RVF, "swift-rvf");
+		services.add(TACNEWS, "tacnews");
+		services.add(METAGRAM, "metagram");
+		services.add(HOSTNAME, "hostname");
+		services.add(ISO_TSAP, "iso-tsap");
+		services.add(X400, "x400");
+		services.add(X400_SND, "x400-snd");
+		services.add(CSNET_NS, "csnet-ns");
+		services.add(RTELNET, "rtelnet");
+		services.add(POP_2, "pop-2");
+		services.add(SUNRPC, "sunrpc");
+		services.add(AUTH, "auth");
+		services.add(SFTP, "sftp");
+		services.add(UUCP_PATH, "uucp-path");
+		services.add(NNTP, "nntp");
+		services.add(ERPC, "erpc");
+		services.add(NTP, "ntp");
+		services.add(LOCUS_MAP, "locus-map");
+		services.add(LOCUS_CON, "locus-con");
+		services.add(PWDGEN, "pwdgen");
+		services.add(CISCO_FNA, "cisco-fna");
+		services.add(CISCO_TNA, "cisco-tna");
+		services.add(CISCO_SYS, "cisco-sys");
+		services.add(STATSRV, "statsrv");
+		services.add(INGRES_NET, "ingres-net");
+		services.add(LOC_SRV, "loc-srv");
+		services.add(PROFILE, "profile");
+		services.add(NETBIOS_NS, "netbios-ns");
+		services.add(NETBIOS_DGM, "netbios-dgm");
+		services.add(NETBIOS_SSN, "netbios-ssn");
+		services.add(EMFIS_DATA, "emfis-data");
+		services.add(EMFIS_CNTL, "emfis-cntl");
+		services.add(BL_IDM, "bl-idm");
+		services.add(SUR_MEAS, "sur-meas");
+		services.add(LINK, "link");
+	}
+
+	/**
+	 * Converts a TCP/UDP service port number into its textual
+	 * representation.
+	 */
+	public static String
+	string(int type) {
+		return services.getText(type);
+	}
+
+	/**
+	 * Converts a textual representation of a TCP/UDP service into its
+	 * port number.  Integers in the range 0..65535 are also accepted.
+	 * @param s The textual representation of the service.
+	 * @return The port number, or -1 on error.
+	 */
+	public static int
+	value(String s) {
+		return services.getValue(s);
+	}
+}
+private byte [] address;
+private int protocol;
+private int [] services;
+
+WKSRecord() {}
+
+Record
+getObject() {
+	return new WKSRecord();
+}
+
+/**
+ * Creates a WKS Record from the given data
+ * @param address The IP address
+ * @param protocol The IP protocol number
+ * @param services An array of supported services, represented by port number.
+ */
+public
+WKSRecord(Name name, int dclass, long ttl, InetAddress address, int protocol,
+	  int [] services)
+{
+	super(name, Type.WKS, dclass, ttl);
+	if (Address.familyOf(address) != Address.IPv4)
+		throw new IllegalArgumentException("invalid IPv4 address");
+	this.address = address.getAddress();
+	this.protocol = checkU8("protocol", protocol);
+	for (int i = 0; i < services.length; i++) {
+		checkU16("service", services[i]);
+	}
+	this.services = new int[services.length];
+	System.arraycopy(services, 0, this.services, 0, services.length);
+	Arrays.sort(this.services);
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	address = in.readByteArray(4);
+	protocol = in.readU8();
+	byte [] array = in.readByteArray();
+	List list = new ArrayList();
+	for (int i = 0; i < array.length; i++) {
+		for (int j = 0; j < 8; j++) {
+			int octet = array[i] & 0xFF;
+			if ((octet & (1 << (7 - j))) != 0) {
+				list.add(new Integer(i * 8 + j));
+			}
+		}
+	}
+	services = new int[list.size()];
+	for (int i = 0; i < list.size(); i++) {
+		services[i] = ((Integer) list.get(i)).intValue();
+	}
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	String s = st.getString();
+	address = Address.toByteArray(s, Address.IPv4);
+	if (address == null)
+		throw st.exception("invalid address");
+
+	s = st.getString();
+	protocol = Protocol.value(s);
+	if (protocol < 0) {
+		throw st.exception("Invalid IP protocol: " + s);
+	}
+
+	List list = new ArrayList();
+	while (true) {
+		Tokenizer.Token t = st.get();
+		if (!t.isString())
+			break;
+		int service = Service.value(t.value);
+		if (service < 0) {
+			throw st.exception("Invalid TCP/UDP service: " +
+					   t.value);
+		}
+		list.add(new Integer(service));
+	}
+	st.unget();
+	services = new int[list.size()];
+	for (int i = 0; i < list.size(); i++) {
+		services[i] = ((Integer) list.get(i)).intValue();
+	}
+}
+
+/**
+ * Converts rdata to a String
+ */
+String
+rrToString() {
+	StringBuffer sb = new StringBuffer();
+	sb.append(Address.toDottedQuad(address));
+	sb.append(" ");
+	sb.append(protocol);
+	for (int i = 0; i < services.length; i++) {
+		sb.append(" " + services[i]);
+	}
+	return sb.toString();
+}
+
+/**
+ * Returns the IP address.
+ */
+public InetAddress
+getAddress() {
+	try {
+		return InetAddress.getByAddress(address);
+	} catch (UnknownHostException e) {
+		return null;
+	}
+}
+
+/**
+ * Returns the IP protocol.
+ */
+public int
+getProtocol() {
+	return protocol;
+}
+
+/**
+ * Returns the services provided by the host on the specified address.
+ */
+public int []
+getServices() {
+	return services;
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeByteArray(address);
+	out.writeU8(protocol);
+	int highestPort = services[services.length - 1];
+	byte [] array = new byte[highestPort / 8 + 1];
+	for (int i = 0; i < services.length; i++) {
+		int port = services[i];
+		array[port / 8] |= (1 << (7 - port % 8));
+	}
+	out.writeByteArray(array);
+}
+
+}
diff --git a/src/org/xbill/DNS/WireParseException.java b/src/org/xbill/DNS/WireParseException.java
new file mode 100644
index 0000000..2842731
--- /dev/null
+++ b/src/org/xbill/DNS/WireParseException.java
@@ -0,0 +1,31 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * An exception thrown when a DNS message is invalid.
+ *
+ * @author Brian Wellington
+ */
+
+public class WireParseException extends IOException {
+
+public
+WireParseException() {
+	super();
+}
+
+public
+WireParseException(String s) {
+	super(s);
+}
+
+public
+WireParseException(String s, Throwable cause) {
+	super(s);
+	initCause(cause);
+}
+
+}
diff --git a/src/org/xbill/DNS/X25Record.java b/src/org/xbill/DNS/X25Record.java
new file mode 100644
index 0000000..1349a1e
--- /dev/null
+++ b/src/org/xbill/DNS/X25Record.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+
+/**
+ * X25 - identifies the PSDN (Public Switched Data Network) address in the
+ * X.121 numbering plan associated with a name.
+ *
+ * @author Brian Wellington
+ */
+
+public class X25Record extends Record {
+
+private static final long serialVersionUID = 4267576252335579764L;
+
+private byte [] address;
+
+X25Record() {}
+
+Record
+getObject() {
+	return new X25Record();
+}
+
+private static final byte []
+checkAndConvertAddress(String address) {
+	int length = address.length();
+	byte [] out = new byte [length];
+	for (int i = 0; i < length; i++) {
+		char c = address.charAt(i);
+		if (!Character.isDigit(c))
+			return null;
+		out[i] = (byte) c;
+	}
+	return out;
+}
+
+/**
+ * Creates an X25 Record from the given data
+ * @param address The X.25 PSDN address.
+ * @throws IllegalArgumentException The address is not a valid PSDN address.
+ */
+public
+X25Record(Name name, int dclass, long ttl, String address) {
+	super(name, Type.X25, dclass, ttl);
+	this.address = checkAndConvertAddress(address);
+	if (this.address == null) {
+		throw new IllegalArgumentException("invalid PSDN address " +
+						   address);
+	}
+}
+
+void
+rrFromWire(DNSInput in) throws IOException {
+	address = in.readCountedString();
+}
+
+void
+rdataFromString(Tokenizer st, Name origin) throws IOException {
+	String addr = st.getString();
+	this.address = checkAndConvertAddress(addr);
+	if (this.address == null)
+		throw st.exception("invalid PSDN address " + addr);
+}
+
+/**
+ * Returns the X.25 PSDN address.
+ */
+public String
+getAddress() {
+	return byteArrayToString(address, false);
+}
+
+void
+rrToWire(DNSOutput out, Compression c, boolean canonical) {
+	out.writeCountedString(address);
+}
+
+String
+rrToString() {
+	return byteArrayToString(address, true);
+}
+
+}
diff --git a/src/org/xbill/DNS/Zone.java b/src/org/xbill/DNS/Zone.java
new file mode 100644
index 0000000..866be77
--- /dev/null
+++ b/src/org/xbill/DNS/Zone.java
@@ -0,0 +1,559 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.util.*;
+
+/**
+ * A DNS Zone.  This encapsulates all data related to a Zone, and provides
+ * convenient lookup methods.
+ *
+ * @author Brian Wellington
+ */
+
+public class Zone implements Serializable {
+
+private static final long serialVersionUID = -9220510891189510942L;
+
+/** A primary zone */
+public static final int PRIMARY = 1;
+
+/** A secondary zone */
+public static final int SECONDARY = 2;
+
+private Map data;
+private Name origin;
+private Object originNode;
+private int dclass = DClass.IN;
+private RRset NS;
+private SOARecord SOA;
+private boolean hasWild;
+
+class ZoneIterator implements Iterator {
+	private Iterator zentries;
+	private RRset [] current;
+	private int count;
+	private boolean wantLastSOA;
+
+	ZoneIterator(boolean axfr) {
+		synchronized (Zone.this) {
+			zentries = data.entrySet().iterator();
+		}
+		wantLastSOA = axfr;
+		RRset [] sets = allRRsets(originNode);
+		current = new RRset[sets.length];
+		for (int i = 0, j = 2; i < sets.length; i++) {
+			int type = sets[i].getType();
+			if (type == Type.SOA)
+				current[0] = sets[i];
+			else if (type == Type.NS)
+				current[1] = sets[i];
+			else
+				current[j++] = sets[i];
+		}
+	}
+
+	public boolean
+	hasNext() {
+		return (current != null || wantLastSOA);
+	}
+
+	public Object
+	next() {
+		if (!hasNext()) {
+			throw new NoSuchElementException();
+		}
+		if (current == null) {
+			wantLastSOA = false;
+			return oneRRset(originNode, Type.SOA);
+		}
+		Object set = current[count++];
+		if (count == current.length) {
+			current = null;
+			while (zentries.hasNext()) {
+				Map.Entry entry = (Map.Entry) zentries.next();
+				if (entry.getKey().equals(origin))
+					continue;
+				RRset [] sets = allRRsets(entry.getValue());
+				if (sets.length == 0)
+					continue;
+				current = sets;
+				count = 0;
+				break;
+			}
+		}
+		return set;
+	}
+
+	public void
+	remove() {
+		throw new UnsupportedOperationException();
+	}
+}
+
+private void
+validate() throws IOException {
+	originNode = exactName(origin);
+	if (originNode == null)
+		throw new IOException(origin + ": no data specified");
+
+	RRset rrset = oneRRset(originNode, Type.SOA);
+	if (rrset == null || rrset.size() != 1)
+		throw new IOException(origin +
+				      ": exactly 1 SOA must be specified");
+	Iterator it = rrset.rrs();
+	SOA = (SOARecord) it.next();
+
+	NS = oneRRset(originNode, Type.NS);
+	if (NS == null)
+		throw new IOException(origin + ": no NS set specified");
+}
+
+private final void
+maybeAddRecord(Record record) throws IOException {
+	int rtype = record.getType();
+	Name name = record.getName();
+
+	if (rtype == Type.SOA && !name.equals(origin)) {
+		throw new IOException("SOA owner " + name +
+				      " does not match zone origin " +
+				      origin);
+	}
+	if (name.subdomain(origin))
+		addRecord(record);
+}
+
+/**
+ * Creates a Zone from the records in the specified master file.
+ * @param zone The name of the zone.
+ * @param file The master file to read from.
+ * @see Master
+ */
+public
+Zone(Name zone, String file) throws IOException {
+	data = new TreeMap();
+
+	if (zone == null)
+		throw new IllegalArgumentException("no zone name specified");
+	Master m = new Master(file, zone);
+	Record record;
+
+	origin = zone;
+	while ((record = m.nextRecord()) != null)
+		maybeAddRecord(record);
+	validate();
+}
+
+/**
+ * Creates a Zone from an array of records.
+ * @param zone The name of the zone.
+ * @param records The records to add to the zone.
+ * @see Master
+ */
+public
+Zone(Name zone, Record [] records) throws IOException {
+	data = new TreeMap();
+
+	if (zone == null)
+		throw new IllegalArgumentException("no zone name specified");
+	origin = zone;
+	for (int i = 0; i < records.length; i++)
+		maybeAddRecord(records[i]);
+	validate();
+}
+
+private void
+fromXFR(ZoneTransferIn xfrin) throws IOException, ZoneTransferException {
+	data = new TreeMap();
+
+	origin = xfrin.getName();
+	List records = xfrin.run();
+	for (Iterator it = records.iterator(); it.hasNext(); ) {
+		Record record = (Record) it.next();
+		maybeAddRecord(record);
+	}
+	if (!xfrin.isAXFR())
+		throw new IllegalArgumentException("zones can only be " +
+						   "created from AXFRs");
+	validate();
+}
+
+/**
+ * Creates a Zone by doing the specified zone transfer.
+ * @param xfrin The incoming zone transfer to execute.
+ * @see ZoneTransferIn
+ */
+public
+Zone(ZoneTransferIn xfrin) throws IOException, ZoneTransferException {
+	fromXFR(xfrin);
+}
+
+/**
+ * Creates a Zone by performing a zone transfer to the specified host.
+ * @see ZoneTransferIn
+ */
+public
+Zone(Name zone, int dclass, String remote)
+throws IOException, ZoneTransferException
+{
+	ZoneTransferIn xfrin = ZoneTransferIn.newAXFR(zone, remote, null);
+	xfrin.setDClass(dclass);
+	fromXFR(xfrin);
+}
+
+/** Returns the Zone's origin */
+public Name
+getOrigin() {
+	return origin;
+}
+
+/** Returns the Zone origin's NS records */
+public RRset
+getNS() {
+	return NS;
+}
+
+/** Returns the Zone's SOA record */
+public SOARecord
+getSOA() {
+	return SOA;
+}
+
+/** Returns the Zone's class */
+public int
+getDClass() {
+	return dclass;
+}
+
+private synchronized Object
+exactName(Name name) {
+	return data.get(name);
+}
+
+private synchronized RRset []
+allRRsets(Object types) {
+	if (types instanceof List) {
+		List typelist = (List) types;
+		return (RRset []) typelist.toArray(new RRset[typelist.size()]);
+	} else {
+		RRset set = (RRset) types;
+		return new RRset [] {set};
+	}
+}
+
+private synchronized RRset
+oneRRset(Object types, int type) {
+	if (type == Type.ANY)
+		throw new IllegalArgumentException("oneRRset(ANY)");
+	if (types instanceof List) {
+		List list = (List) types;
+		for (int i = 0; i < list.size(); i++) {
+			RRset set = (RRset) list.get(i);
+			if (set.getType() == type)
+				return set;
+		}
+	} else {
+		RRset set = (RRset) types;
+		if (set.getType() == type)
+			return set;
+	}
+	return null;
+}
+
+private synchronized RRset
+findRRset(Name name, int type) {
+	Object types = exactName(name);
+	if (types == null)
+		return null;
+	return oneRRset(types, type);
+}
+
+private synchronized void
+addRRset(Name name, RRset rrset) {
+	if (!hasWild && name.isWild())
+		hasWild = true;
+	Object types = data.get(name);
+	if (types == null) {
+		data.put(name, rrset);
+		return;
+	}
+	int rtype = rrset.getType();
+	if (types instanceof List) {
+		List list = (List) types;
+		for (int i = 0; i < list.size(); i++) {
+			RRset set = (RRset) list.get(i);
+			if (set.getType() == rtype) {
+				list.set(i, rrset);
+				return;
+			}
+		}
+		list.add(rrset);
+	} else {
+		RRset set = (RRset) types;
+		if (set.getType() == rtype)
+			data.put(name, rrset);
+		else {
+			LinkedList list = new LinkedList();
+			list.add(set);
+			list.add(rrset);
+			data.put(name, list);
+		}
+	}
+}
+
+private synchronized void
+removeRRset(Name name, int type) {
+	Object types = data.get(name);
+	if (types == null) {
+		return;
+	}
+	if (types instanceof List) {
+		List list = (List) types;
+		for (int i = 0; i < list.size(); i++) {
+			RRset set = (RRset) list.get(i);
+			if (set.getType() == type) {
+				list.remove(i);
+				if (list.size() == 0)
+					data.remove(name);
+				return;
+			}
+		}
+	} else {
+		RRset set = (RRset) types;
+		if (set.getType() != type)
+			return;
+		data.remove(name);
+	}
+}
+
+private synchronized SetResponse
+lookup(Name name, int type) {
+	int labels;
+	int olabels;
+	int tlabels;
+	RRset rrset;
+	Name tname;
+	Object types;
+	SetResponse sr;
+
+	if (!name.subdomain(origin))
+		return SetResponse.ofType(SetResponse.NXDOMAIN);
+
+	labels = name.labels();
+	olabels = origin.labels();
+
+	for (tlabels = olabels; tlabels <= labels; tlabels++) {
+		boolean isOrigin = (tlabels == olabels);
+		boolean isExact = (tlabels == labels);
+
+		if (isOrigin)
+			tname = origin;
+		else if (isExact)
+			tname = name;
+		else
+			tname = new Name(name, labels - tlabels);
+
+		types = exactName(tname);
+		if (types == null)
+			continue;
+
+		/* If this is a delegation, return that. */
+		if (!isOrigin) {
+			RRset ns = oneRRset(types, Type.NS);
+			if (ns != null)
+				return new SetResponse(SetResponse.DELEGATION,
+						       ns);
+		}
+
+		/* If this is an ANY lookup, return everything. */
+		if (isExact && type == Type.ANY) {
+			sr = new SetResponse(SetResponse.SUCCESSFUL);
+			RRset [] sets = allRRsets(types);
+			for (int i = 0; i < sets.length; i++)
+				sr.addRRset(sets[i]);
+			return sr;
+		}
+
+		/*
+		 * If this is the name, look for the actual type or a CNAME.
+		 * Otherwise, look for a DNAME.
+		 */
+		if (isExact) {
+			rrset = oneRRset(types, type);
+			if (rrset != null) {
+				sr = new SetResponse(SetResponse.SUCCESSFUL);
+				sr.addRRset(rrset);
+				return sr;
+			}
+			rrset = oneRRset(types, Type.CNAME);
+			if (rrset != null)
+				return new SetResponse(SetResponse.CNAME,
+						       rrset);
+		} else {
+			rrset = oneRRset(types, Type.DNAME);
+			if (rrset != null)
+				return new SetResponse(SetResponse.DNAME,
+						       rrset);
+		}
+
+		/* We found the name, but not the type. */
+		if (isExact)
+			return SetResponse.ofType(SetResponse.NXRRSET);
+	}
+
+	if (hasWild) {
+		for (int i = 0; i < labels - olabels; i++) {
+			tname = name.wild(i + 1);
+
+			types = exactName(tname);
+			if (types == null)
+				continue;
+
+			rrset = oneRRset(types, type);
+			if (rrset != null) {
+				sr = new SetResponse(SetResponse.SUCCESSFUL);
+				sr.addRRset(rrset);
+				return sr;
+			}
+		}
+	}
+
+	return SetResponse.ofType(SetResponse.NXDOMAIN);
+}
+
+/**     
+ * Looks up Records in the Zone.  This follows CNAMEs and wildcards.
+ * @param name The name to look up
+ * @param type The type to look up
+ * @return A SetResponse object
+ * @see SetResponse
+ */ 
+public SetResponse
+findRecords(Name name, int type) {
+	return lookup(name, type);
+}
+
+/**
+ * Looks up Records in the zone, finding exact matches only.
+ * @param name The name to look up
+ * @param type The type to look up
+ * @return The matching RRset
+ * @see RRset
+ */ 
+public RRset
+findExactMatch(Name name, int type) {
+	Object types = exactName(name);
+	if (types == null)
+		return null;
+	return oneRRset(types, type);
+}
+
+/**
+ * Adds an RRset to the Zone
+ * @param rrset The RRset to be added
+ * @see RRset
+ */
+public void
+addRRset(RRset rrset) {
+	Name name = rrset.getName();
+	addRRset(name, rrset);
+}
+
+/**
+ * Adds a Record to the Zone
+ * @param r The record to be added
+ * @see Record
+ */
+public void
+addRecord(Record r) {
+	Name name = r.getName();
+	int rtype = r.getRRsetType();
+	synchronized (this) {
+		RRset rrset = findRRset(name, rtype);
+		if (rrset == null) {
+			rrset = new RRset(r);
+			addRRset(name, rrset);
+		} else {
+			rrset.addRR(r);
+		}
+	}
+}
+
+/**
+ * Removes a record from the Zone
+ * @param r The record to be removed
+ * @see Record
+ */
+public void
+removeRecord(Record r) {
+	Name name = r.getName();
+	int rtype = r.getRRsetType();
+	synchronized (this) {
+		RRset rrset = findRRset(name, rtype);
+		if (rrset == null)
+			return;
+		if (rrset.size() == 1 && rrset.first().equals(r))
+			removeRRset(name, rtype);
+		else
+			rrset.deleteRR(r);
+	}
+}
+
+/**
+ * Returns an Iterator over the RRsets in the zone.
+ */
+public Iterator
+iterator() {
+	return new ZoneIterator(false);
+}
+
+/**
+ * Returns an Iterator over the RRsets in the zone that can be used to
+ * construct an AXFR response.  This is identical to {@link #iterator} except
+ * that the SOA is returned at the end as well as the beginning.
+ */
+public Iterator
+AXFR() {
+	return new ZoneIterator(true);
+}
+
+private void
+nodeToString(StringBuffer sb, Object node) {
+	RRset [] sets = allRRsets(node);
+	for (int i = 0; i < sets.length; i++) {
+		RRset rrset = sets[i];
+		Iterator it = rrset.rrs();
+		while (it.hasNext())
+			sb.append(it.next() + "\n");
+		it = rrset.sigs();
+		while (it.hasNext())
+			sb.append(it.next() + "\n");
+	}
+}
+
+/**
+ * Returns the contents of the Zone in master file format.
+ */
+public synchronized String
+toMasterFile() {
+	Iterator zentries = data.entrySet().iterator();
+	StringBuffer sb = new StringBuffer();
+	nodeToString(sb, originNode);
+	while (zentries.hasNext()) {
+		Map.Entry entry = (Map.Entry) zentries.next();
+		if (!origin.equals(entry.getKey()))
+			nodeToString(sb, entry.getValue());
+	}
+	return sb.toString();
+}
+
+/**
+ * Returns the contents of the Zone as a string (in master file format).
+ */
+public String
+toString() {
+	return toMasterFile();
+}
+
+}
diff --git a/src/org/xbill/DNS/ZoneTransferException.java b/src/org/xbill/DNS/ZoneTransferException.java
new file mode 100644
index 0000000..3ba487b
--- /dev/null
+++ b/src/org/xbill/DNS/ZoneTransferException.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS;
+
+/**
+ * An exception thrown when a zone transfer fails.
+ *
+ * @author Brian Wellington
+ */
+
+public class ZoneTransferException extends Exception {
+
+public
+ZoneTransferException() {
+	super();
+}
+
+public
+ZoneTransferException(String s) {
+	super(s);
+}
+
+}
diff --git a/src/org/xbill/DNS/ZoneTransferIn.java b/src/org/xbill/DNS/ZoneTransferIn.java
new file mode 100644
index 0000000..8a19992
--- /dev/null
+++ b/src/org/xbill/DNS/ZoneTransferIn.java
@@ -0,0 +1,680 @@
+// Copyright (c) 2003-2004 Brian Wellington (bwelling@xbill.org)
+// Parts of this are derived from lib/dns/xfrin.c from BIND 9; its copyright
+// notice follows.
+
+/*
+ * Copyright (C) 1999-2001  Internet Software Consortium.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM
+ * DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
+ * INTERNET SOFTWARE CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
+ * FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
+ * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
+ * WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+package org.xbill.DNS;
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+/**
+ * An incoming DNS Zone Transfer.  To use this class, first initialize an
+ * object, then call the run() method.  If run() doesn't throw an exception
+ * the result will either be an IXFR-style response, an AXFR-style response,
+ * or an indication that the zone is up to date.
+ *
+ * @author Brian Wellington
+ */
+
+public class ZoneTransferIn {
+
+private static final int INITIALSOA	= 0;
+private static final int FIRSTDATA	= 1;
+private static final int IXFR_DELSOA	= 2;
+private static final int IXFR_DEL	= 3;
+private static final int IXFR_ADDSOA	= 4;
+private static final int IXFR_ADD	= 5;
+private static final int AXFR		= 6;
+private static final int END		= 7;
+
+private Name zname;
+private int qtype;
+private int dclass;
+private long ixfr_serial;
+private boolean want_fallback;
+private ZoneTransferHandler handler;
+
+private SocketAddress localAddress;
+private SocketAddress address;
+private TCPClient client;
+private TSIG tsig;
+private TSIG.StreamVerifier verifier;
+private long timeout = 900 * 1000;
+
+private int state;
+private long end_serial;
+private long current_serial;
+private Record initialsoa;
+
+private int rtype;
+
+public static class Delta {
+	/**
+	 * All changes between two versions of a zone in an IXFR response.
+	 */
+
+	/** The starting serial number of this delta. */
+	public long start;
+
+	/** The ending serial number of this delta. */
+	public long end;
+
+	/** A list of records added between the start and end versions */
+	public List adds;
+
+	/** A list of records deleted between the start and end versions */
+	public List deletes;
+
+	private
+	Delta() {
+		adds = new ArrayList();
+		deletes = new ArrayList();
+	}
+}
+
+public static interface ZoneTransferHandler {
+	/**
+	 * Handles a Zone Transfer.
+	 */
+
+	/**
+	 * Called when an AXFR transfer begins.
+	 */
+	public void startAXFR() throws ZoneTransferException;
+
+	/**
+	 * Called when an IXFR transfer begins.
+	 */
+	public void startIXFR() throws ZoneTransferException;
+
+	/**
+	 * Called when a series of IXFR deletions begins.
+	 * @param soa The starting SOA.
+	 */
+	public void startIXFRDeletes(Record soa) throws ZoneTransferException;
+
+	/**
+	 * Called when a series of IXFR adds begins.
+	 * @param soa The starting SOA.
+	 */
+	public void startIXFRAdds(Record soa) throws ZoneTransferException;
+
+	/**
+	 * Called for each content record in an AXFR.
+	 * @param r The DNS record.
+	 */
+	public void handleRecord(Record r) throws ZoneTransferException;
+};
+
+private static class BasicHandler implements ZoneTransferHandler {
+	private List axfr;
+	private List ixfr;
+
+	public void startAXFR() {
+		axfr = new ArrayList();
+	}
+
+	public void startIXFR() {
+		ixfr = new ArrayList();
+	}
+
+	public void startIXFRDeletes(Record soa) {
+		Delta delta = new Delta();
+		delta.deletes.add(soa);
+		delta.start = getSOASerial(soa);
+		ixfr.add(delta);
+	}
+
+	public void startIXFRAdds(Record soa) {
+		Delta delta = (Delta) ixfr.get(ixfr.size() - 1);
+		delta.adds.add(soa);
+		delta.end = getSOASerial(soa);
+	}
+
+	public void handleRecord(Record r) {
+		List list;
+		if (ixfr != null) {
+			Delta delta = (Delta) ixfr.get(ixfr.size() - 1);
+			if (delta.adds.size() > 0)
+				list = delta.adds;
+			else
+				list = delta.deletes;
+		} else
+			list = axfr;
+		list.add(r);
+	}
+};
+
+private
+ZoneTransferIn() {}
+
+private
+ZoneTransferIn(Name zone, int xfrtype, long serial, boolean fallback,
+	       SocketAddress address, TSIG key)
+{
+	this.address = address;
+	this.tsig = key;
+	if (zone.isAbsolute())
+		zname = zone;
+	else {
+		try {
+			zname = Name.concatenate(zone, Name.root);
+		}
+		catch (NameTooLongException e) {
+			throw new IllegalArgumentException("ZoneTransferIn: " +
+							   "name too long");
+		}
+	}
+	qtype = xfrtype;
+	dclass = DClass.IN;
+	ixfr_serial = serial;
+	want_fallback = fallback;
+	state = INITIALSOA;
+}
+
+/**
+ * Instantiates a ZoneTransferIn object to do an AXFR (full zone transfer).
+ * @param zone The zone to transfer.
+ * @param address The host/port from which to transfer the zone.
+ * @param key The TSIG key used to authenticate the transfer, or null.
+ * @return The ZoneTransferIn object.
+ * @throws UnknownHostException The host does not exist.
+ */
+public static ZoneTransferIn
+newAXFR(Name zone, SocketAddress address, TSIG key) {
+	return new ZoneTransferIn(zone, Type.AXFR, 0, false, address, key);
+}
+
+/**
+ * Instantiates a ZoneTransferIn object to do an AXFR (full zone transfer).
+ * @param zone The zone to transfer.
+ * @param host The host from which to transfer the zone.
+ * @param port The port to connect to on the server, or 0 for the default.
+ * @param key The TSIG key used to authenticate the transfer, or null.
+ * @return The ZoneTransferIn object.
+ * @throws UnknownHostException The host does not exist.
+ */
+public static ZoneTransferIn
+newAXFR(Name zone, String host, int port, TSIG key)
+throws UnknownHostException
+{
+	if (port == 0)
+		port = SimpleResolver.DEFAULT_PORT;
+	return newAXFR(zone, new InetSocketAddress(host, port), key);
+}
+
+/**
+ * Instantiates a ZoneTransferIn object to do an AXFR (full zone transfer).
+ * @param zone The zone to transfer.
+ * @param host The host from which to transfer the zone.
+ * @param key The TSIG key used to authenticate the transfer, or null.
+ * @return The ZoneTransferIn object.
+ * @throws UnknownHostException The host does not exist.
+ */
+public static ZoneTransferIn
+newAXFR(Name zone, String host, TSIG key)
+throws UnknownHostException
+{
+	return newAXFR(zone, host, 0, key);
+}
+
+/**
+ * Instantiates a ZoneTransferIn object to do an IXFR (incremental zone
+ * transfer).
+ * @param zone The zone to transfer.
+ * @param serial The existing serial number.
+ * @param fallback If true, fall back to AXFR if IXFR is not supported.
+ * @param address The host/port from which to transfer the zone.
+ * @param key The TSIG key used to authenticate the transfer, or null.
+ * @return The ZoneTransferIn object.
+ * @throws UnknownHostException The host does not exist.
+ */
+public static ZoneTransferIn
+newIXFR(Name zone, long serial, boolean fallback, SocketAddress address,
+	TSIG key)
+{
+	return new ZoneTransferIn(zone, Type.IXFR, serial, fallback, address,
+				  key);
+}
+
+/**
+ * Instantiates a ZoneTransferIn object to do an IXFR (incremental zone
+ * transfer).
+ * @param zone The zone to transfer.
+ * @param serial The existing serial number.
+ * @param fallback If true, fall back to AXFR if IXFR is not supported.
+ * @param host The host from which to transfer the zone.
+ * @param port The port to connect to on the server, or 0 for the default.
+ * @param key The TSIG key used to authenticate the transfer, or null.
+ * @return The ZoneTransferIn object.
+ * @throws UnknownHostException The host does not exist.
+ */
+public static ZoneTransferIn
+newIXFR(Name zone, long serial, boolean fallback, String host, int port,
+	TSIG key)
+throws UnknownHostException
+{
+	if (port == 0)
+		port = SimpleResolver.DEFAULT_PORT;
+	return newIXFR(zone, serial, fallback,
+		       new InetSocketAddress(host, port), key);
+}
+
+/**
+ * Instantiates a ZoneTransferIn object to do an IXFR (incremental zone
+ * transfer).
+ * @param zone The zone to transfer.
+ * @param serial The existing serial number.
+ * @param fallback If true, fall back to AXFR if IXFR is not supported.
+ * @param host The host from which to transfer the zone.
+ * @param key The TSIG key used to authenticate the transfer, or null.
+ * @return The ZoneTransferIn object.
+ * @throws UnknownHostException The host does not exist.
+ */
+public static ZoneTransferIn
+newIXFR(Name zone, long serial, boolean fallback, String host, TSIG key)
+throws UnknownHostException
+{
+	return newIXFR(zone, serial, fallback, host, 0, key);
+}
+
+/**
+ * Gets the name of the zone being transferred.
+ */
+public Name
+getName() {
+	return zname;
+}
+
+/**
+ * Gets the type of zone transfer (either AXFR or IXFR).
+ */
+public int
+getType() {
+	return qtype;
+}
+
+/**
+ * Sets a timeout on this zone transfer.  The default is 900 seconds (15
+ * minutes).
+ * @param secs The maximum amount of time that this zone transfer can take.
+ */
+public void
+setTimeout(int secs) {
+	if (secs < 0)
+		throw new IllegalArgumentException("timeout cannot be " +
+						   "negative");
+	timeout = 1000L * secs;
+}
+
+/**
+ * Sets an alternate DNS class for this zone transfer.
+ * @param dclass The class to use instead of class IN.
+ */
+public void
+setDClass(int dclass) {
+	DClass.check(dclass);
+	this.dclass = dclass;
+}
+
+/**
+ * Sets the local address to bind to when sending messages.
+ * @param addr The local address to send messages from.
+ */
+public void
+setLocalAddress(SocketAddress addr) {
+	this.localAddress = addr;
+}
+
+private void
+openConnection() throws IOException {
+	long endTime = System.currentTimeMillis() + timeout;
+	client = new TCPClient(endTime);
+	if (localAddress != null)
+		client.bind(localAddress);
+	client.connect(address);
+}
+
+private void
+sendQuery() throws IOException {
+	Record question = Record.newRecord(zname, qtype, dclass);
+
+	Message query = new Message();
+	query.getHeader().setOpcode(Opcode.QUERY);
+	query.addRecord(question, Section.QUESTION);
+	if (qtype == Type.IXFR) {
+		Record soa = new SOARecord(zname, dclass, 0, Name.root,
+					   Name.root, ixfr_serial,
+					   0, 0, 0, 0);
+		query.addRecord(soa, Section.AUTHORITY);
+	}
+	if (tsig != null) {
+		tsig.apply(query, null);
+		verifier = new TSIG.StreamVerifier(tsig, query.getTSIG());
+	}
+	byte [] out = query.toWire(Message.MAXLENGTH);
+	client.send(out);
+}
+
+private static long
+getSOASerial(Record rec) {
+	SOARecord soa = (SOARecord) rec;
+	return soa.getSerial();
+}
+
+private void
+logxfr(String s) {
+	if (Options.check("verbose"))
+		System.out.println(zname + ": " + s);
+}
+
+private void
+fail(String s) throws ZoneTransferException {
+	throw new ZoneTransferException(s);
+}
+
+private void
+fallback() throws ZoneTransferException {
+	if (!want_fallback)
+		fail("server doesn't support IXFR");
+
+	logxfr("falling back to AXFR");
+	qtype = Type.AXFR;
+	state = INITIALSOA;
+}
+
+private void
+parseRR(Record rec) throws ZoneTransferException {
+	int type = rec.getType();
+	Delta delta;
+
+	switch (state) {
+	case INITIALSOA:
+		if (type != Type.SOA)
+			fail("missing initial SOA");
+		initialsoa = rec;
+		// Remember the serial number in the initial SOA; we need it
+		// to recognize the end of an IXFR.
+		end_serial = getSOASerial(rec);
+		if (qtype == Type.IXFR &&
+		    Serial.compare(end_serial, ixfr_serial) <= 0)
+		{
+			logxfr("up to date");
+			state = END;
+			break;
+		}
+		state = FIRSTDATA;
+		break;
+
+	case FIRSTDATA:
+		// If the transfer begins with 1 SOA, it's an AXFR.
+		// If it begins with 2 SOAs, it's an IXFR.
+		if (qtype == Type.IXFR && type == Type.SOA &&
+		    getSOASerial(rec) == ixfr_serial)
+		{
+			rtype = Type.IXFR;
+			handler.startIXFR();
+			logxfr("got incremental response");
+			state = IXFR_DELSOA;
+		} else {
+			rtype = Type.AXFR;
+			handler.startAXFR();
+			handler.handleRecord(initialsoa);
+			logxfr("got nonincremental response");
+			state = AXFR;
+		}
+		parseRR(rec); // Restart...
+		return;
+
+	case IXFR_DELSOA:
+		handler.startIXFRDeletes(rec);
+		state = IXFR_DEL;
+		break;
+
+	case IXFR_DEL:
+		if (type == Type.SOA) {
+			current_serial = getSOASerial(rec);
+			state = IXFR_ADDSOA;
+			parseRR(rec); // Restart...
+			return;
+		}
+		handler.handleRecord(rec);
+		break;
+
+	case IXFR_ADDSOA:
+		handler.startIXFRAdds(rec);
+		state = IXFR_ADD;
+		break;
+
+	case IXFR_ADD:
+		if (type == Type.SOA) {
+			long soa_serial = getSOASerial(rec);
+			if (soa_serial == end_serial) {
+				state = END;
+				break;
+			} else if (soa_serial != current_serial) {
+				fail("IXFR out of sync: expected serial " +
+				     current_serial + " , got " + soa_serial);
+			} else {
+				state = IXFR_DELSOA;
+				parseRR(rec); // Restart...
+				return;
+			}
+		}
+		handler.handleRecord(rec);
+		break;
+
+	case AXFR:
+		// Old BINDs sent cross class A records for non IN classes.
+		if (type == Type.A && rec.getDClass() != dclass)
+			break;
+		handler.handleRecord(rec);
+		if (type == Type.SOA) {
+			state = END;
+		}
+		break;
+
+	case END:
+		fail("extra data");
+		break;
+
+	default:
+		fail("invalid state");
+		break;
+	}
+}
+
+private void
+closeConnection() {
+	try {
+		if (client != null)
+			client.cleanup();
+	}
+	catch (IOException e) {
+	}
+}
+
+private Message
+parseMessage(byte [] b) throws WireParseException {
+	try {
+		return new Message(b);
+	}
+	catch (IOException e) {
+		if (e instanceof WireParseException)
+			throw (WireParseException) e;
+		throw new WireParseException("Error parsing message");
+	}
+}
+
+private void
+doxfr() throws IOException, ZoneTransferException {
+	sendQuery();
+	while (state != END) {
+		byte [] in = client.recv();
+		Message response =  parseMessage(in);
+		if (response.getHeader().getRcode() == Rcode.NOERROR &&
+		    verifier != null)
+		{
+			TSIGRecord tsigrec = response.getTSIG();
+
+			int error = verifier.verify(response, in);
+			if (error != Rcode.NOERROR)
+				fail("TSIG failure");
+		}
+
+		Record [] answers = response.getSectionArray(Section.ANSWER);
+
+		if (state == INITIALSOA) {
+			int rcode = response.getRcode();
+			if (rcode != Rcode.NOERROR) {
+				if (qtype == Type.IXFR &&
+				    rcode == Rcode.NOTIMP)
+				{
+					fallback();
+					doxfr();
+					return;
+				}
+				fail(Rcode.string(rcode));
+			}
+
+			Record question = response.getQuestion();
+			if (question != null && question.getType() != qtype) {
+				fail("invalid question section");
+			}
+
+			if (answers.length == 0 && qtype == Type.IXFR) {
+				fallback();
+				doxfr();
+				return;
+			}
+		}
+
+		for (int i = 0; i < answers.length; i++) {
+			parseRR(answers[i]);
+		}
+
+		if (state == END && verifier != null &&
+		    !response.isVerified())
+			fail("last message must be signed");
+	}
+}
+
+/**
+ * Does the zone transfer.
+ * @param handler The callback object that handles the zone transfer data.
+ * @throws IOException The zone transfer failed to due an IO problem.
+ * @throws ZoneTransferException The zone transfer failed to due a problem
+ * with the zone transfer itself.
+ */
+public void
+run(ZoneTransferHandler handler) throws IOException, ZoneTransferException {
+	this.handler = handler;
+	try {
+		openConnection();
+		doxfr();
+	}
+	finally {
+		closeConnection();
+	}
+}
+
+/**
+ * Does the zone transfer.
+ * @return A list, which is either an AXFR-style response (List of Records),
+ * and IXFR-style response (List of Deltas), or null, which indicates that
+ * an IXFR was performed and the zone is up to date.
+ * @throws IOException The zone transfer failed to due an IO problem.
+ * @throws ZoneTransferException The zone transfer failed to due a problem
+ * with the zone transfer itself.
+ */
+public List
+run() throws IOException, ZoneTransferException {
+	BasicHandler handler = new BasicHandler();
+	run(handler);
+	if (handler.axfr != null)
+		return handler.axfr;
+	return handler.ixfr;
+}
+
+private BasicHandler
+getBasicHandler() throws IllegalArgumentException {
+	if (handler instanceof BasicHandler)
+		return (BasicHandler) handler;
+	throw new IllegalArgumentException("ZoneTransferIn used callback " +
+					   "interface");
+}
+
+/**
+ * Returns true if the response is an AXFR-style response (List of Records).
+ * This will be true if either an IXFR was performed, an IXFR was performed
+ * and the server provided a full zone transfer, or an IXFR failed and
+ * fallback to AXFR occurred.
+ */
+public boolean
+isAXFR() {
+	return (rtype == Type.AXFR);
+}
+
+/**
+ * Gets the AXFR-style response.
+ * @throws IllegalArgumentException The transfer used the callback interface,
+ * so the response was not stored.
+ */
+public List
+getAXFR() {
+	BasicHandler handler = getBasicHandler();
+	return handler.axfr;
+}
+
+/**
+ * Returns true if the response is an IXFR-style response (List of Deltas).
+ * This will be true only if an IXFR was performed and the server provided
+ * an incremental zone transfer.
+ */
+public boolean
+isIXFR() {
+	return (rtype == Type.IXFR);
+}
+
+/**
+ * Gets the IXFR-style response.
+ * @throws IllegalArgumentException The transfer used the callback interface,
+ * so the response was not stored.
+ */
+public List
+getIXFR() {
+	BasicHandler handler = getBasicHandler();
+	return handler.ixfr;
+}
+
+/**
+ * Returns true if the response indicates that the zone is up to date.
+ * This will be true only if an IXFR was performed.
+ * @throws IllegalArgumentException The transfer used the callback interface,
+ * so the response was not stored.
+ */
+public boolean
+isCurrent() {
+	BasicHandler handler = getBasicHandler();
+	return (handler.axfr == null && handler.ixfr == null);
+}
+
+}
diff --git a/src/org/xbill/DNS/spi/DNSJavaNameService.java b/src/org/xbill/DNS/spi/DNSJavaNameService.java
new file mode 100644
index 0000000..14d8adb
--- /dev/null
+++ b/src/org/xbill/DNS/spi/DNSJavaNameService.java
@@ -0,0 +1,176 @@
+// Copyright (c) 2005 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS.spi;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.StringTokenizer;
+
+import org.xbill.DNS.AAAARecord;
+import org.xbill.DNS.ARecord;
+import org.xbill.DNS.ExtendedResolver;
+import org.xbill.DNS.Lookup;
+import org.xbill.DNS.Name;
+import org.xbill.DNS.PTRRecord;
+import org.xbill.DNS.Record;
+import org.xbill.DNS.Resolver;
+import org.xbill.DNS.ReverseMap;
+import org.xbill.DNS.TextParseException;
+import org.xbill.DNS.Type;
+
+/**
+ * This class implements a Name Service Provider, which Java can use 
+ * (starting with version 1.4), to perform DNS resolutions instead of using 
+ * the standard calls. 
+ * <p>
+ * This Name Service Provider uses dnsjava.
+ * <p>
+ * To use this provider, you must set the following system property:
+ * <b>sun.net.spi.nameservice.provider.1=dns,dnsjava</b>
+ *
+ * @author Brian Wellington
+ * @author Paul Cowan (pwc21@yahoo.com)
+ */
+
+public class DNSJavaNameService implements InvocationHandler {
+
+private static final String nsProperty = "sun.net.spi.nameservice.nameservers";
+private static final String domainProperty = "sun.net.spi.nameservice.domain";
+private static final String v6Property = "java.net.preferIPv6Addresses";
+
+private boolean preferV6 = false;
+
+/**
+ * Creates a DNSJavaNameService instance.
+ * <p>
+ * Uses the
+ * <b>sun.net.spi.nameservice.nameservers</b>,
+ * <b>sun.net.spi.nameservice.domain</b>, and
+ * <b>java.net.preferIPv6Addresses</b> properties for configuration.
+ */
+protected
+DNSJavaNameService() {
+	String nameServers = System.getProperty(nsProperty);
+	String domain = System.getProperty(domainProperty);
+	String v6 = System.getProperty(v6Property);
+
+	if (nameServers != null) {
+		StringTokenizer st = new StringTokenizer(nameServers, ",");
+		String [] servers = new String[st.countTokens()];
+		int n = 0;
+		while (st.hasMoreTokens())
+			servers[n++] = st.nextToken();
+		try {
+			Resolver res = new ExtendedResolver(servers);
+			Lookup.setDefaultResolver(res);
+		}
+		catch (UnknownHostException e) {
+			System.err.println("DNSJavaNameService: invalid " +
+					   nsProperty);
+		}
+	}
+
+	if (domain != null) {
+		try {
+			Lookup.setDefaultSearchPath(new String[] {domain});
+		}
+		catch (TextParseException e) {
+			System.err.println("DNSJavaNameService: invalid " +
+					   domainProperty);
+		}
+	}
+
+	if (v6 != null && v6.equalsIgnoreCase("true"))
+		preferV6 = true;
+}
+
+
+public Object
+invoke(Object proxy, Method method, Object[] args) throws Throwable {
+	try {
+		if (method.getName().equals("getHostByAddr")) {
+			return this.getHostByAddr((byte[]) args[0]);
+		} else if (method.getName().equals("lookupAllHostAddr")) {
+			InetAddress[] addresses;
+			addresses = this.lookupAllHostAddr((String) args[0]);
+			Class returnType = method.getReturnType();
+			if (returnType.equals(InetAddress[].class)) {
+				// method for Java >= 1.6
+				return addresses;
+			} else if (returnType.equals(byte[][].class)) {
+				// method for Java <= 1.5
+				int naddrs = addresses.length;
+				byte [][] byteAddresses = new byte[naddrs][];
+				byte [] addr;
+				for (int i = 0; i < naddrs; i++) {
+					addr = addresses[i].getAddress();
+					byteAddresses[i] = addr;
+				}
+				return byteAddresses;
+			}
+		}		
+	} catch (Throwable e) {
+		System.err.println("DNSJavaNameService: Unexpected error.");
+		e.printStackTrace();
+		throw e;
+	}
+	throw new IllegalArgumentException(
+					"Unknown function name or arguments.");
+}
+
+/**
+ * Performs a forward DNS lookup for the host name.
+ * @param host The host name to resolve.
+ * @return All the ip addresses found for the host name.
+ */
+public InetAddress []
+lookupAllHostAddr(String host) throws UnknownHostException {
+	Name name = null;
+
+	try {
+		name = new Name(host);
+	}
+	catch (TextParseException e) {
+		throw new UnknownHostException(host);
+	}
+
+	Record [] records = null;
+	if (preferV6)
+		records = new Lookup(name, Type.AAAA).run();
+	if (records == null)
+		records = new Lookup(name, Type.A).run();
+	if (records == null && !preferV6)
+		records = new Lookup(name, Type.AAAA).run();
+	if (records == null)
+		throw new UnknownHostException(host);
+
+	InetAddress[] array = new InetAddress[records.length];
+	for (int i = 0; i < records.length; i++) {
+		Record record = records[i];
+		if (records[i] instanceof ARecord) {
+			ARecord a = (ARecord) records[i];
+			array[i] = a.getAddress();
+		} else {
+			AAAARecord aaaa = (AAAARecord) records[i];
+			array[i] = aaaa.getAddress();
+		}
+	}
+	return array;
+}
+
+/**
+ * Performs a reverse DNS lookup.
+ * @param addr The ip address to lookup.
+ * @return The host name found for the ip address.
+ */
+public String
+getHostByAddr(byte [] addr) throws UnknownHostException {
+	Name name = ReverseMap.fromAddress(InetAddress.getByAddress(addr));
+	Record [] records = new Lookup(name, Type.PTR).run();
+	if (records == null)
+		throw new UnknownHostException();
+	return ((PTRRecord) records[0]).getTarget().toString();
+}
+}
diff --git a/src/org/xbill/DNS/spi/services/sun.net.spi.nameservice.NameServiceDescriptor b/src/org/xbill/DNS/spi/services/sun.net.spi.nameservice.NameServiceDescriptor
new file mode 100644
index 0000000..1ca895c
--- /dev/null
+++ b/src/org/xbill/DNS/spi/services/sun.net.spi.nameservice.NameServiceDescriptor
@@ -0,0 +1 @@
+org.xbill.DNS.spi.DNSJavaNameServiceDescriptor
diff --git a/src/org/xbill/DNS/tests/primary.java b/src/org/xbill/DNS/tests/primary.java
new file mode 100644
index 0000000..85455b9
--- /dev/null
+++ b/src/org/xbill/DNS/tests/primary.java
@@ -0,0 +1,59 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS.tests;
+
+import java.util.*;
+import org.xbill.DNS.*;
+
+public class primary {
+
+private static void
+usage() {
+	System.out.println("usage: primary [-t] [-a | -i] origin file");
+	System.exit(1);
+}
+
+public static void
+main(String [] args) throws Exception {
+	boolean time = false;
+	boolean axfr = false;
+	boolean iterator = false;
+	int arg = 0;
+
+	if (args.length < 2)
+		usage();
+
+	while (args.length - arg > 2) {
+		if (args[0].equals("-t"))
+			time = true;
+		else if (args[0].equals("-a"))
+			axfr = true;
+		else if (args[0].equals("-i"))
+			iterator = true;
+		arg++;
+	}
+
+	Name origin = Name.fromString(args[arg++], Name.root);
+	String file = args[arg++];
+
+	long start = System.currentTimeMillis();
+	Zone zone = new Zone(origin, file);
+	long end = System.currentTimeMillis();
+	if (axfr) {
+		Iterator it = zone.AXFR();
+		while (it.hasNext()) {
+			System.out.println(it.next());
+		}
+	} else if (iterator) {
+		Iterator it = zone.iterator();
+		while (it.hasNext()) {
+			System.out.println(it.next());
+		}
+	} else {
+		System.out.println(zone);
+	}
+	if (time)
+		System.out.println("; Load time: " + (end - start) + " ms");
+}
+
+}
diff --git a/src/org/xbill/DNS/tests/xfrin.java b/src/org/xbill/DNS/tests/xfrin.java
new file mode 100644
index 0000000..066c70e
--- /dev/null
+++ b/src/org/xbill/DNS/tests/xfrin.java
@@ -0,0 +1,109 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS.tests;
+
+import java.util.*;
+import org.xbill.DNS.*;
+
+public class xfrin {
+
+private static void
+usage(String s) {
+	System.out.println("Error: " + s);
+	System.out.println("usage: xfrin [-i serial] [-k keyname/secret] " +
+			   "[-s server] [-p port] [-f] zone");
+	System.exit(1);
+}
+
+public static void
+main(String [] args) throws Exception {
+	ZoneTransferIn xfrin;
+	TSIG key = null;
+	int ixfr_serial = -1;
+	String server = null;
+	int port = SimpleResolver.DEFAULT_PORT;
+	boolean fallback = false;
+	Name zname;
+
+	int arg = 0;
+	while (arg < args.length) {
+		if (args[arg].equals("-i")) {
+			ixfr_serial = Integer.parseInt(args[++arg]);
+			if (ixfr_serial < 0)
+				usage("invalid serial number");
+		} else if (args[arg].equals("-k")) {
+			String s = args[++arg];
+			int index = s.indexOf('/');
+			if (index < 0)
+				usage("invalid key");
+			key = new TSIG(s.substring(0, index),
+				       s.substring(index+1));
+		} else if (args[arg].equals("-s")) {
+			server = args[++arg];
+		} else if (args[arg].equals("-p")) {
+			port = Integer.parseInt(args[++arg]);
+			if (port < 0 || port > 0xFFFF)
+				usage("invalid port");
+		} else if (args[arg].equals("-f")) {
+			fallback = true;
+		} else if (args[arg].startsWith("-")) {
+			usage("invalid option");
+		} else {
+			break;
+		}
+		arg++;
+	}
+	if (arg >= args.length)
+		usage("no zone name specified");
+	zname = Name.fromString(args[arg]);
+
+	if (server == null) {
+		Lookup l = new Lookup(zname, Type.NS);
+		Record [] ns = l.run();
+		if (ns == null) {
+			System.out.println("failed to look up NS record: " +
+					   l.getErrorString());
+			System.exit(1);
+		}
+		server = ns[0].rdataToString();
+		System.out.println("sending to server '" + server + "'");
+	}
+
+	if (ixfr_serial >= 0)
+		xfrin = ZoneTransferIn.newIXFR(zname, ixfr_serial, fallback,
+					       server, port, key);
+	else
+		xfrin = ZoneTransferIn.newAXFR(zname, server, port, key);
+
+	List response = xfrin.run();
+	if (xfrin.isAXFR()) {
+		if (ixfr_serial >= 0)
+			System.out.println("AXFR-like IXFR response");
+		else
+			System.out.println("AXFR response");
+		Iterator it = response.iterator();
+		while (it.hasNext())
+			System.out.println(it.next());
+	} else if (xfrin.isIXFR()) {
+		System.out.println("IXFR response");
+		Iterator it = response.iterator();
+		while (it.hasNext()) {
+			ZoneTransferIn.Delta delta;
+			delta = (ZoneTransferIn.Delta) it.next();
+			System.out.println("delta from " + delta.start +
+					   " to " + delta.end);
+			System.out.println("deletes");
+			Iterator it2 = delta.deletes.iterator();
+			while (it2.hasNext())
+				System.out.println(it2.next());
+			System.out.println("adds");
+			it2 = delta.adds.iterator();
+			while (it2.hasNext())
+				System.out.println(it2.next());
+		}
+	} else if (xfrin.isCurrent()) {
+		System.out.println("up to date");
+	}
+}
+
+}
diff --git a/src/org/xbill/DNS/utils/HMAC.java b/src/org/xbill/DNS/utils/HMAC.java
new file mode 100644
index 0000000..5eb5afd
--- /dev/null
+++ b/src/org/xbill/DNS/utils/HMAC.java
@@ -0,0 +1,182 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS.utils;
+
+import java.util.Arrays;
+import java.security.*;
+
+/**
+ * An implementation of the HMAC message authentication code.
+ *
+ * @author Brian Wellington
+ */
+
+public class HMAC {
+
+private MessageDigest digest;
+private int blockLength;
+
+private byte [] ipad, opad;
+
+private static final byte IPAD = 0x36;
+private static final byte OPAD = 0x5c;
+
+private void
+init(byte [] key) {
+	int i;
+
+	if (key.length > blockLength) {
+		key = digest.digest(key);
+		digest.reset();
+	}
+	ipad = new byte[blockLength];
+	opad = new byte[blockLength];
+	for (i = 0; i < key.length; i++) {
+		ipad[i] = (byte) (key[i] ^ IPAD);
+		opad[i] = (byte) (key[i] ^ OPAD);
+	}
+	for (; i < blockLength; i++) {
+		ipad[i] = IPAD;
+		opad[i] = OPAD;
+	}
+	digest.update(ipad);
+}
+
+/**
+ * Creates a new HMAC instance
+ * @param digest The message digest object.
+ * @param blockLength The block length of the message digest.
+ * @param key The secret key
+ */
+public
+HMAC(MessageDigest digest, int blockLength, byte [] key) {
+	digest.reset();
+	this.digest = digest;
+  	this.blockLength = blockLength;
+	init(key);
+}
+
+/**
+ * Creates a new HMAC instance
+ * @param digestName The name of the message digest function.
+ * @param blockLength The block length of the message digest.
+ * @param key The secret key.
+ */
+public
+HMAC(String digestName, int blockLength, byte [] key) {
+	try {
+		digest = MessageDigest.getInstance(digestName);
+	} catch (NoSuchAlgorithmException e) {
+		throw new IllegalArgumentException("unknown digest algorithm "
+						   + digestName);
+	}
+	this.blockLength = blockLength;
+	init(key);
+}
+
+/**
+ * Creates a new HMAC instance
+ * @param digest The message digest object.
+ * @param key The secret key
+ * @deprecated won't work with digests using a padding length other than 64;
+ *             use {@code HMAC(MessageDigest digest, int blockLength,
+ *             byte [] key)} instead.
+ * @see        HMAC#HMAC(MessageDigest digest, int blockLength, byte [] key)
+ */
+public
+HMAC(MessageDigest digest, byte [] key) {
+	this(digest, 64, key);
+}
+
+/**
+ * Creates a new HMAC instance
+ * @param digestName The name of the message digest function.
+ * @param key The secret key.
+ * @deprecated won't work with digests using a padding length other than 64;
+ *             use {@code HMAC(String digestName, int blockLength, byte [] key)}
+ *             instead
+ * @see        HMAC#HMAC(String digestName, int blockLength, byte [] key)
+ */
+public
+HMAC(String digestName, byte [] key) {
+	this(digestName, 64, key);
+}
+
+/**
+ * Adds data to the current hash
+ * @param b The data
+ * @param offset The index at which to start adding to the hash
+ * @param length The number of bytes to hash
+ */
+public void
+update(byte [] b, int offset, int length) {
+	digest.update(b, offset, length);
+}
+
+/**
+ * Adds data to the current hash
+ * @param b The data
+ */
+public void
+update(byte [] b) {
+	digest.update(b);
+}
+
+/**
+ * Signs the data (computes the secure hash)
+ * @return An array with the signature
+ */
+public byte []
+sign() {
+	byte [] output = digest.digest();
+	digest.reset();
+	digest.update(opad);
+	return digest.digest(output);
+}
+
+/**
+ * Verifies the data (computes the secure hash and compares it to the input)
+ * @param signature The signature to compare against
+ * @return true if the signature matches, false otherwise
+ */
+public boolean
+verify(byte [] signature) {
+	return verify(signature, false);
+}
+
+/**
+ * Verifies the data (computes the secure hash and compares it to the input)
+ * @param signature The signature to compare against
+ * @param truncation_ok If true, the signature may be truncated; only the
+ * number of bytes in the provided signature are compared.
+ * @return true if the signature matches, false otherwise
+ */
+public boolean
+verify(byte [] signature, boolean truncation_ok) {
+	byte [] expected = sign();
+	if (truncation_ok && signature.length < expected.length) {
+		byte [] truncated = new byte[signature.length];
+		System.arraycopy(expected, 0, truncated, 0, truncated.length);
+		expected = truncated;
+	}
+	return Arrays.equals(signature, expected);
+}
+
+/**
+ * Resets the HMAC object for further use
+ */
+public void
+clear() {
+	digest.reset();
+	digest.update(ipad);
+}
+
+/**
+ * Returns the length of the digest.
+ */
+public int
+digestLength() {
+	return digest.getDigestLength();
+}
+
+}
diff --git a/src/org/xbill/DNS/utils/base16.java b/src/org/xbill/DNS/utils/base16.java
new file mode 100644
index 0000000..58024e6
--- /dev/null
+++ b/src/org/xbill/DNS/utils/base16.java
@@ -0,0 +1,73 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS.utils;
+
+import java.io.*;
+
+/**
+ * Routines for converting between Strings of hex-encoded data and arrays of
+ * binary data.  This is not actually used by DNS.
+ *
+ * @author Brian Wellington
+ */
+
+public class base16 {
+
+private static final String Base16 = "0123456789ABCDEF";
+
+private
+base16() {}
+
+/**
+ * Convert binary data to a hex-encoded String
+ * @param b An array containing binary data
+ * @return A String containing the encoded data
+ */
+public static String
+toString(byte [] b) {
+	ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+	for (int i = 0; i < b.length; i++) {
+		short value = (short) (b[i] & 0xFF);
+		byte high = (byte) (value >> 4);
+		byte low = (byte) (value & 0xF);
+		os.write(Base16.charAt(high));
+		os.write(Base16.charAt(low));
+	}
+	return new String(os.toByteArray());
+}
+
+/**
+ * Convert a hex-encoded String to binary data
+ * @param str A String containing the encoded data
+ * @return An array containing the binary data, or null if the string is invalid
+ */
+public static byte []
+fromString(String str) {
+	ByteArrayOutputStream bs = new ByteArrayOutputStream();
+	byte [] raw = str.getBytes();
+	for (int i = 0; i < raw.length; i++) {
+		if (!Character.isWhitespace((char)raw[i]))
+			bs.write(raw[i]);
+	}
+	byte [] in = bs.toByteArray();
+	if (in.length % 2 != 0) {
+		return null;
+	}
+
+	bs.reset();
+	DataOutputStream ds = new DataOutputStream(bs);
+
+	for (int i = 0; i < in.length; i += 2) {
+		byte high = (byte) Base16.indexOf(Character.toUpperCase((char)in[i]));
+		byte low = (byte) Base16.indexOf(Character.toUpperCase((char)in[i+1]));
+		try {
+			ds.writeByte((high << 4) + low);
+		}
+		catch (IOException e) {
+		}
+	}
+	return bs.toByteArray();
+}
+
+}
diff --git a/src/org/xbill/DNS/utils/base32.java b/src/org/xbill/DNS/utils/base32.java
new file mode 100644
index 0000000..a2f26ea
--- /dev/null
+++ b/src/org/xbill/DNS/utils/base32.java
@@ -0,0 +1,213 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS.utils;
+
+import java.io.*;
+
+/**
+ * Routines for converting between Strings of base32-encoded data and arrays
+ * of binary data.  This currently supports the base32 and base32hex alphabets
+ * specified in RFC 4648, sections 6 and 7.
+ * 
+ * @author Brian Wellington
+ */
+
+public class base32 {
+
+public static class Alphabet {
+	private Alphabet() {}
+
+	public static final String BASE32 =
+		"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567=";
+	public static final String BASE32HEX =
+		"0123456789ABCDEFGHIJKLMNOPQRSTUV=";
+};
+
+private String alphabet;
+private boolean padding, lowercase;
+
+/**
+ * Creates an object that can be used to do base32 conversions.
+ * @param alphabet Which alphabet should be used
+ * @param padding Whether padding should be used
+ * @param lowercase Whether lowercase characters should be used.
+ * default parameters (The standard base32 alphabet, no padding, uppercase)
+ */
+public
+base32(String alphabet, boolean padding, boolean lowercase) {
+	this.alphabet = alphabet;
+	this.padding = padding;
+	this.lowercase = lowercase;
+}
+
+static private int
+blockLenToPadding(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;
+	}
+}
+
+static private int
+paddingToBlockLen(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;
+	}
+}
+
+/**
+ * Convert binary data to a base32-encoded String
+ * 
+ * @param b An array containing binary data
+ * @return A String containing the encoded data
+ */
+public String
+toString(byte [] b) {
+	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 = blockLenToPadding(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]);
+			if (lowercase)
+				c = Character.toLowerCase(c);
+			os.write(c);
+		}
+
+		// write out the padding (if any)
+		if (padding) {
+			for (int j = t.length - padlen; j < t.length; j++)
+				os.write('=');
+		}
+	}
+
+    return new String(os.toByteArray());
+}
+
+/**
+ * Convert a base32-encoded String to binary data
+ * 
+ * @param str A String containing the encoded data
+ * @return An array containing the binary data, or null if the string is invalid
+ */
+public byte[]
+fromString(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);
+		}
+	}
+
+	if (padding) {
+		if (bs.size() % 8 != 0)
+			return null;
+	} else {
+		while (bs.size() % 8 != 0)
+			bs.write('=');
+	}
+
+	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 == '=')
+				break;
+			s[j] = (short) alphabet.indexOf(in[i * 8 + j]);
+			if (s[j] < 0)
+				return null;
+			padlen--;
+		}
+		int blocklen = paddingToBlockLen(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 bs.toByteArray();
+}
+
+}
diff --git a/src/org/xbill/DNS/utils/base64.java b/src/org/xbill/DNS/utils/base64.java
new file mode 100644
index 0000000..54567cf
--- /dev/null
+++ b/src/org/xbill/DNS/utils/base64.java
@@ -0,0 +1,145 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS.utils;
+
+import java.io.*;
+
+/**
+ * Routines for converting between Strings of base64-encoded data and arrays of
+ * binary data.
+ *
+ * @author Brian Wellington
+ */
+
+public class base64 {
+
+private static final String Base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
+
+private
+base64() {}
+
+/**
+ * Convert binary data to a base64-encoded String
+ * @param b An array containing binary data
+ * @return A String containing the encoded data
+ */
+public static String
+toString(byte [] b) {
+	ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+	for (int i = 0; i < (b.length + 2) / 3; i++) {
+		short [] s = new short[3];
+		short [] t = new short[4];
+		for (int j = 0; j < 3; j++) {
+			if ((i * 3 + j) < b.length)
+				s[j] = (short) (b[i*3+j] & 0xFF);
+			else
+				s[j] = -1;
+		}
+		
+		t[0] = (short) (s[0] >> 2);
+		if (s[1] == -1)
+			t[1] = (short) (((s[0] & 0x3) << 4));
+		else
+			t[1] = (short) (((s[0] & 0x3) << 4) + (s[1] >> 4));
+		if (s[1] == -1)
+			t[2] = t[3] = 64;
+		else if (s[2] == -1) {
+			t[2] = (short) (((s[1] & 0xF) << 2));
+			t[3] = 64;
+		}
+		else {
+			t[2] = (short) (((s[1] & 0xF) << 2) + (s[2] >> 6));
+			t[3] = (short) (s[2] & 0x3F);
+		}
+		for (int j = 0; j < 4; j++)
+			os.write(Base64.charAt(t[j]));
+	}
+	return new String(os.toByteArray());
+}
+
+/**
+ * Formats data into a nicely formatted base64 encoded String
+ * @param b An array containing binary data
+ * @param lineLength The number of characters per line
+ * @param prefix A string prefixing the characters on each line
+ * @param addClose Whether to add a close parenthesis or not
+ * @return A String representing the formatted output
+ */
+public static String
+formatString(byte [] b, int lineLength, String prefix, boolean addClose) {
+	String s = toString(b);
+	StringBuffer sb = new StringBuffer();
+	for (int i = 0; i < s.length(); i += lineLength) {
+		sb.append (prefix);
+		if (i + lineLength >= s.length()) {
+			sb.append(s.substring(i));
+			if (addClose)
+				sb.append(" )");
+		}
+		else {
+			sb.append(s.substring(i, i + lineLength));
+			sb.append("\n");
+		}
+	}
+	return sb.toString();
+}
+
+
+/**
+ * Convert a base64-encoded String to binary data
+ * @param str A String containing the encoded data
+ * @return An array containing the binary data, or null if the string is invalid
+ */
+public static byte []
+fromString(String str) {
+	ByteArrayOutputStream bs = new ByteArrayOutputStream();
+	byte [] raw = str.getBytes();
+	for (int i = 0; i < raw.length; i++) {
+		if (!Character.isWhitespace((char)raw[i]))
+			bs.write(raw[i]);
+	}
+	byte [] in = bs.toByteArray();
+	if (in.length % 4 != 0) {
+		return null;
+	}
+
+	bs.reset();
+	DataOutputStream ds = new DataOutputStream(bs);
+
+	for (int i = 0; i < (in.length + 3) / 4; i++) {
+		short [] s = new short[4];
+		short [] t = new short[3];
+
+		for (int j = 0; j < 4; j++)
+			s[j] = (short) Base64.indexOf(in[i*4+j]);
+
+		t[0] = (short) ((s[0] << 2) + (s[1] >> 4));
+		if (s[2] == 64) {
+			t[1] = t[2] = (short) (-1);
+			if ((s[1] & 0xF) != 0)
+				return null;
+		}
+		else if (s[3] == 64) {
+			t[1] = (short) (((s[1] << 4) + (s[2] >> 2)) & 0xFF);
+			t[2] = (short) (-1);
+			if ((s[2] & 0x3) != 0)
+				return null;
+		}
+		else {
+			t[1] = (short) (((s[1] << 4) + (s[2] >> 2)) & 0xFF);
+			t[2] = (short) (((s[2] << 6) + s[3]) & 0xFF);
+		}
+
+		try {
+			for (int j = 0; j < 3; j++)
+				if (t[j] >= 0)
+					ds.writeByte(t[j]);
+		}
+		catch (IOException e) {
+		}
+	}
+	return bs.toByteArray();
+}
+
+}
diff --git a/src/org/xbill/DNS/utils/hexdump.java b/src/org/xbill/DNS/utils/hexdump.java
new file mode 100644
index 0000000..1a79a40
--- /dev/null
+++ b/src/org/xbill/DNS/utils/hexdump.java
@@ -0,0 +1,56 @@
+// Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org)
+
+package org.xbill.DNS.utils;
+
+/**
+ * A routine to produce a nice looking hex dump
+ *
+ * @author Brian Wellington
+ */
+
+public class hexdump {
+
+private static final char [] hex = "0123456789ABCDEF".toCharArray();
+
+/**
+ * Dumps a byte array into hex format.
+ * @param description If not null, a description of the data.
+ * @param b The data to be printed.
+ * @param offset The start of the data in the array.
+ * @param length The length of the data in the array.
+ */
+public static String
+dump(String description, byte [] b, int offset, int length) {
+	StringBuffer sb = new StringBuffer();
+
+	sb.append(length + "b");
+	if (description != null)
+		sb.append(" (" + description + ")");
+	sb.append(':');
+
+	int prefixlen = sb.toString().length();
+	prefixlen = (prefixlen + 8) & ~ 7;
+	sb.append('\t');
+
+	int perline = (80 - prefixlen) / 3;
+	for (int i = 0; i < length; i++) {
+		if (i != 0 && i % perline == 0) {
+			sb.append('\n');
+			for (int j = 0; j < prefixlen / 8 ; j++)
+				sb.append('\t');
+		}
+		int value = (int)(b[i + offset]) & 0xFF;
+		sb.append(hex[(value >> 4)]);
+		sb.append(hex[(value & 0xF)]);
+		sb.append(' ');
+	}
+	sb.append('\n');
+	return sb.toString();
+}
+
+public static String
+dump(String s, byte [] b) {
+	return dump(s, b, 0, b.length);
+}
+
+}
diff --git a/src/org/xbill/DNS/windows/DNSServer.properties b/src/org/xbill/DNS/windows/DNSServer.properties
new file mode 100644
index 0000000..25342f9
--- /dev/null
+++ b/src/org/xbill/DNS/windows/DNSServer.properties
@@ -0,0 +1,4 @@
+host_name=Host Name
+primary_dns_suffix=Primary Dns Suffix
+dns_suffix=DNS Suffix
+dns_servers=DNS Servers
diff --git a/src/org/xbill/DNS/windows/DNSServer_de.properties b/src/org/xbill/DNS/windows/DNSServer_de.properties
new file mode 100644
index 0000000..aa3f4a6
--- /dev/null
+++ b/src/org/xbill/DNS/windows/DNSServer_de.properties
@@ -0,0 +1,4 @@
+host_name=Hostname
+primary_dns_suffix=Prim\u00E4res DNS-Suffix
+dns_suffix=DNS-Suffixsuchliste
+dns_servers=DNS-Server
diff --git a/src/org/xbill/DNS/windows/DNSServer_fr.properties b/src/org/xbill/DNS/windows/DNSServer_fr.properties
new file mode 100644
index 0000000..7c87a25
--- /dev/null
+++ b/src/org/xbill/DNS/windows/DNSServer_fr.properties
@@ -0,0 +1,4 @@
+host_name=Nom de l'h\u00F4te
+primary_dns_suffix=Suffixe DNS principal
+dns_suffix=Suffixe DNS propre \u00E0 la connexion
+dns_servers=Serveurs DNS
diff --git a/src/org/xbill/DNS/windows/DNSServer_ja.properties b/src/org/xbill/DNS/windows/DNSServer_ja.properties
new file mode 100644
index 0000000..f873164
--- /dev/null
+++ b/src/org/xbill/DNS/windows/DNSServer_ja.properties
@@ -0,0 +1,4 @@
+host_name=\u30db\u30b9\u30c8\u540d
+primary_dns_suffix=\u30d7\u30e9\u30a4\u30de\u30ea DNS \u30b5\u30d5\u30a3\u30c3\u30af\u30b9
+dns_suffix=DNS \u30b5\u30d5\u30a3\u30c3\u30af\u30b9
+dns_servers=DNS \u30b5\u30fc\u30d0\u30fc
diff --git a/src/org/xbill/DNS/windows/DNSServer_pl.properties b/src/org/xbill/DNS/windows/DNSServer_pl.properties
new file mode 100644
index 0000000..eab5774
--- /dev/null
+++ b/src/org/xbill/DNS/windows/DNSServer_pl.properties
@@ -0,0 +1,4 @@
+host_name=Nazwa hosta
+primary_dns_suffix=Sufiks podstawowej domeny DNS
+dns_suffix=Sufiks DNS konkretnego po\u0142\u0105czenia
+dns_servers=Serwery DNS
diff --git a/src/overview.html b/src/overview.html
new file mode 100644
index 0000000..a2449b1
--- /dev/null
+++ b/src/overview.html
@@ -0,0 +1,4 @@
+<body>API specification for <a href="http://www.jivesoftware.org/smack">Smack</a>, an Open Source XMPP client library.
+<p>
+The {@link org.jivesoftware.smack.Connection} class is the main entry point for the API.
+</body>
diff --git a/update.sh b/update.sh
new file mode 100755
index 0000000..9826c7b
--- /dev/null
+++ b/update.sh
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+rm -rf src
+cd asmack-master
+rm -rf src build
+./build.bash
+mv build/src/trunk ../src
+rm -rf src build