aboutsummaryrefslogtreecommitdiff
path: root/src/com
diff options
context:
space:
mode:
Diffstat (limited to 'src/com')
-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
-rw-r--r--src/com/novell/sasl/client/DigestChallenge.java393
-rw-r--r--src/com/novell/sasl/client/DigestMD5SaslClient.java820
-rw-r--r--src/com/novell/sasl/client/DirectiveList.java363
-rw-r--r--src/com/novell/sasl/client/ParsedDirective.java56
-rw-r--r--src/com/novell/sasl/client/ResponseAuth.java83
-rw-r--r--src/com/novell/sasl/client/TokenParser.java208
50 files changed, 8488 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
diff --git a/src/com/novell/sasl/client/DigestChallenge.java b/src/com/novell/sasl/client/DigestChallenge.java
new file mode 100644
index 0000000..90e6247
--- /dev/null
+++ b/src/com/novell/sasl/client/DigestChallenge.java
@@ -0,0 +1,393 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/DigestChallenge.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2003 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import java.util.*;
+import org.apache.harmony.javax.security.sasl.*;
+
+/**
+ * Implements the DigestChallenge class which will be used by the
+ * DigestMD5SaslClient class
+ */
+class DigestChallenge extends Object
+{
+ public static final int QOP_AUTH = 0x01;
+ public static final int QOP_AUTH_INT = 0x02;
+ public static final int QOP_AUTH_CONF = 0x04;
+ public static final int QOP_UNRECOGNIZED = 0x08;
+
+ private static final int CIPHER_3DES = 0x01;
+ private static final int CIPHER_DES = 0x02;
+ private static final int CIPHER_RC4_40 = 0x04;
+ private static final int CIPHER_RC4 = 0x08;
+ private static final int CIPHER_RC4_56 = 0x10;
+ private static final int CIPHER_UNRECOGNIZED = 0x20;
+ private static final int CIPHER_RECOGNIZED_MASK =
+ CIPHER_3DES | CIPHER_DES | CIPHER_RC4_40 | CIPHER_RC4 | CIPHER_RC4_56;
+
+ private ArrayList m_realms;
+ private String m_nonce;
+ private int m_qop;
+ private boolean m_staleFlag;
+ private int m_maxBuf;
+ private String m_characterSet;
+ private String m_algorithm;
+ private int m_cipherOptions;
+
+ DigestChallenge(
+ byte[] challenge)
+ throws SaslException
+ {
+ m_realms = new ArrayList(5);
+ m_nonce = null;
+ m_qop = 0;
+ m_staleFlag = false;
+ m_maxBuf = -1;
+ m_characterSet = null;
+ m_algorithm = null;
+ m_cipherOptions = 0;
+
+ DirectiveList dirList = new DirectiveList(challenge);
+ try
+ {
+ dirList.parseDirectives();
+ checkSemantics(dirList);
+ }
+ catch (SaslException e)
+ {
+ }
+ }
+
+ /**
+ * Checks the semantics of the directives in the directive list as parsed
+ * from the digest challenge byte array.
+ *
+ * @param dirList the list of directives parsed from the digest challenge
+ *
+ * @exception SaslException If a semantic error occurs
+ */
+ void checkSemantics(
+ DirectiveList dirList) throws SaslException
+ {
+ Iterator directives = dirList.getIterator();
+ ParsedDirective directive;
+ String name;
+
+ while (directives.hasNext())
+ {
+ directive = (ParsedDirective)directives.next();
+ name = directive.getName();
+ if (name.equals("realm"))
+ handleRealm(directive);
+ else if (name.equals("nonce"))
+ handleNonce(directive);
+ else if (name.equals("qop"))
+ handleQop(directive);
+ else if (name.equals("maxbuf"))
+ handleMaxbuf(directive);
+ else if (name.equals("charset"))
+ handleCharset(directive);
+ else if (name.equals("algorithm"))
+ handleAlgorithm(directive);
+ else if (name.equals("cipher"))
+ handleCipher(directive);
+ else if (name.equals("stale"))
+ handleStale(directive);
+ }
+
+ /* post semantic check */
+ if (-1 == m_maxBuf)
+ m_maxBuf = 65536;
+
+ if (m_qop == 0)
+ m_qop = QOP_AUTH;
+ else if ( (m_qop & QOP_AUTH) != QOP_AUTH )
+ throw new SaslException("Only qop-auth is supported by client");
+ else if ( ((m_qop & QOP_AUTH_CONF) == QOP_AUTH_CONF) &&
+ (0 == (m_cipherOptions & CIPHER_RECOGNIZED_MASK)) )
+ throw new SaslException("Invalid cipher options");
+ else if (null == m_nonce)
+ throw new SaslException("Missing nonce directive");
+ else if (m_staleFlag)
+ throw new SaslException("Unexpected stale flag");
+ else if ( null == m_algorithm )
+ throw new SaslException("Missing algorithm directive");
+ }
+
+ /**
+ * This function implements the semenatics of the nonce directive.
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs due to too many nonce
+ * values
+ */
+ void handleNonce(
+ ParsedDirective pd) throws SaslException
+ {
+ if (null != m_nonce)
+ throw new SaslException("Too many nonce values.");
+
+ m_nonce = pd.getValue();
+ }
+
+ /**
+ * This function implements the semenatics of the realm directive.
+ *
+ * @param pd ParsedDirective
+ */
+ void handleRealm(
+ ParsedDirective pd)
+ {
+ m_realms.add(pd.getValue());
+ }
+
+ /**
+ * This function implements the semenatics of the qop (quality of protection)
+ * directive. The value of the qop directive is as defined below:
+ * qop-options = "qop" "=" <"> qop-list <">
+ * qop-list = 1#qop-value
+ * qop-value = "auth" | "auth-int" | "auth-conf" | token
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs due to too many qop
+ * directives
+ */
+ void handleQop(
+ ParsedDirective pd) throws SaslException
+ {
+ String token;
+ TokenParser parser;
+
+ if (m_qop != 0)
+ throw new SaslException("Too many qop directives.");
+
+ parser = new TokenParser(pd.getValue());
+ for (token = parser.parseToken();
+ token != null;
+ token = parser.parseToken())
+ {
+ if (token.equals("auth"))
+ m_qop |= QOP_AUTH;
+ else if (token.equals("auth-int"))
+ m_qop |= QOP_AUTH_INT;
+ else if (token.equals("auth-conf"))
+ m_qop |= QOP_AUTH_CONF;
+ else
+ m_qop |= QOP_UNRECOGNIZED;
+ }
+ }
+
+ /**
+ * This function implements the semenatics of the Maxbuf directive.
+ * the value is defined as: 1*DIGIT
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occur
+ */
+ void handleMaxbuf(
+ ParsedDirective pd) throws SaslException
+ {
+ if (-1 != m_maxBuf) /*it's initialized to -1 */
+ throw new SaslException("Too many maxBuf directives.");
+
+ m_maxBuf = Integer.parseInt(pd.getValue());
+
+ if (0 == m_maxBuf)
+ throw new SaslException("Max buf value must be greater than zero.");
+ }
+
+ /**
+ * This function implements the semenatics of the charset directive.
+ * the value is defined as: 1*DIGIT
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs dur to too many charset
+ * directives or Invalid character encoding
+ * directive
+ */
+ void handleCharset(
+ ParsedDirective pd) throws SaslException
+ {
+ if (null != m_characterSet)
+ throw new SaslException("Too many charset directives.");
+
+ m_characterSet = pd.getValue();
+
+ if (!m_characterSet.equals("utf-8"))
+ throw new SaslException("Invalid character encoding directive");
+ }
+
+ /**
+ * This function implements the semenatics of the charset directive.
+ * the value is defined as: 1*DIGIT
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs due to too many algorith
+ * directive or Invalid algorithm directive
+ * value
+ */
+ void handleAlgorithm(
+ ParsedDirective pd) throws SaslException
+ {
+ if (null != m_algorithm)
+ throw new SaslException("Too many algorithm directives.");
+
+ m_algorithm = pd.getValue();
+
+ if (!"md5-sess".equals(m_algorithm))
+ throw new SaslException("Invalid algorithm directive value: " +
+ m_algorithm);
+ }
+
+ /**
+ * This function implements the semenatics of the cipher-opts directive
+ * directive. The value of the qop directive is as defined below:
+ * qop-options = "qop" "=" <"> qop-list <">
+ * qop-list = 1#qop-value
+ * qop-value = "auth" | "auth-int" | "auth-conf" | token
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs due to Too many cipher
+ * directives
+ */
+ void handleCipher(
+ ParsedDirective pd) throws SaslException
+ {
+ String token;
+ TokenParser parser;
+
+ if (0 != m_cipherOptions)
+ throw new SaslException("Too many cipher directives.");
+
+ parser = new TokenParser(pd.getValue());
+ token = parser.parseToken();
+ for (token = parser.parseToken();
+ token != null;
+ token = parser.parseToken())
+ {
+ if ("3des".equals(token))
+ m_cipherOptions |= CIPHER_3DES;
+ else if ("des".equals(token))
+ m_cipherOptions |= CIPHER_DES;
+ else if ("rc4-40".equals(token))
+ m_cipherOptions |= CIPHER_RC4_40;
+ else if ("rc4".equals(token))
+ m_cipherOptions |= CIPHER_RC4;
+ else if ("rc4-56".equals(token))
+ m_cipherOptions |= CIPHER_RC4_56;
+ else
+ m_cipherOptions |= CIPHER_UNRECOGNIZED;
+ }
+
+ if (m_cipherOptions == 0)
+ m_cipherOptions = CIPHER_UNRECOGNIZED;
+ }
+
+ /**
+ * This function implements the semenatics of the stale directive.
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs due to Too many stale
+ * directives or Invalid stale directive value
+ */
+ void handleStale(
+ ParsedDirective pd) throws SaslException
+ {
+ if (false != m_staleFlag)
+ throw new SaslException("Too many stale directives.");
+
+ if ("true".equals(pd.getValue()))
+ m_staleFlag = true;
+ else
+ throw new SaslException("Invalid stale directive value: " +
+ pd.getValue());
+ }
+
+ /**
+ * Return the list of the All the Realms
+ *
+ * @return List of all the realms
+ */
+ public ArrayList getRealms()
+ {
+ return m_realms;
+ }
+
+ /**
+ * @return Returns the Nonce
+ */
+ public String getNonce()
+ {
+ return m_nonce;
+ }
+
+ /**
+ * Return the quality-of-protection
+ *
+ * @return The quality-of-protection
+ */
+ public int getQop()
+ {
+ return m_qop;
+ }
+
+ /**
+ * @return The state of the Staleflag
+ */
+ public boolean getStaleFlag()
+ {
+ return m_staleFlag;
+ }
+
+ /**
+ * @return The Maximum Buffer value
+ */
+ public int getMaxBuf()
+ {
+ return m_maxBuf;
+ }
+
+ /**
+ * @return character set values as string
+ */
+ public String getCharacterSet()
+ {
+ return m_characterSet;
+ }
+
+ /**
+ * @return The String value of the algorithm
+ */
+ public String getAlgorithm()
+ {
+ return m_algorithm;
+ }
+
+ /**
+ * @return The cipher options
+ */
+ public int getCipherOptions()
+ {
+ return m_cipherOptions;
+ }
+}
+
diff --git a/src/com/novell/sasl/client/DigestMD5SaslClient.java b/src/com/novell/sasl/client/DigestMD5SaslClient.java
new file mode 100644
index 0000000..141c96b
--- /dev/null
+++ b/src/com/novell/sasl/client/DigestMD5SaslClient.java
@@ -0,0 +1,820 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/DigestMD5SaslClient.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2003 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import org.apache.harmony.javax.security.sasl.*;
+import org.apache.harmony.javax.security.auth.callback.*;
+import java.security.SecureRandom;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.io.UnsupportedEncodingException;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * Implements the Client portion of DigestMD5 Sasl mechanism.
+ */
+public class DigestMD5SaslClient implements SaslClient
+{
+ private String m_authorizationId = "";
+ private String m_protocol = "";
+ private String m_serverName = "";
+ private Map m_props;
+ private CallbackHandler m_cbh;
+ private int m_state;
+ private String m_qopValue = "";
+ private char[] m_HA1 = null;
+ private String m_digestURI;
+ private DigestChallenge m_dc;
+ private String m_clientNonce = "";
+ private String m_realm = "";
+ private String m_name = "";
+
+ private static final int STATE_INITIAL = 0;
+ private static final int STATE_DIGEST_RESPONSE_SENT = 1;
+ private static final int STATE_VALID_SERVER_RESPONSE = 2;
+ private static final int STATE_INVALID_SERVER_RESPONSE = 3;
+ private static final int STATE_DISPOSED = 4;
+
+ private static final int NONCE_BYTE_COUNT = 32;
+ private static final int NONCE_HEX_COUNT = 2*NONCE_BYTE_COUNT;
+
+ private static final String DIGEST_METHOD = "AUTHENTICATE";
+
+ /**
+ * Creates an DigestMD5SaslClient object using the parameters supplied.
+ * Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are
+ * contained in props
+ *
+ * @param authorizationId The possibly null protocol-dependent
+ * identification to be used for authorization. If
+ * null or empty, the server derives an authorization
+ * ID from the client's authentication credentials.
+ * When the SASL authentication completes
+ * successfully, the specified entity is granted
+ * access.
+ *
+ * @param protocol The non-null string name of the protocol for which
+ * the authentication is being performed (e.g. "ldap")
+ *
+ * @param serverName The non-null fully qualified host name of the server
+ * to authenticate to
+ *
+ * @param props The possibly null set of properties used to select
+ * the SASL mechanism and to configure the
+ * authentication exchange of the selected mechanism.
+ * See the Sasl class for a list of standard properties.
+ * Other, possibly mechanism-specific, properties can
+ * be included. Properties not relevant to the selected
+ * mechanism are ignored.
+ *
+ * @param cbh The possibly null callback handler to used by the
+ * SASL mechanisms to get further information from the
+ * application/library to complete the authentication.
+ * For example, a SASL mechanism might require the
+ * authentication ID, password and realm from the
+ * caller. The authentication ID is requested by using
+ * a NameCallback. The password is requested by using
+ * a PasswordCallback. The realm is requested by using
+ * a RealmChoiceCallback if there is a list of realms
+ * to choose from, and by using a RealmCallback if the
+ * realm must be entered.
+ *
+ * @return A possibly null SaslClient created using the
+ * parameters supplied. If null, this factory cannot
+ * produce a SaslClient using the parameters supplied.
+ *
+ * @exception SaslException If a SaslClient instance cannot be created
+ * because of an error
+ */
+ public static SaslClient getClient(
+ String authorizationId,
+ String protocol,
+ String serverName,
+ Map props,
+ CallbackHandler cbh)
+ {
+ String desiredQOP = (String)props.get(Sasl.QOP);
+ String desiredStrength = (String)props.get(Sasl.STRENGTH);
+ String serverAuth = (String)props.get(Sasl.SERVER_AUTH);
+
+ //only support qop equal to auth
+ if ((desiredQOP != null) && !"auth".equals(desiredQOP))
+ return null;
+
+ //doesn't support server authentication
+ if ((serverAuth != null) && !"false".equals(serverAuth))
+ return null;
+
+ //need a callback handler to get the password
+ if (cbh == null)
+ return null;
+
+ return new DigestMD5SaslClient(authorizationId, protocol,
+ serverName, props, cbh);
+ }
+
+ /**
+ * Creates an DigestMD5SaslClient object using the parameters supplied.
+ * Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are
+ * contained in props
+ *
+ * @param authorizationId The possibly null protocol-dependent
+ * identification to be used for authorization. If
+ * null or empty, the server derives an authorization
+ * ID from the client's authentication credentials.
+ * When the SASL authentication completes
+ * successfully, the specified entity is granted
+ * access.
+ *
+ * @param protocol The non-null string name of the protocol for which
+ * the authentication is being performed (e.g. "ldap")
+ *
+ * @param serverName The non-null fully qualified host name of the server
+ * to authenticate to
+ *
+ * @param props The possibly null set of properties used to select
+ * the SASL mechanism and to configure the
+ * authentication exchange of the selected mechanism.
+ * See the Sasl class for a list of standard properties.
+ * Other, possibly mechanism-specific, properties can
+ * be included. Properties not relevant to the selected
+ * mechanism are ignored.
+ *
+ * @param cbh The possibly null callback handler to used by the
+ * SASL mechanisms to get further information from the
+ * application/library to complete the authentication.
+ * For example, a SASL mechanism might require the
+ * authentication ID, password and realm from the
+ * caller. The authentication ID is requested by using
+ * a NameCallback. The password is requested by using
+ * a PasswordCallback. The realm is requested by using
+ * a RealmChoiceCallback if there is a list of realms
+ * to choose from, and by using a RealmCallback if the
+ * realm must be entered.
+ *
+ */
+ private DigestMD5SaslClient(
+ String authorizationId,
+ String protocol,
+ String serverName,
+ Map props,
+ CallbackHandler cbh)
+ {
+ m_authorizationId = authorizationId;
+ m_protocol = protocol;
+ m_serverName = serverName;
+ m_props = props;
+ m_cbh = cbh;
+
+ m_state = STATE_INITIAL;
+ }
+
+ /**
+ * Determines if this mechanism has an optional initial response. If true,
+ * caller should call evaluateChallenge() with an empty array to get the
+ * initial response.
+ *
+ * @return true if this mechanism has an initial response
+ */
+ public boolean hasInitialResponse()
+ {
+ return false;
+ }
+
+ /**
+ * Determines if the authentication exchange has completed. This method
+ * may be called at any time, but typically, it will not be called until
+ * the caller has received indication from the server (in a protocol-
+ * specific manner) that the exchange has completed.
+ *
+ * @return true if the authentication exchange has completed;
+ * false otherwise.
+ */
+ public boolean isComplete()
+ {
+ if ((m_state == STATE_VALID_SERVER_RESPONSE) ||
+ (m_state == STATE_INVALID_SERVER_RESPONSE) ||
+ (m_state == STATE_DISPOSED))
+ return true;
+ else
+ return false;
+ }
+
+ /**
+ * Unwraps a byte array received from the server. This method can be called
+ * only after the authentication exchange has completed (i.e., when
+ * isComplete() returns true) and only if the authentication exchange has
+ * negotiated integrity and/or privacy as the quality of protection;
+ * otherwise, an IllegalStateException is thrown.
+ *
+ * incoming is the contents of the SASL buffer as defined in RFC 2222
+ * without the leading four octet field that represents the length.
+ * offset and len specify the portion of incoming to use.
+ *
+ * @param incoming A non-null byte array containing the encoded bytes
+ * from the server
+ * @param offset The starting position at incoming of the bytes to use
+ *
+ * @param len The number of bytes from incoming to use
+ *
+ * @return A non-null byte array containing the decoded bytes
+ *
+ */
+ public byte[] unwrap(
+ byte[] incoming,
+ int offset,
+ int len)
+ throws SaslException
+ {
+ throw new IllegalStateException(
+ "unwrap: QOP has neither integrity nor privacy>");
+ }
+
+ /**
+ * Wraps a byte array to be sent to the server. This method can be called
+ * only after the authentication exchange has completed (i.e., when
+ * isComplete() returns true) and only if the authentication exchange has
+ * negotiated integrity and/or privacy as the quality of protection;
+ * otherwise, an IllegalStateException is thrown.
+ *
+ * The result of this method will make up the contents of the SASL buffer as
+ * defined in RFC 2222 without the leading four octet field that represents
+ * the length. offset and len specify the portion of outgoing to use.
+ *
+ * @param outgoing A non-null byte array containing the bytes to encode
+ * @param offset The starting position at outgoing of the bytes to use
+ * @param len The number of bytes from outgoing to use
+ *
+ * @return A non-null byte array containing the encoded bytes
+ *
+ * @exception SaslException if incoming cannot be successfully unwrapped.
+ *
+ * @exception IllegalStateException if the authentication exchange has
+ * not completed, or if the negotiated quality of
+ * protection has neither integrity nor privacy.
+ */
+ public byte[] wrap(
+ byte[] outgoing,
+ int offset,
+ int len)
+ throws SaslException
+ {
+ throw new IllegalStateException(
+ "wrap: QOP has neither integrity nor privacy>");
+ }
+
+ /**
+ * Retrieves the negotiated property. This method can be called only after
+ * the authentication exchange has completed (i.e., when isComplete()
+ * returns true); otherwise, an IllegalStateException is thrown.
+ *
+ * @param propName The non-null property name
+ *
+ * @return The value of the negotiated property. If null, the property was
+ * not negotiated or is not applicable to this mechanism.
+ *
+ * @exception IllegalStateException if this authentication exchange has
+ * not completed
+ */
+ public Object getNegotiatedProperty(
+ String propName)
+ {
+ if (m_state != STATE_VALID_SERVER_RESPONSE)
+ throw new IllegalStateException(
+ "getNegotiatedProperty: authentication exchange not complete.");
+
+ if (Sasl.QOP.equals(propName))
+ return "auth";
+ else
+ return null;
+ }
+
+ /**
+ * Disposes of any system resources or security-sensitive information the
+ * SaslClient might be using. Invoking this method invalidates the
+ * SaslClient instance. This method is idempotent.
+ *
+ * @exception SaslException if a problem was encountered while disposing
+ * of the resources
+ */
+ public void dispose()
+ throws SaslException
+ {
+ if (m_state != STATE_DISPOSED)
+ {
+ m_state = STATE_DISPOSED;
+ }
+ }
+
+ /**
+ * Evaluates the challenge data and generates a response. If a challenge
+ * is received from the server during the authentication process, this
+ * method is called to prepare an appropriate next response to submit to
+ * the server.
+ *
+ * @param challenge The non-null challenge sent from the server. The
+ * challenge array may have zero length.
+ *
+ * @return The possibly null reponse to send to the server. It is null
+ * if the challenge accompanied a "SUCCESS" status and the
+ * challenge only contains data for the client to update its
+ * state and no response needs to be sent to the server.
+ * The response is a zero-length byte array if the client is to
+ * send a response with no data.
+ *
+ * @exception SaslException If an error occurred while processing the
+ * challenge or generating a response.
+ */
+ public byte[] evaluateChallenge(
+ byte[] challenge)
+ throws SaslException
+ {
+ byte[] response = null;
+
+ //printState();
+ switch (m_state)
+ {
+ case STATE_INITIAL:
+ if (challenge.length == 0)
+ throw new SaslException("response = byte[0]");
+ else
+ try
+ {
+ response = createDigestResponse(challenge).
+ getBytes("UTF-8");
+ m_state = STATE_DIGEST_RESPONSE_SENT;
+ }
+ catch (java.io.UnsupportedEncodingException e)
+ {
+ throw new SaslException(
+ "UTF-8 encoding not suppported by platform", e);
+ }
+ break;
+ case STATE_DIGEST_RESPONSE_SENT:
+ if (checkServerResponseAuth(challenge))
+ m_state = STATE_VALID_SERVER_RESPONSE;
+ else
+ {
+ m_state = STATE_INVALID_SERVER_RESPONSE;
+ throw new SaslException("Could not validate response-auth " +
+ "value from server");
+ }
+ break;
+ case STATE_VALID_SERVER_RESPONSE:
+ case STATE_INVALID_SERVER_RESPONSE:
+ throw new SaslException("Authentication sequence is complete");
+ case STATE_DISPOSED:
+ throw new SaslException("Client has been disposed");
+ default:
+ throw new SaslException("Unknown client state.");
+ }
+
+ return response;
+ }
+
+ /**
+ * This function takes a 16 byte binary md5-hash value and creates a 32
+ * character (plus a terminating null character) hex-digit
+ * representation of binary data.
+ *
+ * @param hash 16 byte binary md5-hash value in bytes
+ *
+ * @return 32 character (plus a terminating null character) hex-digit
+ * representation of binary data.
+ */
+ char[] convertToHex(
+ byte[] hash)
+ {
+ int i;
+ byte j;
+ byte fifteen = 15;
+ char[] hex = new char[32];
+
+ for (i = 0; i < 16; i++)
+ {
+ //convert value of top 4 bits to hex char
+ hex[i*2] = getHexChar((byte)((hash[i] & 0xf0) >> 4));
+ //convert value of bottom 4 bits to hex char
+ hex[(i*2)+1] = getHexChar((byte)(hash[i] & 0x0f));
+ }
+
+ return hex;
+ }
+
+ /**
+ * Calculates the HA1 portion of the response
+ *
+ * @param algorithm Algorith to use.
+ * @param userName User being authenticated
+ * @param realm realm information
+ * @param password password of teh user
+ * @param nonce nonce value
+ * @param clientNonce Clients Nonce value
+ *
+ * @return HA1 portion of the response in a character array
+ *
+ * @exception SaslException If an error occurs
+ */
+ char[] DigestCalcHA1(
+ String algorithm,
+ String userName,
+ String realm,
+ String password,
+ String nonce,
+ String clientNonce) throws SaslException
+ {
+ byte[] hash;
+
+ try
+ {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+
+ md.update(userName.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(realm.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(password.getBytes("UTF-8"));
+ hash = md.digest();
+
+ if ("md5-sess".equals(algorithm))
+ {
+ md.update(hash);
+ md.update(":".getBytes("UTF-8"));
+ md.update(nonce.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(clientNonce.getBytes("UTF-8"));
+ hash = md.digest();
+ }
+ }
+ catch(NoSuchAlgorithmException e)
+ {
+ throw new SaslException("No provider found for MD5 hash", e);
+ }
+ catch(UnsupportedEncodingException e)
+ {
+ throw new SaslException(
+ "UTF-8 encoding not supported by platform.", e);
+ }
+
+ return convertToHex(hash);
+ }
+
+
+ /**
+ * This function calculates the response-value of the response directive of
+ * the digest-response as documented in RFC 2831
+ *
+ * @param HA1 H(A1)
+ * @param serverNonce nonce from server
+ * @param nonceCount 8 hex digits
+ * @param clientNonce client nonce
+ * @param qop qop-value: "", "auth", "auth-int"
+ * @param method method from the request
+ * @param digestUri requested URL
+ * @param clientResponseFlag request-digest or response-digest
+ *
+ * @return Response-value of the response directive of the digest-response
+ *
+ * @exception SaslException If an error occurs
+ */
+ char[] DigestCalcResponse(
+ char[] HA1, /* H(A1) */
+ String serverNonce, /* nonce from server */
+ String nonceCount, /* 8 hex digits */
+ String clientNonce, /* client nonce */
+ String qop, /* qop-value: "", "auth", "auth-int" */
+ String method, /* method from the request */
+ String digestUri, /* requested URL */
+ boolean clientResponseFlag) /* request-digest or response-digest */
+ throws SaslException
+ {
+ byte[] HA2;
+ byte[] respHash;
+ char[] HA2Hex;
+
+ // calculate H(A2)
+ try
+ {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ if (clientResponseFlag)
+ md.update(method.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(digestUri.getBytes("UTF-8"));
+ if ("auth-int".equals(qop))
+ {
+ md.update(":".getBytes("UTF-8"));
+ md.update("00000000000000000000000000000000".getBytes("UTF-8"));
+ }
+ HA2 = md.digest();
+ HA2Hex = convertToHex(HA2);
+
+ // calculate response
+ md.update(new String(HA1).getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(serverNonce.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ if (qop.length() > 0)
+ {
+ md.update(nonceCount.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(clientNonce.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(qop.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ }
+ md.update(new String(HA2Hex).getBytes("UTF-8"));
+ respHash = md.digest();
+ }
+ catch(NoSuchAlgorithmException e)
+ {
+ throw new SaslException("No provider found for MD5 hash", e);
+ }
+ catch(UnsupportedEncodingException e)
+ {
+ throw new SaslException(
+ "UTF-8 encoding not supported by platform.", e);
+ }
+
+ return convertToHex(respHash);
+ }
+
+
+ /**
+ * Creates the intial response to be sent to the server.
+ *
+ * @param challenge Challenge in bytes recived form the Server
+ *
+ * @return Initial response to be sent to the server
+ */
+ private String createDigestResponse(
+ byte[] challenge)
+ throws SaslException
+ {
+ char[] response;
+ StringBuffer digestResponse = new StringBuffer(512);
+ int realmSize;
+
+ m_dc = new DigestChallenge(challenge);
+
+ m_digestURI = m_protocol + "/" + m_serverName;
+
+ if ((m_dc.getQop() & DigestChallenge.QOP_AUTH)
+ == DigestChallenge.QOP_AUTH )
+ m_qopValue = "auth";
+ else
+ throw new SaslException("Client only supports qop of 'auth'");
+
+ //get call back information
+ Callback[] callbacks = new Callback[3];
+ ArrayList realms = m_dc.getRealms();
+ realmSize = realms.size();
+ if (realmSize == 0)
+ {
+ callbacks[0] = new RealmCallback("Realm");
+ }
+ else if (realmSize == 1)
+ {
+ callbacks[0] = new RealmCallback("Realm", (String)realms.get(0));
+ }
+ else
+ {
+ callbacks[0] =
+ new RealmChoiceCallback(
+ "Realm",
+ (String[])realms.toArray(new String[realmSize]),
+ 0, //the default choice index
+ false); //no multiple selections
+ }
+
+ callbacks[1] = new PasswordCallback("Password", false);
+ //false = no echo
+
+ if (m_authorizationId == null || m_authorizationId.length() == 0)
+ callbacks[2] = new NameCallback("Name");
+ else
+ callbacks[2] = new NameCallback("Name", m_authorizationId);
+
+ try
+ {
+ m_cbh.handle(callbacks);
+ }
+ catch(UnsupportedCallbackException e)
+ {
+ throw new SaslException("Handler does not support" +
+ " necessary callbacks",e);
+ }
+ catch(IOException e)
+ {
+ throw new SaslException("IO exception in CallbackHandler.", e);
+ }
+
+ if (realmSize > 1)
+ {
+ int[] selections =
+ ((RealmChoiceCallback)callbacks[0]).getSelectedIndexes();
+
+ if (selections.length > 0)
+ m_realm =
+ ((RealmChoiceCallback)callbacks[0]).getChoices()[selections[0]];
+ else
+ m_realm = ((RealmChoiceCallback)callbacks[0]).getChoices()[0];
+ }
+ else
+ m_realm = ((RealmCallback)callbacks[0]).getText();
+
+ m_clientNonce = getClientNonce();
+
+ m_name = ((NameCallback)callbacks[2]).getName();
+ if (m_name == null)
+ m_name = ((NameCallback)callbacks[2]).getDefaultName();
+ if (m_name == null)
+ throw new SaslException("No user name was specified.");
+
+ m_HA1 = DigestCalcHA1(
+ m_dc.getAlgorithm(),
+ m_name,
+ m_realm,
+ new String(((PasswordCallback)callbacks[1]).getPassword()),
+ m_dc.getNonce(),
+ m_clientNonce);
+
+ response = DigestCalcResponse(m_HA1,
+ m_dc.getNonce(),
+ "00000001",
+ m_clientNonce,
+ m_qopValue,
+ "AUTHENTICATE",
+ m_digestURI,
+ true);
+
+ digestResponse.append("username=\"");
+ digestResponse.append(m_authorizationId);
+ if (0 != m_realm.length())
+ {
+ digestResponse.append("\",realm=\"");
+ digestResponse.append(m_realm);
+ }
+ digestResponse.append("\",cnonce=\"");
+ digestResponse.append(m_clientNonce);
+ digestResponse.append("\",nc=");
+ digestResponse.append("00000001"); //nounce count
+ digestResponse.append(",qop=");
+ digestResponse.append(m_qopValue);
+ digestResponse.append(",digest-uri=\"");
+ digestResponse.append(m_digestURI);
+ digestResponse.append("\",response=");
+ digestResponse.append(response);
+ digestResponse.append(",charset=utf-8,nonce=\"");
+ digestResponse.append(m_dc.getNonce());
+ digestResponse.append("\"");
+
+ return digestResponse.toString();
+ }
+
+
+ /**
+ * This function validates the server response. This step performs a
+ * modicum of mutual authentication by verifying that the server knows
+ * the user's password
+ *
+ * @param serverResponse Response recived form Server
+ *
+ * @return true if the mutual authentication succeeds;
+ * else return false
+ *
+ * @exception SaslException If an error occurs
+ */
+ boolean checkServerResponseAuth(
+ byte[] serverResponse) throws SaslException
+ {
+ char[] response;
+ ResponseAuth responseAuth = null;
+ String responseStr;
+
+ responseAuth = new ResponseAuth(serverResponse);
+
+ response = DigestCalcResponse(m_HA1,
+ m_dc.getNonce(),
+ "00000001",
+ m_clientNonce,
+ m_qopValue,
+ DIGEST_METHOD,
+ m_digestURI,
+ false);
+
+ responseStr = new String(response);
+
+ return responseStr.equals(responseAuth.getResponseValue());
+ }
+
+
+ /**
+ * This function returns hex character representing the value of the input
+ *
+ * @param value Input value in byte
+ *
+ * @return Hex value of the Input byte value
+ */
+ private static char getHexChar(
+ byte value)
+ {
+ switch (value)
+ {
+ case 0:
+ return '0';
+ case 1:
+ return '1';
+ case 2:
+ return '2';
+ case 3:
+ return '3';
+ case 4:
+ return '4';
+ case 5:
+ return '5';
+ case 6:
+ return '6';
+ case 7:
+ return '7';
+ case 8:
+ return '8';
+ case 9:
+ return '9';
+ case 10:
+ return 'a';
+ case 11:
+ return 'b';
+ case 12:
+ return 'c';
+ case 13:
+ return 'd';
+ case 14:
+ return 'e';
+ case 15:
+ return 'f';
+ default:
+ return 'Z';
+ }
+ }
+
+ /**
+ * Calculates the Nonce value of the Client
+ *
+ * @return Nonce value of the client
+ *
+ * @exception SaslException If an error Occurs
+ */
+ String getClientNonce() throws SaslException
+ {
+ byte[] nonceBytes = new byte[NONCE_BYTE_COUNT];
+ SecureRandom prng;
+ byte nonceByte;
+ char[] hexNonce = new char[NONCE_HEX_COUNT];
+
+ try
+ {
+ prng = SecureRandom.getInstance("SHA1PRNG");
+ prng.nextBytes(nonceBytes);
+ for(int i=0; i<NONCE_BYTE_COUNT; i++)
+ {
+ //low nibble
+ hexNonce[i*2] = getHexChar((byte)(nonceBytes[i] & 0x0f));
+ //high nibble
+ hexNonce[(i*2)+1] = getHexChar((byte)((nonceBytes[i] & 0xf0)
+ >> 4));
+ }
+ return new String(hexNonce);
+ }
+ catch(NoSuchAlgorithmException e)
+ {
+ throw new SaslException("No random number generator available", e);
+ }
+ }
+
+ /**
+ * Returns the IANA-registered mechanism name of this SASL client.
+ * (e.g. "CRAM-MD5", "GSSAPI")
+ *
+ * @return "DIGEST-MD5"the IANA-registered mechanism name of this SASL
+ * client.
+ */
+ public String getMechanismName()
+ {
+ return "DIGEST-MD5";
+ }
+
+} //end class DigestMD5SaslClient
+
diff --git a/src/com/novell/sasl/client/DirectiveList.java b/src/com/novell/sasl/client/DirectiveList.java
new file mode 100644
index 0000000..fc26a6b
--- /dev/null
+++ b/src/com/novell/sasl/client/DirectiveList.java
@@ -0,0 +1,363 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/DirectiveList.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import java.util.*;
+import org.apache.harmony.javax.security.sasl.*;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Implements the DirectiveList class whihc will be used by the
+ * DigestMD5SaslClient class
+ */
+class DirectiveList extends Object
+{
+ private static final int STATE_LOOKING_FOR_FIRST_DIRECTIVE = 1;
+ private static final int STATE_LOOKING_FOR_DIRECTIVE = 2;
+ private static final int STATE_SCANNING_NAME = 3;
+ private static final int STATE_LOOKING_FOR_EQUALS = 4;
+ private static final int STATE_LOOKING_FOR_VALUE = 5;
+ private static final int STATE_LOOKING_FOR_COMMA = 6;
+ private static final int STATE_SCANNING_QUOTED_STRING_VALUE = 7;
+ private static final int STATE_SCANNING_TOKEN_VALUE = 8;
+ private static final int STATE_NO_UTF8_SUPPORT = 9;
+
+ private int m_curPos;
+ private int m_errorPos;
+ private String m_directives;
+ private int m_state;
+ private ArrayList m_directiveList;
+ private String m_curName;
+ private int m_scanStart;
+
+ /**
+ * Constructs a new DirectiveList.
+ */
+ DirectiveList(
+ byte[] directives)
+ {
+ m_curPos = 0;
+ m_state = STATE_LOOKING_FOR_FIRST_DIRECTIVE;
+ m_directiveList = new ArrayList(10);
+ m_scanStart = 0;
+ m_errorPos = -1;
+ try
+ {
+ m_directives = new String(directives, "UTF-8");
+ }
+ catch(UnsupportedEncodingException e)
+ {
+ m_state = STATE_NO_UTF8_SUPPORT;
+ }
+ }
+
+ /**
+ * This function takes a US-ASCII character string containing a list of comma
+ * separated directives, and parses the string into the individual directives
+ * and their values. A directive consists of a token specifying the directive
+ * name followed by an equal sign (=) and the directive value. The value is
+ * either a token or a quoted string
+ *
+ * @exception SaslException If an error Occurs
+ */
+ void parseDirectives() throws SaslException
+ {
+ char prevChar;
+ char currChar;
+ int rc = 0;
+ boolean haveQuotedPair = false;
+ String currentName = "<no name>";
+
+ if (m_state == STATE_NO_UTF8_SUPPORT)
+ throw new SaslException("No UTF-8 support on platform");
+
+ prevChar = 0;
+
+ while (m_curPos < m_directives.length())
+ {
+ currChar = m_directives.charAt(m_curPos);
+ switch (m_state)
+ {
+ case STATE_LOOKING_FOR_FIRST_DIRECTIVE:
+ case STATE_LOOKING_FOR_DIRECTIVE:
+ if (isWhiteSpace(currChar))
+ {
+ break;
+ }
+ else if (isValidTokenChar(currChar))
+ {
+ m_scanStart = m_curPos;
+ m_state = STATE_SCANNING_NAME;
+ }
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Invalid name character");
+ }
+ break;
+
+ case STATE_SCANNING_NAME:
+ if (isValidTokenChar(currChar))
+ {
+ break;
+ }
+ else if (isWhiteSpace(currChar))
+ {
+ currentName = m_directives.substring(m_scanStart, m_curPos);
+ m_state = STATE_LOOKING_FOR_EQUALS;
+ }
+ else if ('=' == currChar)
+ {
+ currentName = m_directives.substring(m_scanStart, m_curPos);
+ m_state = STATE_LOOKING_FOR_VALUE;
+ }
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Invalid name character");
+ }
+ break;
+
+ case STATE_LOOKING_FOR_EQUALS:
+ if (isWhiteSpace(currChar))
+ {
+ break;
+ }
+ else if ('=' == currChar)
+ {
+ m_state = STATE_LOOKING_FOR_VALUE;
+ }
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Expected equals sign '='.");
+ }
+ break;
+
+ case STATE_LOOKING_FOR_VALUE:
+ if (isWhiteSpace(currChar))
+ {
+ break;
+ }
+ else if ('"' == currChar)
+ {
+ m_scanStart = m_curPos+1; /* don't include the quote */
+ m_state = STATE_SCANNING_QUOTED_STRING_VALUE;
+ }
+ else if (isValidTokenChar(currChar))
+ {
+ m_scanStart = m_curPos;
+ m_state = STATE_SCANNING_TOKEN_VALUE;
+ }
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Unexpected character");
+ }
+ break;
+
+ case STATE_SCANNING_TOKEN_VALUE:
+ if (isValidTokenChar(currChar))
+ {
+ break;
+ }
+ else if (isWhiteSpace(currChar))
+ {
+ addDirective(currentName, false);
+ m_state = STATE_LOOKING_FOR_COMMA;
+ }
+ else if (',' == currChar)
+ {
+ addDirective(currentName, false);
+ m_state = STATE_LOOKING_FOR_DIRECTIVE;
+ }
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Invalid value character");
+ }
+ break;
+
+ case STATE_SCANNING_QUOTED_STRING_VALUE:
+ if ('\\' == currChar)
+ haveQuotedPair = true;
+ if ( ('"' == currChar) &&
+ ('\\' != prevChar) )
+ {
+ addDirective(currentName, haveQuotedPair);
+ haveQuotedPair = false;
+ m_state = STATE_LOOKING_FOR_COMMA;
+ }
+ break;
+
+ case STATE_LOOKING_FOR_COMMA:
+ if (isWhiteSpace(currChar))
+ break;
+ else if (currChar == ',')
+ m_state = STATE_LOOKING_FOR_DIRECTIVE;
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Expected a comma.");
+ }
+ break;
+ }
+ if (0 != rc)
+ break;
+ prevChar = currChar;
+ m_curPos++;
+ } /* end while loop */
+
+
+ if (rc == 0)
+ {
+ /* check the ending state */
+ switch (m_state)
+ {
+ case STATE_SCANNING_TOKEN_VALUE:
+ addDirective(currentName, false);
+ break;
+
+ case STATE_LOOKING_FOR_FIRST_DIRECTIVE:
+ case STATE_LOOKING_FOR_COMMA:
+ break;
+
+ case STATE_LOOKING_FOR_DIRECTIVE:
+ throw new SaslException("Parse error: Trailing comma.");
+
+ case STATE_SCANNING_NAME:
+ case STATE_LOOKING_FOR_EQUALS:
+ case STATE_LOOKING_FOR_VALUE:
+ throw new SaslException("Parse error: Missing value.");
+
+ case STATE_SCANNING_QUOTED_STRING_VALUE:
+ throw new SaslException("Parse error: Missing closing quote.");
+ }
+ }
+
+ }
+
+ /**
+ * This function returns TRUE if the character is a valid token character.
+ *
+ * token = 1*<any CHAR except CTLs or separators>
+ *
+ * separators = "(" | ")" | "<" | ">" | "@"
+ * | "," | ";" | ":" | "\" | <">
+ * | "/" | "[" | "]" | "?" | "="
+ * | "{" | "}" | SP | HT
+ *
+ * CTL = <any US-ASCII control character
+ * (octets 0 - 31) and DEL (127)>
+ *
+ * CHAR = <any US-ASCII character (octets 0 - 127)>
+ *
+ * @param c character to be tested
+ *
+ * @return Returns TRUE if the character is a valid token character.
+ */
+ boolean isValidTokenChar(
+ char c)
+ {
+ if ( ( (c >= '\u0000') && (c <='\u0020') ) ||
+ ( (c >= '\u003a') && (c <= '\u0040') ) ||
+ ( (c >= '\u005b') && (c <= '\u005d') ) ||
+ ('\u002c' == c) ||
+ ('\u0025' == c) ||
+ ('\u0028' == c) ||
+ ('\u0029' == c) ||
+ ('\u007b' == c) ||
+ ('\u007d' == c) ||
+ ('\u007f' == c) )
+ return false;
+
+ return true;
+ }
+
+ /**
+ * This function returns TRUE if the character is linear white space (LWS).
+ * LWS = [CRLF] 1*( SP | HT )
+ * @param c Input charcter to be tested
+ *
+ * @return Returns TRUE if the character is linear white space (LWS)
+ */
+ boolean isWhiteSpace(
+ char c)
+ {
+ if ( ('\t' == c) || // HORIZONTAL TABULATION.
+ ('\n' == c) || // LINE FEED.
+ ('\r' == c) || // CARRIAGE RETURN.
+ ('\u0020' == c) )
+ return true;
+
+ return false;
+ }
+
+ /**
+ * This function creates a directive record and adds it to the list, the
+ * value will be added later after it is parsed.
+ *
+ * @param name Name
+ * @param haveQuotedPair true if quoted pair is there else false
+ */
+ void addDirective(
+ String name,
+ boolean haveQuotedPair)
+ {
+ String value;
+ int inputIndex;
+ int valueIndex;
+ char valueChar;
+ int type;
+
+ if (!haveQuotedPair)
+ {
+ value = m_directives.substring(m_scanStart, m_curPos);
+ }
+ else
+ { //copy one character at a time skipping backslash excapes.
+ StringBuffer valueBuf = new StringBuffer(m_curPos - m_scanStart);
+ valueIndex = 0;
+ inputIndex = m_scanStart;
+ while (inputIndex < m_curPos)
+ {
+ if ('\\' == (valueChar = m_directives.charAt(inputIndex)))
+ inputIndex++;
+ valueBuf.setCharAt(valueIndex, m_directives.charAt(inputIndex));
+ valueIndex++;
+ inputIndex++;
+ }
+ value = new String(valueBuf);
+ }
+
+ if (m_state == STATE_SCANNING_QUOTED_STRING_VALUE)
+ type = ParsedDirective.QUOTED_STRING_VALUE;
+ else
+ type = ParsedDirective.TOKEN_VALUE;
+ m_directiveList.add(new ParsedDirective(name, value, type));
+ }
+
+
+ /**
+ * Returns the List iterator.
+ *
+ * @return Returns the Iterator Object for the List.
+ */
+ Iterator getIterator()
+ {
+ return m_directiveList.iterator();
+ }
+}
+
diff --git a/src/com/novell/sasl/client/ParsedDirective.java b/src/com/novell/sasl/client/ParsedDirective.java
new file mode 100644
index 0000000..17bf70e
--- /dev/null
+++ b/src/com/novell/sasl/client/ParsedDirective.java
@@ -0,0 +1,56 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/ParsedDirective.java,v 1.1 2003/08/21 10:06:26 kkanil Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+/**
+ * Implements the ParsedDirective class which will be used in the
+ * DigestMD5SaslClient mechanism.
+ */
+class ParsedDirective
+{
+ public static final int QUOTED_STRING_VALUE = 1;
+ public static final int TOKEN_VALUE = 2;
+
+ private int m_valueType;
+ private String m_name;
+ private String m_value;
+
+ ParsedDirective(
+ String name,
+ String value,
+ int type)
+ {
+ m_name = name;
+ m_value = value;
+ m_valueType = type;
+ }
+
+ String getValue()
+ {
+ return m_value;
+ }
+
+ String getName()
+ {
+ return m_name;
+ }
+
+ int getValueType()
+ {
+ return m_valueType;
+ }
+
+}
+
diff --git a/src/com/novell/sasl/client/ResponseAuth.java b/src/com/novell/sasl/client/ResponseAuth.java
new file mode 100644
index 0000000..0aef955
--- /dev/null
+++ b/src/com/novell/sasl/client/ResponseAuth.java
@@ -0,0 +1,83 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/ResponseAuth.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import java.util.*;
+import org.apache.harmony.javax.security.sasl.*;
+
+/**
+ * Implements the ResponseAuth class used by the DigestMD5SaslClient mechanism
+ */
+class ResponseAuth
+{
+
+ private String m_responseValue;
+
+ ResponseAuth(
+ byte[] responseAuth)
+ throws SaslException
+ {
+ m_responseValue = null;
+
+ DirectiveList dirList = new DirectiveList(responseAuth);
+ try
+ {
+ dirList.parseDirectives();
+ checkSemantics(dirList);
+ }
+ catch (SaslException e)
+ {
+ }
+ }
+
+ /**
+ * Checks the semantics of the directives in the directive list as parsed
+ * from the digest challenge byte array.
+ *
+ * @param dirList the list of directives parsed from the digest challenge
+ *
+ * @exception SaslException If a semantic error occurs
+ */
+ void checkSemantics(
+ DirectiveList dirList) throws SaslException
+ {
+ Iterator directives = dirList.getIterator();
+ ParsedDirective directive;
+ String name;
+
+ while (directives.hasNext())
+ {
+ directive = (ParsedDirective)directives.next();
+ name = directive.getName();
+ if (name.equals("rspauth"))
+ m_responseValue = directive.getValue();
+ }
+
+ /* post semantic check */
+ if (m_responseValue == null)
+ throw new SaslException("Missing response-auth directive.");
+ }
+
+ /**
+ * returns the ResponseValue
+ *
+ * @return the ResponseValue as a String.
+ */
+ public String getResponseValue()
+ {
+ return m_responseValue;
+ }
+}
+
diff --git a/src/com/novell/sasl/client/TokenParser.java b/src/com/novell/sasl/client/TokenParser.java
new file mode 100644
index 0000000..3d3491d
--- /dev/null
+++ b/src/com/novell/sasl/client/TokenParser.java
@@ -0,0 +1,208 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/TokenParser.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import org.apache.harmony.javax.security.sasl.*;
+/**
+ * The TokenParser class will parse individual tokens from a list of tokens that
+ * are a directive value for a DigestMD5 authentication.The tokens are separated
+ * commas.
+ */
+class TokenParser extends Object
+{
+ private static final int STATE_LOOKING_FOR_FIRST_TOKEN = 1;
+ private static final int STATE_LOOKING_FOR_TOKEN = 2;
+ private static final int STATE_SCANNING_TOKEN = 3;
+ private static final int STATE_LOOKING_FOR_COMMA = 4;
+ private static final int STATE_PARSING_ERROR = 5;
+ private static final int STATE_DONE = 6;
+
+ private int m_curPos;
+ private int m_scanStart;
+ private int m_state;
+ private String m_tokens;
+
+
+ TokenParser(
+ String tokens)
+ {
+ m_tokens = tokens;
+ m_curPos = 0;
+ m_scanStart = 0;
+ m_state = STATE_LOOKING_FOR_FIRST_TOKEN;
+ }
+
+ /**
+ * This function parses the next token from the tokens string and returns
+ * it as a string. If there are no more tokens a null reference is returned.
+ *
+ * @return the parsed token or a null reference if there are no more
+ * tokens
+ *
+ * @exception SASLException if an error occurs while parsing
+ */
+ String parseToken() throws SaslException
+ {
+ char currChar;
+ String token = null;
+
+
+ if (m_state == STATE_DONE)
+ return null;
+
+ while (m_curPos < m_tokens.length() && (token == null))
+ {
+ currChar = m_tokens.charAt(m_curPos);
+ switch (m_state)
+ {
+ case STATE_LOOKING_FOR_FIRST_TOKEN:
+ case STATE_LOOKING_FOR_TOKEN:
+ if (isWhiteSpace(currChar))
+ {
+ break;
+ }
+ else if (isValidTokenChar(currChar))
+ {
+ m_scanStart = m_curPos;
+ m_state = STATE_SCANNING_TOKEN;
+ }
+ else
+ {
+ m_state = STATE_PARSING_ERROR;
+ throw new SaslException("Invalid token character at position " + m_curPos);
+ }
+ break;
+
+ case STATE_SCANNING_TOKEN:
+ if (isValidTokenChar(currChar))
+ {
+ break;
+ }
+ else if (isWhiteSpace(currChar))
+ {
+ token = m_tokens.substring(m_scanStart, m_curPos);
+ m_state = STATE_LOOKING_FOR_COMMA;
+ }
+ else if (',' == currChar)
+ {
+ token = m_tokens.substring(m_scanStart, m_curPos);
+ m_state = STATE_LOOKING_FOR_TOKEN;
+ }
+ else
+ {
+ m_state = STATE_PARSING_ERROR;
+ throw new SaslException("Invalid token character at position " + m_curPos);
+ }
+ break;
+
+
+ case STATE_LOOKING_FOR_COMMA:
+ if (isWhiteSpace(currChar))
+ break;
+ else if (currChar == ',')
+ m_state = STATE_LOOKING_FOR_TOKEN;
+ else
+ {
+ m_state = STATE_PARSING_ERROR;
+ throw new SaslException("Expected a comma, found '" +
+ currChar + "' at postion " +
+ m_curPos);
+ }
+ break;
+ }
+ m_curPos++;
+ } /* end while loop */
+
+ if (token == null)
+ { /* check the ending state */
+ switch (m_state)
+ {
+ case STATE_SCANNING_TOKEN:
+ token = m_tokens.substring(m_scanStart);
+ m_state = STATE_DONE;
+ break;
+
+ case STATE_LOOKING_FOR_FIRST_TOKEN:
+ case STATE_LOOKING_FOR_COMMA:
+ break;
+
+ case STATE_LOOKING_FOR_TOKEN:
+ throw new SaslException("Trialing comma");
+ }
+ }
+
+ return token;
+ }
+
+ /**
+ * This function returns TRUE if the character is a valid token character.
+ *
+ * token = 1*<any CHAR except CTLs or separators>
+ *
+ * separators = "(" | ")" | "<" | ">" | "@"
+ * | "," | ";" | ":" | "\" | <">
+ * | "/" | "[" | "]" | "?" | "="
+ * | "{" | "}" | SP | HT
+ *
+ * CTL = <any US-ASCII control character
+ * (octets 0 - 31) and DEL (127)>
+ *
+ * CHAR = <any US-ASCII character (octets 0 - 127)>
+ *
+ * @param c character to be validated
+ *
+ * @return True if character is valid Token character else it returns
+ * false
+ */
+ boolean isValidTokenChar(
+ char c)
+ {
+ if ( ( (c >= '\u0000') && (c <='\u0020') ) ||
+ ( (c >= '\u003a') && (c <= '\u0040') ) ||
+ ( (c >= '\u005b') && (c <= '\u005d') ) ||
+ ('\u002c' == c) ||
+ ('\u0025' == c) ||
+ ('\u0028' == c) ||
+ ('\u0029' == c) ||
+ ('\u007b' == c) ||
+ ('\u007d' == c) ||
+ ('\u007f' == c) )
+ return false;
+
+ return true;
+ }
+
+ /**
+ * This function returns TRUE if the character is linear white space (LWS).
+ * LWS = [CRLF] 1*( SP | HT )
+ *
+ * @param c character to be validated
+ *
+ * @return True if character is liner whitespace else it returns false
+ */
+ boolean isWhiteSpace(
+ char c)
+ {
+ if ( ('\t' == c) || // HORIZONTAL TABULATION.
+ ('\n' == c) || // LINE FEED.
+ ('\r' == c) || // CARRIAGE RETURN.
+ ('\u0020' == c) )
+ return true;
+
+ return false;
+ }
+
+}
+