diff options
author | Shuyi Chen <shuyichen@google.com> | 2013-05-22 14:51:55 -0700 |
---|---|---|
committer | Shuyi Chen <shuyichen@google.com> | 2013-05-22 17:19:30 -0700 |
commit | d7955ce24d294fb2014c59d11fca184471056f44 (patch) | |
tree | e260500b0b7639127038495d46a0ad6dcbb6d96c /src/com/kenai | |
parent | 8f4ce9ea0de51fee918bffe19c434612d6bbb2d7 (diff) | |
download | smack-android-5.0.1_r1.tar.gz |
Add android smack source.HEADandroid-wear-5.0.0_r1android-sdk-4.4.2_r1.0.1android-sdk-4.4.2_r1android-l-preview_r2android-cts-5.1_r9android-cts-5.1_r8android-cts-5.1_r7android-cts-5.1_r6android-cts-5.1_r5android-cts-5.1_r4android-cts-5.1_r3android-cts-5.1_r28android-cts-5.1_r27android-cts-5.1_r26android-cts-5.1_r25android-cts-5.1_r24android-cts-5.1_r23android-cts-5.1_r22android-cts-5.1_r21android-cts-5.1_r20android-cts-5.1_r2android-cts-5.1_r19android-cts-5.1_r18android-cts-5.1_r17android-cts-5.1_r16android-cts-5.1_r15android-cts-5.1_r14android-cts-5.1_r13android-cts-5.1_r10android-cts-5.1_r1android-cts-5.0_r9android-cts-5.0_r8android-cts-5.0_r7android-cts-5.0_r6android-cts-5.0_r5android-cts-5.0_r4android-cts-5.0_r3android-cts-4.4_r4android-cts-4.4_r1android-5.1.1_r9android-5.1.1_r8android-5.1.1_r7android-5.1.1_r6android-5.1.1_r5android-5.1.1_r4android-5.1.1_r38android-5.1.1_r37android-5.1.1_r36android-5.1.1_r35android-5.1.1_r34android-5.1.1_r33android-5.1.1_r30android-5.1.1_r3android-5.1.1_r29android-5.1.1_r28android-5.1.1_r26android-5.1.1_r25android-5.1.1_r24android-5.1.1_r23android-5.1.1_r22android-5.1.1_r20android-5.1.1_r2android-5.1.1_r19android-5.1.1_r18android-5.1.1_r17android-5.1.1_r16android-5.1.1_r15android-5.1.1_r14android-5.1.1_r13android-5.1.1_r12android-5.1.1_r10android-5.1.1_r1android-5.1.0_r5android-5.1.0_r4android-5.1.0_r3android-5.1.0_r1android-5.0.2_r3android-5.0.2_r1android-5.0.1_r1android-5.0.0_r7android-5.0.0_r6android-5.0.0_r5.1android-5.0.0_r5android-5.0.0_r4android-5.0.0_r3android-5.0.0_r2android-5.0.0_r1android-4.4w_r1android-4.4_r1.2.0.1android-4.4_r1.2android-4.4_r1.1.0.1android-4.4_r1.1android-4.4_r1.0.1android-4.4_r1android-4.4_r0.9android-4.4_r0.8android-4.4_r0.7android-4.4.4_r2.0.1android-4.4.4_r2android-4.4.4_r1.0.1android-4.4.4_r1android-4.4.3_r1.1.0.1android-4.4.3_r1.1android-4.4.3_r1.0.1android-4.4.3_r1android-4.4.2_r2.0.1android-4.4.2_r2android-4.4.2_r1.0.1android-4.4.2_r1android-4.4.1_r1.0.1android-4.4.1_r1android-4.3_r3.1android-4.3_r3android-4.3_r2.3android-4.3_r2.2android-4.3_r2.1android-4.3_r2android-4.3_r1.1android-4.3_r1android-4.3_r0.9.1android-4.3_r0.9android-4.3.1_r1tools_r22.2mastermainlollipop-wear-releaselollipop-releaselollipop-mr1-wfc-releaselollipop-mr1-releaselollipop-mr1-fi-releaselollipop-mr1-devlollipop-mr1-cts-releaselollipop-devlollipop-cts-releasel-previewkitkat-wearkitkat-releasekitkat-mr2.2-releasekitkat-mr2.1-releasekitkat-mr2-releasekitkat-mr1.1-releasekitkat-mr1-releasekitkat-devkitkat-cts-releasekitkat-cts-devjb-mr2.0.0-releasejb-mr2.0-releasejb-mr2-releasejb-mr2-devidea133-weekly-releaseidea133
Change-Id: I49ce97136c17173c4ae3965c694af6e7bc49897d
Diffstat (limited to 'src/com/kenai')
44 files changed, 6565 insertions, 0 deletions
diff --git a/src/com/kenai/jbosh/AbstractAttr.java b/src/com/kenai/jbosh/AbstractAttr.java new file mode 100644 index 0000000..0d6f84c --- /dev/null +++ b/src/com/kenai/jbosh/AbstractAttr.java @@ -0,0 +1,116 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package com.kenai.jbosh; + +/** + * Abstract base class for creating BOSH attribute classes. Concrete + * implementations of this class will naturally inherit the underlying + * type's behavior for {@code equals()}, {@code hashCode()}, + * {@code toString()}, and {@code compareTo()}, allowing for the easy + * creation of objects which extend existing trivial types. This was done + * to comply with the prefactoring rule declaring, "when you are being + * abstract, be abstract all the way". + * + * @param <T> type of the extension object + */ +abstract class AbstractAttr<T extends Comparable> + implements Comparable { + + /** + * Captured value. + */ + private final T value; + + /** + * Creates a new encapsulated object instance. + * + * @param aValue encapsulated getValue + */ + protected AbstractAttr(final T aValue) { + value = aValue; + } + + /** + * Gets the encapsulated data value. + * + * @return data value + */ + public final T getValue() { + return value; + } + + /////////////////////////////////////////////////////////////////////////// + // Object method overrides: + + /** + * {@inheritDoc} + * + * @param otherObj object to compare to + * @return true if the objects are equal, false otherwise + */ + @Override + public boolean equals(final Object otherObj) { + if (otherObj == null) { + return false; + } else if (otherObj instanceof AbstractAttr) { + AbstractAttr other = + (AbstractAttr) otherObj; + return value.equals(other.value); + } else { + return false; + } + } + + /** + * {@inheritDoc} + * + * @return hashCode of the encapsulated object + */ + @Override + public int hashCode() { + return value.hashCode(); + } + + /** + * {@inheritDoc} + * + * @return string representation of the encapsulated object + */ + @Override + public String toString() { + return value.toString(); + } + + /////////////////////////////////////////////////////////////////////////// + // Comparable interface: + + /** + * {@inheritDoc} + * + * @param otherObj object to compare to + * @return -1, 0, or 1 + */ + @SuppressWarnings("unchecked") + public int compareTo(final Object otherObj) { + if (otherObj == null) { + return 1; + } else { + return value.compareTo(otherObj); + } + } + +} diff --git a/src/com/kenai/jbosh/AbstractBody.java b/src/com/kenai/jbosh/AbstractBody.java new file mode 100644 index 0000000..4d66c8c --- /dev/null +++ b/src/com/kenai/jbosh/AbstractBody.java @@ -0,0 +1,104 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * Class representing a single message to or from the BOSH connection + * manager (CM). + * <p/> + * These messages consist of a single {@code body} element + * (qualified within the BOSH namespace: + * {@code http://jabber.org/protocol/httpbind}) and contain zero or more + * child elements (of any namespace). These child elements constitute the + * message payload. + * <p/> + * In addition to the message payload, the attributes of the wrapper + * {@code body} element may also need to be used as part of the communication + * protocol being implemented on top of BOSH, or to define additional + * namespaces used by the child "payload" elements. These attributes are + * exposed via accessors. + */ +public abstract class AbstractBody { + + /////////////////////////////////////////////////////////////////////////// + // Constructor: + + /** + * Restrict subclasses to the local package. + */ + AbstractBody() { + // Empty + } + + /////////////////////////////////////////////////////////////////////////// + // Public methods: + + /** + * Get a set of all defined attribute names. + * + * @return set of qualified attribute names + */ + public final Set<BodyQName> getAttributeNames() { + Map<BodyQName, String> attrs = getAttributes(); + return Collections.unmodifiableSet(attrs.keySet()); + } + + /** + * Get the value of the specified attribute. + * + * @param attr name of the attribute to retriece + * @return attribute value, or {@code null} if not defined + */ + public final String getAttribute(final BodyQName attr) { + Map<BodyQName, String> attrs = getAttributes(); + return attrs.get(attr); + } + + /////////////////////////////////////////////////////////////////////////// + // Abstract methods: + + /** + * Get a map of all defined attribute names with their corresponding values. + * + * @return map of qualified attributes + */ + public abstract Map<BodyQName, String> getAttributes(); + + /** + * Get an XML String representation of this message. + * + * @return XML string representing the body message + */ + public abstract String toXML(); + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Returns the qualified name of the root/wrapper element. + * + * @return qualified name + */ + static BodyQName getBodyQName() { + return BodyQName.createBOSH("body"); + } + +} diff --git a/src/com/kenai/jbosh/AbstractIntegerAttr.java b/src/com/kenai/jbosh/AbstractIntegerAttr.java new file mode 100644 index 0000000..1b827f9 --- /dev/null +++ b/src/com/kenai/jbosh/AbstractIntegerAttr.java @@ -0,0 +1,97 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Abstract base class for attribute implementations based on {@code Integer} + * types. Additional support for parsing of integer values from their + * {@code String} representations as well as callback handling of value + * validity checks are also provided. + */ +abstract class AbstractIntegerAttr extends AbstractAttr<Integer> { + + /** + * Creates a new attribute object. + * + * @param val attribute value + * @throws BOSHException on parse or validation failure + */ + protected AbstractIntegerAttr(final int val) throws BOSHException { + super(Integer.valueOf(val)); + } + + /** + * Creates a new attribute object. + * + * @param val attribute value in string form + * @throws BOSHException on parse or validation failure + */ + protected AbstractIntegerAttr(final String val) throws BOSHException { + super(parseInt(val)); + } + + /** + * Utility method intended to be called by concrete implementation + * classes from within the {@code check()} method when the concrete + * class needs to ensure that the integer value does not drop below + * the specified minimum value. + * + * @param minVal minimum value to allow + * @throws BOSHException if the integer value is below the specific + * minimum + */ + protected final void checkMinValue(int minVal) throws BOSHException { + int intVal = getValue(); + if (intVal < minVal) { + throw(new BOSHException( + "Illegal attribute value '" + intVal + "' provided. " + + "Must be >= " + minVal)); + } + } + + /** + * Utility method to parse a {@code String} into an {@code Integer}, + * converting any possible {@code NumberFormatException} thrown into + * a {@code BOSHException}. + * + * @param str string to parse + * @return integer value + * @throws BOSHException on {@code NumberFormatException} + */ + private static int parseInt(final String str) throws BOSHException { + try { + return Integer.parseInt(str); + } catch (NumberFormatException nfx) { + throw(new BOSHException( + "Could not parse an integer from the value provided: " + + str, + nfx)); + } + } + + /** + * Returns the native {@code int} value of the underlying {@code Integer}. + * Will throw {@code NullPointerException} if the underlying + * integer was {@code null}. + * + * @return native {@code int} value + */ + public int intValue() { + return getValue().intValue(); + } + +} diff --git a/src/com/kenai/jbosh/ApacheHTTPResponse.java b/src/com/kenai/jbosh/ApacheHTTPResponse.java new file mode 100644 index 0000000..9f6731f --- /dev/null +++ b/src/com/kenai/jbosh/ApacheHTTPResponse.java @@ -0,0 +1,253 @@ +/* + * Copyright 2009 Guenther Niess + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.io.IOException; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; + +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpContext; +import org.apache.http.util.EntityUtils; + +final class ApacheHTTPResponse implements HTTPResponse { + + /////////////////////////////////////////////////////////////////////////// + // Constants: + + /** + * Name of the accept encoding header. + */ + private static final String ACCEPT_ENCODING = "Accept-Encoding"; + + /** + * Value to use for the ACCEPT_ENCODING header. + */ + private static final String ACCEPT_ENCODING_VAL = + ZLIBCodec.getID() + ", " + GZIPCodec.getID(); + + /** + * Name of the character set to encode the body to/from. + */ + private static final String CHARSET = "UTF-8"; + + /** + * Content type to use when transmitting the body data. + */ + private static final String CONTENT_TYPE = "text/xml; charset=utf-8"; + + /////////////////////////////////////////////////////////////////////////// + // Class variables: + + /** + * Lock used for internal synchronization. + */ + private final Lock lock = new ReentrantLock(); + + /** + * The execution state of an HTTP process. + */ + private final HttpContext context; + + /** + * HttpClient instance to use to communicate. + */ + private final HttpClient client; + + /** + * The HTTP POST request is sent to the server. + */ + private final HttpPost post; + + /** + * A flag which indicates if the transmission was already done. + */ + private boolean sent; + + /** + * Exception to throw when the response data is attempted to be accessed, + * or {@code null} if no exception should be thrown. + */ + private BOSHException toThrow; + + /** + * The response body which was received from the server or {@code null} + * if that has not yet happened. + */ + private AbstractBody body; + + /** + * The HTTP response status code. + */ + private int statusCode; + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Create and send a new request to the upstream connection manager, + * providing deferred access to the results to be returned. + * + * @param client client instance to use when sending the request + * @param cfg client configuration + * @param params connection manager parameters from the session creation + * response, or {@code null} if the session has not yet been established + * @param request body of the client request + */ + ApacheHTTPResponse( + final HttpClient client, + final BOSHClientConfig cfg, + final CMSessionParams params, + final AbstractBody request) { + super(); + this.client = client; + this.context = new BasicHttpContext(); + this.post = new HttpPost(cfg.getURI().toString()); + this.sent = false; + + try { + String xml = request.toXML(); + byte[] data = xml.getBytes(CHARSET); + + String encoding = null; + if (cfg.isCompressionEnabled() && params != null) { + AttrAccept accept = params.getAccept(); + if (accept != null) { + if (accept.isAccepted(ZLIBCodec.getID())) { + encoding = ZLIBCodec.getID(); + data = ZLIBCodec.encode(data); + } else if (accept.isAccepted(GZIPCodec.getID())) { + encoding = GZIPCodec.getID(); + data = GZIPCodec.encode(data); + } + } + } + + ByteArrayEntity entity = new ByteArrayEntity(data); + entity.setContentType(CONTENT_TYPE); + if (encoding != null) { + entity.setContentEncoding(encoding); + } + post.setEntity(entity); + if (cfg.isCompressionEnabled()) { + post.setHeader(ACCEPT_ENCODING, ACCEPT_ENCODING_VAL); + } + } catch (Exception e) { + toThrow = new BOSHException("Could not generate request", e); + } + } + + /////////////////////////////////////////////////////////////////////////// + // HTTPResponse interface methods: + + /** + * Abort the client transmission and response processing. + */ + public void abort() { + if (post != null) { + post.abort(); + toThrow = new BOSHException("HTTP request aborted"); + } + } + + /** + * Wait for and then return the response body. + * + * @return body of the response + * @throws InterruptedException if interrupted while awaiting the response + * @throws BOSHException on communication failure + */ + public AbstractBody getBody() throws InterruptedException, BOSHException { + if (toThrow != null) { + throw(toThrow); + } + lock.lock(); + try { + if (!sent) { + awaitResponse(); + } + } finally { + lock.unlock(); + } + return body; + } + + /** + * Wait for and then return the response HTTP status code. + * + * @return HTTP status code of the response + * @throws InterruptedException if interrupted while awaiting the response + * @throws BOSHException on communication failure + */ + public int getHTTPStatus() throws InterruptedException, BOSHException { + if (toThrow != null) { + throw(toThrow); + } + lock.lock(); + try { + if (!sent) { + awaitResponse(); + } + } finally { + lock.unlock(); + } + return statusCode; + } + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Await the response, storing the result in the instance variables of + * this class when they arrive. + * + * @throws InterruptedException if interrupted while awaiting the response + * @throws BOSHException on communication failure + */ + private synchronized void awaitResponse() throws BOSHException { + HttpEntity entity = null; + try { + HttpResponse httpResp = client.execute(post, context); + entity = httpResp.getEntity(); + byte[] data = EntityUtils.toByteArray(entity); + String encoding = entity.getContentEncoding() != null ? + entity.getContentEncoding().getValue() : + null; + if (ZLIBCodec.getID().equalsIgnoreCase(encoding)) { + data = ZLIBCodec.decode(data); + } else if (GZIPCodec.getID().equalsIgnoreCase(encoding)) { + data = GZIPCodec.decode(data); + } + body = StaticBody.fromString(new String(data, CHARSET)); + statusCode = httpResp.getStatusLine().getStatusCode(); + sent = true; + } catch (IOException iox) { + abort(); + toThrow = new BOSHException("Could not obtain response", iox); + throw(toThrow); + } catch (RuntimeException ex) { + abort(); + throw(ex); + } + } +} diff --git a/src/com/kenai/jbosh/ApacheHTTPSender.java b/src/com/kenai/jbosh/ApacheHTTPSender.java new file mode 100644 index 0000000..b3d3c93 --- /dev/null +++ b/src/com/kenai/jbosh/ApacheHTTPSender.java @@ -0,0 +1,156 @@ +/* + * Copyright 2009 Guenther Niess + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.http.HttpHost; +import org.apache.http.HttpVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.conn.params.ConnRoutePNames; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; + +/** + * Implementation of the {@code HTTPSender} interface which uses the + * Apache HttpClient API to send messages to the connection manager. + */ +final class ApacheHTTPSender implements HTTPSender { + + /** + * Lock used for internal synchronization. + */ + private final Lock lock = new ReentrantLock(); + + /** + * Session configuration. + */ + private BOSHClientConfig cfg; + + /** + * HttpClient instance to use to communicate. + */ + private HttpClient httpClient; + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Prevent construction apart from our package. + */ + ApacheHTTPSender() { + // Load Apache HTTP client class + HttpClient.class.getName(); + } + + /////////////////////////////////////////////////////////////////////////// + // HTTPSender interface methods: + + /** + * {@inheritDoc} + */ + public void init(final BOSHClientConfig session) { + lock.lock(); + try { + cfg = session; + httpClient = initHttpClient(session); + } finally { + lock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + public void destroy() { + lock.lock(); + try { + if (httpClient != null) { + httpClient.getConnectionManager().shutdown(); + } + } finally { + cfg = null; + httpClient = null; + lock.unlock(); + } + } + + /** + * {@inheritDoc} + */ + public HTTPResponse send( + final CMSessionParams params, + final AbstractBody body) { + HttpClient mClient; + BOSHClientConfig mCfg; + lock.lock(); + try { + if (httpClient == null) { + httpClient = initHttpClient(cfg); + } + mClient = httpClient; + mCfg = cfg; + } finally { + lock.unlock(); + } + return new ApacheHTTPResponse(mClient, mCfg, params, body); + } + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + private synchronized HttpClient initHttpClient(final BOSHClientConfig config) { + // Create and initialize HTTP parameters + HttpParams params = new BasicHttpParams(); + ConnManagerParams.setMaxTotalConnections(params, 100); + HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); + HttpProtocolParams.setUseExpectContinue(params, false); + if (config != null && + config.getProxyHost() != null && + config.getProxyPort() != 0) { + HttpHost proxy = new HttpHost( + config.getProxyHost(), + config.getProxyPort()); + params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy); + } + + // Create and initialize scheme registry + SchemeRegistry schemeRegistry = new SchemeRegistry(); + schemeRegistry.register( + new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); + SSLSocketFactory sslFactory = SSLSocketFactory.getSocketFactory(); + sslFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + schemeRegistry.register( + new Scheme("https", sslFactory, 443)); + + // Create an HttpClient with the ThreadSafeClientConnManager. + // This connection manager must be used if more than one thread will + // be using the HttpClient. + ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry); + return new DefaultHttpClient(cm, params); + } +} diff --git a/src/com/kenai/jbosh/AttrAccept.java b/src/com/kenai/jbosh/AttrAccept.java new file mode 100644 index 0000000..4f767df --- /dev/null +++ b/src/com/kenai/jbosh/AttrAccept.java @@ -0,0 +1,74 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code accept} attribute of the + * {@code bosh} element. + */ +final class AttrAccept extends AbstractAttr<String> { + + /** + * Array of the accepted encodings. + */ + private final String[] encodings; + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrAccept(final String val) { + super(val); + encodings = val.split("[\\s,]+"); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrAccept createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrAccept(str); + } + } + + /** + * Determines whether or not the specified encoding is supported. + * + * @param name encoding name + * @result {@code true} if the encoding is accepted, {@code false} + * otherwise + */ + boolean isAccepted(final String name) { + for (String str : encodings) { + if (str.equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + +} diff --git a/src/com/kenai/jbosh/AttrAck.java b/src/com/kenai/jbosh/AttrAck.java new file mode 100644 index 0000000..6cfe22b --- /dev/null +++ b/src/com/kenai/jbosh/AttrAck.java @@ -0,0 +1,52 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code ack} attribute of the + * {@code bosh} element. + */ +final class AttrAck extends AbstractAttr<String> { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrAck(final String val) throws BOSHException { + super(val); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrAck createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrAck(str); + } + } + +} diff --git a/src/com/kenai/jbosh/AttrCharsets.java b/src/com/kenai/jbosh/AttrCharsets.java new file mode 100644 index 0000000..45ce78c --- /dev/null +++ b/src/com/kenai/jbosh/AttrCharsets.java @@ -0,0 +1,71 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code charsets} attribute of the + * {@code bosh} element. + */ +final class AttrCharsets extends AbstractAttr<String> { + + /** + * Array of the accepted character sets. + */ + private final String[] charsets; + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + */ + private AttrCharsets(final String val) { + super(val); + charsets = val.split("\\ +"); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + */ + static AttrCharsets createFromString(final String str) { + if (str == null) { + return null; + } else { + return new AttrCharsets(str); + } + } + + /** + * Determines whether or not the specified charset is supported. + * + * @param name encoding name + * @result {@code true} if the encoding is accepted, {@code false} + * otherwise + */ + boolean isAccepted(final String name) { + for (String str : charsets) { + if (str.equalsIgnoreCase(name)) { + return true; + } + } + return false; + } + +} diff --git a/src/com/kenai/jbosh/AttrHold.java b/src/com/kenai/jbosh/AttrHold.java new file mode 100644 index 0000000..56f21dd --- /dev/null +++ b/src/com/kenai/jbosh/AttrHold.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code hold} attribute of the + * {@code bosh} element. + */ +final class AttrHold extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrHold(final String val) throws BOSHException { + super(val); + checkMinValue(0); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrHold createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrHold(str); + } + } + +} diff --git a/src/com/kenai/jbosh/AttrInactivity.java b/src/com/kenai/jbosh/AttrInactivity.java new file mode 100644 index 0000000..14ab7d4 --- /dev/null +++ b/src/com/kenai/jbosh/AttrInactivity.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the value of the {@code inactivity} attribute of the + * {@code bosh} element. + */ +final class AttrInactivity extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute value + * @throws BOSHException on parse or validation failure + */ + private AttrInactivity(final String val) throws BOSHException { + super(val); + checkMinValue(0); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return instance of the attribute for the specified string, or + * {@code null} if input string is {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrInactivity createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrInactivity(str); + } + } + +} diff --git a/src/com/kenai/jbosh/AttrMaxPause.java b/src/com/kenai/jbosh/AttrMaxPause.java new file mode 100644 index 0000000..8d1d98b --- /dev/null +++ b/src/com/kenai/jbosh/AttrMaxPause.java @@ -0,0 +1,65 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.util.concurrent.TimeUnit; + +/** + * Data type representing the getValue of the {@code maxpause} attribute of the + * {@code bosh} element. + */ +final class AttrMaxPause extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrMaxPause(final String val) throws BOSHException { + super(val); + checkMinValue(1); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrMaxPause createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrMaxPause(str); + } + } + + /** + * Get the max pause time in milliseconds. + * + * @return pause tme in milliseconds + */ + public int getInMilliseconds() { + return (int) TimeUnit.MILLISECONDS.convert( + intValue(), TimeUnit.SECONDS); + } + +} diff --git a/src/com/kenai/jbosh/AttrPause.java b/src/com/kenai/jbosh/AttrPause.java new file mode 100644 index 0000000..5fb3282 --- /dev/null +++ b/src/com/kenai/jbosh/AttrPause.java @@ -0,0 +1,65 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.util.concurrent.TimeUnit; + +/** + * Data type representing the getValue of the {@code pause} attribute of the + * {@code bosh} element. + */ +final class AttrPause extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrPause(final String val) throws BOSHException { + super(val); + checkMinValue(1); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrPause createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrPause(str); + } + } + + /** + * Get the pause time in milliseconds. + * + * @return pause tme in milliseconds + */ + public int getInMilliseconds() { + return (int) TimeUnit.MILLISECONDS.convert( + intValue(), TimeUnit.SECONDS); + } + +} diff --git a/src/com/kenai/jbosh/AttrPolling.java b/src/com/kenai/jbosh/AttrPolling.java new file mode 100644 index 0000000..3f0b08d --- /dev/null +++ b/src/com/kenai/jbosh/AttrPolling.java @@ -0,0 +1,65 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.util.concurrent.TimeUnit; + +/** + * Data type representing the getValue of the {@code polling} attribute of the + * {@code bosh} element. + */ +final class AttrPolling extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrPolling(final String str) throws BOSHException { + super(str); + checkMinValue(0); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return instance of the attribute for the specified string, or + * {@code null} if input string is {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrPolling createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrPolling(str); + } + } + + /** + * Get the polling interval in milliseconds. + * + * @return polling interval in milliseconds + */ + public int getInMilliseconds() { + return (int) TimeUnit.MILLISECONDS.convert( + intValue(), TimeUnit.SECONDS); + } + +} diff --git a/src/com/kenai/jbosh/AttrRequests.java b/src/com/kenai/jbosh/AttrRequests.java new file mode 100644 index 0000000..bfdc529 --- /dev/null +++ b/src/com/kenai/jbosh/AttrRequests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the value of the {@code requests} attribute of the + * {@code bosh} element. + */ +final class AttrRequests extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute value + * @throws BOSHException on parse or validation failure + */ + private AttrRequests(final String val) throws BOSHException { + super(val); + checkMinValue(1); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return instance of the attribute for the specified string, or + * {@code null} if input string is {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrRequests createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrRequests(str); + } + } + +} diff --git a/src/com/kenai/jbosh/AttrSessionID.java b/src/com/kenai/jbosh/AttrSessionID.java new file mode 100644 index 0000000..1998968 --- /dev/null +++ b/src/com/kenai/jbosh/AttrSessionID.java @@ -0,0 +1,44 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code sid} attribute of the + * {@code bosh} element. + */ +final class AttrSessionID extends AbstractAttr<String> { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + */ + private AttrSessionID(final String val) { + super(val); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance + */ + static AttrSessionID createFromString(final String str) { + return new AttrSessionID(str); + } + +} diff --git a/src/com/kenai/jbosh/AttrVersion.java b/src/com/kenai/jbosh/AttrVersion.java new file mode 100644 index 0000000..9396e3b --- /dev/null +++ b/src/com/kenai/jbosh/AttrVersion.java @@ -0,0 +1,165 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code ver} attribute of the + * {@code bosh} element. + */ +final class AttrVersion extends AbstractAttr<String> implements Comparable { + + /** + * Default value if none is provided. + */ + private static final AttrVersion DEFAULT; + static { + try { + DEFAULT = createFromString("1.8"); + } catch (BOSHException boshx) { + throw(new IllegalStateException(boshx)); + } + } + + /** + * Major portion of the version. + */ + private final int major; + + /** + * Minor portion of the version. + */ + private final int minor; + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrVersion(final String val) throws BOSHException { + super(val); + + int idx = val.indexOf('.'); + if (idx <= 0) { + throw(new BOSHException( + "Illegal ver attribute value (not in major.minor form): " + + val)); + } + + String majorStr = val.substring(0, idx); + try { + major = Integer.parseInt(majorStr); + } catch (NumberFormatException nfx) { + throw(new BOSHException( + "Could not parse ver attribute value (major ver): " + + majorStr, + nfx)); + } + if (major < 0) { + throw(new BOSHException( + "Major version may not be < 0")); + } + + String minorStr = val.substring(idx + 1); + try { + minor = Integer.parseInt(minorStr); + } catch (NumberFormatException nfx) { + throw(new BOSHException( + "Could not parse ver attribute value (minor ver): " + + minorStr, + nfx)); + } + if (minor < 0) { + throw(new BOSHException( + "Minor version may not be < 0")); + } + } + + /** + * Get the version of specifcation that we support. + * + * @return max spec version the code supports + */ + static AttrVersion getSupportedVersion() { + return DEFAULT; + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrVersion createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrVersion(str); + } + } + + /** + * Returns the 'major' portion of the version number. + * + * @return major digits only + */ + int getMajor() { + return major; + } + + /** + * Returns the 'minor' portion of the version number. + * + * @return minor digits only + */ + int getMinor() { + return minor; + } + + /////////////////////////////////////////////////////////////////////////// + // Comparable interface: + + /** + * {@inheritDoc} + * + * @param otherObj object to compare to + * @return -1, 0, or 1 + */ + @Override + public int compareTo(final Object otherObj) { + if (otherObj instanceof AttrVersion) { + AttrVersion other = (AttrVersion) otherObj; + if (major < other.major) { + return -1; + } else if (major > other.major) { + return 1; + } else if (minor < other.minor) { + return -1; + } else if (minor > other.minor) { + return 1; + } else { + return 0; + } + } else { + return 0; + } + } + +} diff --git a/src/com/kenai/jbosh/AttrWait.java b/src/com/kenai/jbosh/AttrWait.java new file mode 100644 index 0000000..d2c95f7 --- /dev/null +++ b/src/com/kenai/jbosh/AttrWait.java @@ -0,0 +1,53 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Data type representing the getValue of the {@code wait} attribute of the + * {@code bosh} element. + */ +final class AttrWait extends AbstractIntegerAttr { + + /** + * Creates a new attribute object. + * + * @param val attribute getValue + * @throws BOSHException on parse or validation failure + */ + private AttrWait(final String val) throws BOSHException { + super(val); + checkMinValue(1); + } + + /** + * Creates a new attribute instance from the provided String. + * + * @param str string representation of the attribute + * @return attribute instance or {@code null} if provided string is + * {@code null} + * @throws BOSHException on parse or validation failure + */ + static AttrWait createFromString(final String str) + throws BOSHException { + if (str == null) { + return null; + } else { + return new AttrWait(str); + } + } + +} diff --git a/src/com/kenai/jbosh/Attributes.java b/src/com/kenai/jbosh/Attributes.java new file mode 100644 index 0000000..d01541e --- /dev/null +++ b/src/com/kenai/jbosh/Attributes.java @@ -0,0 +1,64 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import javax.xml.XMLConstants; + +/** + * Class containing constants for attribute definitions used by the + * XEP-0124 specification. We shouldn't need to expose these outside + * our package, since nobody else should be needing to worry about + * them. + */ +final class Attributes { + + /** + * Private constructor to prevent construction of library class. + */ + private Attributes() { + super(); + } + + static final BodyQName ACCEPT = BodyQName.createBOSH("accept"); + static final BodyQName AUTHID = BodyQName.createBOSH("authid"); + static final BodyQName ACK = BodyQName.createBOSH("ack"); + static final BodyQName CHARSETS = BodyQName.createBOSH("charsets"); + static final BodyQName CONDITION = BodyQName.createBOSH("condition"); + static final BodyQName CONTENT = BodyQName.createBOSH("content"); + static final BodyQName FROM = BodyQName.createBOSH("from"); + static final BodyQName HOLD = BodyQName.createBOSH("hold"); + static final BodyQName INACTIVITY = BodyQName.createBOSH("inactivity"); + static final BodyQName KEY = BodyQName.createBOSH("key"); + static final BodyQName MAXPAUSE = BodyQName.createBOSH("maxpause"); + static final BodyQName NEWKEY = BodyQName.createBOSH("newkey"); + static final BodyQName PAUSE = BodyQName.createBOSH("pause"); + static final BodyQName POLLING = BodyQName.createBOSH("polling"); + static final BodyQName REPORT = BodyQName.createBOSH("report"); + static final BodyQName REQUESTS = BodyQName.createBOSH("requests"); + static final BodyQName RID = BodyQName.createBOSH("rid"); + static final BodyQName ROUTE = BodyQName.createBOSH("route"); + static final BodyQName SECURE = BodyQName.createBOSH("secure"); + static final BodyQName SID = BodyQName.createBOSH("sid"); + static final BodyQName STREAM = BodyQName.createBOSH("stream"); + static final BodyQName TIME = BodyQName.createBOSH("time"); + static final BodyQName TO = BodyQName.createBOSH("to"); + static final BodyQName TYPE = BodyQName.createBOSH("type"); + static final BodyQName VER = BodyQName.createBOSH("ver"); + static final BodyQName WAIT = BodyQName.createBOSH("wait"); + static final BodyQName XML_LANG = + BodyQName.createWithPrefix(XMLConstants.XML_NS_URI, "lang", "xml"); +} diff --git a/src/com/kenai/jbosh/BOSHClient.java b/src/com/kenai/jbosh/BOSHClient.java new file mode 100644 index 0000000..b96d188 --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClient.java @@ -0,0 +1,1536 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import com.kenai.jbosh.ComposableBody.Builder; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.Executors; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * BOSH Client session instance. Each communication session with a remote + * connection manager is represented and handled by an instance of this + * class. This is the main entry point for client-side communications. + * To create a new session, a client configuration must first be created + * and then used to create a client instance: + * <pre> + * BOSHClientConfig cfg = BOSHClientConfig.Builder.create( + * "http://server:1234/httpbind", "jabber.org") + * .setFrom("user@jabber.org") + * .build(); + * BOSHClient client = BOSHClient.create(cfg); + * </pre> + * Additional client configuration options are available. See the + * {@code BOSHClientConfig.Builder} class for more information. + * <p/> + * Once a {@code BOSHClient} instance has been created, communication with + * the remote connection manager can begin. No attempt will be made to + * establish a connection to the connection manager until the first call + * is made to the {@code send(ComposableBody)} method. Note that it is + * possible to send an empty body to cause an immediate connection attempt + * to the connection manager. Sending an empty message would look like + * the following: + * <pre> + * client.send(ComposableBody.builder().build()); + * </pre> + * For more information on creating body messages with content, see the + * {@code ComposableBody.Builder} class documentation. + * <p/> + * Once a session has been successfully started, the client instance can be + * used to send arbitrary payload data. All aspects of the BOSH + * protocol involving setting and processing attributes in the BOSH + * namespace will be handled by the client code transparently and behind the + * scenes. The user of the client instance can therefore concentrate + * entirely on the content of the message payload, leaving the semantics of + * the BOSH protocol to the client implementation. + * <p/> + * To be notified of incoming messages from the remote connection manager, + * a {@code BOSHClientResponseListener} should be added to the client instance. + * All incoming messages will be published to all response listeners as they + * arrive and are processed. As with the transmission of payload data via + * the {@code send(ComposableBody)} method, there is no need to worry about + * handling of the BOSH attributes, since this is handled behind the scenes. + * <p/> + * If the connection to the remote connection manager is terminated (either + * explicitly or due to a terminal condition of some sort), all connection + * listeners will be notified. After the connection has been closed, the + * client instance is considered dead and a new one must be created in order + * to resume communications with the remote server. + * <p/> + * Instances of this class are thread-safe. + * + * @see BOSHClientConfig.Builder + * @see BOSHClientResponseListener + * @see BOSHClientConnListener + * @see ComposableBody.Builder + */ +public final class BOSHClient { + + /** + * Logger. + */ + private static final Logger LOG = Logger.getLogger( + BOSHClient.class.getName()); + + /** + * Value of the 'type' attribute used for session termination. + */ + private static final String TERMINATE = "terminate"; + + /** + * Value of the 'type' attribute used for recoverable errors. + */ + private static final String ERROR = "error"; + + /** + * Message to use for interrupted exceptions. + */ + private static final String INTERRUPTED = "Interrupted"; + + /** + * Message used for unhandled exceptions. + */ + private static final String UNHANDLED = "Unhandled Exception"; + + /** + * Message used whena null listener is detected. + */ + private static final String NULL_LISTENER = "Listener may not b enull"; + + /** + * Default empty request delay. + */ + private static final int DEFAULT_EMPTY_REQUEST_DELAY = 100; + + /** + * Amount of time to wait before sending an empty request, in + * milliseconds. + */ + private static final int EMPTY_REQUEST_DELAY = Integer.getInteger( + BOSHClient.class.getName() + ".emptyRequestDelay", + DEFAULT_EMPTY_REQUEST_DELAY); + + /** + * Default value for the pause margin. + */ + private static final int DEFAULT_PAUSE_MARGIN = 500; + + /** + * The amount of time in milliseconds which will be reserved as a + * safety margin when scheduling empty requests against a maxpause + * value. This should give us enough time to build the message + * and transport it to the remote host. + */ + private static final int PAUSE_MARGIN = Integer.getInteger( + BOSHClient.class.getName() + ".pauseMargin", + DEFAULT_PAUSE_MARGIN); + + /** + * Flag indicating whether or not we want to perform assertions. + */ + private static final boolean ASSERTIONS; + + /** + * Connection listeners. + */ + private final Set<BOSHClientConnListener> connListeners = + new CopyOnWriteArraySet<BOSHClientConnListener>(); + + /** + * Request listeners. + */ + private final Set<BOSHClientRequestListener> requestListeners = + new CopyOnWriteArraySet<BOSHClientRequestListener>(); + + /** + * Response listeners. + */ + private final Set<BOSHClientResponseListener> responseListeners = + new CopyOnWriteArraySet<BOSHClientResponseListener>(); + + /** + * Lock instance. + */ + private final ReentrantLock lock = new ReentrantLock(); + + /** + * Condition indicating that there are messages to be exchanged. + */ + private final Condition notEmpty = lock.newCondition(); + + /** + * Condition indicating that there are available slots for sending + * messages. + */ + private final Condition notFull = lock.newCondition(); + + /** + * Condition indicating that there are no outstanding connections. + */ + private final Condition drained = lock.newCondition(); + + /** + * Session configuration. + */ + private final BOSHClientConfig cfg; + + /** + * Processor thread runnable instance. + */ + private final Runnable procRunnable = new Runnable() { + /** + * Process incoming messages. + */ + public void run() { + processMessages(); + } + }; + + /** + * Processor thread runnable instance. + */ + private final Runnable emptyRequestRunnable = new Runnable() { + /** + * Process incoming messages. + */ + public void run() { + sendEmptyRequest(); + } + }; + + /** + * HTTPSender instance. + */ + private final HTTPSender httpSender = + new ApacheHTTPSender(); + + /** + * Storage for test hook implementation. + */ + private final AtomicReference<ExchangeInterceptor> exchInterceptor = + new AtomicReference<ExchangeInterceptor>(); + + /** + * Request ID sequence to use for the session. + */ + private final RequestIDSequence requestIDSeq = new RequestIDSequence(); + + /** + * ScheduledExcecutor to use for deferred tasks. + */ + private final ScheduledExecutorService schedExec = + Executors.newSingleThreadScheduledExecutor(); + + /************************************************************ + * The following vars must be accessed via the lock instance. + */ + + /** + * Thread which is used to process responses from the connection + * manager. Becomes null when session is terminated. + */ + private Thread procThread; + + /** + * Future for sending a deferred empty request, if needed. + */ + private ScheduledFuture emptyRequestFuture; + + /** + * Connection Manager session parameters. Only available when in a + * connected state. + */ + private CMSessionParams cmParams; + + /** + * List of active/outstanding requests. + */ + private Queue<HTTPExchange> exchanges = new LinkedList<HTTPExchange>(); + + /** + * Set of RIDs which have been received, for the purpose of sending + * response acknowledgements. + */ + private SortedSet<Long> pendingResponseAcks = new TreeSet<Long>(); + + /** + * The highest RID that we've already received a response for. This value + * is used to implement response acks. + */ + private Long responseAck = Long.valueOf(-1L); + + /** + * List of requests which have been made but not yet acknowledged. This + * list remains unpopulated if the CM is not acking requests. + */ + private List<ComposableBody> pendingRequestAcks = + new ArrayList<ComposableBody>(); + + /////////////////////////////////////////////////////////////////////////// + // Classes: + + /** + * Class used in testing to dynamically manipulate received exchanges + * at test runtime. + */ + abstract static class ExchangeInterceptor { + /** + * Limit construction. + */ + ExchangeInterceptor() { + // Empty; + } + + /** + * Hook to manipulate an HTTPExchange as is is about to be processed. + * + * @param exch original exchange that would be processed + * @return replacement exchange instance, or {@code null} to skip + * processing of this exchange + */ + abstract HTTPExchange interceptExchange(final HTTPExchange exch); + } + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Determine whether or not we should perform assertions. Assertions + * can be specified via system property explicitly, or defaulted to + * the JVM assertions status. + */ + static { + final String prop = + BOSHClient.class.getSimpleName() + ".assertionsEnabled"; + boolean enabled = false; + if (System.getProperty(prop) == null) { + assert enabled = true; + } else { + enabled = Boolean.getBoolean(prop); + } + ASSERTIONS = enabled; + } + + /** + * Prevent direct construction. + */ + private BOSHClient(final BOSHClientConfig sessCfg) { + cfg = sessCfg; + init(); + } + + /////////////////////////////////////////////////////////////////////////// + // Public methods: + + /** + * Create a new BOSH client session using the client configuration + * information provided. + * + * @param clientCfg session configuration + * @return BOSH session instance + */ + public static BOSHClient create(final BOSHClientConfig clientCfg) { + if (clientCfg == null) { + throw(new IllegalArgumentException( + "Client configuration may not be null")); + } + return new BOSHClient(clientCfg); + } + + /** + * Get the client configuration that was used to create this client + * instance. + * + * @return client configuration + */ + public BOSHClientConfig getBOSHClientConfig() { + return cfg; + } + + /** + * Adds a connection listener to the session. + * + * @param listener connection listener to add, if not already added + */ + public void addBOSHClientConnListener( + final BOSHClientConnListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + connListeners.add(listener); + } + + /** + * Removes a connection listener from the session. + * + * @param listener connection listener to remove, if previously added + */ + public void removeBOSHClientConnListener( + final BOSHClientConnListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + connListeners.remove(listener); + } + + /** + * Adds a request message listener to the session. + * + * @param listener request listener to add, if not already added + */ + public void addBOSHClientRequestListener( + final BOSHClientRequestListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + requestListeners.add(listener); + } + + /** + * Removes a request message listener from the session, if previously + * added. + * + * @param listener instance to remove + */ + public void removeBOSHClientRequestListener( + final BOSHClientRequestListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + requestListeners.remove(listener); + } + + /** + * Adds a response message listener to the session. + * + * @param listener response listener to add, if not already added + */ + public void addBOSHClientResponseListener( + final BOSHClientResponseListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + responseListeners.add(listener); + } + + /** + * Removes a response message listener from the session, if previously + * added. + * + * @param listener instance to remove + */ + public void removeBOSHClientResponseListener( + final BOSHClientResponseListener listener) { + if (listener == null) { + throw(new IllegalArgumentException(NULL_LISTENER)); + } + responseListeners.remove(listener); + } + + /** + * Send the provided message data to the remote connection manager. The + * provided message body does not need to have any BOSH-specific attribute + * information set. It only needs to contain the actual message payload + * that should be delivered to the remote server. + * <p/> + * The first call to this method will result in a connection attempt + * to the remote connection manager. Subsequent calls to this method + * will block until the underlying session state allows for the message + * to be transmitted. In certain scenarios - such as when the maximum + * number of outbound connections has been reached - calls to this method + * will block for short periods of time. + * + * @param body message data to send to remote server + * @throws BOSHException on message transmission failure + */ + public void send(final ComposableBody body) throws BOSHException { + assertUnlocked(); + if (body == null) { + throw(new IllegalArgumentException( + "Message body may not be null")); + } + + HTTPExchange exch; + CMSessionParams params; + lock.lock(); + try { + blockUntilSendable(body); + if (!isWorking() && !isTermination(body)) { + throw(new BOSHException( + "Cannot send message when session is closed")); + } + + long rid = requestIDSeq.getNextRID(); + ComposableBody request = body; + params = cmParams; + if (params == null && exchanges.isEmpty()) { + // This is the first message being sent + request = applySessionCreationRequest(rid, body); + } else { + request = applySessionData(rid, body); + if (cmParams.isAckingRequests()) { + pendingRequestAcks.add(request); + } + } + exch = new HTTPExchange(request); + exchanges.add(exch); + notEmpty.signalAll(); + clearEmptyRequest(); + } finally { + lock.unlock(); + } + AbstractBody finalReq = exch.getRequest(); + HTTPResponse resp = httpSender.send(params, finalReq); + exch.setHTTPResponse(resp); + fireRequestSent(finalReq); + } + + /** + * Attempt to pause the current session. When supported by the remote + * connection manager, pausing the session will result in the connection + * manager closing out all outstanding requests (including the pause + * request) and increases the inactivity timeout of the session. The + * exact value of the temporary timeout is dependent upon the connection + * manager. This method should be used if a client encounters an + * exceptional temporary situation during which it will be unable to send + * requests to the connection manager for a period of time greater than + * the maximum inactivity period. + * + * The session will revert back to it's normal, unpaused state when the + * client sends it's next message. + * + * @return {@code true} if the connection manager supports session pausing, + * {@code false} if the connection manager does not support session + * pausing or if the session has not yet been established + */ + public boolean pause() { + assertUnlocked(); + lock.lock(); + AttrMaxPause maxPause = null; + try { + if (cmParams == null) { + return false; + } + + maxPause = cmParams.getMaxPause(); + if (maxPause == null) { + return false; + } + } finally { + lock.unlock(); + } + try { + send(ComposableBody.builder() + .setAttribute(Attributes.PAUSE, maxPause.toString()) + .build()); + } catch (BOSHException boshx) { + LOG.log(Level.FINEST, "Could not send pause", boshx); + } + return true; + } + + /** + * End the BOSH session by disconnecting from the remote BOSH connection + * manager. + * + * @throws BOSHException when termination message cannot be sent + */ + public void disconnect() throws BOSHException { + disconnect(ComposableBody.builder().build()); + } + + /** + * End the BOSH session by disconnecting from the remote BOSH connection + * manager, sending the provided content in the final connection + * termination message. + * + * @param msg final message to send + * @throws BOSHException when termination message cannot be sent + */ + public void disconnect(final ComposableBody msg) throws BOSHException { + if (msg == null) { + throw(new IllegalArgumentException( + "Message body may not be null")); + } + + Builder builder = msg.rebuild(); + builder.setAttribute(Attributes.TYPE, TERMINATE); + send(builder.build()); + } + + /** + * Forcibly close this client session instance. The preferred mechanism + * to close the connection is to send a disconnect message and wait for + * organic termination. Calling this method simply shuts down the local + * session without sending a termination message, releasing all resources + * associated with the session. + */ + public void close() { + dispose(new BOSHException("Session explicitly closed by caller")); + } + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Get the current CM session params. + * + * @return current session params, or {@code null} + */ + CMSessionParams getCMSessionParams() { + lock.lock(); + try { + return cmParams; + } finally { + lock.unlock(); + } + } + + /** + * Wait until no more messages are waiting to be processed. + */ + void drain() { + lock.lock(); + try { + LOG.finest("Waiting while draining..."); + while (isWorking() + && (emptyRequestFuture == null + || emptyRequestFuture.isDone())) { + try { + drained.await(); + } catch (InterruptedException intx) { + LOG.log(Level.FINEST, INTERRUPTED, intx); + } + } + LOG.finest("Drained"); + } finally { + lock.unlock(); + } + } + + /** + * Test method used to forcibly discard next exchange. + * + * @param interceptor exchange interceptor + */ + void setExchangeInterceptor(final ExchangeInterceptor interceptor) { + exchInterceptor.set(interceptor); + } + + + /////////////////////////////////////////////////////////////////////////// + // Private methods: + + /** + * Initialize the session. This initializes the underlying HTTP + * transport implementation and starts the receive thread. + */ + private void init() { + assertUnlocked(); + + lock.lock(); + try { + httpSender.init(cfg); + procThread = new Thread(procRunnable); + procThread.setDaemon(true); + procThread.setName(BOSHClient.class.getSimpleName() + + "[" + System.identityHashCode(this) + + "]: Receive thread"); + procThread.start(); + } finally { + lock.unlock(); + } + } + + /** + * Destroy this session. + * + * @param cause the reason for the session termination, or {@code null} + * for normal termination + */ + private void dispose(final Throwable cause) { + assertUnlocked(); + + lock.lock(); + try { + if (procThread == null) { + // Already disposed + return; + } + procThread = null; + } finally { + lock.unlock(); + } + + if (cause == null) { + fireConnectionClosed(); + } else { + fireConnectionClosedOnError(cause); + } + + lock.lock(); + try { + clearEmptyRequest(); + exchanges = null; + cmParams = null; + pendingResponseAcks = null; + pendingRequestAcks = null; + notEmpty.signalAll(); + notFull.signalAll(); + drained.signalAll(); + } finally { + lock.unlock(); + } + + httpSender.destroy(); + schedExec.shutdownNow(); + } + + /** + * Determines if the message body specified indicates a request to + * pause the session. + * + * @param msg message to evaluate + * @return {@code true} if the message is a pause request, {@code false} + * otherwise + */ + private static boolean isPause(final AbstractBody msg) { + return msg.getAttribute(Attributes.PAUSE) != null; + } + + /** + * Determines if the message body specified indicates a termination of + * the session. + * + * @param msg message to evaluate + * @return {@code true} if the message is a session termination, + * {@code false} otherwise + */ + private static boolean isTermination(final AbstractBody msg) { + return TERMINATE.equals(msg.getAttribute(Attributes.TYPE)); + } + + /** + * Evaluates the HTTP response code and response message and returns the + * terminal binding condition that it describes, if any. + * + * @param respCode HTTP response code + * @param respBody response body + * @return terminal binding condition, or {@code null} if not a terminal + * binding condition message + */ + private TerminalBindingCondition getTerminalBindingCondition( + final int respCode, + final AbstractBody respBody) { + assertLocked(); + + if (isTermination(respBody)) { + String str = respBody.getAttribute(Attributes.CONDITION); + return TerminalBindingCondition.forString(str); + } + // Check for deprecated HTTP Error Conditions + if (cmParams != null && cmParams.getVersion() == null) { + return TerminalBindingCondition.forHTTPResponseCode(respCode); + } + return null; + } + + /** + * Determines if the message specified is immediately sendable or if it + * needs to block until the session state changes. + * + * @param msg message to evaluate + * @return {@code true} if the message can be immediately sent, + * {@code false} otherwise + */ + private boolean isImmediatelySendable(final AbstractBody msg) { + assertLocked(); + + if (cmParams == null) { + // block if we're waiting for a response to our first request + return exchanges.isEmpty(); + } + + AttrRequests requests = cmParams.getRequests(); + if (requests == null) { + return true; + } + int maxRequests = requests.intValue(); + if (exchanges.size() < maxRequests) { + return true; + } + if (exchanges.size() == maxRequests + && (isTermination(msg) || isPause(msg))) { + // One additional terminate or pause message is allowed + return true; + } + return false; + } + + /** + * Determines whether or not the session is still active. + * + * @return {@code true} if it is, {@code false} otherwise + */ + private boolean isWorking() { + assertLocked(); + + return procThread != null; + } + + /** + * Blocks until either the message provided becomes immediately + * sendable or until the session is terminated. + * + * @param msg message to evaluate + */ + private void blockUntilSendable(final AbstractBody msg) { + assertLocked(); + + while (isWorking() && !isImmediatelySendable(msg)) { + try { + notFull.await(); + } catch (InterruptedException intx) { + LOG.log(Level.FINEST, INTERRUPTED, intx); + } + } + } + + /** + * Modifies the specified body message such that it becomes a new + * BOSH session creation request. + * + * @param rid request ID to use + * @param orig original body to modify + * @return modified message which acts as a session creation request + */ + private ComposableBody applySessionCreationRequest( + final long rid, final ComposableBody orig) throws BOSHException { + assertLocked(); + + Builder builder = orig.rebuild(); + builder.setAttribute(Attributes.TO, cfg.getTo()); + builder.setAttribute(Attributes.XML_LANG, cfg.getLang()); + builder.setAttribute(Attributes.VER, + AttrVersion.getSupportedVersion().toString()); + builder.setAttribute(Attributes.WAIT, "60"); + builder.setAttribute(Attributes.HOLD, "1"); + builder.setAttribute(Attributes.RID, Long.toString(rid)); + applyRoute(builder); + applyFrom(builder); + builder.setAttribute(Attributes.ACK, "1"); + + // Make sure the following are NOT present (i.e., during retries) + builder.setAttribute(Attributes.SID, null); + return builder.build(); + } + + /** + * Applies routing information to the request message who's builder has + * been provided. + * + * @param builder builder instance to add routing information to + */ + private void applyRoute(final Builder builder) { + assertLocked(); + + String route = cfg.getRoute(); + if (route != null) { + builder.setAttribute(Attributes.ROUTE, route); + } + } + + /** + * Applies the local station ID information to the request message who's + * builder has been provided. + * + * @param builder builder instance to add station ID information to + */ + private void applyFrom(final Builder builder) { + assertLocked(); + + String from = cfg.getFrom(); + if (from != null) { + builder.setAttribute(Attributes.FROM, from); + } + } + + /** + * Applies existing session data to the outbound request, returning the + * modified request. + * + * This method assumes the lock is currently held. + * + * @param rid request ID to use + * @param orig original/raw request + * @return modified request with session information applied + */ + private ComposableBody applySessionData( + final long rid, + final ComposableBody orig) throws BOSHException { + assertLocked(); + + Builder builder = orig.rebuild(); + builder.setAttribute(Attributes.SID, + cmParams.getSessionID().toString()); + builder.setAttribute(Attributes.RID, Long.toString(rid)); + applyResponseAcknowledgement(builder, rid); + return builder.build(); + } + + /** + * Sets the 'ack' attribute of the request to the value of the highest + * 'rid' of a request for which it has already received a response in the + * case where it has also received all responses associated with lower + * 'rid' values. The only exception is that, after its session creation + * request, the client SHOULD NOT include an 'ack' attribute in any request + * if it has received responses to all its previous requests. + * + * @param builder message builder + * @param rid current request RID + */ + private void applyResponseAcknowledgement( + final Builder builder, + final long rid) { + assertLocked(); + + if (responseAck.equals(Long.valueOf(-1L))) { + // We have not received any responses yet + return; + } + + Long prevRID = Long.valueOf(rid - 1L); + if (responseAck.equals(prevRID)) { + // Implicit ack + return; + } + + builder.setAttribute(Attributes.ACK, responseAck.toString()); + } + + /** + * While we are "connected", process received responses. + * + * This method is run in the processing thread. + */ + private void processMessages() { + LOG.log(Level.FINEST, "Processing thread starting"); + try { + HTTPExchange exch; + do { + exch = nextExchange(); + if (exch == null) { + break; + } + + // Test hook to manipulate what the client sees: + ExchangeInterceptor interceptor = exchInterceptor.get(); + if (interceptor != null) { + HTTPExchange newExch = interceptor.interceptExchange(exch); + if (newExch == null) { + LOG.log(Level.FINE, "Discarding exchange on request " + + "of test hook: RID=" + + exch.getRequest().getAttribute( + Attributes.RID)); + lock.lock(); + try { + exchanges.remove(exch); + } finally { + lock.unlock(); + } + continue; + } + exch = newExch; + } + + processExchange(exch); + } while (true); + } finally { + LOG.log(Level.FINEST, "Processing thread exiting"); + } + + } + + /** + * Get the next message exchange to process, blocking until one becomes + * available if nothing is already waiting for processing. + * + * @return next available exchange to process, or {@code null} if no + * exchanges are immediately available + */ + private HTTPExchange nextExchange() { + assertUnlocked(); + + final Thread thread = Thread.currentThread(); + HTTPExchange exch = null; + lock.lock(); + try { + do { + if (!thread.equals(procThread)) { + break; + } + exch = exchanges.peek(); + if (exch == null) { + try { + notEmpty.await(); + } catch (InterruptedException intx) { + LOG.log(Level.FINEST, INTERRUPTED, intx); + } + } + } while (exch == null); + } finally { + lock.unlock(); + } + return exch; + } + + /** + * Process the next, provided exchange. This is the main processing + * method of the receive thread. + * + * @param exch message exchange to process + */ + private void processExchange(final HTTPExchange exch) { + assertUnlocked(); + + HTTPResponse resp; + AbstractBody body; + int respCode; + try { + resp = exch.getHTTPResponse(); + body = resp.getBody(); + respCode = resp.getHTTPStatus(); + } catch (BOSHException boshx) { + LOG.log(Level.FINEST, "Could not obtain response", boshx); + dispose(boshx); + return; + } catch (InterruptedException intx) { + LOG.log(Level.FINEST, INTERRUPTED, intx); + dispose(intx); + return; + } + fireResponseReceived(body); + + // Process the message with the current session state + AbstractBody req = exch.getRequest(); + CMSessionParams params; + List<HTTPExchange> toResend = null; + lock.lock(); + try { + // Check for session creation response info, if needed + if (cmParams == null) { + cmParams = CMSessionParams.fromSessionInit(req, body); + + // The following call handles the lock. It's not an escape. + fireConnectionEstablished(); + } + params = cmParams; + + checkForTerminalBindingConditions(body, respCode); + if (isTermination(body)) { + // Explicit termination + lock.unlock(); + dispose(null); + return; + } + + if (isRecoverableBindingCondition(body)) { + // Retransmit outstanding requests + if (toResend == null) { + toResend = new ArrayList<HTTPExchange>(exchanges.size()); + } + for (HTTPExchange exchange : exchanges) { + HTTPExchange resendExch = + new HTTPExchange(exchange.getRequest()); + toResend.add(resendExch); + } + for (HTTPExchange exchange : toResend) { + exchanges.add(exchange); + } + } else { + // Process message as normal + processRequestAcknowledgements(req, body); + processResponseAcknowledgementData(req); + HTTPExchange resendExch = + processResponseAcknowledgementReport(body); + if (resendExch != null && toResend == null) { + toResend = new ArrayList<HTTPExchange>(1); + toResend.add(resendExch); + exchanges.add(resendExch); + } + } + } catch (BOSHException boshx) { + LOG.log(Level.FINEST, "Could not process response", boshx); + lock.unlock(); + dispose(boshx); + return; + } finally { + if (lock.isHeldByCurrentThread()) { + try { + exchanges.remove(exch); + if (exchanges.isEmpty()) { + scheduleEmptyRequest(processPauseRequest(req)); + } + notFull.signalAll(); + } finally { + lock.unlock(); + } + } + } + + if (toResend != null) { + for (HTTPExchange resend : toResend) { + HTTPResponse response = + httpSender.send(params, resend.getRequest()); + resend.setHTTPResponse(response); + fireRequestSent(resend.getRequest()); + } + } + } + + /** + * Clears any scheduled empty requests. + */ + private void clearEmptyRequest() { + assertLocked(); + + if (emptyRequestFuture != null) { + emptyRequestFuture.cancel(false); + emptyRequestFuture = null; + } + } + + /** + * Calculates the default empty request delay/interval to use for the + * active session. + * + * @return delay in milliseconds + */ + private long getDefaultEmptyRequestDelay() { + assertLocked(); + + // Figure out how long we should wait before sending an empty request + AttrPolling polling = cmParams.getPollingInterval(); + long delay; + if (polling == null) { + delay = EMPTY_REQUEST_DELAY; + } else { + delay = polling.getInMilliseconds(); + } + return delay; + } + + /** + * Schedule an empty request to be sent if no other requests are + * sent in a reasonable amount of time. + */ + private void scheduleEmptyRequest(long delay) { + assertLocked(); + if (delay < 0L) { + throw(new IllegalArgumentException( + "Empty request delay must be >= 0 (was: " + delay + ")")); + } + + clearEmptyRequest(); + if (!isWorking()) { + return; + } + + // Schedule the transmission + if (LOG.isLoggable(Level.FINER)) { + LOG.finer("Scheduling empty request in " + delay + "ms"); + } + try { + emptyRequestFuture = schedExec.schedule(emptyRequestRunnable, + delay, TimeUnit.MILLISECONDS); + } catch (RejectedExecutionException rex) { + LOG.log(Level.FINEST, "Could not schedule empty request", rex); + } + drained.signalAll(); + } + + /** + * Sends an empty request to maintain session requirements. If a request + * is sent within a reasonable time window, the empty request transmission + * will be cancelled. + */ + private void sendEmptyRequest() { + assertUnlocked(); + // Send an empty request + LOG.finest("Sending empty request"); + try { + send(ComposableBody.builder().build()); + } catch (BOSHException boshx) { + dispose(boshx); + } + } + + /** + * Assert that the internal lock is held. + */ + private void assertLocked() { + if (ASSERTIONS) { + if (!lock.isHeldByCurrentThread()) { + throw(new AssertionError("Lock is not held by current thread")); + } + return; + } + } + + /** + * Assert that the internal lock is *not* held. + */ + private void assertUnlocked() { + if (ASSERTIONS) { + if (lock.isHeldByCurrentThread()) { + throw(new AssertionError("Lock is held by current thread")); + } + return; + } + } + + /** + * Checks to see if the response indicates a terminal binding condition + * (as per XEP-0124 section 17). If it does, an exception is thrown. + * + * @param body response body to evaluate + * @param code HTTP response code + * @throws BOSHException if a terminal binding condition is detected + */ + private void checkForTerminalBindingConditions( + final AbstractBody body, + final int code) + throws BOSHException { + TerminalBindingCondition cond = + getTerminalBindingCondition(code, body); + if (cond != null) { + throw(new BOSHException( + "Terminal binding condition encountered: " + + cond.getCondition() + " (" + + cond.getMessage() + ")")); + } + } + + /** + * Determines whether or not the response indicates a recoverable + * binding condition (as per XEP-0124 section 17). + * + * @param resp response body + * @return {@code true} if it does, {@code false} otherwise + */ + private static boolean isRecoverableBindingCondition( + final AbstractBody resp) { + return ERROR.equals(resp.getAttribute(Attributes.TYPE)); + } + + /** + * Process the request to determine if the empty request delay + * can be determined by looking to see if the request is a pause + * request. If it can, the request's delay is returned, otherwise + * the default delay is returned. + * + * @return delay in milliseconds that should elapse prior to an + * empty message being sent + */ + private long processPauseRequest( + final AbstractBody req) { + assertLocked(); + + if (cmParams != null && cmParams.getMaxPause() != null) { + try { + AttrPause pause = AttrPause.createFromString( + req.getAttribute(Attributes.PAUSE)); + if (pause != null) { + long delay = pause.getInMilliseconds() - PAUSE_MARGIN; + if (delay < 0) { + delay = EMPTY_REQUEST_DELAY; + } + return delay; + } + } catch (BOSHException boshx) { + LOG.log(Level.FINEST, "Could not extract", boshx); + } + } + + return getDefaultEmptyRequestDelay(); + } + + /** + * Check the response for request acknowledgements and take appropriate + * action. + * + * This method assumes the lock is currently held. + * + * @param req request + * @param resp response + */ + private void processRequestAcknowledgements( + final AbstractBody req, final AbstractBody resp) { + assertLocked(); + + if (!cmParams.isAckingRequests()) { + return; + } + + // If a report or time attribute is set, we aren't acking anything + if (resp.getAttribute(Attributes.REPORT) != null) { + return; + } + + // Figure out what the highest acked RID is + String acked = resp.getAttribute(Attributes.ACK); + Long ackUpTo; + if (acked == null) { + // Implicit ack of all prior requests up until RID + ackUpTo = Long.parseLong(req.getAttribute(Attributes.RID)); + } else { + ackUpTo = Long.parseLong(acked); + } + + // Remove the acked requests from the list + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Removing pending acks up to: " + ackUpTo); + } + Iterator<ComposableBody> iter = pendingRequestAcks.iterator(); + while (iter.hasNext()) { + AbstractBody pending = iter.next(); + Long pendingRID = Long.parseLong( + pending.getAttribute(Attributes.RID)); + if (pendingRID.compareTo(ackUpTo) <= 0) { + iter.remove(); + } + } + } + + /** + * Process the response in order to update the response acknowlegement + * data. + * + * This method assumes the lock is currently held. + * + * @param req request + */ + private void processResponseAcknowledgementData( + final AbstractBody req) { + assertLocked(); + + Long rid = Long.parseLong(req.getAttribute(Attributes.RID)); + if (responseAck.equals(Long.valueOf(-1L))) { + // This is the first request + responseAck = rid; + } else { + pendingResponseAcks.add(rid); + // Remove up until the first missing response (or end of queue) + Long whileVal = responseAck; + while (whileVal.equals(pendingResponseAcks.first())) { + responseAck = whileVal; + pendingResponseAcks.remove(whileVal); + whileVal = Long.valueOf(whileVal.longValue() + 1); + } + } + } + + /** + * Process the response in order to check for and respond to any potential + * ack reports. + * + * This method assumes the lock is currently held. + * + * @param resp response + * @return exchange to transmit if a resend is to be performed, or + * {@code null} if no resend is necessary + * @throws BOSHException when a a retry is needed but cannot be performed + */ + private HTTPExchange processResponseAcknowledgementReport( + final AbstractBody resp) + throws BOSHException { + assertLocked(); + + String reportStr = resp.getAttribute(Attributes.REPORT); + if (reportStr == null) { + // No report on this message + return null; + } + + Long report = Long.parseLong(reportStr); + Long time = Long.parseLong(resp.getAttribute(Attributes.TIME)); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Received report of missing request (RID=" + + report + ", time=" + time + "ms)"); + } + + // Find the missing request + Iterator<ComposableBody> iter = pendingRequestAcks.iterator(); + AbstractBody req = null; + while (iter.hasNext() && req == null) { + AbstractBody pending = iter.next(); + Long pendingRID = Long.parseLong( + pending.getAttribute(Attributes.RID)); + if (report.equals(pendingRID)) { + req = pending; + } + } + + if (req == null) { + throw(new BOSHException("Report of missing message with RID '" + + reportStr + + "' but local copy of that request was not found")); + } + + // Resend the missing request + HTTPExchange exch = new HTTPExchange(req); + exchanges.add(exch); + notEmpty.signalAll(); + return exch; + } + + /** + * Notifies all request listeners that the specified request is being + * sent. + * + * @param request request being sent + */ + private void fireRequestSent(final AbstractBody request) { + assertUnlocked(); + + BOSHMessageEvent event = null; + for (BOSHClientRequestListener listener : requestListeners) { + if (event == null) { + event = BOSHMessageEvent.createRequestSentEvent(this, request); + } + try { + listener.requestSent(event); + } catch (Exception ex) { + LOG.log(Level.WARNING, UNHANDLED, ex); + } + } + } + + /** + * Notifies all response listeners that the specified response has been + * received. + * + * @param response response received + */ + private void fireResponseReceived(final AbstractBody response) { + assertUnlocked(); + + BOSHMessageEvent event = null; + for (BOSHClientResponseListener listener : responseListeners) { + if (event == null) { + event = BOSHMessageEvent.createResponseReceivedEvent( + this, response); + } + try { + listener.responseReceived(event); + } catch (Exception ex) { + LOG.log(Level.WARNING, UNHANDLED, ex); + } + } + } + + /** + * Notifies all connection listeners that the session has been successfully + * established. + */ + private void fireConnectionEstablished() { + final boolean hadLock = lock.isHeldByCurrentThread(); + if (hadLock) { + lock.unlock(); + } + try { + BOSHClientConnEvent event = null; + for (BOSHClientConnListener listener : connListeners) { + if (event == null) { + event = BOSHClientConnEvent + .createConnectionEstablishedEvent(this); + } + try { + listener.connectionEvent(event); + } catch (Exception ex) { + LOG.log(Level.WARNING, UNHANDLED, ex); + } + } + } finally { + if (hadLock) { + lock.lock(); + } + } + } + + /** + * Notifies all connection listeners that the session has been + * terminated normally. + */ + private void fireConnectionClosed() { + assertUnlocked(); + + BOSHClientConnEvent event = null; + for (BOSHClientConnListener listener : connListeners) { + if (event == null) { + event = BOSHClientConnEvent.createConnectionClosedEvent(this); + } + try { + listener.connectionEvent(event); + } catch (Exception ex) { + LOG.log(Level.WARNING, UNHANDLED, ex); + } + } + } + + /** + * Notifies all connection listeners that the session has been + * terminated due to the exceptional condition provided. + * + * @param cause cause of the termination + */ + private void fireConnectionClosedOnError( + final Throwable cause) { + assertUnlocked(); + + BOSHClientConnEvent event = null; + for (BOSHClientConnListener listener : connListeners) { + if (event == null) { + event = BOSHClientConnEvent + .createConnectionClosedOnErrorEvent( + this, pendingRequestAcks, cause); + } + try { + listener.connectionEvent(event); + } catch (Exception ex) { + LOG.log(Level.WARNING, UNHANDLED, ex); + } + } + } + +} diff --git a/src/com/kenai/jbosh/BOSHClientConfig.java b/src/com/kenai/jbosh/BOSHClientConfig.java new file mode 100644 index 0000000..23915b6 --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClientConfig.java @@ -0,0 +1,446 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.net.URI; +import javax.net.ssl.SSLContext; + +/** + * BOSH client configuration information. Instances of this class contain + * all information necessary to establish connectivity with a remote + * connection manager. + * <p/> + * Instances of this class are immutable, thread-safe, + * and can be re-used to configure multiple client session instances. + */ +public final class BOSHClientConfig { + + /** + * Connection manager URI. + */ + private final URI uri; + + /** + * Target domain. + */ + private final String to; + + /** + * Client ID of this station. + */ + private final String from; + + /** + * Default XML language. + */ + private final String lang; + + /** + * Routing information for messages sent to CM. + */ + private final String route; + + /** + * Proxy host. + */ + private final String proxyHost; + + /** + * Proxy port. + */ + private final int proxyPort; + + /** + * SSL context. + */ + private final SSLContext sslContext; + + /** + * Flag indicating that compression should be attempted, if possible. + */ + private final boolean compressionEnabled; + + /////////////////////////////////////////////////////////////////////////// + // Classes: + + /** + * Class instance builder, after the builder pattern. This allows each + * {@code BOSHClientConfig} instance to be immutable while providing + * flexibility when building new {@code BOSHClientConfig} instances. + * <p/> + * Instances of this class are <b>not</b> thread-safe. If template-style + * use is desired, see the {@code create(BOSHClientConfig)} method. + */ + public static final class Builder { + // Required args + private final URI bURI; + private final String bDomain; + + // Optional args + private String bFrom; + private String bLang; + private String bRoute; + private String bProxyHost; + private int bProxyPort; + private SSLContext bSSLContext; + private Boolean bCompression; + + /** + * Creates a new builder instance, used to create instances of the + * {@code BOSHClientConfig} class. + * + * @param cmURI URI to use to contact the connection manager + * @param domain target domain to communicate with + */ + private Builder(final URI cmURI, final String domain) { + bURI = cmURI; + bDomain = domain; + } + + /** + * Creates a new builder instance, used to create instances of the + * {@code BOSHClientConfig} class. + * + * @param cmURI URI to use to contact the connection manager + * @param domain target domain to communicate with + * @return builder instance + */ + public static Builder create(final URI cmURI, final String domain) { + if (cmURI == null) { + throw(new IllegalArgumentException( + "Connection manager URI must not be null")); + } + if (domain == null) { + throw(new IllegalArgumentException( + "Target domain must not be null")); + } + String scheme = cmURI.getScheme(); + if (!("http".equals(scheme) || "https".equals(scheme))) { + throw(new IllegalArgumentException( + "Only 'http' and 'https' URI are allowed")); + } + return new Builder(cmURI, domain); + } + + /** + * Creates a new builder instance using the existing configuration + * provided as a starting point. + * + * @param cfg configuration to copy + * @return builder instance + */ + public static Builder create(final BOSHClientConfig cfg) { + Builder result = new Builder(cfg.getURI(), cfg.getTo()); + result.bFrom = cfg.getFrom(); + result.bLang = cfg.getLang(); + result.bRoute = cfg.getRoute(); + result.bProxyHost = cfg.getProxyHost(); + result.bProxyPort = cfg.getProxyPort(); + result.bSSLContext = cfg.getSSLContext(); + result.bCompression = cfg.isCompressionEnabled(); + return result; + } + + /** + * Set the ID of the client station, to be forwarded to the connection + * manager when new sessions are created. + * + * @param id client ID + * @return builder instance + */ + public Builder setFrom(final String id) { + if (id == null) { + throw(new IllegalArgumentException( + "Client ID must not be null")); + } + bFrom = id; + return this; + } + + /** + * Set the default language of any human-readable content within the + * XML. + * + * @param lang XML language ID + * @return builder instance + */ + public Builder setXMLLang(final String lang) { + if (lang == null) { + throw(new IllegalArgumentException( + "Default language ID must not be null")); + } + bLang = lang; + return this; + } + + /** + * Sets the destination server/domain that the client should connect to. + * Connection managers may be configured to enable sessions with more + * that one server in different domains. When requesting a session with + * such a "proxy" connection manager, a client should use this method to + * specify the server with which it wants to communicate. + * + * @param protocol connection protocol (e.g, "xmpp") + * @param host host or domain to be served by the remote server. Note + * that this is not necessarily the host name or domain name of the + * remote server. + * @param port port number of the remote server + * @return builder instance + */ + public Builder setRoute( + final String protocol, + final String host, + final int port) { + if (protocol == null) { + throw(new IllegalArgumentException("Protocol cannot be null")); + } + if (protocol.contains(":")) { + throw(new IllegalArgumentException( + "Protocol cannot contain the ':' character")); + } + if (host == null) { + throw(new IllegalArgumentException("Host cannot be null")); + } + if (host.contains(":")) { + throw(new IllegalArgumentException( + "Host cannot contain the ':' character")); + } + if (port <= 0) { + throw(new IllegalArgumentException("Port number must be > 0")); + } + bRoute = protocol + ":" + host + ":" + port; + return this; + } + + /** + * Specify the hostname and port of an HTTP proxy to connect through. + * + * @param hostName proxy hostname + * @param port proxy port number + * @return builder instance + */ + public Builder setProxy(final String hostName, final int port) { + if (hostName == null || hostName.length() == 0) { + throw(new IllegalArgumentException( + "Proxy host name cannot be null or empty")); + } + if (port <= 0) { + throw(new IllegalArgumentException( + "Proxy port must be > 0")); + } + bProxyHost = hostName; + bProxyPort = port; + return this; + } + + /** + * Set the SSL context to use for this session. This can be used + * to configure certificate-based authentication, etc.. + * + * @param ctx SSL context + * @return builder instance + */ + public Builder setSSLContext(final SSLContext ctx) { + if (ctx == null) { + throw(new IllegalArgumentException( + "SSL context cannot be null")); + } + bSSLContext = ctx; + return this; + } + + /** + * Set whether or not compression of the underlying data stream + * should be attempted. By default, compression is disabled. + * + * @param enabled set to {@code true} if compression should be + * attempted when possible, {@code false} to disable compression + * @return builder instance + */ + public Builder setCompressionEnabled(final boolean enabled) { + bCompression = Boolean.valueOf(enabled); + return this; + } + + /** + * Build the immutable object instance with the current configuration. + * + * @return BOSHClientConfig instance + */ + public BOSHClientConfig build() { + // Default XML language + String lang; + if (bLang == null) { + lang = "en"; + } else { + lang = bLang; + } + + // Default proxy port + int port; + if (bProxyHost == null) { + port = 0; + } else { + port = bProxyPort; + } + + // Default compression + boolean compression; + if (bCompression == null) { + compression = false; + } else { + compression = bCompression.booleanValue(); + } + + return new BOSHClientConfig( + bURI, + bDomain, + bFrom, + lang, + bRoute, + bProxyHost, + port, + bSSLContext, + compression); + } + + } + + /////////////////////////////////////////////////////////////////////////// + // Constructor: + + /** + * Prevent direct construction. + * + * @param cURI URI of the connection manager to connect to + * @param cDomain the target domain of the first stream + * @param cFrom client ID + * @param cLang default XML language + * @param cRoute target route + * @param cProxyHost proxy host + * @param cProxyPort proxy port + * @param cSSLContext SSL context + * @param cCompression compression enabled flag + */ + private BOSHClientConfig( + final URI cURI, + final String cDomain, + final String cFrom, + final String cLang, + final String cRoute, + final String cProxyHost, + final int cProxyPort, + final SSLContext cSSLContext, + final boolean cCompression) { + uri = cURI; + to = cDomain; + from = cFrom; + lang = cLang; + route = cRoute; + proxyHost = cProxyHost; + proxyPort = cProxyPort; + sslContext = cSSLContext; + compressionEnabled = cCompression; + } + + /** + * Get the URI to use to contact the connection manager. + * + * @return connection manager URI. + */ + public URI getURI() { + return uri; + } + + /** + * Get the ID of the target domain. + * + * @return domain id + */ + public String getTo() { + return to; + } + + /** + * Get the ID of the local client. + * + * @return client id, or {@code null} + */ + public String getFrom() { + return from; + } + + /** + * Get the default language of any human-readable content within the + * XML. Defaults to "en". + * + * @return XML language ID + */ + public String getLang() { + return lang; + } + + /** + * Get the routing information for messages sent to the CM. + * + * @return route attribute string, or {@code null} if no routing + * info was provided. + */ + public String getRoute() { + return route; + } + + /** + * Get the HTTP proxy host to use. + * + * @return proxy host, or {@code null} if no proxy information was specified + */ + public String getProxyHost() { + return proxyHost; + } + + /** + * Get the HTTP proxy port to use. + * + * @return proxy port, or 0 if no proxy information was specified + */ + public int getProxyPort() { + return proxyPort; + } + + /** + * Get the SSL context to use for this session. + * + * @return SSL context instance to use, or {@code null} if no + * context instance was provided. + */ + public SSLContext getSSLContext() { + return sslContext; + } + + /** + * Determines whether or not compression of the underlying data stream + * should be attempted/allowed. Defaults to {@code false}. + * + * @return {@code true} if compression should be attempted, {@code false} + * if compression is disabled or was not specified + */ + boolean isCompressionEnabled() { + return compressionEnabled; + } + +} diff --git a/src/com/kenai/jbosh/BOSHClientConnEvent.java b/src/com/kenai/jbosh/BOSHClientConnEvent.java new file mode 100644 index 0000000..0ac7943 --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClientConnEvent.java @@ -0,0 +1,189 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EventObject; +import java.util.List; + +/** + * Client connection event, notifying of changes in connection state. + * <p/> + * This class is immutable and thread-safe. + */ +public final class BOSHClientConnEvent extends EventObject { + + /** + * Serialized version. + */ + private static final long serialVersionUID = 1L; + + /** + * Boolean flag indicating whether or not a session has been established + * and is currently active. + */ + private final boolean connected; + + /** + * List of outstanding requests which may not have been sent and/or + * acknowledged by the remote CM. + */ + private final List<ComposableBody> requests; + + /** + * Cause of the session termination, or {@code null}. + */ + private final Throwable cause; + + /** + * Creates a new connection event instance. + * + * @param source event source + * @param cConnected flag indicating whether or not the session is + * currently active + * @param cRequests outstanding requests when an error condition is + * detected, or {@code null} when not an error condition + * @param cCause cause of the error condition, or {@code null} when no + * error condition is present + */ + private BOSHClientConnEvent( + final BOSHClient source, + final boolean cConnected, + final List<ComposableBody> cRequests, + final Throwable cCause) { + super(source); + connected = cConnected; + cause = cCause; + + if (connected) { + if (cCause != null) { + throw(new IllegalStateException( + "Cannot be connected and have a cause")); + } + if (cRequests != null && cRequests.size() > 0) { + throw(new IllegalStateException( + "Cannot be connected and have outstanding requests")); + } + } + + if (cRequests == null) { + requests = Collections.emptyList(); + } else { + // Defensive copy: + requests = Collections.unmodifiableList( + new ArrayList<ComposableBody>(cRequests)); + } + } + + /** + * Creates a new connection establishment event. + * + * @param source client which has become connected + * @return event instance + */ + static BOSHClientConnEvent createConnectionEstablishedEvent( + final BOSHClient source) { + return new BOSHClientConnEvent(source, true, null, null); + } + + /** + * Creates a new successful connection closed event. This represents + * a clean termination of the client session. + * + * @param source client which has been disconnected + * @return event instance + */ + static BOSHClientConnEvent createConnectionClosedEvent( + final BOSHClient source) { + return new BOSHClientConnEvent(source, false, null, null); + } + + /** + * Creates a connection closed on error event. This represents + * an unexpected termination of the client session. + * + * @param source client which has been disconnected + * @param outstanding list of requests which may not have been received + * by the remote connection manager + * @param cause cause of termination + * @return event instance + */ + static BOSHClientConnEvent createConnectionClosedOnErrorEvent( + final BOSHClient source, + final List<ComposableBody> outstanding, + final Throwable cause) { + return new BOSHClientConnEvent(source, false, outstanding, cause); + } + + /** + * Gets the client from which this event originated. + * + * @return client instance + */ + public BOSHClient getBOSHClient() { + return (BOSHClient) getSource(); + } + + /** + * Returns whether or not the session has been successfully established + * and is currently active. + * + * @return {@code true} if a session is active, {@code false} otherwise + */ + public boolean isConnected() { + return connected; + } + + /** + * Returns whether or not this event indicates an error condition. This + * will never return {@code true} when {@code isConnected()} returns + * {@code true}. + * + * @return {@code true} if the event indicates a terminal error has + * occurred, {@code false} otherwise. + */ + public boolean isError() { + return cause != null; + } + + /** + * Returns the underlying cause of the error condition. This method is + * guaranteed to return {@code null} when @{code isError()} returns + * {@code false}. Similarly, this method is guaranteed to return + * non-@{code null} if {@code isError()} returns {@code true}. + * + * @return underlying cause of the error condition, or {@code null} if + * this event does not represent an error condition + */ + public Throwable getCause() { + return cause; + } + + /** + * Get the list of requests which may not have been sent or were not + * acknowledged by the remote connection manager prior to session + * termination. + * + * @return list of messages which may not have been received by the remote + * connection manager, or an empty list if the session is still connected + */ + public List<ComposableBody> getOutstandingRequests() { + return requests; + } + +} diff --git a/src/com/kenai/jbosh/BOSHClientConnListener.java b/src/com/kenai/jbosh/BOSHClientConnListener.java new file mode 100644 index 0000000..6d646cb --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClientConnListener.java @@ -0,0 +1,34 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Interface used by parties interested in monitoring the connection state + * of a client session. + */ +public interface BOSHClientConnListener { + + /** + * Called when the connection state of the client which the listener + * is registered against has changed. The event object supplied can + * be used to determine the current session state. + * + * @param connEvent connection event describing the state + */ + void connectionEvent(BOSHClientConnEvent connEvent); + +} diff --git a/src/com/kenai/jbosh/BOSHClientRequestListener.java b/src/com/kenai/jbosh/BOSHClientRequestListener.java new file mode 100644 index 0000000..2cc92f3 --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClientRequestListener.java @@ -0,0 +1,45 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Interface used by parties interested in monitoring outbound requests made + * by the client to the connection manager (CM). No opportunity is provided + * to manipulate the outbound request. + * <p/> + * The messages being sent are typically modified copies of the message + * body provided to the {@code BOSHClient} instance, built from the + * originally provided message body plus additional BOSH protocol + * state and information. Messages may also be sent automatically when the + * protocol requires it, such as maintaining a minimum number of open + * connections to the connection manager. + * <p/> + * Listeners are executed by the sending thread immediately prior to + * message transmission and should not block for any significant amount + * of time. + */ +public interface BOSHClientRequestListener { + + /** + * Called when the listener is being notified that a client request is + * about to be sent to the connection manager. + * + * @param event event instance containing the message being sent + */ + void requestSent(BOSHMessageEvent event); + +} diff --git a/src/com/kenai/jbosh/BOSHClientResponseListener.java b/src/com/kenai/jbosh/BOSHClientResponseListener.java new file mode 100644 index 0000000..1d86e4f --- /dev/null +++ b/src/com/kenai/jbosh/BOSHClientResponseListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Interface used by parties interested in monitoring inbound responses + * to the client from the connection manager (CM). No opportunity is provided + * to manipulate the response. + * <p/> + * Listeners are executed by the message processing thread and should not + * block for any significant amount of time. + */ +public interface BOSHClientResponseListener { + + /** + * Called when the listener is being notified that a response has been + * received from the connection manager. + * + * @param event event instance containing the message being sent + */ + void responseReceived(BOSHMessageEvent event); + +} diff --git a/src/com/kenai/jbosh/BOSHException.java b/src/com/kenai/jbosh/BOSHException.java new file mode 100644 index 0000000..e0bc05b --- /dev/null +++ b/src/com/kenai/jbosh/BOSHException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Exception class used by the BOSH API to minimize the number of checked + * exceptions which must be handled by the user of the API. + */ +public class BOSHException extends Exception { + + /** + * Servial version UID. + */ + private static final long serialVersionUID = 1L; + + /** + * Creates a new exception isntance with the specified descriptive message. + * + * @param msg description of the exceptional condition + */ + public BOSHException(final String msg) { + super(msg); + } + + /** + * Creates a new exception isntance with the specified descriptive + * message and the underlying root cause of the exceptional condition. + * + * @param msg description of the exceptional condition + * @param cause root cause or instigator of the condition + */ + public BOSHException(final String msg, final Throwable cause) { + super(msg, cause); + } + +} diff --git a/src/com/kenai/jbosh/BOSHMessageEvent.java b/src/com/kenai/jbosh/BOSHMessageEvent.java new file mode 100644 index 0000000..550903e --- /dev/null +++ b/src/com/kenai/jbosh/BOSHMessageEvent.java @@ -0,0 +1,92 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.util.EventObject; + +/** + * Event representing a message sent to or from a BOSH connection manager. + * <p/> + * This class is immutable and thread-safe. + */ +public final class BOSHMessageEvent extends EventObject { + + /** + * Serialized version. + */ + private static final long serialVersionUID = 1L; + + /** + * Message which was sent or received. + */ + private final AbstractBody body; + + /** + * Creates a new message event instance. + * + * @param source event source + * @param cBody message body + */ + private BOSHMessageEvent( + final Object source, + final AbstractBody cBody) { + super(source); + if (cBody == null) { + throw(new IllegalArgumentException( + "message body may not be null")); + } + body = cBody; + } + + /** + * Creates a new message event for clients sending events to the + * connection manager. + * + * @param source sender of the message + * @param body message body + * @return event instance + */ + static BOSHMessageEvent createRequestSentEvent( + final BOSHClient source, + final AbstractBody body) { + return new BOSHMessageEvent(source, body); + } + + /** + * Creates a new message event for clients receiving new messages + * from the connection manager. + * + * @param source receiver of the message + * @param body message body + * @return event instance + */ + static BOSHMessageEvent createResponseReceivedEvent( + final BOSHClient source, + final AbstractBody body) { + return new BOSHMessageEvent(source, body); + } + + /** + * Gets the message body which was sent or received. + * + * @return message body + */ + public AbstractBody getBody() { + return body; + } + +} diff --git a/src/com/kenai/jbosh/BodyParser.java b/src/com/kenai/jbosh/BodyParser.java new file mode 100644 index 0000000..5ef5276 --- /dev/null +++ b/src/com/kenai/jbosh/BodyParser.java @@ -0,0 +1,36 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Interface for parser implementations to implement in order to abstract the + * business of XML parsing out of the Body class. This allows us to leverage + * a variety of parser implementations to gain performance advantages. + */ +interface BodyParser { + + /** + * Parses the XML message, extracting the useful data from the initial + * body element and returning it in a results object. + * + * @param xml XML to parse + * @return useful data parsed out of the XML + * @throws BOSHException on parse error + */ + BodyParserResults parse(String xml) throws BOSHException; + +} diff --git a/src/com/kenai/jbosh/BodyParserResults.java b/src/com/kenai/jbosh/BodyParserResults.java new file mode 100644 index 0000000..955e4bf --- /dev/null +++ b/src/com/kenai/jbosh/BodyParserResults.java @@ -0,0 +1,64 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.util.HashMap; +import java.util.Map; + +/** + * Data extracted from a raw XML message by a BodyParser implementation. + * Currently, this is limited to the attributes of the wrapper element. + */ +final class BodyParserResults { + + /** + * Map of qualified names to their values. This map is defined to + * match the requirement of the {@code Body} class to prevent + * excessive copying. + */ + private final Map<BodyQName, String> attrs = + new HashMap<BodyQName, String>(); + + /** + * Constructor. + */ + BodyParserResults() { + // Empty + } + + /** + * Add an attribute definition to the results. + * + * @param name attribute's qualified name + * @param value attribute value + */ + void addBodyAttributeValue( + final BodyQName name, + final String value) { + attrs.put(name, value); + } + + /** + * Returns the map of attributes added by the parser. + * + * @return map of atributes. Note: This is the live instance, not a copy. + */ + Map<BodyQName, String> getAttributes() { + return attrs; + } + +} diff --git a/src/com/kenai/jbosh/BodyParserSAX.java b/src/com/kenai/jbosh/BodyParserSAX.java new file mode 100644 index 0000000..54c6c01 --- /dev/null +++ b/src/com/kenai/jbosh/BodyParserSAX.java @@ -0,0 +1,206 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.ref.SoftReference; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Implementation of the BodyParser interface which uses the SAX API + * that is part of the JDK. Due to the fact that we can cache and reuse + * SAXPArser instances, this has proven to be significantly faster than the + * use of the javax.xml.stream API introduced in Java 6 while simultaneously + * providing an implementation accessible to Java 5 users. + */ +final class BodyParserSAX implements BodyParser { + + /** + * Logger. + */ + private static final Logger LOG = + Logger.getLogger(BodyParserSAX.class.getName()); + + /** + * SAX parser factory. + */ + private static final SAXParserFactory SAX_FACTORY; + static { + SAX_FACTORY = SAXParserFactory.newInstance(); + SAX_FACTORY.setNamespaceAware(true); + SAX_FACTORY.setValidating(false); + } + + /** + * Thread local to contain a SAX parser instance for each thread that + * attempts to use one. This allows us to gain an order of magnitude of + * performance as a result of not constructing parsers for each + * invocation while retaining thread safety. + */ + private static final ThreadLocal<SoftReference<SAXParser>> PARSER = + new ThreadLocal<SoftReference<SAXParser>>() { + @Override protected SoftReference<SAXParser> initialValue() { + return new SoftReference<SAXParser>(null); + } + }; + + /** + * SAX event handler class. + */ + private static final class Handler extends DefaultHandler { + private final BodyParserResults result; + private final SAXParser parser; + private String defaultNS = null; + + private Handler(SAXParser theParser, BodyParserResults results) { + parser = theParser; + result = results; + } + + /** + * {@inheritDoc} + */ + @Override + public void startElement( + final String uri, + final String localName, + final String qName, + final Attributes attributes) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Start element: " + qName); + LOG.finest(" URI: " + uri); + LOG.finest(" local: " + localName); + } + + BodyQName bodyName = AbstractBody.getBodyQName(); + // Make sure the first element is correct + if (!(bodyName.getNamespaceURI().equals(uri) + && bodyName.getLocalPart().equals(localName))) { + throw(new IllegalStateException( + "Root element was not '" + bodyName.getLocalPart() + + "' in the '" + bodyName.getNamespaceURI() + + "' namespace. (Was '" + localName + "' in '" + uri + + "')")); + } + + // Read in the attributes, making sure to expand the namespaces + // as needed. + for (int idx=0; idx < attributes.getLength(); idx++) { + String attrURI = attributes.getURI(idx); + if (attrURI.length() == 0) { + attrURI = defaultNS; + } + String attrLN = attributes.getLocalName(idx); + String attrVal = attributes.getValue(idx); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest(" Attribute: {" + attrURI + "}" + + attrLN + " = '" + attrVal + "'"); + } + + BodyQName aqn = BodyQName.create(attrURI, attrLN); + result.addBodyAttributeValue(aqn, attrVal); + } + + parser.reset(); + } + + /** + * {@inheritDoc} + * + * This implementation uses this event hook to keep track of the + * default namespace on the body element. + */ + @Override + public void startPrefixMapping( + final String prefix, + final String uri) { + if (prefix.length() == 0) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Prefix mapping: <DEFAULT> => " + uri); + } + defaultNS = uri; + } else { + if (LOG.isLoggable(Level.FINEST)) { + LOG.info("Prefix mapping: " + prefix + " => " + uri); + } + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // BodyParser interface methods: + + /** + * {@inheritDoc} + */ + public BodyParserResults parse(String xml) throws BOSHException { + BodyParserResults result = new BodyParserResults(); + Exception thrown; + try { + InputStream inStream = new ByteArrayInputStream(xml.getBytes()); + SAXParser parser = getSAXParser(); + parser.parse(inStream, new Handler(parser, result)); + return result; + } catch (SAXException saxx) { + thrown = saxx; + } catch (IOException iox) { + thrown = iox; + } + throw(new BOSHException("Could not parse body:\n" + xml, thrown)); + } + + /////////////////////////////////////////////////////////////////////////// + // Private methods: + + /** + * Gets a SAXParser for use in parsing incoming messages. + * + * @return parser instance + */ + private static SAXParser getSAXParser() { + SoftReference<SAXParser> ref = PARSER.get(); + SAXParser result = ref.get(); + if (result == null) { + Exception thrown; + try { + result = SAX_FACTORY.newSAXParser(); + ref = new SoftReference<SAXParser>(result); + PARSER.set(ref); + return result; + } catch (ParserConfigurationException ex) { + thrown = ex; + } catch (SAXException ex) { + thrown = ex; + } + throw(new IllegalStateException( + "Could not create SAX parser", thrown)); + } else { + result.reset(); + return result; + } + } + +} diff --git a/src/com/kenai/jbosh/BodyParserXmlPull.java b/src/com/kenai/jbosh/BodyParserXmlPull.java new file mode 100644 index 0000000..5f23b06 --- /dev/null +++ b/src/com/kenai/jbosh/BodyParserXmlPull.java @@ -0,0 +1,165 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.io.IOException; +import java.io.StringReader; +import java.lang.ref.SoftReference; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.xml.XMLConstants; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +/** + * Implementation of the BodyParser interface which uses the XmlPullParser + * API. When available, this API provides an order of magnitude performance + * improvement over the default SAX parser implementation. + */ +final class BodyParserXmlPull implements BodyParser { + + /** + * Logger. + */ + private static final Logger LOG = + Logger.getLogger(BodyParserXmlPull.class.getName()); + + /** + * Thread local to contain a XmlPullParser instance for each thread that + * attempts to use one. This allows us to gain an order of magnitude of + * performance as a result of not constructing parsers for each + * invocation while retaining thread safety. + */ + private static final ThreadLocal<SoftReference<XmlPullParser>> XPP_PARSER = + new ThreadLocal<SoftReference<XmlPullParser>>() { + @Override protected SoftReference<XmlPullParser> initialValue() { + return new SoftReference<XmlPullParser>(null); + } + }; + + /////////////////////////////////////////////////////////////////////////// + // BodyParser interface methods: + + /** + * {@inheritDoc} + */ + public BodyParserResults parse(final String xml) throws BOSHException { + BodyParserResults result = new BodyParserResults(); + Exception thrown; + try { + XmlPullParser xpp = getXmlPullParser(); + + xpp.setInput(new StringReader(xml)); + int eventType = xpp.getEventType(); + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Start tag: " + xpp.getName()); + } + } else { + eventType = xpp.next(); + continue; + } + + String prefix = xpp.getPrefix(); + if (prefix == null) { + prefix = XMLConstants.DEFAULT_NS_PREFIX; + } + String uri = xpp.getNamespace(); + String localName = xpp.getName(); + QName name = new QName(uri, localName, prefix); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("Start element: "); + LOG.finest(" prefix: " + prefix); + LOG.finest(" URI: " + uri); + LOG.finest(" local: " + localName); + } + + BodyQName bodyName = AbstractBody.getBodyQName(); + if (!bodyName.equalsQName(name)) { + throw(new IllegalStateException( + "Root element was not '" + bodyName.getLocalPart() + + "' in the '" + bodyName.getNamespaceURI() + + "' namespace. (Was '" + localName + + "' in '" + uri + "')")); + } + + for (int idx=0; idx < xpp.getAttributeCount(); idx++) { + String attrURI = xpp.getAttributeNamespace(idx); + if (attrURI.length() == 0) { + attrURI = xpp.getNamespace(null); + } + String attrPrefix = xpp.getAttributePrefix(idx); + if (attrPrefix == null) { + attrPrefix = XMLConstants.DEFAULT_NS_PREFIX; + } + String attrLN = xpp.getAttributeName(idx); + String attrVal = xpp.getAttributeValue(idx); + BodyQName aqn = BodyQName.createWithPrefix( + attrURI, attrLN, attrPrefix); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest(" Attribute: {" + attrURI + "}" + + attrLN + " = '" + attrVal + "'"); + } + result.addBodyAttributeValue(aqn, attrVal); + } + break; + } + return result; + } catch (RuntimeException rtx) { + thrown = rtx; + } catch (XmlPullParserException xmlppx) { + thrown = xmlppx; + } catch (IOException iox) { + thrown = iox; + } + throw(new BOSHException("Could not parse body:\n" + xml, thrown)); + } + + /////////////////////////////////////////////////////////////////////////// + // Private methods: + + /** + * Gets a XmlPullParser for use in parsing incoming messages. + * + * @return parser instance + */ + private static XmlPullParser getXmlPullParser() { + SoftReference<XmlPullParser> ref = XPP_PARSER.get(); + XmlPullParser result = ref.get(); + if (result == null) { + Exception thrown; + try { + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); + result = factory.newPullParser(); + ref = new SoftReference<XmlPullParser>(result); + XPP_PARSER.set(ref); + return result; + } catch (Exception ex) { + thrown = ex; + } + throw(new IllegalStateException( + "Could not create XmlPull parser", thrown)); + } else { + return result; + } + } + +} diff --git a/src/com/kenai/jbosh/BodyQName.java b/src/com/kenai/jbosh/BodyQName.java new file mode 100644 index 0000000..83acdf1 --- /dev/null +++ b/src/com/kenai/jbosh/BodyQName.java @@ -0,0 +1,165 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * Qualified name of an attribute of the wrapper element. This class is + * analagous to the {@code javax.xml.namespace.QName} class. + * Each qualified name consists of a namespace URI and a local name. + * <p/> + * Instances of this class are immutable and thread-safe. + */ +public final class BodyQName { + + /** + * BOSH namespace URI. + */ + static final String BOSH_NS_URI = + "http://jabber.org/protocol/httpbind"; + + /** + * Namespace URI. + */ + private final QName qname; + + /** + * Private constructor to prevent direct construction. + * + * @param wrapped QName instance to wrap + */ + private BodyQName( + final QName wrapped) { + qname = wrapped; + } + + /** + * Creates a new qualified name using a namespace URI and local name. + * + * @param uri namespace URI + * @param local local name + * @return BodyQName instance + */ + public static BodyQName create( + final String uri, + final String local) { + return createWithPrefix(uri, local, null); + } + + /** + * Creates a new qualified name using a namespace URI and local name + * along with an optional prefix. + * + * @param uri namespace URI + * @param local local name + * @param prefix optional prefix or @{code null} for no prefix + * @return BodyQName instance + */ + public static BodyQName createWithPrefix( + final String uri, + final String local, + final String prefix) { + if (uri == null || uri.length() == 0) { + throw(new IllegalArgumentException( + "URI is required and may not be null/empty")); + } + if (local == null || local.length() == 0) { + throw(new IllegalArgumentException( + "Local arg is required and may not be null/empty")); + } + if (prefix == null || prefix.length() == 0) { + return new BodyQName(new QName(uri, local)); + } else { + return new BodyQName(new QName(uri, local, prefix)); + } + } + + /** + * Get the namespace URI of this qualified name. + * + * @return namespace uri + */ + public String getNamespaceURI() { + return qname.getNamespaceURI(); + } + + /** + * Get the local part of this qualified name. + * + * @return local name + */ + public String getLocalPart() { + return qname.getLocalPart(); + } + + /** + * Get the optional prefix used with this qualified name, or {@code null} + * if no prefix has been assiciated. + * + * @return prefix, or {@code null} if no prefix was supplied + */ + public String getPrefix() { + return qname.getPrefix(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(final Object obj) { + if (obj instanceof BodyQName) { + BodyQName other = (BodyQName) obj; + return qname.equals(other.qname); + } else { + return false; + } + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return qname.hashCode(); + } + + /////////////////////////////////////////////////////////////////////////// + // Package-private methods: + + /** + * Creates a new qualified name using the BOSH namespace URI and local name. + * + * @param local local name + * @return BodyQName instance + */ + static BodyQName createBOSH( + final String local) { + return createWithPrefix(BOSH_NS_URI, local, null); + } + + /** + * Convenience method to compare this qualified name with a + * {@code javax.xml.namespace.QName}. + * + * @param otherName QName to compare to + * @return @{code true} if the qualified name is the same, {@code false} + * otherwise + */ + boolean equalsQName(final QName otherName) { + return qname.equals(otherName); + } + +} diff --git a/src/com/kenai/jbosh/CMSessionParams.java b/src/com/kenai/jbosh/CMSessionParams.java new file mode 100644 index 0000000..bbed628 --- /dev/null +++ b/src/com/kenai/jbosh/CMSessionParams.java @@ -0,0 +1,177 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +/** + * A BOSH connection manager session instance. This consolidates the + * configuration knowledge related to the CM session and provides a + * mechanism by which + */ +final class CMSessionParams { + + private final AttrSessionID sid; + + private final AttrWait wait; + + private final AttrVersion ver; + + private final AttrPolling polling; + + private final AttrInactivity inactivity; + + private final AttrRequests requests; + + private final AttrHold hold; + + private final AttrAccept accept; + + private final AttrMaxPause maxPause; + + private final AttrAck ack; + + private final AttrCharsets charsets; + + private final boolean ackingRequests; + + /** + * Prevent direct construction. + */ + private CMSessionParams( + final AttrSessionID aSid, + final AttrWait aWait, + final AttrVersion aVer, + final AttrPolling aPolling, + final AttrInactivity aInactivity, + final AttrRequests aRequests, + final AttrHold aHold, + final AttrAccept aAccept, + final AttrMaxPause aMaxPause, + final AttrAck aAck, + final AttrCharsets aCharsets, + final boolean amAckingRequests) { + sid = aSid; + wait = aWait; + ver = aVer; + polling = aPolling; + inactivity = aInactivity; + requests = aRequests; + hold = aHold; + accept = aAccept; + maxPause = aMaxPause; + ack = aAck; + charsets = aCharsets; + ackingRequests = amAckingRequests; + } + + static CMSessionParams fromSessionInit( + final AbstractBody req, + final AbstractBody resp) + throws BOSHException { + AttrAck aAck = AttrAck.createFromString( + resp.getAttribute(Attributes.ACK)); + String rid = req.getAttribute(Attributes.RID); + boolean acking = (aAck != null && aAck.getValue().equals(rid)); + + return new CMSessionParams( + AttrSessionID.createFromString( + getRequiredAttribute(resp, Attributes.SID)), + AttrWait.createFromString( + getRequiredAttribute(resp, Attributes.WAIT)), + AttrVersion.createFromString( + resp.getAttribute(Attributes.VER)), + AttrPolling.createFromString( + resp.getAttribute(Attributes.POLLING)), + AttrInactivity.createFromString( + resp.getAttribute(Attributes.INACTIVITY)), + AttrRequests.createFromString( + resp.getAttribute(Attributes.REQUESTS)), + AttrHold.createFromString( + resp.getAttribute(Attributes.HOLD)), + AttrAccept.createFromString( + resp.getAttribute(Attributes.ACCEPT)), + AttrMaxPause.createFromString( + resp.getAttribute(Attributes.MAXPAUSE)), + aAck, + AttrCharsets.createFromString( + resp.getAttribute(Attributes.CHARSETS)), + acking + ); + } + + private static String getRequiredAttribute( + final AbstractBody body, + final BodyQName name) + throws BOSHException { + String attrStr = body.getAttribute(name); + if (attrStr == null) { + throw(new BOSHException( + "Connection Manager session creation response did not " + + "include required '" + name.getLocalPart() + + "' attribute")); + } + return attrStr; + } + + AttrSessionID getSessionID() { + return sid; + } + + AttrWait getWait() { + return wait; + } + + AttrVersion getVersion() { + return ver; + } + + AttrPolling getPollingInterval() { + return polling; + } + + AttrInactivity getInactivityPeriod() { + return inactivity; + } + + AttrRequests getRequests() { + return requests; + } + + AttrHold getHold() { + return hold; + } + + AttrAccept getAccept() { + return accept; + } + + AttrMaxPause getMaxPause() { + return maxPause; + } + + AttrAck getAck() { + return ack; + } + + AttrCharsets getCharsets() { + return charsets; + } + + boolean isAckingRequests() { + return ackingRequests; + } + +} diff --git a/src/com/kenai/jbosh/ComposableBody.java b/src/com/kenai/jbosh/ComposableBody.java new file mode 100644 index 0000000..d375478 --- /dev/null +++ b/src/com/kenai/jbosh/ComposableBody.java @@ -0,0 +1,345 @@ +/* + * Copyright 2009 Mike Cumings + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.kenai.jbosh; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.xml.XMLConstants; + +/** + * Implementation of the {@code AbstractBody} class which allows for the + * definition of messages from individual elements of a body. + * <p/> + * A message is constructed by creating a builder, manipulating the + * configuration of the builder, and then building it into a class instance, + * as in the following example: + * <pre> + * ComposableBody body = ComposableBody.builder() + * .setNamespaceDefinition("foo", "http://foo.com/bar") + * .setPayloadXML("<foo:data>Data to send to remote server</foo:data>") + * .build(); + * </pre> + * Class instances can also be "rebuilt", allowing them to be used as templates + * when building many similar messages: + * <pre> + * ComposableBody body2 = body.rebuild() + * .setPayloadXML("<foo:data>More data to send</foo:data>") + * .build(); + * </pre> + * This class does only minimal syntactic and semantic checking with respect + * to what the generated XML will look like. It is up to the developer to + * protect against the definition of malformed XML messages when building + * instances of this class. + * <p/> + * Instances of this class are immutable and thread-safe. + */ +public final class ComposableBody extends AbstractBody { + + /** + * Pattern used to identify the beginning {@code body} element of a + * BOSH message. + */ + private static final Pattern BOSH_START = + Pattern.compile("<" + "(?:(?:[^:\t\n\r >]+:)|(?:\\{[^\\}>]*?}))?" + + "body" + "(?:[\t\n\r ][^>]*?)?" + "(/>|>)"); + + /** + * Map of all attributes to their values. + */ + private final Map<BodyQName, String> attrs; + + /** + * Payload XML. + */ + private final String payload; + + /** + * Computed raw XML. + */ + private final AtomicReference<String> computed = + new AtomicReference<String>(); + + /** + * Class instance builder, after the builder pattern. This allows each + * message instance to be immutable while providing flexibility when + * building new messages. + * <p/> + * Instances of this class are <b>not</b> thread-safe. + */ + public static final class Builder { + private Map<BodyQName, String> map; + private boolean doMapCopy; + private String payloadXML; + + /** + * Prevent direct construction. + */ + private Builder() { + // Empty + } + + /** + * Creates a builder which is initialized to the values of the + * provided {@code ComposableBody} instance. This allows an + * existing {@code ComposableBody} to be used as a + * template/starting point. + * + * @param source body template + * @return builder instance + */ + private static Builder fromBody(final ComposableBody source) { + Builder result = new Builder(); + result.map = source.getAttributes(); + result.doMapCopy = true; + result.payloadXML = source.payload; + return result; + } + + /** + * Set the body message's wrapped payload content. Any previous + * content will be replaced. + * + * @param xml payload XML content + * @return builder instance + */ + public Builder setPayloadXML(final String xml) { + if (xml == null) { + throw(new IllegalArgumentException( + "payload XML argument cannot be null")); + } + payloadXML = xml; + return this; + } + + /** + * Set an attribute on the message body / wrapper element. + * + * @param name qualified name of the attribute + * @param value value of the attribute + * @return builder instance + */ + public Builder setAttribute( + final BodyQName name, final String value) { + if (map == null) { + map = new HashMap<BodyQName, String>(); + } else if (doMapCopy) { + map = new HashMap<BodyQName, String>(map); + doMapCopy = false; + } + if (value == null) { + map.remove(name); + } else { + map.put(name, value); + } + return this; + } + + /** + * Convenience method to set a namespace definition. This would result + * in a namespace prefix definition similar to: + * {@code <body xmlns:prefix="uri"/>} + * + * @param prefix prefix to define + * @param uri namespace URI to associate with the prefix + * @return builder instance + */ + public Builder setNamespaceDefinition( + final String prefix, final String uri) { + BodyQName qname = BodyQName.createWithPrefix( + XMLConstants.XML_NS_URI, prefix, + XMLConstants.XMLNS_ATTRIBUTE); + return setAttribute(qname, uri); + } + + /** + * Build the immutable object instance with the current configuration. + * + * @return composable body instance + */ + public ComposableBody build() { + if (map == null) { + map = new HashMap<BodyQName, String>(); + } + if (payloadXML == null) { + payloadXML = ""; + } + return new ComposableBody(map, payloadXML); + } + } + + /////////////////////////////////////////////////////////////////////////// + // Constructors: + + /** + * Prevent direct construction. This constructor is for body messages + * which are dynamically assembled. + */ + private ComposableBody( + final Map<BodyQName, String> attrMap, + final String payloadXML) { + super(); + attrs = attrMap; + payload = payloadXML; + } + + /** + * Parse a static body instance into a composable instance. This is an + * expensive operation and should not be used lightly. + * <p/> + * The current implementation does not obtain the payload XML by means of + * a proper XML parser. It uses some string pattern searching to find the + * first @{code body} element and the last element's closing tag. It is + * assumed that the static body's XML is well formed, etc.. This + * implementation may change in the future. + * + * @param body static body instance to convert + * @return composable bosy instance + * @throws BOSHException + */ + static ComposableBody fromStaticBody(final StaticBody body) + throws BOSHException { + String raw = body.toXML(); + Matcher matcher = BOSH_START.matcher(raw); + if (!matcher.find()) { + throw(new BOSHException( + "Could not locate 'body' element in XML. The raw XML did" + + " not match the pattern: " + BOSH_START)); + } + String payload; + if (">".equals(matcher.group(1))) { + int first = matcher.end(); + int last = raw.lastIndexOf("</"); + if (last < first) { + last = first; + } + payload = raw.substring(first, last); + } else { + payload = ""; + } + + return new ComposableBody(body.getAttributes(), payload); + } + + /** + * Create a builder instance to build new instances of this class. + * + * @return AbstractBody instance + */ + public static Builder builder() { + return new Builder(); + } + + /** + * If this {@code ComposableBody} instance is a dynamic instance, uses this + * {@code ComposableBody} instance as a starting point, create a builder + * which can be used to create another {@code ComposableBody} instance + * based on this one. This allows a {@code ComposableBody} instance to be + * used as a template. Note that the use of the returned builder in no + * way modifies or manipulates the current {@code ComposableBody} instance. + * + * @return builder instance which can be used to build similar + * {@code ComposableBody} instances + */ + public Builder rebuild() { + return Builder.fromBody(this); + } + + /////////////////////////////////////////////////////////////////////////// + // Accessors: + + /** + * {@inheritDoc} + */ + public Map<BodyQName, String> getAttributes() { + return Collections.unmodifiableMap(attrs); + } + + /** + * {@inheritDoc} + */ + public String toXML() { + String comp = computed.get(); + if (comp == null) { + comp = computeXML(); + computed.set(comp); + } + return comp; + } + + /** + * Get the paylaod XML in String form. + * + * @return payload XML + */ + public String getPayloadXML() { + return payload; + } + + /////////////////////////////////////////////////////////////////////////// + // Private methods: + + /** + * Escape the value of an attribute to ensure we maintain valid + * XML syntax. + * + * @param value value to escape + * @return escaped value + */ + private String escape(final String value) { + return value.replace("'", "'"); + } + + /** + * 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 <= 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 |