aboutsummaryrefslogtreecommitdiff
path: root/src/com/kenai
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/kenai')
-rw-r--r--src/com/kenai/jbosh/AbstractAttr.java116
-rw-r--r--src/com/kenai/jbosh/AbstractBody.java104
-rw-r--r--src/com/kenai/jbosh/AbstractIntegerAttr.java97
-rw-r--r--src/com/kenai/jbosh/ApacheHTTPResponse.java253
-rw-r--r--src/com/kenai/jbosh/ApacheHTTPSender.java156
-rw-r--r--src/com/kenai/jbosh/AttrAccept.java74
-rw-r--r--src/com/kenai/jbosh/AttrAck.java52
-rw-r--r--src/com/kenai/jbosh/AttrCharsets.java71
-rw-r--r--src/com/kenai/jbosh/AttrHold.java53
-rw-r--r--src/com/kenai/jbosh/AttrInactivity.java53
-rw-r--r--src/com/kenai/jbosh/AttrMaxPause.java65
-rw-r--r--src/com/kenai/jbosh/AttrPause.java65
-rw-r--r--src/com/kenai/jbosh/AttrPolling.java65
-rw-r--r--src/com/kenai/jbosh/AttrRequests.java53
-rw-r--r--src/com/kenai/jbosh/AttrSessionID.java44
-rw-r--r--src/com/kenai/jbosh/AttrVersion.java165
-rw-r--r--src/com/kenai/jbosh/AttrWait.java53
-rw-r--r--src/com/kenai/jbosh/Attributes.java64
-rw-r--r--src/com/kenai/jbosh/BOSHClient.java1536
-rw-r--r--src/com/kenai/jbosh/BOSHClientConfig.java446
-rw-r--r--src/com/kenai/jbosh/BOSHClientConnEvent.java189
-rw-r--r--src/com/kenai/jbosh/BOSHClientConnListener.java34
-rw-r--r--src/com/kenai/jbosh/BOSHClientRequestListener.java45
-rw-r--r--src/com/kenai/jbosh/BOSHClientResponseListener.java37
-rw-r--r--src/com/kenai/jbosh/BOSHException.java50
-rw-r--r--src/com/kenai/jbosh/BOSHMessageEvent.java92
-rw-r--r--src/com/kenai/jbosh/BodyParser.java36
-rw-r--r--src/com/kenai/jbosh/BodyParserResults.java64
-rw-r--r--src/com/kenai/jbosh/BodyParserSAX.java206
-rw-r--r--src/com/kenai/jbosh/BodyParserXmlPull.java165
-rw-r--r--src/com/kenai/jbosh/BodyQName.java165
-rw-r--r--src/com/kenai/jbosh/CMSessionParams.java177
-rw-r--r--src/com/kenai/jbosh/ComposableBody.java345
-rw-r--r--src/com/kenai/jbosh/GZIPCodec.java104
-rw-r--r--src/com/kenai/jbosh/HTTPExchange.java126
-rw-r--r--src/com/kenai/jbosh/HTTPResponse.java54
-rw-r--r--src/com/kenai/jbosh/HTTPSender.java54
-rw-r--r--src/com/kenai/jbosh/QName.java269
-rw-r--r--src/com/kenai/jbosh/RequestIDSequence.java120
-rw-r--r--src/com/kenai/jbosh/ServiceLib.java195
-rw-r--r--src/com/kenai/jbosh/StaticBody.java133
-rw-r--r--src/com/kenai/jbosh/TerminalBindingCondition.java208
-rw-r--r--src/com/kenai/jbosh/ZLIBCodec.java104
-rw-r--r--src/com/kenai/jbosh/package.html8
44 files changed, 6565 insertions, 0 deletions
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