Add android smack source.
Change-Id: I49ce97136c17173c4ae3965c694af6e7bc49897d
diff --git a/Android.mk b/Android.mk
new file mode 100644
index 0000000..b503e0e
--- /dev/null
+++ b/Android.mk
@@ -0,0 +1,28 @@
+# Copyright 2011, The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+LOCAL_PATH := $(call my-dir)
+
+##################################################
+# Static library
+##################################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := smackxmpp
+LOCAL_MODULE_TAGS := optional
+LOCAL_SDK_VERSION := 9
+
+LOCAL_SRC_FILES := $(call all-java-files-under,src)
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/README b/README
new file mode 100644
index 0000000..c986de5
--- /dev/null
+++ b/README
@@ -0,0 +1,7 @@
+The directory contains the build environment of smack for Android (located in asmack-master/)
+and the generated source files (located in src/).
+
+To update to the latest smack source. Please run the following:
+
+./update.sh
+m -j24 smackxmpp
diff --git a/asmack-master/build.bash b/asmack-master/build.bash
index 874f238..d291220 100755
--- a/asmack-master/build.bash
+++ b/asmack-master/build.bash
@@ -542,6 +542,16 @@
createVersionTag
createbuildsrc
patchsrc "patch"
+
+##
+## BEGIN Modification for android platform build system
+##
+echo done with android modifications
+exit
+##
+## END Modification for android platform build system
+##
+
if $BUILD_JINGLE ; then
patchsrc "jingle"
JINGLE_ARGS="-Djingle=lib/jstun.jar"
diff --git a/src/META-INF/services/com.kenai.jbosh.HTTPSender b/src/META-INF/services/com.kenai.jbosh.HTTPSender
new file mode 100644
index 0000000..3608d8e
--- /dev/null
+++ b/src/META-INF/services/com.kenai.jbosh.HTTPSender
@@ -0,0 +1 @@
+com.kenai.jbosh.ApacheHTTPSender
diff --git a/src/com/kenai/jbosh/AbstractAttr.java b/src/com/kenai/jbosh/AbstractAttr.java
new file mode 100644
index 0000000..0d6f84c
--- /dev/null
+++ b/src/com/kenai/jbosh/AbstractAttr.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+ package com.kenai.jbosh;
+
+/**
+ * Abstract base class for creating BOSH attribute classes. Concrete
+ * implementations of this class will naturally inherit the underlying
+ * type's behavior for {@code equals()}, {@code hashCode()},
+ * {@code toString()}, and {@code compareTo()}, allowing for the easy
+ * creation of objects which extend existing trivial types. This was done
+ * to comply with the prefactoring rule declaring, "when you are being
+ * abstract, be abstract all the way".
+ *
+ * @param <T> type of the extension object
+ */
+abstract class AbstractAttr<T extends Comparable>
+ implements Comparable {
+
+ /**
+ * Captured value.
+ */
+ private final T value;
+
+ /**
+ * Creates a new encapsulated object instance.
+ *
+ * @param aValue encapsulated getValue
+ */
+ protected AbstractAttr(final T aValue) {
+ value = aValue;
+ }
+
+ /**
+ * Gets the encapsulated data value.
+ *
+ * @return data value
+ */
+ public final T getValue() {
+ return value;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Object method overrides:
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param otherObj object to compare to
+ * @return true if the objects are equal, false otherwise
+ */
+ @Override
+ public boolean equals(final Object otherObj) {
+ if (otherObj == null) {
+ return false;
+ } else if (otherObj instanceof AbstractAttr) {
+ AbstractAttr other =
+ (AbstractAttr) otherObj;
+ return value.equals(other.value);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return hashCode of the encapsulated object
+ */
+ @Override
+ public int hashCode() {
+ return value.hashCode();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return string representation of the encapsulated object
+ */
+ @Override
+ public String toString() {
+ return value.toString();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Comparable interface:
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param otherObj object to compare to
+ * @return -1, 0, or 1
+ */
+ @SuppressWarnings("unchecked")
+ public int compareTo(final Object otherObj) {
+ if (otherObj == null) {
+ return 1;
+ } else {
+ return value.compareTo(otherObj);
+ }
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AbstractBody.java b/src/com/kenai/jbosh/AbstractBody.java
new file mode 100644
index 0000000..4d66c8c
--- /dev/null
+++ b/src/com/kenai/jbosh/AbstractBody.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Class representing a single message to or from the BOSH connection
+ * manager (CM).
+ * <p/>
+ * These messages consist of a single {@code body} element
+ * (qualified within the BOSH namespace:
+ * {@code http://jabber.org/protocol/httpbind}) and contain zero or more
+ * child elements (of any namespace). These child elements constitute the
+ * message payload.
+ * <p/>
+ * In addition to the message payload, the attributes of the wrapper
+ * {@code body} element may also need to be used as part of the communication
+ * protocol being implemented on top of BOSH, or to define additional
+ * namespaces used by the child "payload" elements. These attributes are
+ * exposed via accessors.
+ */
+public abstract class AbstractBody {
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Constructor:
+
+ /**
+ * Restrict subclasses to the local package.
+ */
+ AbstractBody() {
+ // Empty
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Public methods:
+
+ /**
+ * Get a set of all defined attribute names.
+ *
+ * @return set of qualified attribute names
+ */
+ public final Set<BodyQName> getAttributeNames() {
+ Map<BodyQName, String> attrs = getAttributes();
+ return Collections.unmodifiableSet(attrs.keySet());
+ }
+
+ /**
+ * Get the value of the specified attribute.
+ *
+ * @param attr name of the attribute to retriece
+ * @return attribute value, or {@code null} if not defined
+ */
+ public final String getAttribute(final BodyQName attr) {
+ Map<BodyQName, String> attrs = getAttributes();
+ return attrs.get(attr);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Abstract methods:
+
+ /**
+ * Get a map of all defined attribute names with their corresponding values.
+ *
+ * @return map of qualified attributes
+ */
+ public abstract Map<BodyQName, String> getAttributes();
+
+ /**
+ * Get an XML String representation of this message.
+ *
+ * @return XML string representing the body message
+ */
+ public abstract String toXML();
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Package-private methods:
+
+ /**
+ * Returns the qualified name of the root/wrapper element.
+ *
+ * @return qualified name
+ */
+ static BodyQName getBodyQName() {
+ return BodyQName.createBOSH("body");
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AbstractIntegerAttr.java b/src/com/kenai/jbosh/AbstractIntegerAttr.java
new file mode 100644
index 0000000..1b827f9
--- /dev/null
+++ b/src/com/kenai/jbosh/AbstractIntegerAttr.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Abstract base class for attribute implementations based on {@code Integer}
+ * types. Additional support for parsing of integer values from their
+ * {@code String} representations as well as callback handling of value
+ * validity checks are also provided.
+ */
+abstract class AbstractIntegerAttr extends AbstractAttr<Integer> {
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute value
+ * @throws BOSHException on parse or validation failure
+ */
+ protected AbstractIntegerAttr(final int val) throws BOSHException {
+ super(Integer.valueOf(val));
+ }
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute value in string form
+ * @throws BOSHException on parse or validation failure
+ */
+ protected AbstractIntegerAttr(final String val) throws BOSHException {
+ super(parseInt(val));
+ }
+
+ /**
+ * Utility method intended to be called by concrete implementation
+ * classes from within the {@code check()} method when the concrete
+ * class needs to ensure that the integer value does not drop below
+ * the specified minimum value.
+ *
+ * @param minVal minimum value to allow
+ * @throws BOSHException if the integer value is below the specific
+ * minimum
+ */
+ protected final void checkMinValue(int minVal) throws BOSHException {
+ int intVal = getValue();
+ if (intVal < minVal) {
+ throw(new BOSHException(
+ "Illegal attribute value '" + intVal + "' provided. "
+ + "Must be >= " + minVal));
+ }
+ }
+
+ /**
+ * Utility method to parse a {@code String} into an {@code Integer},
+ * converting any possible {@code NumberFormatException} thrown into
+ * a {@code BOSHException}.
+ *
+ * @param str string to parse
+ * @return integer value
+ * @throws BOSHException on {@code NumberFormatException}
+ */
+ private static int parseInt(final String str) throws BOSHException {
+ try {
+ return Integer.parseInt(str);
+ } catch (NumberFormatException nfx) {
+ throw(new BOSHException(
+ "Could not parse an integer from the value provided: "
+ + str,
+ nfx));
+ }
+ }
+
+ /**
+ * Returns the native {@code int} value of the underlying {@code Integer}.
+ * Will throw {@code NullPointerException} if the underlying
+ * integer was {@code null}.
+ *
+ * @return native {@code int} value
+ */
+ public int intValue() {
+ return getValue().intValue();
+ }
+
+}
diff --git a/src/com/kenai/jbosh/ApacheHTTPResponse.java b/src/com/kenai/jbosh/ApacheHTTPResponse.java
new file mode 100644
index 0000000..9f6731f
--- /dev/null
+++ b/src/com/kenai/jbosh/ApacheHTTPResponse.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright 2009 Guenther Niess
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.io.IOException;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ByteArrayEntity;
+
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.util.EntityUtils;
+
+final class ApacheHTTPResponse implements HTTPResponse {
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Constants:
+
+ /**
+ * Name of the accept encoding header.
+ */
+ private static final String ACCEPT_ENCODING = "Accept-Encoding";
+
+ /**
+ * Value to use for the ACCEPT_ENCODING header.
+ */
+ private static final String ACCEPT_ENCODING_VAL =
+ ZLIBCodec.getID() + ", " + GZIPCodec.getID();
+
+ /**
+ * Name of the character set to encode the body to/from.
+ */
+ private static final String CHARSET = "UTF-8";
+
+ /**
+ * Content type to use when transmitting the body data.
+ */
+ private static final String CONTENT_TYPE = "text/xml; charset=utf-8";
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Class variables:
+
+ /**
+ * Lock used for internal synchronization.
+ */
+ private final Lock lock = new ReentrantLock();
+
+ /**
+ * The execution state of an HTTP process.
+ */
+ private final HttpContext context;
+
+ /**
+ * HttpClient instance to use to communicate.
+ */
+ private final HttpClient client;
+
+ /**
+ * The HTTP POST request is sent to the server.
+ */
+ private final HttpPost post;
+
+ /**
+ * A flag which indicates if the transmission was already done.
+ */
+ private boolean sent;
+
+ /**
+ * Exception to throw when the response data is attempted to be accessed,
+ * or {@code null} if no exception should be thrown.
+ */
+ private BOSHException toThrow;
+
+ /**
+ * The response body which was received from the server or {@code null}
+ * if that has not yet happened.
+ */
+ private AbstractBody body;
+
+ /**
+ * The HTTP response status code.
+ */
+ private int statusCode;
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Constructors:
+
+ /**
+ * Create and send a new request to the upstream connection manager,
+ * providing deferred access to the results to be returned.
+ *
+ * @param client client instance to use when sending the request
+ * @param cfg client configuration
+ * @param params connection manager parameters from the session creation
+ * response, or {@code null} if the session has not yet been established
+ * @param request body of the client request
+ */
+ ApacheHTTPResponse(
+ final HttpClient client,
+ final BOSHClientConfig cfg,
+ final CMSessionParams params,
+ final AbstractBody request) {
+ super();
+ this.client = client;
+ this.context = new BasicHttpContext();
+ this.post = new HttpPost(cfg.getURI().toString());
+ this.sent = false;
+
+ try {
+ String xml = request.toXML();
+ byte[] data = xml.getBytes(CHARSET);
+
+ String encoding = null;
+ if (cfg.isCompressionEnabled() && params != null) {
+ AttrAccept accept = params.getAccept();
+ if (accept != null) {
+ if (accept.isAccepted(ZLIBCodec.getID())) {
+ encoding = ZLIBCodec.getID();
+ data = ZLIBCodec.encode(data);
+ } else if (accept.isAccepted(GZIPCodec.getID())) {
+ encoding = GZIPCodec.getID();
+ data = GZIPCodec.encode(data);
+ }
+ }
+ }
+
+ ByteArrayEntity entity = new ByteArrayEntity(data);
+ entity.setContentType(CONTENT_TYPE);
+ if (encoding != null) {
+ entity.setContentEncoding(encoding);
+ }
+ post.setEntity(entity);
+ if (cfg.isCompressionEnabled()) {
+ post.setHeader(ACCEPT_ENCODING, ACCEPT_ENCODING_VAL);
+ }
+ } catch (Exception e) {
+ toThrow = new BOSHException("Could not generate request", e);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // HTTPResponse interface methods:
+
+ /**
+ * Abort the client transmission and response processing.
+ */
+ public void abort() {
+ if (post != null) {
+ post.abort();
+ toThrow = new BOSHException("HTTP request aborted");
+ }
+ }
+
+ /**
+ * Wait for and then return the response body.
+ *
+ * @return body of the response
+ * @throws InterruptedException if interrupted while awaiting the response
+ * @throws BOSHException on communication failure
+ */
+ public AbstractBody getBody() throws InterruptedException, BOSHException {
+ if (toThrow != null) {
+ throw(toThrow);
+ }
+ lock.lock();
+ try {
+ if (!sent) {
+ awaitResponse();
+ }
+ } finally {
+ lock.unlock();
+ }
+ return body;
+ }
+
+ /**
+ * Wait for and then return the response HTTP status code.
+ *
+ * @return HTTP status code of the response
+ * @throws InterruptedException if interrupted while awaiting the response
+ * @throws BOSHException on communication failure
+ */
+ public int getHTTPStatus() throws InterruptedException, BOSHException {
+ if (toThrow != null) {
+ throw(toThrow);
+ }
+ lock.lock();
+ try {
+ if (!sent) {
+ awaitResponse();
+ }
+ } finally {
+ lock.unlock();
+ }
+ return statusCode;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Package-private methods:
+
+ /**
+ * Await the response, storing the result in the instance variables of
+ * this class when they arrive.
+ *
+ * @throws InterruptedException if interrupted while awaiting the response
+ * @throws BOSHException on communication failure
+ */
+ private synchronized void awaitResponse() throws BOSHException {
+ HttpEntity entity = null;
+ try {
+ HttpResponse httpResp = client.execute(post, context);
+ entity = httpResp.getEntity();
+ byte[] data = EntityUtils.toByteArray(entity);
+ String encoding = entity.getContentEncoding() != null ?
+ entity.getContentEncoding().getValue() :
+ null;
+ if (ZLIBCodec.getID().equalsIgnoreCase(encoding)) {
+ data = ZLIBCodec.decode(data);
+ } else if (GZIPCodec.getID().equalsIgnoreCase(encoding)) {
+ data = GZIPCodec.decode(data);
+ }
+ body = StaticBody.fromString(new String(data, CHARSET));
+ statusCode = httpResp.getStatusLine().getStatusCode();
+ sent = true;
+ } catch (IOException iox) {
+ abort();
+ toThrow = new BOSHException("Could not obtain response", iox);
+ throw(toThrow);
+ } catch (RuntimeException ex) {
+ abort();
+ throw(ex);
+ }
+ }
+}
diff --git a/src/com/kenai/jbosh/ApacheHTTPSender.java b/src/com/kenai/jbosh/ApacheHTTPSender.java
new file mode 100644
index 0000000..b3d3c93
--- /dev/null
+++ b/src/com/kenai/jbosh/ApacheHTTPSender.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2009 Guenther Niess
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.http.HttpHost;
+import org.apache.http.HttpVersion;
+import org.apache.http.client.HttpClient;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.params.ConnManagerParams;
+import org.apache.http.conn.params.ConnRoutePNames;
+import org.apache.http.conn.scheme.PlainSocketFactory;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.conn.ssl.SSLSocketFactory;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+
+/**
+ * Implementation of the {@code HTTPSender} interface which uses the
+ * Apache HttpClient API to send messages to the connection manager.
+ */
+final class ApacheHTTPSender implements HTTPSender {
+
+ /**
+ * Lock used for internal synchronization.
+ */
+ private final Lock lock = new ReentrantLock();
+
+ /**
+ * Session configuration.
+ */
+ private BOSHClientConfig cfg;
+
+ /**
+ * HttpClient instance to use to communicate.
+ */
+ private HttpClient httpClient;
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Constructors:
+
+ /**
+ * Prevent construction apart from our package.
+ */
+ ApacheHTTPSender() {
+ // Load Apache HTTP client class
+ HttpClient.class.getName();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // HTTPSender interface methods:
+
+ /**
+ * {@inheritDoc}
+ */
+ public void init(final BOSHClientConfig session) {
+ lock.lock();
+ try {
+ cfg = session;
+ httpClient = initHttpClient(session);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public void destroy() {
+ lock.lock();
+ try {
+ if (httpClient != null) {
+ httpClient.getConnectionManager().shutdown();
+ }
+ } finally {
+ cfg = null;
+ httpClient = null;
+ lock.unlock();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public HTTPResponse send(
+ final CMSessionParams params,
+ final AbstractBody body) {
+ HttpClient mClient;
+ BOSHClientConfig mCfg;
+ lock.lock();
+ try {
+ if (httpClient == null) {
+ httpClient = initHttpClient(cfg);
+ }
+ mClient = httpClient;
+ mCfg = cfg;
+ } finally {
+ lock.unlock();
+ }
+ return new ApacheHTTPResponse(mClient, mCfg, params, body);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Package-private methods:
+
+ private synchronized HttpClient initHttpClient(final BOSHClientConfig config) {
+ // Create and initialize HTTP parameters
+ HttpParams params = new BasicHttpParams();
+ ConnManagerParams.setMaxTotalConnections(params, 100);
+ HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
+ HttpProtocolParams.setUseExpectContinue(params, false);
+ if (config != null &&
+ config.getProxyHost() != null &&
+ config.getProxyPort() != 0) {
+ HttpHost proxy = new HttpHost(
+ config.getProxyHost(),
+ config.getProxyPort());
+ params.setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
+ }
+
+ // Create and initialize scheme registry
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(
+ new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
+ SSLSocketFactory sslFactory = SSLSocketFactory.getSocketFactory();
+ sslFactory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
+ schemeRegistry.register(
+ new Scheme("https", sslFactory, 443));
+
+ // Create an HttpClient with the ThreadSafeClientConnManager.
+ // This connection manager must be used if more than one thread will
+ // be using the HttpClient.
+ ClientConnectionManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
+ return new DefaultHttpClient(cm, params);
+ }
+}
diff --git a/src/com/kenai/jbosh/AttrAccept.java b/src/com/kenai/jbosh/AttrAccept.java
new file mode 100644
index 0000000..4f767df
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrAccept.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code accept} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrAccept extends AbstractAttr<String> {
+
+ /**
+ * Array of the accepted encodings.
+ */
+ private final String[] encodings;
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute getValue
+ * @throws BOSHException on parse or validation failure
+ */
+ private AttrAccept(final String val) {
+ super(val);
+ encodings = val.split("[\\s,]+");
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return attribute instance or {@code null} if provided string is
+ * {@code null}
+ * @throws BOSHException on parse or validation failure
+ */
+ static AttrAccept createFromString(final String str)
+ throws BOSHException {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrAccept(str);
+ }
+ }
+
+ /**
+ * Determines whether or not the specified encoding is supported.
+ *
+ * @param name encoding name
+ * @result {@code true} if the encoding is accepted, {@code false}
+ * otherwise
+ */
+ boolean isAccepted(final String name) {
+ for (String str : encodings) {
+ if (str.equalsIgnoreCase(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrAck.java b/src/com/kenai/jbosh/AttrAck.java
new file mode 100644
index 0000000..6cfe22b
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrAck.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code ack} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrAck extends AbstractAttr<String> {
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute getValue
+ * @throws BOSHException on parse or validation failure
+ */
+ private AttrAck(final String val) throws BOSHException {
+ super(val);
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return attribute instance or {@code null} if provided string is
+ * {@code null}
+ * @throws BOSHException on parse or validation failure
+ */
+ static AttrAck createFromString(final String str)
+ throws BOSHException {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrAck(str);
+ }
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrCharsets.java b/src/com/kenai/jbosh/AttrCharsets.java
new file mode 100644
index 0000000..45ce78c
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrCharsets.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code charsets} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrCharsets extends AbstractAttr<String> {
+
+ /**
+ * Array of the accepted character sets.
+ */
+ private final String[] charsets;
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute getValue
+ */
+ private AttrCharsets(final String val) {
+ super(val);
+ charsets = val.split("\\ +");
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return attribute instance or {@code null} if provided string is
+ * {@code null}
+ */
+ static AttrCharsets createFromString(final String str) {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrCharsets(str);
+ }
+ }
+
+ /**
+ * Determines whether or not the specified charset is supported.
+ *
+ * @param name encoding name
+ * @result {@code true} if the encoding is accepted, {@code false}
+ * otherwise
+ */
+ boolean isAccepted(final String name) {
+ for (String str : charsets) {
+ if (str.equalsIgnoreCase(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrHold.java b/src/com/kenai/jbosh/AttrHold.java
new file mode 100644
index 0000000..56f21dd
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrHold.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code hold} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrHold extends AbstractIntegerAttr {
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute getValue
+ * @throws BOSHException on parse or validation failure
+ */
+ private AttrHold(final String val) throws BOSHException {
+ super(val);
+ checkMinValue(0);
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return attribute instance or {@code null} if provided string is
+ * {@code null}
+ * @throws BOSHException on parse or validation failure
+ */
+ static AttrHold createFromString(final String str)
+ throws BOSHException {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrHold(str);
+ }
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrInactivity.java b/src/com/kenai/jbosh/AttrInactivity.java
new file mode 100644
index 0000000..14ab7d4
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrInactivity.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the value of the {@code inactivity} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrInactivity extends AbstractIntegerAttr {
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute value
+ * @throws BOSHException on parse or validation failure
+ */
+ private AttrInactivity(final String val) throws BOSHException {
+ super(val);
+ checkMinValue(0);
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return instance of the attribute for the specified string, or
+ * {@code null} if input string is {@code null}
+ * @throws BOSHException on parse or validation failure
+ */
+ static AttrInactivity createFromString(final String str)
+ throws BOSHException {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrInactivity(str);
+ }
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrMaxPause.java b/src/com/kenai/jbosh/AttrMaxPause.java
new file mode 100644
index 0000000..8d1d98b
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrMaxPause.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Data type representing the getValue of the {@code maxpause} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrMaxPause extends AbstractIntegerAttr {
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute getValue
+ * @throws BOSHException on parse or validation failure
+ */
+ private AttrMaxPause(final String val) throws BOSHException {
+ super(val);
+ checkMinValue(1);
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return attribute instance or {@code null} if provided string is
+ * {@code null}
+ * @throws BOSHException on parse or validation failure
+ */
+ static AttrMaxPause createFromString(final String str)
+ throws BOSHException {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrMaxPause(str);
+ }
+ }
+
+ /**
+ * Get the max pause time in milliseconds.
+ *
+ * @return pause tme in milliseconds
+ */
+ public int getInMilliseconds() {
+ return (int) TimeUnit.MILLISECONDS.convert(
+ intValue(), TimeUnit.SECONDS);
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrPause.java b/src/com/kenai/jbosh/AttrPause.java
new file mode 100644
index 0000000..5fb3282
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrPause.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Data type representing the getValue of the {@code pause} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrPause extends AbstractIntegerAttr {
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute getValue
+ * @throws BOSHException on parse or validation failure
+ */
+ private AttrPause(final String val) throws BOSHException {
+ super(val);
+ checkMinValue(1);
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return attribute instance or {@code null} if provided string is
+ * {@code null}
+ * @throws BOSHException on parse or validation failure
+ */
+ static AttrPause createFromString(final String str)
+ throws BOSHException {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrPause(str);
+ }
+ }
+
+ /**
+ * Get the pause time in milliseconds.
+ *
+ * @return pause tme in milliseconds
+ */
+ public int getInMilliseconds() {
+ return (int) TimeUnit.MILLISECONDS.convert(
+ intValue(), TimeUnit.SECONDS);
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrPolling.java b/src/com/kenai/jbosh/AttrPolling.java
new file mode 100644
index 0000000..3f0b08d
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrPolling.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Data type representing the getValue of the {@code polling} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrPolling extends AbstractIntegerAttr {
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute getValue
+ * @throws BOSHException on parse or validation failure
+ */
+ private AttrPolling(final String str) throws BOSHException {
+ super(str);
+ checkMinValue(0);
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return instance of the attribute for the specified string, or
+ * {@code null} if input string is {@code null}
+ * @throws BOSHException on parse or validation failure
+ */
+ static AttrPolling createFromString(final String str)
+ throws BOSHException {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrPolling(str);
+ }
+ }
+
+ /**
+ * Get the polling interval in milliseconds.
+ *
+ * @return polling interval in milliseconds
+ */
+ public int getInMilliseconds() {
+ return (int) TimeUnit.MILLISECONDS.convert(
+ intValue(), TimeUnit.SECONDS);
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrRequests.java b/src/com/kenai/jbosh/AttrRequests.java
new file mode 100644
index 0000000..bfdc529
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrRequests.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the value of the {@code requests} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrRequests extends AbstractIntegerAttr {
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute value
+ * @throws BOSHException on parse or validation failure
+ */
+ private AttrRequests(final String val) throws BOSHException {
+ super(val);
+ checkMinValue(1);
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return instance of the attribute for the specified string, or
+ * {@code null} if input string is {@code null}
+ * @throws BOSHException on parse or validation failure
+ */
+ static AttrRequests createFromString(final String str)
+ throws BOSHException {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrRequests(str);
+ }
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrSessionID.java b/src/com/kenai/jbosh/AttrSessionID.java
new file mode 100644
index 0000000..1998968
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrSessionID.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code sid} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrSessionID extends AbstractAttr<String> {
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute getValue
+ */
+ private AttrSessionID(final String val) {
+ super(val);
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return attribute instance
+ */
+ static AttrSessionID createFromString(final String str) {
+ return new AttrSessionID(str);
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrVersion.java b/src/com/kenai/jbosh/AttrVersion.java
new file mode 100644
index 0000000..9396e3b
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrVersion.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code ver} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrVersion extends AbstractAttr<String> implements Comparable {
+
+ /**
+ * Default value if none is provided.
+ */
+ private static final AttrVersion DEFAULT;
+ static {
+ try {
+ DEFAULT = createFromString("1.8");
+ } catch (BOSHException boshx) {
+ throw(new IllegalStateException(boshx));
+ }
+ }
+
+ /**
+ * Major portion of the version.
+ */
+ private final int major;
+
+ /**
+ * Minor portion of the version.
+ */
+ private final int minor;
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute getValue
+ * @throws BOSHException on parse or validation failure
+ */
+ private AttrVersion(final String val) throws BOSHException {
+ super(val);
+
+ int idx = val.indexOf('.');
+ if (idx <= 0) {
+ throw(new BOSHException(
+ "Illegal ver attribute value (not in major.minor form): "
+ + val));
+ }
+
+ String majorStr = val.substring(0, idx);
+ try {
+ major = Integer.parseInt(majorStr);
+ } catch (NumberFormatException nfx) {
+ throw(new BOSHException(
+ "Could not parse ver attribute value (major ver): "
+ + majorStr,
+ nfx));
+ }
+ if (major < 0) {
+ throw(new BOSHException(
+ "Major version may not be < 0"));
+ }
+
+ String minorStr = val.substring(idx + 1);
+ try {
+ minor = Integer.parseInt(minorStr);
+ } catch (NumberFormatException nfx) {
+ throw(new BOSHException(
+ "Could not parse ver attribute value (minor ver): "
+ + minorStr,
+ nfx));
+ }
+ if (minor < 0) {
+ throw(new BOSHException(
+ "Minor version may not be < 0"));
+ }
+ }
+
+ /**
+ * Get the version of specifcation that we support.
+ *
+ * @return max spec version the code supports
+ */
+ static AttrVersion getSupportedVersion() {
+ return DEFAULT;
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return attribute instance or {@code null} if provided string is
+ * {@code null}
+ * @throws BOSHException on parse or validation failure
+ */
+ static AttrVersion createFromString(final String str)
+ throws BOSHException {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrVersion(str);
+ }
+ }
+
+ /**
+ * Returns the 'major' portion of the version number.
+ *
+ * @return major digits only
+ */
+ int getMajor() {
+ return major;
+ }
+
+ /**
+ * Returns the 'minor' portion of the version number.
+ *
+ * @return minor digits only
+ */
+ int getMinor() {
+ return minor;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Comparable interface:
+
+ /**
+ * {@inheritDoc}
+ *
+ * @param otherObj object to compare to
+ * @return -1, 0, or 1
+ */
+ @Override
+ public int compareTo(final Object otherObj) {
+ if (otherObj instanceof AttrVersion) {
+ AttrVersion other = (AttrVersion) otherObj;
+ if (major < other.major) {
+ return -1;
+ } else if (major > other.major) {
+ return 1;
+ } else if (minor < other.minor) {
+ return -1;
+ } else if (minor > other.minor) {
+ return 1;
+ } else {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+}
diff --git a/src/com/kenai/jbosh/AttrWait.java b/src/com/kenai/jbosh/AttrWait.java
new file mode 100644
index 0000000..d2c95f7
--- /dev/null
+++ b/src/com/kenai/jbosh/AttrWait.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Data type representing the getValue of the {@code wait} attribute of the
+ * {@code bosh} element.
+ */
+final class AttrWait extends AbstractIntegerAttr {
+
+ /**
+ * Creates a new attribute object.
+ *
+ * @param val attribute getValue
+ * @throws BOSHException on parse or validation failure
+ */
+ private AttrWait(final String val) throws BOSHException {
+ super(val);
+ checkMinValue(1);
+ }
+
+ /**
+ * Creates a new attribute instance from the provided String.
+ *
+ * @param str string representation of the attribute
+ * @return attribute instance or {@code null} if provided string is
+ * {@code null}
+ * @throws BOSHException on parse or validation failure
+ */
+ static AttrWait createFromString(final String str)
+ throws BOSHException {
+ if (str == null) {
+ return null;
+ } else {
+ return new AttrWait(str);
+ }
+ }
+
+}
diff --git a/src/com/kenai/jbosh/Attributes.java b/src/com/kenai/jbosh/Attributes.java
new file mode 100644
index 0000000..d01541e
--- /dev/null
+++ b/src/com/kenai/jbosh/Attributes.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import javax.xml.XMLConstants;
+
+/**
+ * Class containing constants for attribute definitions used by the
+ * XEP-0124 specification. We shouldn't need to expose these outside
+ * our package, since nobody else should be needing to worry about
+ * them.
+ */
+final class Attributes {
+
+ /**
+ * Private constructor to prevent construction of library class.
+ */
+ private Attributes() {
+ super();
+ }
+
+ static final BodyQName ACCEPT = BodyQName.createBOSH("accept");
+ static final BodyQName AUTHID = BodyQName.createBOSH("authid");
+ static final BodyQName ACK = BodyQName.createBOSH("ack");
+ static final BodyQName CHARSETS = BodyQName.createBOSH("charsets");
+ static final BodyQName CONDITION = BodyQName.createBOSH("condition");
+ static final BodyQName CONTENT = BodyQName.createBOSH("content");
+ static final BodyQName FROM = BodyQName.createBOSH("from");
+ static final BodyQName HOLD = BodyQName.createBOSH("hold");
+ static final BodyQName INACTIVITY = BodyQName.createBOSH("inactivity");
+ static final BodyQName KEY = BodyQName.createBOSH("key");
+ static final BodyQName MAXPAUSE = BodyQName.createBOSH("maxpause");
+ static final BodyQName NEWKEY = BodyQName.createBOSH("newkey");
+ static final BodyQName PAUSE = BodyQName.createBOSH("pause");
+ static final BodyQName POLLING = BodyQName.createBOSH("polling");
+ static final BodyQName REPORT = BodyQName.createBOSH("report");
+ static final BodyQName REQUESTS = BodyQName.createBOSH("requests");
+ static final BodyQName RID = BodyQName.createBOSH("rid");
+ static final BodyQName ROUTE = BodyQName.createBOSH("route");
+ static final BodyQName SECURE = BodyQName.createBOSH("secure");
+ static final BodyQName SID = BodyQName.createBOSH("sid");
+ static final BodyQName STREAM = BodyQName.createBOSH("stream");
+ static final BodyQName TIME = BodyQName.createBOSH("time");
+ static final BodyQName TO = BodyQName.createBOSH("to");
+ static final BodyQName TYPE = BodyQName.createBOSH("type");
+ static final BodyQName VER = BodyQName.createBOSH("ver");
+ static final BodyQName WAIT = BodyQName.createBOSH("wait");
+ static final BodyQName XML_LANG =
+ BodyQName.createWithPrefix(XMLConstants.XML_NS_URI, "lang", "xml");
+}
diff --git a/src/com/kenai/jbosh/BOSHClient.java b/src/com/kenai/jbosh/BOSHClient.java
new file mode 100644
index 0000000..b96d188
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClient.java
@@ -0,0 +1,1536 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import com.kenai.jbosh.ComposableBody.Builder;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * BOSH Client session instance. Each communication session with a remote
+ * connection manager is represented and handled by an instance of this
+ * class. This is the main entry point for client-side communications.
+ * To create a new session, a client configuration must first be created
+ * and then used to create a client instance:
+ * <pre>
+ * BOSHClientConfig cfg = BOSHClientConfig.Builder.create(
+ * "http://server:1234/httpbind", "jabber.org")
+ * .setFrom("user@jabber.org")
+ * .build();
+ * BOSHClient client = BOSHClient.create(cfg);
+ * </pre>
+ * Additional client configuration options are available. See the
+ * {@code BOSHClientConfig.Builder} class for more information.
+ * <p/>
+ * Once a {@code BOSHClient} instance has been created, communication with
+ * the remote connection manager can begin. No attempt will be made to
+ * establish a connection to the connection manager until the first call
+ * is made to the {@code send(ComposableBody)} method. Note that it is
+ * possible to send an empty body to cause an immediate connection attempt
+ * to the connection manager. Sending an empty message would look like
+ * the following:
+ * <pre>
+ * client.send(ComposableBody.builder().build());
+ * </pre>
+ * For more information on creating body messages with content, see the
+ * {@code ComposableBody.Builder} class documentation.
+ * <p/>
+ * Once a session has been successfully started, the client instance can be
+ * used to send arbitrary payload data. All aspects of the BOSH
+ * protocol involving setting and processing attributes in the BOSH
+ * namespace will be handled by the client code transparently and behind the
+ * scenes. The user of the client instance can therefore concentrate
+ * entirely on the content of the message payload, leaving the semantics of
+ * the BOSH protocol to the client implementation.
+ * <p/>
+ * To be notified of incoming messages from the remote connection manager,
+ * a {@code BOSHClientResponseListener} should be added to the client instance.
+ * All incoming messages will be published to all response listeners as they
+ * arrive and are processed. As with the transmission of payload data via
+ * the {@code send(ComposableBody)} method, there is no need to worry about
+ * handling of the BOSH attributes, since this is handled behind the scenes.
+ * <p/>
+ * If the connection to the remote connection manager is terminated (either
+ * explicitly or due to a terminal condition of some sort), all connection
+ * listeners will be notified. After the connection has been closed, the
+ * client instance is considered dead and a new one must be created in order
+ * to resume communications with the remote server.
+ * <p/>
+ * Instances of this class are thread-safe.
+ *
+ * @see BOSHClientConfig.Builder
+ * @see BOSHClientResponseListener
+ * @see BOSHClientConnListener
+ * @see ComposableBody.Builder
+ */
+public final class BOSHClient {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOG = Logger.getLogger(
+ BOSHClient.class.getName());
+
+ /**
+ * Value of the 'type' attribute used for session termination.
+ */
+ private static final String TERMINATE = "terminate";
+
+ /**
+ * Value of the 'type' attribute used for recoverable errors.
+ */
+ private static final String ERROR = "error";
+
+ /**
+ * Message to use for interrupted exceptions.
+ */
+ private static final String INTERRUPTED = "Interrupted";
+
+ /**
+ * Message used for unhandled exceptions.
+ */
+ private static final String UNHANDLED = "Unhandled Exception";
+
+ /**
+ * Message used whena null listener is detected.
+ */
+ private static final String NULL_LISTENER = "Listener may not b enull";
+
+ /**
+ * Default empty request delay.
+ */
+ private static final int DEFAULT_EMPTY_REQUEST_DELAY = 100;
+
+ /**
+ * Amount of time to wait before sending an empty request, in
+ * milliseconds.
+ */
+ private static final int EMPTY_REQUEST_DELAY = Integer.getInteger(
+ BOSHClient.class.getName() + ".emptyRequestDelay",
+ DEFAULT_EMPTY_REQUEST_DELAY);
+
+ /**
+ * Default value for the pause margin.
+ */
+ private static final int DEFAULT_PAUSE_MARGIN = 500;
+
+ /**
+ * The amount of time in milliseconds which will be reserved as a
+ * safety margin when scheduling empty requests against a maxpause
+ * value. This should give us enough time to build the message
+ * and transport it to the remote host.
+ */
+ private static final int PAUSE_MARGIN = Integer.getInteger(
+ BOSHClient.class.getName() + ".pauseMargin",
+ DEFAULT_PAUSE_MARGIN);
+
+ /**
+ * Flag indicating whether or not we want to perform assertions.
+ */
+ private static final boolean ASSERTIONS;
+
+ /**
+ * Connection listeners.
+ */
+ private final Set<BOSHClientConnListener> connListeners =
+ new CopyOnWriteArraySet<BOSHClientConnListener>();
+
+ /**
+ * Request listeners.
+ */
+ private final Set<BOSHClientRequestListener> requestListeners =
+ new CopyOnWriteArraySet<BOSHClientRequestListener>();
+
+ /**
+ * Response listeners.
+ */
+ private final Set<BOSHClientResponseListener> responseListeners =
+ new CopyOnWriteArraySet<BOSHClientResponseListener>();
+
+ /**
+ * Lock instance.
+ */
+ private final ReentrantLock lock = new ReentrantLock();
+
+ /**
+ * Condition indicating that there are messages to be exchanged.
+ */
+ private final Condition notEmpty = lock.newCondition();
+
+ /**
+ * Condition indicating that there are available slots for sending
+ * messages.
+ */
+ private final Condition notFull = lock.newCondition();
+
+ /**
+ * Condition indicating that there are no outstanding connections.
+ */
+ private final Condition drained = lock.newCondition();
+
+ /**
+ * Session configuration.
+ */
+ private final BOSHClientConfig cfg;
+
+ /**
+ * Processor thread runnable instance.
+ */
+ private final Runnable procRunnable = new Runnable() {
+ /**
+ * Process incoming messages.
+ */
+ public void run() {
+ processMessages();
+ }
+ };
+
+ /**
+ * Processor thread runnable instance.
+ */
+ private final Runnable emptyRequestRunnable = new Runnable() {
+ /**
+ * Process incoming messages.
+ */
+ public void run() {
+ sendEmptyRequest();
+ }
+ };
+
+ /**
+ * HTTPSender instance.
+ */
+ private final HTTPSender httpSender =
+ new ApacheHTTPSender();
+
+ /**
+ * Storage for test hook implementation.
+ */
+ private final AtomicReference<ExchangeInterceptor> exchInterceptor =
+ new AtomicReference<ExchangeInterceptor>();
+
+ /**
+ * Request ID sequence to use for the session.
+ */
+ private final RequestIDSequence requestIDSeq = new RequestIDSequence();
+
+ /**
+ * ScheduledExcecutor to use for deferred tasks.
+ */
+ private final ScheduledExecutorService schedExec =
+ Executors.newSingleThreadScheduledExecutor();
+
+ /************************************************************
+ * The following vars must be accessed via the lock instance.
+ */
+
+ /**
+ * Thread which is used to process responses from the connection
+ * manager. Becomes null when session is terminated.
+ */
+ private Thread procThread;
+
+ /**
+ * Future for sending a deferred empty request, if needed.
+ */
+ private ScheduledFuture emptyRequestFuture;
+
+ /**
+ * Connection Manager session parameters. Only available when in a
+ * connected state.
+ */
+ private CMSessionParams cmParams;
+
+ /**
+ * List of active/outstanding requests.
+ */
+ private Queue<HTTPExchange> exchanges = new LinkedList<HTTPExchange>();
+
+ /**
+ * Set of RIDs which have been received, for the purpose of sending
+ * response acknowledgements.
+ */
+ private SortedSet<Long> pendingResponseAcks = new TreeSet<Long>();
+
+ /**
+ * The highest RID that we've already received a response for. This value
+ * is used to implement response acks.
+ */
+ private Long responseAck = Long.valueOf(-1L);
+
+ /**
+ * List of requests which have been made but not yet acknowledged. This
+ * list remains unpopulated if the CM is not acking requests.
+ */
+ private List<ComposableBody> pendingRequestAcks =
+ new ArrayList<ComposableBody>();
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Classes:
+
+ /**
+ * Class used in testing to dynamically manipulate received exchanges
+ * at test runtime.
+ */
+ abstract static class ExchangeInterceptor {
+ /**
+ * Limit construction.
+ */
+ ExchangeInterceptor() {
+ // Empty;
+ }
+
+ /**
+ * Hook to manipulate an HTTPExchange as is is about to be processed.
+ *
+ * @param exch original exchange that would be processed
+ * @return replacement exchange instance, or {@code null} to skip
+ * processing of this exchange
+ */
+ abstract HTTPExchange interceptExchange(final HTTPExchange exch);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Constructors:
+
+ /**
+ * Determine whether or not we should perform assertions. Assertions
+ * can be specified via system property explicitly, or defaulted to
+ * the JVM assertions status.
+ */
+ static {
+ final String prop =
+ BOSHClient.class.getSimpleName() + ".assertionsEnabled";
+ boolean enabled = false;
+ if (System.getProperty(prop) == null) {
+ assert enabled = true;
+ } else {
+ enabled = Boolean.getBoolean(prop);
+ }
+ ASSERTIONS = enabled;
+ }
+
+ /**
+ * Prevent direct construction.
+ */
+ private BOSHClient(final BOSHClientConfig sessCfg) {
+ cfg = sessCfg;
+ init();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Public methods:
+
+ /**
+ * Create a new BOSH client session using the client configuration
+ * information provided.
+ *
+ * @param clientCfg session configuration
+ * @return BOSH session instance
+ */
+ public static BOSHClient create(final BOSHClientConfig clientCfg) {
+ if (clientCfg == null) {
+ throw(new IllegalArgumentException(
+ "Client configuration may not be null"));
+ }
+ return new BOSHClient(clientCfg);
+ }
+
+ /**
+ * Get the client configuration that was used to create this client
+ * instance.
+ *
+ * @return client configuration
+ */
+ public BOSHClientConfig getBOSHClientConfig() {
+ return cfg;
+ }
+
+ /**
+ * Adds a connection listener to the session.
+ *
+ * @param listener connection listener to add, if not already added
+ */
+ public void addBOSHClientConnListener(
+ final BOSHClientConnListener listener) {
+ if (listener == null) {
+ throw(new IllegalArgumentException(NULL_LISTENER));
+ }
+ connListeners.add(listener);
+ }
+
+ /**
+ * Removes a connection listener from the session.
+ *
+ * @param listener connection listener to remove, if previously added
+ */
+ public void removeBOSHClientConnListener(
+ final BOSHClientConnListener listener) {
+ if (listener == null) {
+ throw(new IllegalArgumentException(NULL_LISTENER));
+ }
+ connListeners.remove(listener);
+ }
+
+ /**
+ * Adds a request message listener to the session.
+ *
+ * @param listener request listener to add, if not already added
+ */
+ public void addBOSHClientRequestListener(
+ final BOSHClientRequestListener listener) {
+ if (listener == null) {
+ throw(new IllegalArgumentException(NULL_LISTENER));
+ }
+ requestListeners.add(listener);
+ }
+
+ /**
+ * Removes a request message listener from the session, if previously
+ * added.
+ *
+ * @param listener instance to remove
+ */
+ public void removeBOSHClientRequestListener(
+ final BOSHClientRequestListener listener) {
+ if (listener == null) {
+ throw(new IllegalArgumentException(NULL_LISTENER));
+ }
+ requestListeners.remove(listener);
+ }
+
+ /**
+ * Adds a response message listener to the session.
+ *
+ * @param listener response listener to add, if not already added
+ */
+ public void addBOSHClientResponseListener(
+ final BOSHClientResponseListener listener) {
+ if (listener == null) {
+ throw(new IllegalArgumentException(NULL_LISTENER));
+ }
+ responseListeners.add(listener);
+ }
+
+ /**
+ * Removes a response message listener from the session, if previously
+ * added.
+ *
+ * @param listener instance to remove
+ */
+ public void removeBOSHClientResponseListener(
+ final BOSHClientResponseListener listener) {
+ if (listener == null) {
+ throw(new IllegalArgumentException(NULL_LISTENER));
+ }
+ responseListeners.remove(listener);
+ }
+
+ /**
+ * Send the provided message data to the remote connection manager. The
+ * provided message body does not need to have any BOSH-specific attribute
+ * information set. It only needs to contain the actual message payload
+ * that should be delivered to the remote server.
+ * <p/>
+ * The first call to this method will result in a connection attempt
+ * to the remote connection manager. Subsequent calls to this method
+ * will block until the underlying session state allows for the message
+ * to be transmitted. In certain scenarios - such as when the maximum
+ * number of outbound connections has been reached - calls to this method
+ * will block for short periods of time.
+ *
+ * @param body message data to send to remote server
+ * @throws BOSHException on message transmission failure
+ */
+ public void send(final ComposableBody body) throws BOSHException {
+ assertUnlocked();
+ if (body == null) {
+ throw(new IllegalArgumentException(
+ "Message body may not be null"));
+ }
+
+ HTTPExchange exch;
+ CMSessionParams params;
+ lock.lock();
+ try {
+ blockUntilSendable(body);
+ if (!isWorking() && !isTermination(body)) {
+ throw(new BOSHException(
+ "Cannot send message when session is closed"));
+ }
+
+ long rid = requestIDSeq.getNextRID();
+ ComposableBody request = body;
+ params = cmParams;
+ if (params == null && exchanges.isEmpty()) {
+ // This is the first message being sent
+ request = applySessionCreationRequest(rid, body);
+ } else {
+ request = applySessionData(rid, body);
+ if (cmParams.isAckingRequests()) {
+ pendingRequestAcks.add(request);
+ }
+ }
+ exch = new HTTPExchange(request);
+ exchanges.add(exch);
+ notEmpty.signalAll();
+ clearEmptyRequest();
+ } finally {
+ lock.unlock();
+ }
+ AbstractBody finalReq = exch.getRequest();
+ HTTPResponse resp = httpSender.send(params, finalReq);
+ exch.setHTTPResponse(resp);
+ fireRequestSent(finalReq);
+ }
+
+ /**
+ * Attempt to pause the current session. When supported by the remote
+ * connection manager, pausing the session will result in the connection
+ * manager closing out all outstanding requests (including the pause
+ * request) and increases the inactivity timeout of the session. The
+ * exact value of the temporary timeout is dependent upon the connection
+ * manager. This method should be used if a client encounters an
+ * exceptional temporary situation during which it will be unable to send
+ * requests to the connection manager for a period of time greater than
+ * the maximum inactivity period.
+ *
+ * The session will revert back to it's normal, unpaused state when the
+ * client sends it's next message.
+ *
+ * @return {@code true} if the connection manager supports session pausing,
+ * {@code false} if the connection manager does not support session
+ * pausing or if the session has not yet been established
+ */
+ public boolean pause() {
+ assertUnlocked();
+ lock.lock();
+ AttrMaxPause maxPause = null;
+ try {
+ if (cmParams == null) {
+ return false;
+ }
+
+ maxPause = cmParams.getMaxPause();
+ if (maxPause == null) {
+ return false;
+ }
+ } finally {
+ lock.unlock();
+ }
+ try {
+ send(ComposableBody.builder()
+ .setAttribute(Attributes.PAUSE, maxPause.toString())
+ .build());
+ } catch (BOSHException boshx) {
+ LOG.log(Level.FINEST, "Could not send pause", boshx);
+ }
+ return true;
+ }
+
+ /**
+ * End the BOSH session by disconnecting from the remote BOSH connection
+ * manager.
+ *
+ * @throws BOSHException when termination message cannot be sent
+ */
+ public void disconnect() throws BOSHException {
+ disconnect(ComposableBody.builder().build());
+ }
+
+ /**
+ * End the BOSH session by disconnecting from the remote BOSH connection
+ * manager, sending the provided content in the final connection
+ * termination message.
+ *
+ * @param msg final message to send
+ * @throws BOSHException when termination message cannot be sent
+ */
+ public void disconnect(final ComposableBody msg) throws BOSHException {
+ if (msg == null) {
+ throw(new IllegalArgumentException(
+ "Message body may not be null"));
+ }
+
+ Builder builder = msg.rebuild();
+ builder.setAttribute(Attributes.TYPE, TERMINATE);
+ send(builder.build());
+ }
+
+ /**
+ * Forcibly close this client session instance. The preferred mechanism
+ * to close the connection is to send a disconnect message and wait for
+ * organic termination. Calling this method simply shuts down the local
+ * session without sending a termination message, releasing all resources
+ * associated with the session.
+ */
+ public void close() {
+ dispose(new BOSHException("Session explicitly closed by caller"));
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Package-private methods:
+
+ /**
+ * Get the current CM session params.
+ *
+ * @return current session params, or {@code null}
+ */
+ CMSessionParams getCMSessionParams() {
+ lock.lock();
+ try {
+ return cmParams;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Wait until no more messages are waiting to be processed.
+ */
+ void drain() {
+ lock.lock();
+ try {
+ LOG.finest("Waiting while draining...");
+ while (isWorking()
+ && (emptyRequestFuture == null
+ || emptyRequestFuture.isDone())) {
+ try {
+ drained.await();
+ } catch (InterruptedException intx) {
+ LOG.log(Level.FINEST, INTERRUPTED, intx);
+ }
+ }
+ LOG.finest("Drained");
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Test method used to forcibly discard next exchange.
+ *
+ * @param interceptor exchange interceptor
+ */
+ void setExchangeInterceptor(final ExchangeInterceptor interceptor) {
+ exchInterceptor.set(interceptor);
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Private methods:
+
+ /**
+ * Initialize the session. This initializes the underlying HTTP
+ * transport implementation and starts the receive thread.
+ */
+ private void init() {
+ assertUnlocked();
+
+ lock.lock();
+ try {
+ httpSender.init(cfg);
+ procThread = new Thread(procRunnable);
+ procThread.setDaemon(true);
+ procThread.setName(BOSHClient.class.getSimpleName()
+ + "[" + System.identityHashCode(this)
+ + "]: Receive thread");
+ procThread.start();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ /**
+ * Destroy this session.
+ *
+ * @param cause the reason for the session termination, or {@code null}
+ * for normal termination
+ */
+ private void dispose(final Throwable cause) {
+ assertUnlocked();
+
+ lock.lock();
+ try {
+ if (procThread == null) {
+ // Already disposed
+ return;
+ }
+ procThread = null;
+ } finally {
+ lock.unlock();
+ }
+
+ if (cause == null) {
+ fireConnectionClosed();
+ } else {
+ fireConnectionClosedOnError(cause);
+ }
+
+ lock.lock();
+ try {
+ clearEmptyRequest();
+ exchanges = null;
+ cmParams = null;
+ pendingResponseAcks = null;
+ pendingRequestAcks = null;
+ notEmpty.signalAll();
+ notFull.signalAll();
+ drained.signalAll();
+ } finally {
+ lock.unlock();
+ }
+
+ httpSender.destroy();
+ schedExec.shutdownNow();
+ }
+
+ /**
+ * Determines if the message body specified indicates a request to
+ * pause the session.
+ *
+ * @param msg message to evaluate
+ * @return {@code true} if the message is a pause request, {@code false}
+ * otherwise
+ */
+ private static boolean isPause(final AbstractBody msg) {
+ return msg.getAttribute(Attributes.PAUSE) != null;
+ }
+
+ /**
+ * Determines if the message body specified indicates a termination of
+ * the session.
+ *
+ * @param msg message to evaluate
+ * @return {@code true} if the message is a session termination,
+ * {@code false} otherwise
+ */
+ private static boolean isTermination(final AbstractBody msg) {
+ return TERMINATE.equals(msg.getAttribute(Attributes.TYPE));
+ }
+
+ /**
+ * Evaluates the HTTP response code and response message and returns the
+ * terminal binding condition that it describes, if any.
+ *
+ * @param respCode HTTP response code
+ * @param respBody response body
+ * @return terminal binding condition, or {@code null} if not a terminal
+ * binding condition message
+ */
+ private TerminalBindingCondition getTerminalBindingCondition(
+ final int respCode,
+ final AbstractBody respBody) {
+ assertLocked();
+
+ if (isTermination(respBody)) {
+ String str = respBody.getAttribute(Attributes.CONDITION);
+ return TerminalBindingCondition.forString(str);
+ }
+ // Check for deprecated HTTP Error Conditions
+ if (cmParams != null && cmParams.getVersion() == null) {
+ return TerminalBindingCondition.forHTTPResponseCode(respCode);
+ }
+ return null;
+ }
+
+ /**
+ * Determines if the message specified is immediately sendable or if it
+ * needs to block until the session state changes.
+ *
+ * @param msg message to evaluate
+ * @return {@code true} if the message can be immediately sent,
+ * {@code false} otherwise
+ */
+ private boolean isImmediatelySendable(final AbstractBody msg) {
+ assertLocked();
+
+ if (cmParams == null) {
+ // block if we're waiting for a response to our first request
+ return exchanges.isEmpty();
+ }
+
+ AttrRequests requests = cmParams.getRequests();
+ if (requests == null) {
+ return true;
+ }
+ int maxRequests = requests.intValue();
+ if (exchanges.size() < maxRequests) {
+ return true;
+ }
+ if (exchanges.size() == maxRequests
+ && (isTermination(msg) || isPause(msg))) {
+ // One additional terminate or pause message is allowed
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether or not the session is still active.
+ *
+ * @return {@code true} if it is, {@code false} otherwise
+ */
+ private boolean isWorking() {
+ assertLocked();
+
+ return procThread != null;
+ }
+
+ /**
+ * Blocks until either the message provided becomes immediately
+ * sendable or until the session is terminated.
+ *
+ * @param msg message to evaluate
+ */
+ private void blockUntilSendable(final AbstractBody msg) {
+ assertLocked();
+
+ while (isWorking() && !isImmediatelySendable(msg)) {
+ try {
+ notFull.await();
+ } catch (InterruptedException intx) {
+ LOG.log(Level.FINEST, INTERRUPTED, intx);
+ }
+ }
+ }
+
+ /**
+ * Modifies the specified body message such that it becomes a new
+ * BOSH session creation request.
+ *
+ * @param rid request ID to use
+ * @param orig original body to modify
+ * @return modified message which acts as a session creation request
+ */
+ private ComposableBody applySessionCreationRequest(
+ final long rid, final ComposableBody orig) throws BOSHException {
+ assertLocked();
+
+ Builder builder = orig.rebuild();
+ builder.setAttribute(Attributes.TO, cfg.getTo());
+ builder.setAttribute(Attributes.XML_LANG, cfg.getLang());
+ builder.setAttribute(Attributes.VER,
+ AttrVersion.getSupportedVersion().toString());
+ builder.setAttribute(Attributes.WAIT, "60");
+ builder.setAttribute(Attributes.HOLD, "1");
+ builder.setAttribute(Attributes.RID, Long.toString(rid));
+ applyRoute(builder);
+ applyFrom(builder);
+ builder.setAttribute(Attributes.ACK, "1");
+
+ // Make sure the following are NOT present (i.e., during retries)
+ builder.setAttribute(Attributes.SID, null);
+ return builder.build();
+ }
+
+ /**
+ * Applies routing information to the request message who's builder has
+ * been provided.
+ *
+ * @param builder builder instance to add routing information to
+ */
+ private void applyRoute(final Builder builder) {
+ assertLocked();
+
+ String route = cfg.getRoute();
+ if (route != null) {
+ builder.setAttribute(Attributes.ROUTE, route);
+ }
+ }
+
+ /**
+ * Applies the local station ID information to the request message who's
+ * builder has been provided.
+ *
+ * @param builder builder instance to add station ID information to
+ */
+ private void applyFrom(final Builder builder) {
+ assertLocked();
+
+ String from = cfg.getFrom();
+ if (from != null) {
+ builder.setAttribute(Attributes.FROM, from);
+ }
+ }
+
+ /**
+ * Applies existing session data to the outbound request, returning the
+ * modified request.
+ *
+ * This method assumes the lock is currently held.
+ *
+ * @param rid request ID to use
+ * @param orig original/raw request
+ * @return modified request with session information applied
+ */
+ private ComposableBody applySessionData(
+ final long rid,
+ final ComposableBody orig) throws BOSHException {
+ assertLocked();
+
+ Builder builder = orig.rebuild();
+ builder.setAttribute(Attributes.SID,
+ cmParams.getSessionID().toString());
+ builder.setAttribute(Attributes.RID, Long.toString(rid));
+ applyResponseAcknowledgement(builder, rid);
+ return builder.build();
+ }
+
+ /**
+ * Sets the 'ack' attribute of the request to the value of the highest
+ * 'rid' of a request for which it has already received a response in the
+ * case where it has also received all responses associated with lower
+ * 'rid' values. The only exception is that, after its session creation
+ * request, the client SHOULD NOT include an 'ack' attribute in any request
+ * if it has received responses to all its previous requests.
+ *
+ * @param builder message builder
+ * @param rid current request RID
+ */
+ private void applyResponseAcknowledgement(
+ final Builder builder,
+ final long rid) {
+ assertLocked();
+
+ if (responseAck.equals(Long.valueOf(-1L))) {
+ // We have not received any responses yet
+ return;
+ }
+
+ Long prevRID = Long.valueOf(rid - 1L);
+ if (responseAck.equals(prevRID)) {
+ // Implicit ack
+ return;
+ }
+
+ builder.setAttribute(Attributes.ACK, responseAck.toString());
+ }
+
+ /**
+ * While we are "connected", process received responses.
+ *
+ * This method is run in the processing thread.
+ */
+ private void processMessages() {
+ LOG.log(Level.FINEST, "Processing thread starting");
+ try {
+ HTTPExchange exch;
+ do {
+ exch = nextExchange();
+ if (exch == null) {
+ break;
+ }
+
+ // Test hook to manipulate what the client sees:
+ ExchangeInterceptor interceptor = exchInterceptor.get();
+ if (interceptor != null) {
+ HTTPExchange newExch = interceptor.interceptExchange(exch);
+ if (newExch == null) {
+ LOG.log(Level.FINE, "Discarding exchange on request "
+ + "of test hook: RID="
+ + exch.getRequest().getAttribute(
+ Attributes.RID));
+ lock.lock();
+ try {
+ exchanges.remove(exch);
+ } finally {
+ lock.unlock();
+ }
+ continue;
+ }
+ exch = newExch;
+ }
+
+ processExchange(exch);
+ } while (true);
+ } finally {
+ LOG.log(Level.FINEST, "Processing thread exiting");
+ }
+
+ }
+
+ /**
+ * Get the next message exchange to process, blocking until one becomes
+ * available if nothing is already waiting for processing.
+ *
+ * @return next available exchange to process, or {@code null} if no
+ * exchanges are immediately available
+ */
+ private HTTPExchange nextExchange() {
+ assertUnlocked();
+
+ final Thread thread = Thread.currentThread();
+ HTTPExchange exch = null;
+ lock.lock();
+ try {
+ do {
+ if (!thread.equals(procThread)) {
+ break;
+ }
+ exch = exchanges.peek();
+ if (exch == null) {
+ try {
+ notEmpty.await();
+ } catch (InterruptedException intx) {
+ LOG.log(Level.FINEST, INTERRUPTED, intx);
+ }
+ }
+ } while (exch == null);
+ } finally {
+ lock.unlock();
+ }
+ return exch;
+ }
+
+ /**
+ * Process the next, provided exchange. This is the main processing
+ * method of the receive thread.
+ *
+ * @param exch message exchange to process
+ */
+ private void processExchange(final HTTPExchange exch) {
+ assertUnlocked();
+
+ HTTPResponse resp;
+ AbstractBody body;
+ int respCode;
+ try {
+ resp = exch.getHTTPResponse();
+ body = resp.getBody();
+ respCode = resp.getHTTPStatus();
+ } catch (BOSHException boshx) {
+ LOG.log(Level.FINEST, "Could not obtain response", boshx);
+ dispose(boshx);
+ return;
+ } catch (InterruptedException intx) {
+ LOG.log(Level.FINEST, INTERRUPTED, intx);
+ dispose(intx);
+ return;
+ }
+ fireResponseReceived(body);
+
+ // Process the message with the current session state
+ AbstractBody req = exch.getRequest();
+ CMSessionParams params;
+ List<HTTPExchange> toResend = null;
+ lock.lock();
+ try {
+ // Check for session creation response info, if needed
+ if (cmParams == null) {
+ cmParams = CMSessionParams.fromSessionInit(req, body);
+
+ // The following call handles the lock. It's not an escape.
+ fireConnectionEstablished();
+ }
+ params = cmParams;
+
+ checkForTerminalBindingConditions(body, respCode);
+ if (isTermination(body)) {
+ // Explicit termination
+ lock.unlock();
+ dispose(null);
+ return;
+ }
+
+ if (isRecoverableBindingCondition(body)) {
+ // Retransmit outstanding requests
+ if (toResend == null) {
+ toResend = new ArrayList<HTTPExchange>(exchanges.size());
+ }
+ for (HTTPExchange exchange : exchanges) {
+ HTTPExchange resendExch =
+ new HTTPExchange(exchange.getRequest());
+ toResend.add(resendExch);
+ }
+ for (HTTPExchange exchange : toResend) {
+ exchanges.add(exchange);
+ }
+ } else {
+ // Process message as normal
+ processRequestAcknowledgements(req, body);
+ processResponseAcknowledgementData(req);
+ HTTPExchange resendExch =
+ processResponseAcknowledgementReport(body);
+ if (resendExch != null && toResend == null) {
+ toResend = new ArrayList<HTTPExchange>(1);
+ toResend.add(resendExch);
+ exchanges.add(resendExch);
+ }
+ }
+ } catch (BOSHException boshx) {
+ LOG.log(Level.FINEST, "Could not process response", boshx);
+ lock.unlock();
+ dispose(boshx);
+ return;
+ } finally {
+ if (lock.isHeldByCurrentThread()) {
+ try {
+ exchanges.remove(exch);
+ if (exchanges.isEmpty()) {
+ scheduleEmptyRequest(processPauseRequest(req));
+ }
+ notFull.signalAll();
+ } finally {
+ lock.unlock();
+ }
+ }
+ }
+
+ if (toResend != null) {
+ for (HTTPExchange resend : toResend) {
+ HTTPResponse response =
+ httpSender.send(params, resend.getRequest());
+ resend.setHTTPResponse(response);
+ fireRequestSent(resend.getRequest());
+ }
+ }
+ }
+
+ /**
+ * Clears any scheduled empty requests.
+ */
+ private void clearEmptyRequest() {
+ assertLocked();
+
+ if (emptyRequestFuture != null) {
+ emptyRequestFuture.cancel(false);
+ emptyRequestFuture = null;
+ }
+ }
+
+ /**
+ * Calculates the default empty request delay/interval to use for the
+ * active session.
+ *
+ * @return delay in milliseconds
+ */
+ private long getDefaultEmptyRequestDelay() {
+ assertLocked();
+
+ // Figure out how long we should wait before sending an empty request
+ AttrPolling polling = cmParams.getPollingInterval();
+ long delay;
+ if (polling == null) {
+ delay = EMPTY_REQUEST_DELAY;
+ } else {
+ delay = polling.getInMilliseconds();
+ }
+ return delay;
+ }
+
+ /**
+ * Schedule an empty request to be sent if no other requests are
+ * sent in a reasonable amount of time.
+ */
+ private void scheduleEmptyRequest(long delay) {
+ assertLocked();
+ if (delay < 0L) {
+ throw(new IllegalArgumentException(
+ "Empty request delay must be >= 0 (was: " + delay + ")"));
+ }
+
+ clearEmptyRequest();
+ if (!isWorking()) {
+ return;
+ }
+
+ // Schedule the transmission
+ if (LOG.isLoggable(Level.FINER)) {
+ LOG.finer("Scheduling empty request in " + delay + "ms");
+ }
+ try {
+ emptyRequestFuture = schedExec.schedule(emptyRequestRunnable,
+ delay, TimeUnit.MILLISECONDS);
+ } catch (RejectedExecutionException rex) {
+ LOG.log(Level.FINEST, "Could not schedule empty request", rex);
+ }
+ drained.signalAll();
+ }
+
+ /**
+ * Sends an empty request to maintain session requirements. If a request
+ * is sent within a reasonable time window, the empty request transmission
+ * will be cancelled.
+ */
+ private void sendEmptyRequest() {
+ assertUnlocked();
+ // Send an empty request
+ LOG.finest("Sending empty request");
+ try {
+ send(ComposableBody.builder().build());
+ } catch (BOSHException boshx) {
+ dispose(boshx);
+ }
+ }
+
+ /**
+ * Assert that the internal lock is held.
+ */
+ private void assertLocked() {
+ if (ASSERTIONS) {
+ if (!lock.isHeldByCurrentThread()) {
+ throw(new AssertionError("Lock is not held by current thread"));
+ }
+ return;
+ }
+ }
+
+ /**
+ * Assert that the internal lock is *not* held.
+ */
+ private void assertUnlocked() {
+ if (ASSERTIONS) {
+ if (lock.isHeldByCurrentThread()) {
+ throw(new AssertionError("Lock is held by current thread"));
+ }
+ return;
+ }
+ }
+
+ /**
+ * Checks to see if the response indicates a terminal binding condition
+ * (as per XEP-0124 section 17). If it does, an exception is thrown.
+ *
+ * @param body response body to evaluate
+ * @param code HTTP response code
+ * @throws BOSHException if a terminal binding condition is detected
+ */
+ private void checkForTerminalBindingConditions(
+ final AbstractBody body,
+ final int code)
+ throws BOSHException {
+ TerminalBindingCondition cond =
+ getTerminalBindingCondition(code, body);
+ if (cond != null) {
+ throw(new BOSHException(
+ "Terminal binding condition encountered: "
+ + cond.getCondition() + " ("
+ + cond.getMessage() + ")"));
+ }
+ }
+
+ /**
+ * Determines whether or not the response indicates a recoverable
+ * binding condition (as per XEP-0124 section 17).
+ *
+ * @param resp response body
+ * @return {@code true} if it does, {@code false} otherwise
+ */
+ private static boolean isRecoverableBindingCondition(
+ final AbstractBody resp) {
+ return ERROR.equals(resp.getAttribute(Attributes.TYPE));
+ }
+
+ /**
+ * Process the request to determine if the empty request delay
+ * can be determined by looking to see if the request is a pause
+ * request. If it can, the request's delay is returned, otherwise
+ * the default delay is returned.
+ *
+ * @return delay in milliseconds that should elapse prior to an
+ * empty message being sent
+ */
+ private long processPauseRequest(
+ final AbstractBody req) {
+ assertLocked();
+
+ if (cmParams != null && cmParams.getMaxPause() != null) {
+ try {
+ AttrPause pause = AttrPause.createFromString(
+ req.getAttribute(Attributes.PAUSE));
+ if (pause != null) {
+ long delay = pause.getInMilliseconds() - PAUSE_MARGIN;
+ if (delay < 0) {
+ delay = EMPTY_REQUEST_DELAY;
+ }
+ return delay;
+ }
+ } catch (BOSHException boshx) {
+ LOG.log(Level.FINEST, "Could not extract", boshx);
+ }
+ }
+
+ return getDefaultEmptyRequestDelay();
+ }
+
+ /**
+ * Check the response for request acknowledgements and take appropriate
+ * action.
+ *
+ * This method assumes the lock is currently held.
+ *
+ * @param req request
+ * @param resp response
+ */
+ private void processRequestAcknowledgements(
+ final AbstractBody req, final AbstractBody resp) {
+ assertLocked();
+
+ if (!cmParams.isAckingRequests()) {
+ return;
+ }
+
+ // If a report or time attribute is set, we aren't acking anything
+ if (resp.getAttribute(Attributes.REPORT) != null) {
+ return;
+ }
+
+ // Figure out what the highest acked RID is
+ String acked = resp.getAttribute(Attributes.ACK);
+ Long ackUpTo;
+ if (acked == null) {
+ // Implicit ack of all prior requests up until RID
+ ackUpTo = Long.parseLong(req.getAttribute(Attributes.RID));
+ } else {
+ ackUpTo = Long.parseLong(acked);
+ }
+
+ // Remove the acked requests from the list
+ if (LOG.isLoggable(Level.FINEST)) {
+ LOG.finest("Removing pending acks up to: " + ackUpTo);
+ }
+ Iterator<ComposableBody> iter = pendingRequestAcks.iterator();
+ while (iter.hasNext()) {
+ AbstractBody pending = iter.next();
+ Long pendingRID = Long.parseLong(
+ pending.getAttribute(Attributes.RID));
+ if (pendingRID.compareTo(ackUpTo) <= 0) {
+ iter.remove();
+ }
+ }
+ }
+
+ /**
+ * Process the response in order to update the response acknowlegement
+ * data.
+ *
+ * This method assumes the lock is currently held.
+ *
+ * @param req request
+ */
+ private void processResponseAcknowledgementData(
+ final AbstractBody req) {
+ assertLocked();
+
+ Long rid = Long.parseLong(req.getAttribute(Attributes.RID));
+ if (responseAck.equals(Long.valueOf(-1L))) {
+ // This is the first request
+ responseAck = rid;
+ } else {
+ pendingResponseAcks.add(rid);
+ // Remove up until the first missing response (or end of queue)
+ Long whileVal = responseAck;
+ while (whileVal.equals(pendingResponseAcks.first())) {
+ responseAck = whileVal;
+ pendingResponseAcks.remove(whileVal);
+ whileVal = Long.valueOf(whileVal.longValue() + 1);
+ }
+ }
+ }
+
+ /**
+ * Process the response in order to check for and respond to any potential
+ * ack reports.
+ *
+ * This method assumes the lock is currently held.
+ *
+ * @param resp response
+ * @return exchange to transmit if a resend is to be performed, or
+ * {@code null} if no resend is necessary
+ * @throws BOSHException when a a retry is needed but cannot be performed
+ */
+ private HTTPExchange processResponseAcknowledgementReport(
+ final AbstractBody resp)
+ throws BOSHException {
+ assertLocked();
+
+ String reportStr = resp.getAttribute(Attributes.REPORT);
+ if (reportStr == null) {
+ // No report on this message
+ return null;
+ }
+
+ Long report = Long.parseLong(reportStr);
+ Long time = Long.parseLong(resp.getAttribute(Attributes.TIME));
+ if (LOG.isLoggable(Level.FINE)) {
+ LOG.fine("Received report of missing request (RID="
+ + report + ", time=" + time + "ms)");
+ }
+
+ // Find the missing request
+ Iterator<ComposableBody> iter = pendingRequestAcks.iterator();
+ AbstractBody req = null;
+ while (iter.hasNext() && req == null) {
+ AbstractBody pending = iter.next();
+ Long pendingRID = Long.parseLong(
+ pending.getAttribute(Attributes.RID));
+ if (report.equals(pendingRID)) {
+ req = pending;
+ }
+ }
+
+ if (req == null) {
+ throw(new BOSHException("Report of missing message with RID '"
+ + reportStr
+ + "' but local copy of that request was not found"));
+ }
+
+ // Resend the missing request
+ HTTPExchange exch = new HTTPExchange(req);
+ exchanges.add(exch);
+ notEmpty.signalAll();
+ return exch;
+ }
+
+ /**
+ * Notifies all request listeners that the specified request is being
+ * sent.
+ *
+ * @param request request being sent
+ */
+ private void fireRequestSent(final AbstractBody request) {
+ assertUnlocked();
+
+ BOSHMessageEvent event = null;
+ for (BOSHClientRequestListener listener : requestListeners) {
+ if (event == null) {
+ event = BOSHMessageEvent.createRequestSentEvent(this, request);
+ }
+ try {
+ listener.requestSent(event);
+ } catch (Exception ex) {
+ LOG.log(Level.WARNING, UNHANDLED, ex);
+ }
+ }
+ }
+
+ /**
+ * Notifies all response listeners that the specified response has been
+ * received.
+ *
+ * @param response response received
+ */
+ private void fireResponseReceived(final AbstractBody response) {
+ assertUnlocked();
+
+ BOSHMessageEvent event = null;
+ for (BOSHClientResponseListener listener : responseListeners) {
+ if (event == null) {
+ event = BOSHMessageEvent.createResponseReceivedEvent(
+ this, response);
+ }
+ try {
+ listener.responseReceived(event);
+ } catch (Exception ex) {
+ LOG.log(Level.WARNING, UNHANDLED, ex);
+ }
+ }
+ }
+
+ /**
+ * Notifies all connection listeners that the session has been successfully
+ * established.
+ */
+ private void fireConnectionEstablished() {
+ final boolean hadLock = lock.isHeldByCurrentThread();
+ if (hadLock) {
+ lock.unlock();
+ }
+ try {
+ BOSHClientConnEvent event = null;
+ for (BOSHClientConnListener listener : connListeners) {
+ if (event == null) {
+ event = BOSHClientConnEvent
+ .createConnectionEstablishedEvent(this);
+ }
+ try {
+ listener.connectionEvent(event);
+ } catch (Exception ex) {
+ LOG.log(Level.WARNING, UNHANDLED, ex);
+ }
+ }
+ } finally {
+ if (hadLock) {
+ lock.lock();
+ }
+ }
+ }
+
+ /**
+ * Notifies all connection listeners that the session has been
+ * terminated normally.
+ */
+ private void fireConnectionClosed() {
+ assertUnlocked();
+
+ BOSHClientConnEvent event = null;
+ for (BOSHClientConnListener listener : connListeners) {
+ if (event == null) {
+ event = BOSHClientConnEvent.createConnectionClosedEvent(this);
+ }
+ try {
+ listener.connectionEvent(event);
+ } catch (Exception ex) {
+ LOG.log(Level.WARNING, UNHANDLED, ex);
+ }
+ }
+ }
+
+ /**
+ * Notifies all connection listeners that the session has been
+ * terminated due to the exceptional condition provided.
+ *
+ * @param cause cause of the termination
+ */
+ private void fireConnectionClosedOnError(
+ final Throwable cause) {
+ assertUnlocked();
+
+ BOSHClientConnEvent event = null;
+ for (BOSHClientConnListener listener : connListeners) {
+ if (event == null) {
+ event = BOSHClientConnEvent
+ .createConnectionClosedOnErrorEvent(
+ this, pendingRequestAcks, cause);
+ }
+ try {
+ listener.connectionEvent(event);
+ } catch (Exception ex) {
+ LOG.log(Level.WARNING, UNHANDLED, ex);
+ }
+ }
+ }
+
+}
diff --git a/src/com/kenai/jbosh/BOSHClientConfig.java b/src/com/kenai/jbosh/BOSHClientConfig.java
new file mode 100644
index 0000000..23915b6
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClientConfig.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.net.URI;
+import javax.net.ssl.SSLContext;
+
+/**
+ * BOSH client configuration information. Instances of this class contain
+ * all information necessary to establish connectivity with a remote
+ * connection manager.
+ * <p/>
+ * Instances of this class are immutable, thread-safe,
+ * and can be re-used to configure multiple client session instances.
+ */
+public final class BOSHClientConfig {
+
+ /**
+ * Connection manager URI.
+ */
+ private final URI uri;
+
+ /**
+ * Target domain.
+ */
+ private final String to;
+
+ /**
+ * Client ID of this station.
+ */
+ private final String from;
+
+ /**
+ * Default XML language.
+ */
+ private final String lang;
+
+ /**
+ * Routing information for messages sent to CM.
+ */
+ private final String route;
+
+ /**
+ * Proxy host.
+ */
+ private final String proxyHost;
+
+ /**
+ * Proxy port.
+ */
+ private final int proxyPort;
+
+ /**
+ * SSL context.
+ */
+ private final SSLContext sslContext;
+
+ /**
+ * Flag indicating that compression should be attempted, if possible.
+ */
+ private final boolean compressionEnabled;
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Classes:
+
+ /**
+ * Class instance builder, after the builder pattern. This allows each
+ * {@code BOSHClientConfig} instance to be immutable while providing
+ * flexibility when building new {@code BOSHClientConfig} instances.
+ * <p/>
+ * Instances of this class are <b>not</b> thread-safe. If template-style
+ * use is desired, see the {@code create(BOSHClientConfig)} method.
+ */
+ public static final class Builder {
+ // Required args
+ private final URI bURI;
+ private final String bDomain;
+
+ // Optional args
+ private String bFrom;
+ private String bLang;
+ private String bRoute;
+ private String bProxyHost;
+ private int bProxyPort;
+ private SSLContext bSSLContext;
+ private Boolean bCompression;
+
+ /**
+ * Creates a new builder instance, used to create instances of the
+ * {@code BOSHClientConfig} class.
+ *
+ * @param cmURI URI to use to contact the connection manager
+ * @param domain target domain to communicate with
+ */
+ private Builder(final URI cmURI, final String domain) {
+ bURI = cmURI;
+ bDomain = domain;
+ }
+
+ /**
+ * Creates a new builder instance, used to create instances of the
+ * {@code BOSHClientConfig} class.
+ *
+ * @param cmURI URI to use to contact the connection manager
+ * @param domain target domain to communicate with
+ * @return builder instance
+ */
+ public static Builder create(final URI cmURI, final String domain) {
+ if (cmURI == null) {
+ throw(new IllegalArgumentException(
+ "Connection manager URI must not be null"));
+ }
+ if (domain == null) {
+ throw(new IllegalArgumentException(
+ "Target domain must not be null"));
+ }
+ String scheme = cmURI.getScheme();
+ if (!("http".equals(scheme) || "https".equals(scheme))) {
+ throw(new IllegalArgumentException(
+ "Only 'http' and 'https' URI are allowed"));
+ }
+ return new Builder(cmURI, domain);
+ }
+
+ /**
+ * Creates a new builder instance using the existing configuration
+ * provided as a starting point.
+ *
+ * @param cfg configuration to copy
+ * @return builder instance
+ */
+ public static Builder create(final BOSHClientConfig cfg) {
+ Builder result = new Builder(cfg.getURI(), cfg.getTo());
+ result.bFrom = cfg.getFrom();
+ result.bLang = cfg.getLang();
+ result.bRoute = cfg.getRoute();
+ result.bProxyHost = cfg.getProxyHost();
+ result.bProxyPort = cfg.getProxyPort();
+ result.bSSLContext = cfg.getSSLContext();
+ result.bCompression = cfg.isCompressionEnabled();
+ return result;
+ }
+
+ /**
+ * Set the ID of the client station, to be forwarded to the connection
+ * manager when new sessions are created.
+ *
+ * @param id client ID
+ * @return builder instance
+ */
+ public Builder setFrom(final String id) {
+ if (id == null) {
+ throw(new IllegalArgumentException(
+ "Client ID must not be null"));
+ }
+ bFrom = id;
+ return this;
+ }
+
+ /**
+ * Set the default language of any human-readable content within the
+ * XML.
+ *
+ * @param lang XML language ID
+ * @return builder instance
+ */
+ public Builder setXMLLang(final String lang) {
+ if (lang == null) {
+ throw(new IllegalArgumentException(
+ "Default language ID must not be null"));
+ }
+ bLang = lang;
+ return this;
+ }
+
+ /**
+ * Sets the destination server/domain that the client should connect to.
+ * Connection managers may be configured to enable sessions with more
+ * that one server in different domains. When requesting a session with
+ * such a "proxy" connection manager, a client should use this method to
+ * specify the server with which it wants to communicate.
+ *
+ * @param protocol connection protocol (e.g, "xmpp")
+ * @param host host or domain to be served by the remote server. Note
+ * that this is not necessarily the host name or domain name of the
+ * remote server.
+ * @param port port number of the remote server
+ * @return builder instance
+ */
+ public Builder setRoute(
+ final String protocol,
+ final String host,
+ final int port) {
+ if (protocol == null) {
+ throw(new IllegalArgumentException("Protocol cannot be null"));
+ }
+ if (protocol.contains(":")) {
+ throw(new IllegalArgumentException(
+ "Protocol cannot contain the ':' character"));
+ }
+ if (host == null) {
+ throw(new IllegalArgumentException("Host cannot be null"));
+ }
+ if (host.contains(":")) {
+ throw(new IllegalArgumentException(
+ "Host cannot contain the ':' character"));
+ }
+ if (port <= 0) {
+ throw(new IllegalArgumentException("Port number must be > 0"));
+ }
+ bRoute = protocol + ":" + host + ":" + port;
+ return this;
+ }
+
+ /**
+ * Specify the hostname and port of an HTTP proxy to connect through.
+ *
+ * @param hostName proxy hostname
+ * @param port proxy port number
+ * @return builder instance
+ */
+ public Builder setProxy(final String hostName, final int port) {
+ if (hostName == null || hostName.length() == 0) {
+ throw(new IllegalArgumentException(
+ "Proxy host name cannot be null or empty"));
+ }
+ if (port <= 0) {
+ throw(new IllegalArgumentException(
+ "Proxy port must be > 0"));
+ }
+ bProxyHost = hostName;
+ bProxyPort = port;
+ return this;
+ }
+
+ /**
+ * Set the SSL context to use for this session. This can be used
+ * to configure certificate-based authentication, etc..
+ *
+ * @param ctx SSL context
+ * @return builder instance
+ */
+ public Builder setSSLContext(final SSLContext ctx) {
+ if (ctx == null) {
+ throw(new IllegalArgumentException(
+ "SSL context cannot be null"));
+ }
+ bSSLContext = ctx;
+ return this;
+ }
+
+ /**
+ * Set whether or not compression of the underlying data stream
+ * should be attempted. By default, compression is disabled.
+ *
+ * @param enabled set to {@code true} if compression should be
+ * attempted when possible, {@code false} to disable compression
+ * @return builder instance
+ */
+ public Builder setCompressionEnabled(final boolean enabled) {
+ bCompression = Boolean.valueOf(enabled);
+ return this;
+ }
+
+ /**
+ * Build the immutable object instance with the current configuration.
+ *
+ * @return BOSHClientConfig instance
+ */
+ public BOSHClientConfig build() {
+ // Default XML language
+ String lang;
+ if (bLang == null) {
+ lang = "en";
+ } else {
+ lang = bLang;
+ }
+
+ // Default proxy port
+ int port;
+ if (bProxyHost == null) {
+ port = 0;
+ } else {
+ port = bProxyPort;
+ }
+
+ // Default compression
+ boolean compression;
+ if (bCompression == null) {
+ compression = false;
+ } else {
+ compression = bCompression.booleanValue();
+ }
+
+ return new BOSHClientConfig(
+ bURI,
+ bDomain,
+ bFrom,
+ lang,
+ bRoute,
+ bProxyHost,
+ port,
+ bSSLContext,
+ compression);
+ }
+
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Constructor:
+
+ /**
+ * Prevent direct construction.
+ *
+ * @param cURI URI of the connection manager to connect to
+ * @param cDomain the target domain of the first stream
+ * @param cFrom client ID
+ * @param cLang default XML language
+ * @param cRoute target route
+ * @param cProxyHost proxy host
+ * @param cProxyPort proxy port
+ * @param cSSLContext SSL context
+ * @param cCompression compression enabled flag
+ */
+ private BOSHClientConfig(
+ final URI cURI,
+ final String cDomain,
+ final String cFrom,
+ final String cLang,
+ final String cRoute,
+ final String cProxyHost,
+ final int cProxyPort,
+ final SSLContext cSSLContext,
+ final boolean cCompression) {
+ uri = cURI;
+ to = cDomain;
+ from = cFrom;
+ lang = cLang;
+ route = cRoute;
+ proxyHost = cProxyHost;
+ proxyPort = cProxyPort;
+ sslContext = cSSLContext;
+ compressionEnabled = cCompression;
+ }
+
+ /**
+ * Get the URI to use to contact the connection manager.
+ *
+ * @return connection manager URI.
+ */
+ public URI getURI() {
+ return uri;
+ }
+
+ /**
+ * Get the ID of the target domain.
+ *
+ * @return domain id
+ */
+ public String getTo() {
+ return to;
+ }
+
+ /**
+ * Get the ID of the local client.
+ *
+ * @return client id, or {@code null}
+ */
+ public String getFrom() {
+ return from;
+ }
+
+ /**
+ * Get the default language of any human-readable content within the
+ * XML. Defaults to "en".
+ *
+ * @return XML language ID
+ */
+ public String getLang() {
+ return lang;
+ }
+
+ /**
+ * Get the routing information for messages sent to the CM.
+ *
+ * @return route attribute string, or {@code null} if no routing
+ * info was provided.
+ */
+ public String getRoute() {
+ return route;
+ }
+
+ /**
+ * Get the HTTP proxy host to use.
+ *
+ * @return proxy host, or {@code null} if no proxy information was specified
+ */
+ public String getProxyHost() {
+ return proxyHost;
+ }
+
+ /**
+ * Get the HTTP proxy port to use.
+ *
+ * @return proxy port, or 0 if no proxy information was specified
+ */
+ public int getProxyPort() {
+ return proxyPort;
+ }
+
+ /**
+ * Get the SSL context to use for this session.
+ *
+ * @return SSL context instance to use, or {@code null} if no
+ * context instance was provided.
+ */
+ public SSLContext getSSLContext() {
+ return sslContext;
+ }
+
+ /**
+ * Determines whether or not compression of the underlying data stream
+ * should be attempted/allowed. Defaults to {@code false}.
+ *
+ * @return {@code true} if compression should be attempted, {@code false}
+ * if compression is disabled or was not specified
+ */
+ boolean isCompressionEnabled() {
+ return compressionEnabled;
+ }
+
+}
diff --git a/src/com/kenai/jbosh/BOSHClientConnEvent.java b/src/com/kenai/jbosh/BOSHClientConnEvent.java
new file mode 100644
index 0000000..0ac7943
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClientConnEvent.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EventObject;
+import java.util.List;
+
+/**
+ * Client connection event, notifying of changes in connection state.
+ * <p/>
+ * This class is immutable and thread-safe.
+ */
+public final class BOSHClientConnEvent extends EventObject {
+
+ /**
+ * Serialized version.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Boolean flag indicating whether or not a session has been established
+ * and is currently active.
+ */
+ private final boolean connected;
+
+ /**
+ * List of outstanding requests which may not have been sent and/or
+ * acknowledged by the remote CM.
+ */
+ private final List<ComposableBody> requests;
+
+ /**
+ * Cause of the session termination, or {@code null}.
+ */
+ private final Throwable cause;
+
+ /**
+ * Creates a new connection event instance.
+ *
+ * @param source event source
+ * @param cConnected flag indicating whether or not the session is
+ * currently active
+ * @param cRequests outstanding requests when an error condition is
+ * detected, or {@code null} when not an error condition
+ * @param cCause cause of the error condition, or {@code null} when no
+ * error condition is present
+ */
+ private BOSHClientConnEvent(
+ final BOSHClient source,
+ final boolean cConnected,
+ final List<ComposableBody> cRequests,
+ final Throwable cCause) {
+ super(source);
+ connected = cConnected;
+ cause = cCause;
+
+ if (connected) {
+ if (cCause != null) {
+ throw(new IllegalStateException(
+ "Cannot be connected and have a cause"));
+ }
+ if (cRequests != null && cRequests.size() > 0) {
+ throw(new IllegalStateException(
+ "Cannot be connected and have outstanding requests"));
+ }
+ }
+
+ if (cRequests == null) {
+ requests = Collections.emptyList();
+ } else {
+ // Defensive copy:
+ requests = Collections.unmodifiableList(
+ new ArrayList<ComposableBody>(cRequests));
+ }
+ }
+
+ /**
+ * Creates a new connection establishment event.
+ *
+ * @param source client which has become connected
+ * @return event instance
+ */
+ static BOSHClientConnEvent createConnectionEstablishedEvent(
+ final BOSHClient source) {
+ return new BOSHClientConnEvent(source, true, null, null);
+ }
+
+ /**
+ * Creates a new successful connection closed event. This represents
+ * a clean termination of the client session.
+ *
+ * @param source client which has been disconnected
+ * @return event instance
+ */
+ static BOSHClientConnEvent createConnectionClosedEvent(
+ final BOSHClient source) {
+ return new BOSHClientConnEvent(source, false, null, null);
+ }
+
+ /**
+ * Creates a connection closed on error event. This represents
+ * an unexpected termination of the client session.
+ *
+ * @param source client which has been disconnected
+ * @param outstanding list of requests which may not have been received
+ * by the remote connection manager
+ * @param cause cause of termination
+ * @return event instance
+ */
+ static BOSHClientConnEvent createConnectionClosedOnErrorEvent(
+ final BOSHClient source,
+ final List<ComposableBody> outstanding,
+ final Throwable cause) {
+ return new BOSHClientConnEvent(source, false, outstanding, cause);
+ }
+
+ /**
+ * Gets the client from which this event originated.
+ *
+ * @return client instance
+ */
+ public BOSHClient getBOSHClient() {
+ return (BOSHClient) getSource();
+ }
+
+ /**
+ * Returns whether or not the session has been successfully established
+ * and is currently active.
+ *
+ * @return {@code true} if a session is active, {@code false} otherwise
+ */
+ public boolean isConnected() {
+ return connected;
+ }
+
+ /**
+ * Returns whether or not this event indicates an error condition. This
+ * will never return {@code true} when {@code isConnected()} returns
+ * {@code true}.
+ *
+ * @return {@code true} if the event indicates a terminal error has
+ * occurred, {@code false} otherwise.
+ */
+ public boolean isError() {
+ return cause != null;
+ }
+
+ /**
+ * Returns the underlying cause of the error condition. This method is
+ * guaranteed to return {@code null} when @{code isError()} returns
+ * {@code false}. Similarly, this method is guaranteed to return
+ * non-@{code null} if {@code isError()} returns {@code true}.
+ *
+ * @return underlying cause of the error condition, or {@code null} if
+ * this event does not represent an error condition
+ */
+ public Throwable getCause() {
+ return cause;
+ }
+
+ /**
+ * Get the list of requests which may not have been sent or were not
+ * acknowledged by the remote connection manager prior to session
+ * termination.
+ *
+ * @return list of messages which may not have been received by the remote
+ * connection manager, or an empty list if the session is still connected
+ */
+ public List<ComposableBody> getOutstandingRequests() {
+ return requests;
+ }
+
+}
diff --git a/src/com/kenai/jbosh/BOSHClientConnListener.java b/src/com/kenai/jbosh/BOSHClientConnListener.java
new file mode 100644
index 0000000..6d646cb
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClientConnListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Interface used by parties interested in monitoring the connection state
+ * of a client session.
+ */
+public interface BOSHClientConnListener {
+
+ /**
+ * Called when the connection state of the client which the listener
+ * is registered against has changed. The event object supplied can
+ * be used to determine the current session state.
+ *
+ * @param connEvent connection event describing the state
+ */
+ void connectionEvent(BOSHClientConnEvent connEvent);
+
+}
diff --git a/src/com/kenai/jbosh/BOSHClientRequestListener.java b/src/com/kenai/jbosh/BOSHClientRequestListener.java
new file mode 100644
index 0000000..2cc92f3
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClientRequestListener.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Interface used by parties interested in monitoring outbound requests made
+ * by the client to the connection manager (CM). No opportunity is provided
+ * to manipulate the outbound request.
+ * <p/>
+ * The messages being sent are typically modified copies of the message
+ * body provided to the {@code BOSHClient} instance, built from the
+ * originally provided message body plus additional BOSH protocol
+ * state and information. Messages may also be sent automatically when the
+ * protocol requires it, such as maintaining a minimum number of open
+ * connections to the connection manager.
+ * <p/>
+ * Listeners are executed by the sending thread immediately prior to
+ * message transmission and should not block for any significant amount
+ * of time.
+ */
+public interface BOSHClientRequestListener {
+
+ /**
+ * Called when the listener is being notified that a client request is
+ * about to be sent to the connection manager.
+ *
+ * @param event event instance containing the message being sent
+ */
+ void requestSent(BOSHMessageEvent event);
+
+}
diff --git a/src/com/kenai/jbosh/BOSHClientResponseListener.java b/src/com/kenai/jbosh/BOSHClientResponseListener.java
new file mode 100644
index 0000000..1d86e4f
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHClientResponseListener.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Interface used by parties interested in monitoring inbound responses
+ * to the client from the connection manager (CM). No opportunity is provided
+ * to manipulate the response.
+ * <p/>
+ * Listeners are executed by the message processing thread and should not
+ * block for any significant amount of time.
+ */
+public interface BOSHClientResponseListener {
+
+ /**
+ * Called when the listener is being notified that a response has been
+ * received from the connection manager.
+ *
+ * @param event event instance containing the message being sent
+ */
+ void responseReceived(BOSHMessageEvent event);
+
+}
diff --git a/src/com/kenai/jbosh/BOSHException.java b/src/com/kenai/jbosh/BOSHException.java
new file mode 100644
index 0000000..e0bc05b
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHException.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Exception class used by the BOSH API to minimize the number of checked
+ * exceptions which must be handled by the user of the API.
+ */
+public class BOSHException extends Exception {
+
+ /**
+ * Servial version UID.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates a new exception isntance with the specified descriptive message.
+ *
+ * @param msg description of the exceptional condition
+ */
+ public BOSHException(final String msg) {
+ super(msg);
+ }
+
+ /**
+ * Creates a new exception isntance with the specified descriptive
+ * message and the underlying root cause of the exceptional condition.
+ *
+ * @param msg description of the exceptional condition
+ * @param cause root cause or instigator of the condition
+ */
+ public BOSHException(final String msg, final Throwable cause) {
+ super(msg, cause);
+ }
+
+}
diff --git a/src/com/kenai/jbosh/BOSHMessageEvent.java b/src/com/kenai/jbosh/BOSHMessageEvent.java
new file mode 100644
index 0000000..550903e
--- /dev/null
+++ b/src/com/kenai/jbosh/BOSHMessageEvent.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.EventObject;
+
+/**
+ * Event representing a message sent to or from a BOSH connection manager.
+ * <p/>
+ * This class is immutable and thread-safe.
+ */
+public final class BOSHMessageEvent extends EventObject {
+
+ /**
+ * Serialized version.
+ */
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Message which was sent or received.
+ */
+ private final AbstractBody body;
+
+ /**
+ * Creates a new message event instance.
+ *
+ * @param source event source
+ * @param cBody message body
+ */
+ private BOSHMessageEvent(
+ final Object source,
+ final AbstractBody cBody) {
+ super(source);
+ if (cBody == null) {
+ throw(new IllegalArgumentException(
+ "message body may not be null"));
+ }
+ body = cBody;
+ }
+
+ /**
+ * Creates a new message event for clients sending events to the
+ * connection manager.
+ *
+ * @param source sender of the message
+ * @param body message body
+ * @return event instance
+ */
+ static BOSHMessageEvent createRequestSentEvent(
+ final BOSHClient source,
+ final AbstractBody body) {
+ return new BOSHMessageEvent(source, body);
+ }
+
+ /**
+ * Creates a new message event for clients receiving new messages
+ * from the connection manager.
+ *
+ * @param source receiver of the message
+ * @param body message body
+ * @return event instance
+ */
+ static BOSHMessageEvent createResponseReceivedEvent(
+ final BOSHClient source,
+ final AbstractBody body) {
+ return new BOSHMessageEvent(source, body);
+ }
+
+ /**
+ * Gets the message body which was sent or received.
+ *
+ * @return message body
+ */
+ public AbstractBody getBody() {
+ return body;
+ }
+
+}
diff --git a/src/com/kenai/jbosh/BodyParser.java b/src/com/kenai/jbosh/BodyParser.java
new file mode 100644
index 0000000..5ef5276
--- /dev/null
+++ b/src/com/kenai/jbosh/BodyParser.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Interface for parser implementations to implement in order to abstract the
+ * business of XML parsing out of the Body class. This allows us to leverage
+ * a variety of parser implementations to gain performance advantages.
+ */
+interface BodyParser {
+
+ /**
+ * Parses the XML message, extracting the useful data from the initial
+ * body element and returning it in a results object.
+ *
+ * @param xml XML to parse
+ * @return useful data parsed out of the XML
+ * @throws BOSHException on parse error
+ */
+ BodyParserResults parse(String xml) throws BOSHException;
+
+}
diff --git a/src/com/kenai/jbosh/BodyParserResults.java b/src/com/kenai/jbosh/BodyParserResults.java
new file mode 100644
index 0000000..955e4bf
--- /dev/null
+++ b/src/com/kenai/jbosh/BodyParserResults.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Data extracted from a raw XML message by a BodyParser implementation.
+ * Currently, this is limited to the attributes of the wrapper element.
+ */
+final class BodyParserResults {
+
+ /**
+ * Map of qualified names to their values. This map is defined to
+ * match the requirement of the {@code Body} class to prevent
+ * excessive copying.
+ */
+ private final Map<BodyQName, String> attrs =
+ new HashMap<BodyQName, String>();
+
+ /**
+ * Constructor.
+ */
+ BodyParserResults() {
+ // Empty
+ }
+
+ /**
+ * Add an attribute definition to the results.
+ *
+ * @param name attribute's qualified name
+ * @param value attribute value
+ */
+ void addBodyAttributeValue(
+ final BodyQName name,
+ final String value) {
+ attrs.put(name, value);
+ }
+
+ /**
+ * Returns the map of attributes added by the parser.
+ *
+ * @return map of atributes. Note: This is the live instance, not a copy.
+ */
+ Map<BodyQName, String> getAttributes() {
+ return attrs;
+ }
+
+}
diff --git a/src/com/kenai/jbosh/BodyParserSAX.java b/src/com/kenai/jbosh/BodyParserSAX.java
new file mode 100644
index 0000000..54c6c01
--- /dev/null
+++ b/src/com/kenai/jbosh/BodyParserSAX.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.SoftReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Implementation of the BodyParser interface which uses the SAX API
+ * that is part of the JDK. Due to the fact that we can cache and reuse
+ * SAXPArser instances, this has proven to be significantly faster than the
+ * use of the javax.xml.stream API introduced in Java 6 while simultaneously
+ * providing an implementation accessible to Java 5 users.
+ */
+final class BodyParserSAX implements BodyParser {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOG =
+ Logger.getLogger(BodyParserSAX.class.getName());
+
+ /**
+ * SAX parser factory.
+ */
+ private static final SAXParserFactory SAX_FACTORY;
+ static {
+ SAX_FACTORY = SAXParserFactory.newInstance();
+ SAX_FACTORY.setNamespaceAware(true);
+ SAX_FACTORY.setValidating(false);
+ }
+
+ /**
+ * Thread local to contain a SAX parser instance for each thread that
+ * attempts to use one. This allows us to gain an order of magnitude of
+ * performance as a result of not constructing parsers for each
+ * invocation while retaining thread safety.
+ */
+ private static final ThreadLocal<SoftReference<SAXParser>> PARSER =
+ new ThreadLocal<SoftReference<SAXParser>>() {
+ @Override protected SoftReference<SAXParser> initialValue() {
+ return new SoftReference<SAXParser>(null);
+ }
+ };
+
+ /**
+ * SAX event handler class.
+ */
+ private static final class Handler extends DefaultHandler {
+ private final BodyParserResults result;
+ private final SAXParser parser;
+ private String defaultNS = null;
+
+ private Handler(SAXParser theParser, BodyParserResults results) {
+ parser = theParser;
+ result = results;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void startElement(
+ final String uri,
+ final String localName,
+ final String qName,
+ final Attributes attributes) {
+ if (LOG.isLoggable(Level.FINEST)) {
+ LOG.finest("Start element: " + qName);
+ LOG.finest(" URI: " + uri);
+ LOG.finest(" local: " + localName);
+ }
+
+ BodyQName bodyName = AbstractBody.getBodyQName();
+ // Make sure the first element is correct
+ if (!(bodyName.getNamespaceURI().equals(uri)
+ && bodyName.getLocalPart().equals(localName))) {
+ throw(new IllegalStateException(
+ "Root element was not '" + bodyName.getLocalPart()
+ + "' in the '" + bodyName.getNamespaceURI()
+ + "' namespace. (Was '" + localName + "' in '" + uri
+ + "')"));
+ }
+
+ // Read in the attributes, making sure to expand the namespaces
+ // as needed.
+ for (int idx=0; idx < attributes.getLength(); idx++) {
+ String attrURI = attributes.getURI(idx);
+ if (attrURI.length() == 0) {
+ attrURI = defaultNS;
+ }
+ String attrLN = attributes.getLocalName(idx);
+ String attrVal = attributes.getValue(idx);
+ if (LOG.isLoggable(Level.FINEST)) {
+ LOG.finest(" Attribute: {" + attrURI + "}"
+ + attrLN + " = '" + attrVal + "'");
+ }
+
+ BodyQName aqn = BodyQName.create(attrURI, attrLN);
+ result.addBodyAttributeValue(aqn, attrVal);
+ }
+
+ parser.reset();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * This implementation uses this event hook to keep track of the
+ * default namespace on the body element.
+ */
+ @Override
+ public void startPrefixMapping(
+ final String prefix,
+ final String uri) {
+ if (prefix.length() == 0) {
+ if (LOG.isLoggable(Level.FINEST)) {
+ LOG.finest("Prefix mapping: <DEFAULT> => " + uri);
+ }
+ defaultNS = uri;
+ } else {
+ if (LOG.isLoggable(Level.FINEST)) {
+ LOG.info("Prefix mapping: " + prefix + " => " + uri);
+ }
+ }
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // BodyParser interface methods:
+
+ /**
+ * {@inheritDoc}
+ */
+ public BodyParserResults parse(String xml) throws BOSHException {
+ BodyParserResults result = new BodyParserResults();
+ Exception thrown;
+ try {
+ InputStream inStream = new ByteArrayInputStream(xml.getBytes());
+ SAXParser parser = getSAXParser();
+ parser.parse(inStream, new Handler(parser, result));
+ return result;
+ } catch (SAXException saxx) {
+ thrown = saxx;
+ } catch (IOException iox) {
+ thrown = iox;
+ }
+ throw(new BOSHException("Could not parse body:\n" + xml, thrown));
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Private methods:
+
+ /**
+ * Gets a SAXParser for use in parsing incoming messages.
+ *
+ * @return parser instance
+ */
+ private static SAXParser getSAXParser() {
+ SoftReference<SAXParser> ref = PARSER.get();
+ SAXParser result = ref.get();
+ if (result == null) {
+ Exception thrown;
+ try {
+ result = SAX_FACTORY.newSAXParser();
+ ref = new SoftReference<SAXParser>(result);
+ PARSER.set(ref);
+ return result;
+ } catch (ParserConfigurationException ex) {
+ thrown = ex;
+ } catch (SAXException ex) {
+ thrown = ex;
+ }
+ throw(new IllegalStateException(
+ "Could not create SAX parser", thrown));
+ } else {
+ result.reset();
+ return result;
+ }
+ }
+
+}
diff --git a/src/com/kenai/jbosh/BodyParserXmlPull.java b/src/com/kenai/jbosh/BodyParserXmlPull.java
new file mode 100644
index 0000000..5f23b06
--- /dev/null
+++ b/src/com/kenai/jbosh/BodyParserXmlPull.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.lang.ref.SoftReference;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.xml.XMLConstants;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+/**
+ * Implementation of the BodyParser interface which uses the XmlPullParser
+ * API. When available, this API provides an order of magnitude performance
+ * improvement over the default SAX parser implementation.
+ */
+final class BodyParserXmlPull implements BodyParser {
+
+ /**
+ * Logger.
+ */
+ private static final Logger LOG =
+ Logger.getLogger(BodyParserXmlPull.class.getName());
+
+ /**
+ * Thread local to contain a XmlPullParser instance for each thread that
+ * attempts to use one. This allows us to gain an order of magnitude of
+ * performance as a result of not constructing parsers for each
+ * invocation while retaining thread safety.
+ */
+ private static final ThreadLocal<SoftReference<XmlPullParser>> XPP_PARSER =
+ new ThreadLocal<SoftReference<XmlPullParser>>() {
+ @Override protected SoftReference<XmlPullParser> initialValue() {
+ return new SoftReference<XmlPullParser>(null);
+ }
+ };
+
+ ///////////////////////////////////////////////////////////////////////////
+ // BodyParser interface methods:
+
+ /**
+ * {@inheritDoc}
+ */
+ public BodyParserResults parse(final String xml) throws BOSHException {
+ BodyParserResults result = new BodyParserResults();
+ Exception thrown;
+ try {
+ XmlPullParser xpp = getXmlPullParser();
+
+ xpp.setInput(new StringReader(xml));
+ int eventType = xpp.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG) {
+ if (LOG.isLoggable(Level.FINEST)) {
+ LOG.finest("Start tag: " + xpp.getName());
+ }
+ } else {
+ eventType = xpp.next();
+ continue;
+ }
+
+ String prefix = xpp.getPrefix();
+ if (prefix == null) {
+ prefix = XMLConstants.DEFAULT_NS_PREFIX;
+ }
+ String uri = xpp.getNamespace();
+ String localName = xpp.getName();
+ QName name = new QName(uri, localName, prefix);
+ if (LOG.isLoggable(Level.FINEST)) {
+ LOG.finest("Start element: ");
+ LOG.finest(" prefix: " + prefix);
+ LOG.finest(" URI: " + uri);
+ LOG.finest(" local: " + localName);
+ }
+
+ BodyQName bodyName = AbstractBody.getBodyQName();
+ if (!bodyName.equalsQName(name)) {
+ throw(new IllegalStateException(
+ "Root element was not '" + bodyName.getLocalPart()
+ + "' in the '" + bodyName.getNamespaceURI()
+ + "' namespace. (Was '" + localName
+ + "' in '" + uri + "')"));
+ }
+
+ for (int idx=0; idx < xpp.getAttributeCount(); idx++) {
+ String attrURI = xpp.getAttributeNamespace(idx);
+ if (attrURI.length() == 0) {
+ attrURI = xpp.getNamespace(null);
+ }
+ String attrPrefix = xpp.getAttributePrefix(idx);
+ if (attrPrefix == null) {
+ attrPrefix = XMLConstants.DEFAULT_NS_PREFIX;
+ }
+ String attrLN = xpp.getAttributeName(idx);
+ String attrVal = xpp.getAttributeValue(idx);
+ BodyQName aqn = BodyQName.createWithPrefix(
+ attrURI, attrLN, attrPrefix);
+ if (LOG.isLoggable(Level.FINEST)) {
+ LOG.finest(" Attribute: {" + attrURI + "}"
+ + attrLN + " = '" + attrVal + "'");
+ }
+ result.addBodyAttributeValue(aqn, attrVal);
+ }
+ break;
+ }
+ return result;
+ } catch (RuntimeException rtx) {
+ thrown = rtx;
+ } catch (XmlPullParserException xmlppx) {
+ thrown = xmlppx;
+ } catch (IOException iox) {
+ thrown = iox;
+ }
+ throw(new BOSHException("Could not parse body:\n" + xml, thrown));
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Private methods:
+
+ /**
+ * Gets a XmlPullParser for use in parsing incoming messages.
+ *
+ * @return parser instance
+ */
+ private static XmlPullParser getXmlPullParser() {
+ SoftReference<XmlPullParser> ref = XPP_PARSER.get();
+ XmlPullParser result = ref.get();
+ if (result == null) {
+ Exception thrown;
+ try {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ factory.setValidating(false);
+ result = factory.newPullParser();
+ ref = new SoftReference<XmlPullParser>(result);
+ XPP_PARSER.set(ref);
+ return result;
+ } catch (Exception ex) {
+ thrown = ex;
+ }
+ throw(new IllegalStateException(
+ "Could not create XmlPull parser", thrown));
+ } else {
+ return result;
+ }
+ }
+
+}
diff --git a/src/com/kenai/jbosh/BodyQName.java b/src/com/kenai/jbosh/BodyQName.java
new file mode 100644
index 0000000..83acdf1
--- /dev/null
+++ b/src/com/kenai/jbosh/BodyQName.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * Qualified name of an attribute of the wrapper element. This class is
+ * analagous to the {@code javax.xml.namespace.QName} class.
+ * Each qualified name consists of a namespace URI and a local name.
+ * <p/>
+ * Instances of this class are immutable and thread-safe.
+ */
+public final class BodyQName {
+
+ /**
+ * BOSH namespace URI.
+ */
+ static final String BOSH_NS_URI =
+ "http://jabber.org/protocol/httpbind";
+
+ /**
+ * Namespace URI.
+ */
+ private final QName qname;
+
+ /**
+ * Private constructor to prevent direct construction.
+ *
+ * @param wrapped QName instance to wrap
+ */
+ private BodyQName(
+ final QName wrapped) {
+ qname = wrapped;
+ }
+
+ /**
+ * Creates a new qualified name using a namespace URI and local name.
+ *
+ * @param uri namespace URI
+ * @param local local name
+ * @return BodyQName instance
+ */
+ public static BodyQName create(
+ final String uri,
+ final String local) {
+ return createWithPrefix(uri, local, null);
+ }
+
+ /**
+ * Creates a new qualified name using a namespace URI and local name
+ * along with an optional prefix.
+ *
+ * @param uri namespace URI
+ * @param local local name
+ * @param prefix optional prefix or @{code null} for no prefix
+ * @return BodyQName instance
+ */
+ public static BodyQName createWithPrefix(
+ final String uri,
+ final String local,
+ final String prefix) {
+ if (uri == null || uri.length() == 0) {
+ throw(new IllegalArgumentException(
+ "URI is required and may not be null/empty"));
+ }
+ if (local == null || local.length() == 0) {
+ throw(new IllegalArgumentException(
+ "Local arg is required and may not be null/empty"));
+ }
+ if (prefix == null || prefix.length() == 0) {
+ return new BodyQName(new QName(uri, local));
+ } else {
+ return new BodyQName(new QName(uri, local, prefix));
+ }
+ }
+
+ /**
+ * Get the namespace URI of this qualified name.
+ *
+ * @return namespace uri
+ */
+ public String getNamespaceURI() {
+ return qname.getNamespaceURI();
+ }
+
+ /**
+ * Get the local part of this qualified name.
+ *
+ * @return local name
+ */
+ public String getLocalPart() {
+ return qname.getLocalPart();
+ }
+
+ /**
+ * Get the optional prefix used with this qualified name, or {@code null}
+ * if no prefix has been assiciated.
+ *
+ * @return prefix, or {@code null} if no prefix was supplied
+ */
+ public String getPrefix() {
+ return qname.getPrefix();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj instanceof BodyQName) {
+ BodyQName other = (BodyQName) obj;
+ return qname.equals(other.qname);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public int hashCode() {
+ return qname.hashCode();
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Package-private methods:
+
+ /**
+ * Creates a new qualified name using the BOSH namespace URI and local name.
+ *
+ * @param local local name
+ * @return BodyQName instance
+ */
+ static BodyQName createBOSH(
+ final String local) {
+ return createWithPrefix(BOSH_NS_URI, local, null);
+ }
+
+ /**
+ * Convenience method to compare this qualified name with a
+ * {@code javax.xml.namespace.QName}.
+ *
+ * @param otherName QName to compare to
+ * @return @{code true} if the qualified name is the same, {@code false}
+ * otherwise
+ */
+ boolean equalsQName(final QName otherName) {
+ return qname.equals(otherName);
+ }
+
+}
diff --git a/src/com/kenai/jbosh/CMSessionParams.java b/src/com/kenai/jbosh/CMSessionParams.java
new file mode 100644
index 0000000..bbed628
--- /dev/null
+++ b/src/com/kenai/jbosh/CMSessionParams.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+/**
+ * A BOSH connection manager session instance. This consolidates the
+ * configuration knowledge related to the CM session and provides a
+ * mechanism by which
+ */
+final class CMSessionParams {
+
+ private final AttrSessionID sid;
+
+ private final AttrWait wait;
+
+ private final AttrVersion ver;
+
+ private final AttrPolling polling;
+
+ private final AttrInactivity inactivity;
+
+ private final AttrRequests requests;
+
+ private final AttrHold hold;
+
+ private final AttrAccept accept;
+
+ private final AttrMaxPause maxPause;
+
+ private final AttrAck ack;
+
+ private final AttrCharsets charsets;
+
+ private final boolean ackingRequests;
+
+ /**
+ * Prevent direct construction.
+ */
+ private CMSessionParams(
+ final AttrSessionID aSid,
+ final AttrWait aWait,
+ final AttrVersion aVer,
+ final AttrPolling aPolling,
+ final AttrInactivity aInactivity,
+ final AttrRequests aRequests,
+ final AttrHold aHold,
+ final AttrAccept aAccept,
+ final AttrMaxPause aMaxPause,
+ final AttrAck aAck,
+ final AttrCharsets aCharsets,
+ final boolean amAckingRequests) {
+ sid = aSid;
+ wait = aWait;
+ ver = aVer;
+ polling = aPolling;
+ inactivity = aInactivity;
+ requests = aRequests;
+ hold = aHold;
+ accept = aAccept;
+ maxPause = aMaxPause;
+ ack = aAck;
+ charsets = aCharsets;
+ ackingRequests = amAckingRequests;
+ }
+
+ static CMSessionParams fromSessionInit(
+ final AbstractBody req,
+ final AbstractBody resp)
+ throws BOSHException {
+ AttrAck aAck = AttrAck.createFromString(
+ resp.getAttribute(Attributes.ACK));
+ String rid = req.getAttribute(Attributes.RID);
+ boolean acking = (aAck != null && aAck.getValue().equals(rid));
+
+ return new CMSessionParams(
+ AttrSessionID.createFromString(
+ getRequiredAttribute(resp, Attributes.SID)),
+ AttrWait.createFromString(
+ getRequiredAttribute(resp, Attributes.WAIT)),
+ AttrVersion.createFromString(
+ resp.getAttribute(Attributes.VER)),
+ AttrPolling.createFromString(
+ resp.getAttribute(Attributes.POLLING)),
+ AttrInactivity.createFromString(
+ resp.getAttribute(Attributes.INACTIVITY)),
+ AttrRequests.createFromString(
+ resp.getAttribute(Attributes.REQUESTS)),
+ AttrHold.createFromString(
+ resp.getAttribute(Attributes.HOLD)),
+ AttrAccept.createFromString(
+ resp.getAttribute(Attributes.ACCEPT)),
+ AttrMaxPause.createFromString(
+ resp.getAttribute(Attributes.MAXPAUSE)),
+ aAck,
+ AttrCharsets.createFromString(
+ resp.getAttribute(Attributes.CHARSETS)),
+ acking
+ );
+ }
+
+ private static String getRequiredAttribute(
+ final AbstractBody body,
+ final BodyQName name)
+ throws BOSHException {
+ String attrStr = body.getAttribute(name);
+ if (attrStr == null) {
+ throw(new BOSHException(
+ "Connection Manager session creation response did not "
+ + "include required '" + name.getLocalPart()
+ + "' attribute"));
+ }
+ return attrStr;
+ }
+
+ AttrSessionID getSessionID() {
+ return sid;
+ }
+
+ AttrWait getWait() {
+ return wait;
+ }
+
+ AttrVersion getVersion() {
+ return ver;
+ }
+
+ AttrPolling getPollingInterval() {
+ return polling;
+ }
+
+ AttrInactivity getInactivityPeriod() {
+ return inactivity;
+ }
+
+ AttrRequests getRequests() {
+ return requests;
+ }
+
+ AttrHold getHold() {
+ return hold;
+ }
+
+ AttrAccept getAccept() {
+ return accept;
+ }
+
+ AttrMaxPause getMaxPause() {
+ return maxPause;
+ }
+
+ AttrAck getAck() {
+ return ack;
+ }
+
+ AttrCharsets getCharsets() {
+ return charsets;
+ }
+
+ boolean isAckingRequests() {
+ return ackingRequests;
+ }
+
+}
diff --git a/src/com/kenai/jbosh/ComposableBody.java b/src/com/kenai/jbosh/ComposableBody.java
new file mode 100644
index 0000000..d375478
--- /dev/null
+++ b/src/com/kenai/jbosh/ComposableBody.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright 2009 Mike Cumings
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.kenai.jbosh;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.xml.XMLConstants;
+
+/**
+ * Implementation of the {@code AbstractBody} class which allows for the
+ * definition of messages from individual elements of a body.
+ * <p/>
+ * A message is constructed by creating a builder, manipulating the
+ * configuration of the builder, and then building it into a class instance,
+ * as in the following example:
+ * <pre>
+ * ComposableBody body = ComposableBody.builder()
+ * .setNamespaceDefinition("foo", "http://foo.com/bar")
+ * .setPayloadXML("<foo:data>Data to send to remote server</foo:data>")
+ * .build();
+ * </pre>
+ * Class instances can also be "rebuilt", allowing them to be used as templates
+ * when building many similar messages:
+ * <pre>
+ * ComposableBody body2 = body.rebuild()
+ * .setPayloadXML("<foo:data>More data to send</foo:data>")
+ * .build();
+ * </pre>
+ * This class does only minimal syntactic and semantic checking with respect
+ * to what the generated XML will look like. It is up to the developer to
+ * protect against the definition of malformed XML messages when building
+ * instances of this class.
+ * <p/>
+ * Instances of this class are immutable and thread-safe.
+ */
+public final class ComposableBody extends AbstractBody {
+
+ /**
+ * Pattern used to identify the beginning {@code body} element of a
+ * BOSH message.
+ */
+ private static final Pattern BOSH_START =
+ Pattern.compile("<" + "(?:(?:[^:\t\n\r >]+:)|(?:\\{[^\\}>]*?}))?"
+ + "body" + "(?:[\t\n\r ][^>]*?)?" + "(/>|>)");
+
+ /**
+ * Map of all attributes to their values.
+ */
+ private final Map<BodyQName, String> attrs;
+
+ /**
+ * Payload XML.
+ */
+ private final String payload;
+
+ /**
+ * Computed raw XML.
+ */
+ private final AtomicReference<String> computed =
+ new AtomicReference<String>();
+
+ /**
+ * Class instance builder, after the builder pattern. This allows each
+ * message instance to be immutable while providing flexibility when
+ * building new messages.
+ * <p/>
+ * Instances of this class are <b>not</b> thread-safe.
+ */
+ public static final class Builder {
+ private Map<BodyQName, String> map;
+ private boolean doMapCopy;
+ private String payloadXML;
+
+ /**
+ * Prevent direct construction.
+ */
+ private Builder() {
+ // Empty
+ }
+
+ /**
+ * Creates a builder which is initialized to the values of the
+ * provided {@code ComposableBody} instance. This allows an
+ * existing {@code ComposableBody} to be used as a
+ * template/starting point.
+ *
+ * @param source body template
+ * @return builder instance
+ */
+ private static Builder fromBody(final ComposableBody source) {
+ Builder result = new Builder();
+ result.map = source.getAttributes();
+ result.doMapCopy = true;
+ result.payloadXML = source.payload;
+ return result;
+ }
+
+ /**
+ * Set the body message's wrapped payload content. Any previous
+ * content will be replaced.
+ *
+ * @param xml payload XML content
+ * @return builder instance
+ */
+ public Builder setPayloadXML(final String xml) {
+ if (xml == null) {
+ throw(new IllegalArgumentException(
+ "payload XML argument cannot be null"));
+ }
+ payloadXML = xml;
+ return this;
+ }
+
+ /**
+ * Set an attribute on the message body / wrapper element.
+ *
+ * @param name qualified name of the attribute
+ * @param value value of the attribute
+ * @return builder instance
+ */
+ public Builder setAttribute(
+ final BodyQName name, final String value) {
+ if (map == null) {
+ map = new HashMap<BodyQName, String>();
+ } else if (doMapCopy) {
+ map = new HashMap<BodyQName, String>(map);
+ doMapCopy = false;
+ }
+ if (value == null) {
+ map.remove(name);
+ } else {
+ map.put(name, value);
+ }
+ return this;
+ }
+
+ /**
+ * Convenience method to set a namespace definition. This would result
+ * in a namespace prefix definition similar to:
+ * {@code <body xmlns:prefix="uri"/>}
+ *
+ * @param prefix prefix to define
+ * @param uri namespace URI to associate with the prefix
+ * @return builder instance
+ */
+ public Builder setNamespaceDefinition(
+ final String prefix, final String uri) {
+ BodyQName qname = BodyQName.createWithPrefix(
+ XMLConstants.XML_NS_URI, prefix,
+ XMLConstants.XMLNS_ATTRIBUTE);
+ return setAttribute(qname, uri);
+ }
+
+ /**
+ * Build the immutable object instance with the current configuration.
+ *
+ * @return composable body instance
+ */
+ public ComposableBody build() {
+ if (map == null) {
+ map = new HashMap<BodyQName, String>();
+ }
+ if (payloadXML == null) {
+ payloadXML = "";
+ }
+ return new ComposableBody(map, payloadXML);
+ }
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Constructors:
+
+ /**
+ * Prevent direct construction. This constructor is for body messages
+ * which are dynamically assembled.
+ */
+ private ComposableBody(
+ final Map<BodyQName, String> attrMap,
+ final String payloadXML) {
+ super();
+ attrs = attrMap;
+ payload = payloadXML;
+ }
+
+ /**
+ * Parse a static body instance into a composable instance. This is an
+ * expensive operation and should not be used lightly.
+ * <p/>
+ * The current implementation does not obtain the payload XML by means of
+ * a proper XML parser. It uses some string pattern searching to find the
+ * first @{code body} element and the last element's closing tag. It is
+ * assumed that the static body's XML is well formed, etc.. This
+ * implementation may change in the future.
+ *
+ * @param body static body instance to convert
+ * @return composable bosy instance
+ * @throws BOSHException
+ */
+ static ComposableBody fromStaticBody(final StaticBody body)
+ throws BOSHException {
+ String raw = body.toXML();
+ Matcher matcher = BOSH_START.matcher(raw);
+ if (!matcher.find()) {
+ throw(new BOSHException(
+ "Could not locate 'body' element in XML. The raw XML did"
+ + " not match the pattern: " + BOSH_START));
+ }
+ String payload;
+ if (">".equals(matcher.group(1))) {
+ int first = matcher.end();
+ int last = raw.lastIndexOf("</");
+ if (last < first) {
+ last = first;
+ }
+ payload = raw.substring(first, last);
+ } else {
+ payload = "";
+ }
+
+ return new ComposableBody(body.getAttributes(), payload);
+ }
+
+ /**
+ * Create a builder instance to build new instances of this class.
+ *
+ * @return AbstractBody instance
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * If this {@code ComposableBody} instance is a dynamic instance, uses this
+ * {@code ComposableBody} instance as a starting point, create a builder
+ * which can be used to create another {@code ComposableBody} instance
+ * based on this one. This allows a {@code ComposableBody} instance to be
+ * used as a template. Note that the use of the returned builder in no
+ * way modifies or manipulates the current {@code ComposableBody} instance.
+ *
+ * @return builder instance which can be used to build similar
+ * {@code ComposableBody} instances
+ */
+ public Builder rebuild() {
+ return Builder.fromBody(this);
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Accessors:
+
+ /**
+ * {@inheritDoc}
+ */
+ public Map<BodyQName, String> getAttributes() {
+ return Collections.unmodifiableMap(attrs);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public String toXML() {
+ String comp = computed.get();
+ if (comp == null) {
+ comp = computeXML();
+ computed.set(comp);
+ }
+ return comp;
+ }
+
+ /**
+ * Get the paylaod XML in String form.
+ *
+ * @return payload XML
+ */
+ public String getPayloadXML() {
+ return payload;
+ }
+
+ ///////////////////////////////////////////////////////////////////////////
+ // Private methods:
+
+ /**
+ * Escape the value of an attribute to ensure we maintain valid
+ * XML syntax.
+ *
+ * @param value value to escape
+ * @return escaped value
+ */
+ private String escape(final String value) {
+ return value.replace("'", "'");
+ }
+
+ /**
+ * 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
diff --git a/src/com/novell/sasl/client/DigestChallenge.java b/src/com/novell/sasl/client/DigestChallenge.java
new file mode 100644
index 0000000..90e6247
--- /dev/null
+++ b/src/com/novell/sasl/client/DigestChallenge.java
@@ -0,0 +1,393 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/DigestChallenge.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2003 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import java.util.*;
+import org.apache.harmony.javax.security.sasl.*;
+
+/**
+ * Implements the DigestChallenge class which will be used by the
+ * DigestMD5SaslClient class
+ */
+class DigestChallenge extends Object
+{
+ public static final int QOP_AUTH = 0x01;
+ public static final int QOP_AUTH_INT = 0x02;
+ public static final int QOP_AUTH_CONF = 0x04;
+ public static final int QOP_UNRECOGNIZED = 0x08;
+
+ private static final int CIPHER_3DES = 0x01;
+ private static final int CIPHER_DES = 0x02;
+ private static final int CIPHER_RC4_40 = 0x04;
+ private static final int CIPHER_RC4 = 0x08;
+ private static final int CIPHER_RC4_56 = 0x10;
+ private static final int CIPHER_UNRECOGNIZED = 0x20;
+ private static final int CIPHER_RECOGNIZED_MASK =
+ CIPHER_3DES | CIPHER_DES | CIPHER_RC4_40 | CIPHER_RC4 | CIPHER_RC4_56;
+
+ private ArrayList m_realms;
+ private String m_nonce;
+ private int m_qop;
+ private boolean m_staleFlag;
+ private int m_maxBuf;
+ private String m_characterSet;
+ private String m_algorithm;
+ private int m_cipherOptions;
+
+ DigestChallenge(
+ byte[] challenge)
+ throws SaslException
+ {
+ m_realms = new ArrayList(5);
+ m_nonce = null;
+ m_qop = 0;
+ m_staleFlag = false;
+ m_maxBuf = -1;
+ m_characterSet = null;
+ m_algorithm = null;
+ m_cipherOptions = 0;
+
+ DirectiveList dirList = new DirectiveList(challenge);
+ try
+ {
+ dirList.parseDirectives();
+ checkSemantics(dirList);
+ }
+ catch (SaslException e)
+ {
+ }
+ }
+
+ /**
+ * Checks the semantics of the directives in the directive list as parsed
+ * from the digest challenge byte array.
+ *
+ * @param dirList the list of directives parsed from the digest challenge
+ *
+ * @exception SaslException If a semantic error occurs
+ */
+ void checkSemantics(
+ DirectiveList dirList) throws SaslException
+ {
+ Iterator directives = dirList.getIterator();
+ ParsedDirective directive;
+ String name;
+
+ while (directives.hasNext())
+ {
+ directive = (ParsedDirective)directives.next();
+ name = directive.getName();
+ if (name.equals("realm"))
+ handleRealm(directive);
+ else if (name.equals("nonce"))
+ handleNonce(directive);
+ else if (name.equals("qop"))
+ handleQop(directive);
+ else if (name.equals("maxbuf"))
+ handleMaxbuf(directive);
+ else if (name.equals("charset"))
+ handleCharset(directive);
+ else if (name.equals("algorithm"))
+ handleAlgorithm(directive);
+ else if (name.equals("cipher"))
+ handleCipher(directive);
+ else if (name.equals("stale"))
+ handleStale(directive);
+ }
+
+ /* post semantic check */
+ if (-1 == m_maxBuf)
+ m_maxBuf = 65536;
+
+ if (m_qop == 0)
+ m_qop = QOP_AUTH;
+ else if ( (m_qop & QOP_AUTH) != QOP_AUTH )
+ throw new SaslException("Only qop-auth is supported by client");
+ else if ( ((m_qop & QOP_AUTH_CONF) == QOP_AUTH_CONF) &&
+ (0 == (m_cipherOptions & CIPHER_RECOGNIZED_MASK)) )
+ throw new SaslException("Invalid cipher options");
+ else if (null == m_nonce)
+ throw new SaslException("Missing nonce directive");
+ else if (m_staleFlag)
+ throw new SaslException("Unexpected stale flag");
+ else if ( null == m_algorithm )
+ throw new SaslException("Missing algorithm directive");
+ }
+
+ /**
+ * This function implements the semenatics of the nonce directive.
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs due to too many nonce
+ * values
+ */
+ void handleNonce(
+ ParsedDirective pd) throws SaslException
+ {
+ if (null != m_nonce)
+ throw new SaslException("Too many nonce values.");
+
+ m_nonce = pd.getValue();
+ }
+
+ /**
+ * This function implements the semenatics of the realm directive.
+ *
+ * @param pd ParsedDirective
+ */
+ void handleRealm(
+ ParsedDirective pd)
+ {
+ m_realms.add(pd.getValue());
+ }
+
+ /**
+ * This function implements the semenatics of the qop (quality of protection)
+ * directive. The value of the qop directive is as defined below:
+ * qop-options = "qop" "=" <"> qop-list <">
+ * qop-list = 1#qop-value
+ * qop-value = "auth" | "auth-int" | "auth-conf" | token
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs due to too many qop
+ * directives
+ */
+ void handleQop(
+ ParsedDirective pd) throws SaslException
+ {
+ String token;
+ TokenParser parser;
+
+ if (m_qop != 0)
+ throw new SaslException("Too many qop directives.");
+
+ parser = new TokenParser(pd.getValue());
+ for (token = parser.parseToken();
+ token != null;
+ token = parser.parseToken())
+ {
+ if (token.equals("auth"))
+ m_qop |= QOP_AUTH;
+ else if (token.equals("auth-int"))
+ m_qop |= QOP_AUTH_INT;
+ else if (token.equals("auth-conf"))
+ m_qop |= QOP_AUTH_CONF;
+ else
+ m_qop |= QOP_UNRECOGNIZED;
+ }
+ }
+
+ /**
+ * This function implements the semenatics of the Maxbuf directive.
+ * the value is defined as: 1*DIGIT
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occur
+ */
+ void handleMaxbuf(
+ ParsedDirective pd) throws SaslException
+ {
+ if (-1 != m_maxBuf) /*it's initialized to -1 */
+ throw new SaslException("Too many maxBuf directives.");
+
+ m_maxBuf = Integer.parseInt(pd.getValue());
+
+ if (0 == m_maxBuf)
+ throw new SaslException("Max buf value must be greater than zero.");
+ }
+
+ /**
+ * This function implements the semenatics of the charset directive.
+ * the value is defined as: 1*DIGIT
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs dur to too many charset
+ * directives or Invalid character encoding
+ * directive
+ */
+ void handleCharset(
+ ParsedDirective pd) throws SaslException
+ {
+ if (null != m_characterSet)
+ throw new SaslException("Too many charset directives.");
+
+ m_characterSet = pd.getValue();
+
+ if (!m_characterSet.equals("utf-8"))
+ throw new SaslException("Invalid character encoding directive");
+ }
+
+ /**
+ * This function implements the semenatics of the charset directive.
+ * the value is defined as: 1*DIGIT
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs due to too many algorith
+ * directive or Invalid algorithm directive
+ * value
+ */
+ void handleAlgorithm(
+ ParsedDirective pd) throws SaslException
+ {
+ if (null != m_algorithm)
+ throw new SaslException("Too many algorithm directives.");
+
+ m_algorithm = pd.getValue();
+
+ if (!"md5-sess".equals(m_algorithm))
+ throw new SaslException("Invalid algorithm directive value: " +
+ m_algorithm);
+ }
+
+ /**
+ * This function implements the semenatics of the cipher-opts directive
+ * directive. The value of the qop directive is as defined below:
+ * qop-options = "qop" "=" <"> qop-list <">
+ * qop-list = 1#qop-value
+ * qop-value = "auth" | "auth-int" | "auth-conf" | token
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs due to Too many cipher
+ * directives
+ */
+ void handleCipher(
+ ParsedDirective pd) throws SaslException
+ {
+ String token;
+ TokenParser parser;
+
+ if (0 != m_cipherOptions)
+ throw new SaslException("Too many cipher directives.");
+
+ parser = new TokenParser(pd.getValue());
+ token = parser.parseToken();
+ for (token = parser.parseToken();
+ token != null;
+ token = parser.parseToken())
+ {
+ if ("3des".equals(token))
+ m_cipherOptions |= CIPHER_3DES;
+ else if ("des".equals(token))
+ m_cipherOptions |= CIPHER_DES;
+ else if ("rc4-40".equals(token))
+ m_cipherOptions |= CIPHER_RC4_40;
+ else if ("rc4".equals(token))
+ m_cipherOptions |= CIPHER_RC4;
+ else if ("rc4-56".equals(token))
+ m_cipherOptions |= CIPHER_RC4_56;
+ else
+ m_cipherOptions |= CIPHER_UNRECOGNIZED;
+ }
+
+ if (m_cipherOptions == 0)
+ m_cipherOptions = CIPHER_UNRECOGNIZED;
+ }
+
+ /**
+ * This function implements the semenatics of the stale directive.
+ *
+ * @param pd ParsedDirective
+ *
+ * @exception SaslException If an error occurs due to Too many stale
+ * directives or Invalid stale directive value
+ */
+ void handleStale(
+ ParsedDirective pd) throws SaslException
+ {
+ if (false != m_staleFlag)
+ throw new SaslException("Too many stale directives.");
+
+ if ("true".equals(pd.getValue()))
+ m_staleFlag = true;
+ else
+ throw new SaslException("Invalid stale directive value: " +
+ pd.getValue());
+ }
+
+ /**
+ * Return the list of the All the Realms
+ *
+ * @return List of all the realms
+ */
+ public ArrayList getRealms()
+ {
+ return m_realms;
+ }
+
+ /**
+ * @return Returns the Nonce
+ */
+ public String getNonce()
+ {
+ return m_nonce;
+ }
+
+ /**
+ * Return the quality-of-protection
+ *
+ * @return The quality-of-protection
+ */
+ public int getQop()
+ {
+ return m_qop;
+ }
+
+ /**
+ * @return The state of the Staleflag
+ */
+ public boolean getStaleFlag()
+ {
+ return m_staleFlag;
+ }
+
+ /**
+ * @return The Maximum Buffer value
+ */
+ public int getMaxBuf()
+ {
+ return m_maxBuf;
+ }
+
+ /**
+ * @return character set values as string
+ */
+ public String getCharacterSet()
+ {
+ return m_characterSet;
+ }
+
+ /**
+ * @return The String value of the algorithm
+ */
+ public String getAlgorithm()
+ {
+ return m_algorithm;
+ }
+
+ /**
+ * @return The cipher options
+ */
+ public int getCipherOptions()
+ {
+ return m_cipherOptions;
+ }
+}
+
diff --git a/src/com/novell/sasl/client/DigestMD5SaslClient.java b/src/com/novell/sasl/client/DigestMD5SaslClient.java
new file mode 100644
index 0000000..141c96b
--- /dev/null
+++ b/src/com/novell/sasl/client/DigestMD5SaslClient.java
@@ -0,0 +1,820 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/DigestMD5SaslClient.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2003 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import org.apache.harmony.javax.security.sasl.*;
+import org.apache.harmony.javax.security.auth.callback.*;
+import java.security.SecureRandom;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.io.UnsupportedEncodingException;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * Implements the Client portion of DigestMD5 Sasl mechanism.
+ */
+public class DigestMD5SaslClient implements SaslClient
+{
+ private String m_authorizationId = "";
+ private String m_protocol = "";
+ private String m_serverName = "";
+ private Map m_props;
+ private CallbackHandler m_cbh;
+ private int m_state;
+ private String m_qopValue = "";
+ private char[] m_HA1 = null;
+ private String m_digestURI;
+ private DigestChallenge m_dc;
+ private String m_clientNonce = "";
+ private String m_realm = "";
+ private String m_name = "";
+
+ private static final int STATE_INITIAL = 0;
+ private static final int STATE_DIGEST_RESPONSE_SENT = 1;
+ private static final int STATE_VALID_SERVER_RESPONSE = 2;
+ private static final int STATE_INVALID_SERVER_RESPONSE = 3;
+ private static final int STATE_DISPOSED = 4;
+
+ private static final int NONCE_BYTE_COUNT = 32;
+ private static final int NONCE_HEX_COUNT = 2*NONCE_BYTE_COUNT;
+
+ private static final String DIGEST_METHOD = "AUTHENTICATE";
+
+ /**
+ * Creates an DigestMD5SaslClient object using the parameters supplied.
+ * Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are
+ * contained in props
+ *
+ * @param authorizationId The possibly null protocol-dependent
+ * identification to be used for authorization. If
+ * null or empty, the server derives an authorization
+ * ID from the client's authentication credentials.
+ * When the SASL authentication completes
+ * successfully, the specified entity is granted
+ * access.
+ *
+ * @param protocol The non-null string name of the protocol for which
+ * the authentication is being performed (e.g. "ldap")
+ *
+ * @param serverName The non-null fully qualified host name of the server
+ * to authenticate to
+ *
+ * @param props The possibly null set of properties used to select
+ * the SASL mechanism and to configure the
+ * authentication exchange of the selected mechanism.
+ * See the Sasl class for a list of standard properties.
+ * Other, possibly mechanism-specific, properties can
+ * be included. Properties not relevant to the selected
+ * mechanism are ignored.
+ *
+ * @param cbh The possibly null callback handler to used by the
+ * SASL mechanisms to get further information from the
+ * application/library to complete the authentication.
+ * For example, a SASL mechanism might require the
+ * authentication ID, password and realm from the
+ * caller. The authentication ID is requested by using
+ * a NameCallback. The password is requested by using
+ * a PasswordCallback. The realm is requested by using
+ * a RealmChoiceCallback if there is a list of realms
+ * to choose from, and by using a RealmCallback if the
+ * realm must be entered.
+ *
+ * @return A possibly null SaslClient created using the
+ * parameters supplied. If null, this factory cannot
+ * produce a SaslClient using the parameters supplied.
+ *
+ * @exception SaslException If a SaslClient instance cannot be created
+ * because of an error
+ */
+ public static SaslClient getClient(
+ String authorizationId,
+ String protocol,
+ String serverName,
+ Map props,
+ CallbackHandler cbh)
+ {
+ String desiredQOP = (String)props.get(Sasl.QOP);
+ String desiredStrength = (String)props.get(Sasl.STRENGTH);
+ String serverAuth = (String)props.get(Sasl.SERVER_AUTH);
+
+ //only support qop equal to auth
+ if ((desiredQOP != null) && !"auth".equals(desiredQOP))
+ return null;
+
+ //doesn't support server authentication
+ if ((serverAuth != null) && !"false".equals(serverAuth))
+ return null;
+
+ //need a callback handler to get the password
+ if (cbh == null)
+ return null;
+
+ return new DigestMD5SaslClient(authorizationId, protocol,
+ serverName, props, cbh);
+ }
+
+ /**
+ * Creates an DigestMD5SaslClient object using the parameters supplied.
+ * Assumes that the QOP, STRENGTH, and SERVER_AUTH properties are
+ * contained in props
+ *
+ * @param authorizationId The possibly null protocol-dependent
+ * identification to be used for authorization. If
+ * null or empty, the server derives an authorization
+ * ID from the client's authentication credentials.
+ * When the SASL authentication completes
+ * successfully, the specified entity is granted
+ * access.
+ *
+ * @param protocol The non-null string name of the protocol for which
+ * the authentication is being performed (e.g. "ldap")
+ *
+ * @param serverName The non-null fully qualified host name of the server
+ * to authenticate to
+ *
+ * @param props The possibly null set of properties used to select
+ * the SASL mechanism and to configure the
+ * authentication exchange of the selected mechanism.
+ * See the Sasl class for a list of standard properties.
+ * Other, possibly mechanism-specific, properties can
+ * be included. Properties not relevant to the selected
+ * mechanism are ignored.
+ *
+ * @param cbh The possibly null callback handler to used by the
+ * SASL mechanisms to get further information from the
+ * application/library to complete the authentication.
+ * For example, a SASL mechanism might require the
+ * authentication ID, password and realm from the
+ * caller. The authentication ID is requested by using
+ * a NameCallback. The password is requested by using
+ * a PasswordCallback. The realm is requested by using
+ * a RealmChoiceCallback if there is a list of realms
+ * to choose from, and by using a RealmCallback if the
+ * realm must be entered.
+ *
+ */
+ private DigestMD5SaslClient(
+ String authorizationId,
+ String protocol,
+ String serverName,
+ Map props,
+ CallbackHandler cbh)
+ {
+ m_authorizationId = authorizationId;
+ m_protocol = protocol;
+ m_serverName = serverName;
+ m_props = props;
+ m_cbh = cbh;
+
+ m_state = STATE_INITIAL;
+ }
+
+ /**
+ * Determines if this mechanism has an optional initial response. If true,
+ * caller should call evaluateChallenge() with an empty array to get the
+ * initial response.
+ *
+ * @return true if this mechanism has an initial response
+ */
+ public boolean hasInitialResponse()
+ {
+ return false;
+ }
+
+ /**
+ * Determines if the authentication exchange has completed. This method
+ * may be called at any time, but typically, it will not be called until
+ * the caller has received indication from the server (in a protocol-
+ * specific manner) that the exchange has completed.
+ *
+ * @return true if the authentication exchange has completed;
+ * false otherwise.
+ */
+ public boolean isComplete()
+ {
+ if ((m_state == STATE_VALID_SERVER_RESPONSE) ||
+ (m_state == STATE_INVALID_SERVER_RESPONSE) ||
+ (m_state == STATE_DISPOSED))
+ return true;
+ else
+ return false;
+ }
+
+ /**
+ * Unwraps a byte array received from the server. This method can be called
+ * only after the authentication exchange has completed (i.e., when
+ * isComplete() returns true) and only if the authentication exchange has
+ * negotiated integrity and/or privacy as the quality of protection;
+ * otherwise, an IllegalStateException is thrown.
+ *
+ * incoming is the contents of the SASL buffer as defined in RFC 2222
+ * without the leading four octet field that represents the length.
+ * offset and len specify the portion of incoming to use.
+ *
+ * @param incoming A non-null byte array containing the encoded bytes
+ * from the server
+ * @param offset The starting position at incoming of the bytes to use
+ *
+ * @param len The number of bytes from incoming to use
+ *
+ * @return A non-null byte array containing the decoded bytes
+ *
+ */
+ public byte[] unwrap(
+ byte[] incoming,
+ int offset,
+ int len)
+ throws SaslException
+ {
+ throw new IllegalStateException(
+ "unwrap: QOP has neither integrity nor privacy>");
+ }
+
+ /**
+ * Wraps a byte array to be sent to the server. This method can be called
+ * only after the authentication exchange has completed (i.e., when
+ * isComplete() returns true) and only if the authentication exchange has
+ * negotiated integrity and/or privacy as the quality of protection;
+ * otherwise, an IllegalStateException is thrown.
+ *
+ * The result of this method will make up the contents of the SASL buffer as
+ * defined in RFC 2222 without the leading four octet field that represents
+ * the length. offset and len specify the portion of outgoing to use.
+ *
+ * @param outgoing A non-null byte array containing the bytes to encode
+ * @param offset The starting position at outgoing of the bytes to use
+ * @param len The number of bytes from outgoing to use
+ *
+ * @return A non-null byte array containing the encoded bytes
+ *
+ * @exception SaslException if incoming cannot be successfully unwrapped.
+ *
+ * @exception IllegalStateException if the authentication exchange has
+ * not completed, or if the negotiated quality of
+ * protection has neither integrity nor privacy.
+ */
+ public byte[] wrap(
+ byte[] outgoing,
+ int offset,
+ int len)
+ throws SaslException
+ {
+ throw new IllegalStateException(
+ "wrap: QOP has neither integrity nor privacy>");
+ }
+
+ /**
+ * Retrieves the negotiated property. This method can be called only after
+ * the authentication exchange has completed (i.e., when isComplete()
+ * returns true); otherwise, an IllegalStateException is thrown.
+ *
+ * @param propName The non-null property name
+ *
+ * @return The value of the negotiated property. If null, the property was
+ * not negotiated or is not applicable to this mechanism.
+ *
+ * @exception IllegalStateException if this authentication exchange has
+ * not completed
+ */
+ public Object getNegotiatedProperty(
+ String propName)
+ {
+ if (m_state != STATE_VALID_SERVER_RESPONSE)
+ throw new IllegalStateException(
+ "getNegotiatedProperty: authentication exchange not complete.");
+
+ if (Sasl.QOP.equals(propName))
+ return "auth";
+ else
+ return null;
+ }
+
+ /**
+ * Disposes of any system resources or security-sensitive information the
+ * SaslClient might be using. Invoking this method invalidates the
+ * SaslClient instance. This method is idempotent.
+ *
+ * @exception SaslException if a problem was encountered while disposing
+ * of the resources
+ */
+ public void dispose()
+ throws SaslException
+ {
+ if (m_state != STATE_DISPOSED)
+ {
+ m_state = STATE_DISPOSED;
+ }
+ }
+
+ /**
+ * Evaluates the challenge data and generates a response. If a challenge
+ * is received from the server during the authentication process, this
+ * method is called to prepare an appropriate next response to submit to
+ * the server.
+ *
+ * @param challenge The non-null challenge sent from the server. The
+ * challenge array may have zero length.
+ *
+ * @return The possibly null reponse to send to the server. It is null
+ * if the challenge accompanied a "SUCCESS" status and the
+ * challenge only contains data for the client to update its
+ * state and no response needs to be sent to the server.
+ * The response is a zero-length byte array if the client is to
+ * send a response with no data.
+ *
+ * @exception SaslException If an error occurred while processing the
+ * challenge or generating a response.
+ */
+ public byte[] evaluateChallenge(
+ byte[] challenge)
+ throws SaslException
+ {
+ byte[] response = null;
+
+ //printState();
+ switch (m_state)
+ {
+ case STATE_INITIAL:
+ if (challenge.length == 0)
+ throw new SaslException("response = byte[0]");
+ else
+ try
+ {
+ response = createDigestResponse(challenge).
+ getBytes("UTF-8");
+ m_state = STATE_DIGEST_RESPONSE_SENT;
+ }
+ catch (java.io.UnsupportedEncodingException e)
+ {
+ throw new SaslException(
+ "UTF-8 encoding not suppported by platform", e);
+ }
+ break;
+ case STATE_DIGEST_RESPONSE_SENT:
+ if (checkServerResponseAuth(challenge))
+ m_state = STATE_VALID_SERVER_RESPONSE;
+ else
+ {
+ m_state = STATE_INVALID_SERVER_RESPONSE;
+ throw new SaslException("Could not validate response-auth " +
+ "value from server");
+ }
+ break;
+ case STATE_VALID_SERVER_RESPONSE:
+ case STATE_INVALID_SERVER_RESPONSE:
+ throw new SaslException("Authentication sequence is complete");
+ case STATE_DISPOSED:
+ throw new SaslException("Client has been disposed");
+ default:
+ throw new SaslException("Unknown client state.");
+ }
+
+ return response;
+ }
+
+ /**
+ * This function takes a 16 byte binary md5-hash value and creates a 32
+ * character (plus a terminating null character) hex-digit
+ * representation of binary data.
+ *
+ * @param hash 16 byte binary md5-hash value in bytes
+ *
+ * @return 32 character (plus a terminating null character) hex-digit
+ * representation of binary data.
+ */
+ char[] convertToHex(
+ byte[] hash)
+ {
+ int i;
+ byte j;
+ byte fifteen = 15;
+ char[] hex = new char[32];
+
+ for (i = 0; i < 16; i++)
+ {
+ //convert value of top 4 bits to hex char
+ hex[i*2] = getHexChar((byte)((hash[i] & 0xf0) >> 4));
+ //convert value of bottom 4 bits to hex char
+ hex[(i*2)+1] = getHexChar((byte)(hash[i] & 0x0f));
+ }
+
+ return hex;
+ }
+
+ /**
+ * Calculates the HA1 portion of the response
+ *
+ * @param algorithm Algorith to use.
+ * @param userName User being authenticated
+ * @param realm realm information
+ * @param password password of teh user
+ * @param nonce nonce value
+ * @param clientNonce Clients Nonce value
+ *
+ * @return HA1 portion of the response in a character array
+ *
+ * @exception SaslException If an error occurs
+ */
+ char[] DigestCalcHA1(
+ String algorithm,
+ String userName,
+ String realm,
+ String password,
+ String nonce,
+ String clientNonce) throws SaslException
+ {
+ byte[] hash;
+
+ try
+ {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+
+ md.update(userName.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(realm.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(password.getBytes("UTF-8"));
+ hash = md.digest();
+
+ if ("md5-sess".equals(algorithm))
+ {
+ md.update(hash);
+ md.update(":".getBytes("UTF-8"));
+ md.update(nonce.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(clientNonce.getBytes("UTF-8"));
+ hash = md.digest();
+ }
+ }
+ catch(NoSuchAlgorithmException e)
+ {
+ throw new SaslException("No provider found for MD5 hash", e);
+ }
+ catch(UnsupportedEncodingException e)
+ {
+ throw new SaslException(
+ "UTF-8 encoding not supported by platform.", e);
+ }
+
+ return convertToHex(hash);
+ }
+
+
+ /**
+ * This function calculates the response-value of the response directive of
+ * the digest-response as documented in RFC 2831
+ *
+ * @param HA1 H(A1)
+ * @param serverNonce nonce from server
+ * @param nonceCount 8 hex digits
+ * @param clientNonce client nonce
+ * @param qop qop-value: "", "auth", "auth-int"
+ * @param method method from the request
+ * @param digestUri requested URL
+ * @param clientResponseFlag request-digest or response-digest
+ *
+ * @return Response-value of the response directive of the digest-response
+ *
+ * @exception SaslException If an error occurs
+ */
+ char[] DigestCalcResponse(
+ char[] HA1, /* H(A1) */
+ String serverNonce, /* nonce from server */
+ String nonceCount, /* 8 hex digits */
+ String clientNonce, /* client nonce */
+ String qop, /* qop-value: "", "auth", "auth-int" */
+ String method, /* method from the request */
+ String digestUri, /* requested URL */
+ boolean clientResponseFlag) /* request-digest or response-digest */
+ throws SaslException
+ {
+ byte[] HA2;
+ byte[] respHash;
+ char[] HA2Hex;
+
+ // calculate H(A2)
+ try
+ {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ if (clientResponseFlag)
+ md.update(method.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(digestUri.getBytes("UTF-8"));
+ if ("auth-int".equals(qop))
+ {
+ md.update(":".getBytes("UTF-8"));
+ md.update("00000000000000000000000000000000".getBytes("UTF-8"));
+ }
+ HA2 = md.digest();
+ HA2Hex = convertToHex(HA2);
+
+ // calculate response
+ md.update(new String(HA1).getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(serverNonce.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ if (qop.length() > 0)
+ {
+ md.update(nonceCount.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(clientNonce.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ md.update(qop.getBytes("UTF-8"));
+ md.update(":".getBytes("UTF-8"));
+ }
+ md.update(new String(HA2Hex).getBytes("UTF-8"));
+ respHash = md.digest();
+ }
+ catch(NoSuchAlgorithmException e)
+ {
+ throw new SaslException("No provider found for MD5 hash", e);
+ }
+ catch(UnsupportedEncodingException e)
+ {
+ throw new SaslException(
+ "UTF-8 encoding not supported by platform.", e);
+ }
+
+ return convertToHex(respHash);
+ }
+
+
+ /**
+ * Creates the intial response to be sent to the server.
+ *
+ * @param challenge Challenge in bytes recived form the Server
+ *
+ * @return Initial response to be sent to the server
+ */
+ private String createDigestResponse(
+ byte[] challenge)
+ throws SaslException
+ {
+ char[] response;
+ StringBuffer digestResponse = new StringBuffer(512);
+ int realmSize;
+
+ m_dc = new DigestChallenge(challenge);
+
+ m_digestURI = m_protocol + "/" + m_serverName;
+
+ if ((m_dc.getQop() & DigestChallenge.QOP_AUTH)
+ == DigestChallenge.QOP_AUTH )
+ m_qopValue = "auth";
+ else
+ throw new SaslException("Client only supports qop of 'auth'");
+
+ //get call back information
+ Callback[] callbacks = new Callback[3];
+ ArrayList realms = m_dc.getRealms();
+ realmSize = realms.size();
+ if (realmSize == 0)
+ {
+ callbacks[0] = new RealmCallback("Realm");
+ }
+ else if (realmSize == 1)
+ {
+ callbacks[0] = new RealmCallback("Realm", (String)realms.get(0));
+ }
+ else
+ {
+ callbacks[0] =
+ new RealmChoiceCallback(
+ "Realm",
+ (String[])realms.toArray(new String[realmSize]),
+ 0, //the default choice index
+ false); //no multiple selections
+ }
+
+ callbacks[1] = new PasswordCallback("Password", false);
+ //false = no echo
+
+ if (m_authorizationId == null || m_authorizationId.length() == 0)
+ callbacks[2] = new NameCallback("Name");
+ else
+ callbacks[2] = new NameCallback("Name", m_authorizationId);
+
+ try
+ {
+ m_cbh.handle(callbacks);
+ }
+ catch(UnsupportedCallbackException e)
+ {
+ throw new SaslException("Handler does not support" +
+ " necessary callbacks",e);
+ }
+ catch(IOException e)
+ {
+ throw new SaslException("IO exception in CallbackHandler.", e);
+ }
+
+ if (realmSize > 1)
+ {
+ int[] selections =
+ ((RealmChoiceCallback)callbacks[0]).getSelectedIndexes();
+
+ if (selections.length > 0)
+ m_realm =
+ ((RealmChoiceCallback)callbacks[0]).getChoices()[selections[0]];
+ else
+ m_realm = ((RealmChoiceCallback)callbacks[0]).getChoices()[0];
+ }
+ else
+ m_realm = ((RealmCallback)callbacks[0]).getText();
+
+ m_clientNonce = getClientNonce();
+
+ m_name = ((NameCallback)callbacks[2]).getName();
+ if (m_name == null)
+ m_name = ((NameCallback)callbacks[2]).getDefaultName();
+ if (m_name == null)
+ throw new SaslException("No user name was specified.");
+
+ m_HA1 = DigestCalcHA1(
+ m_dc.getAlgorithm(),
+ m_name,
+ m_realm,
+ new String(((PasswordCallback)callbacks[1]).getPassword()),
+ m_dc.getNonce(),
+ m_clientNonce);
+
+ response = DigestCalcResponse(m_HA1,
+ m_dc.getNonce(),
+ "00000001",
+ m_clientNonce,
+ m_qopValue,
+ "AUTHENTICATE",
+ m_digestURI,
+ true);
+
+ digestResponse.append("username=\"");
+ digestResponse.append(m_authorizationId);
+ if (0 != m_realm.length())
+ {
+ digestResponse.append("\",realm=\"");
+ digestResponse.append(m_realm);
+ }
+ digestResponse.append("\",cnonce=\"");
+ digestResponse.append(m_clientNonce);
+ digestResponse.append("\",nc=");
+ digestResponse.append("00000001"); //nounce count
+ digestResponse.append(",qop=");
+ digestResponse.append(m_qopValue);
+ digestResponse.append(",digest-uri=\"");
+ digestResponse.append(m_digestURI);
+ digestResponse.append("\",response=");
+ digestResponse.append(response);
+ digestResponse.append(",charset=utf-8,nonce=\"");
+ digestResponse.append(m_dc.getNonce());
+ digestResponse.append("\"");
+
+ return digestResponse.toString();
+ }
+
+
+ /**
+ * This function validates the server response. This step performs a
+ * modicum of mutual authentication by verifying that the server knows
+ * the user's password
+ *
+ * @param serverResponse Response recived form Server
+ *
+ * @return true if the mutual authentication succeeds;
+ * else return false
+ *
+ * @exception SaslException If an error occurs
+ */
+ boolean checkServerResponseAuth(
+ byte[] serverResponse) throws SaslException
+ {
+ char[] response;
+ ResponseAuth responseAuth = null;
+ String responseStr;
+
+ responseAuth = new ResponseAuth(serverResponse);
+
+ response = DigestCalcResponse(m_HA1,
+ m_dc.getNonce(),
+ "00000001",
+ m_clientNonce,
+ m_qopValue,
+ DIGEST_METHOD,
+ m_digestURI,
+ false);
+
+ responseStr = new String(response);
+
+ return responseStr.equals(responseAuth.getResponseValue());
+ }
+
+
+ /**
+ * This function returns hex character representing the value of the input
+ *
+ * @param value Input value in byte
+ *
+ * @return Hex value of the Input byte value
+ */
+ private static char getHexChar(
+ byte value)
+ {
+ switch (value)
+ {
+ case 0:
+ return '0';
+ case 1:
+ return '1';
+ case 2:
+ return '2';
+ case 3:
+ return '3';
+ case 4:
+ return '4';
+ case 5:
+ return '5';
+ case 6:
+ return '6';
+ case 7:
+ return '7';
+ case 8:
+ return '8';
+ case 9:
+ return '9';
+ case 10:
+ return 'a';
+ case 11:
+ return 'b';
+ case 12:
+ return 'c';
+ case 13:
+ return 'd';
+ case 14:
+ return 'e';
+ case 15:
+ return 'f';
+ default:
+ return 'Z';
+ }
+ }
+
+ /**
+ * Calculates the Nonce value of the Client
+ *
+ * @return Nonce value of the client
+ *
+ * @exception SaslException If an error Occurs
+ */
+ String getClientNonce() throws SaslException
+ {
+ byte[] nonceBytes = new byte[NONCE_BYTE_COUNT];
+ SecureRandom prng;
+ byte nonceByte;
+ char[] hexNonce = new char[NONCE_HEX_COUNT];
+
+ try
+ {
+ prng = SecureRandom.getInstance("SHA1PRNG");
+ prng.nextBytes(nonceBytes);
+ for(int i=0; i<NONCE_BYTE_COUNT; i++)
+ {
+ //low nibble
+ hexNonce[i*2] = getHexChar((byte)(nonceBytes[i] & 0x0f));
+ //high nibble
+ hexNonce[(i*2)+1] = getHexChar((byte)((nonceBytes[i] & 0xf0)
+ >> 4));
+ }
+ return new String(hexNonce);
+ }
+ catch(NoSuchAlgorithmException e)
+ {
+ throw new SaslException("No random number generator available", e);
+ }
+ }
+
+ /**
+ * Returns the IANA-registered mechanism name of this SASL client.
+ * (e.g. "CRAM-MD5", "GSSAPI")
+ *
+ * @return "DIGEST-MD5"the IANA-registered mechanism name of this SASL
+ * client.
+ */
+ public String getMechanismName()
+ {
+ return "DIGEST-MD5";
+ }
+
+} //end class DigestMD5SaslClient
+
diff --git a/src/com/novell/sasl/client/DirectiveList.java b/src/com/novell/sasl/client/DirectiveList.java
new file mode 100644
index 0000000..fc26a6b
--- /dev/null
+++ b/src/com/novell/sasl/client/DirectiveList.java
@@ -0,0 +1,363 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/DirectiveList.java,v 1.4 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import java.util.*;
+import org.apache.harmony.javax.security.sasl.*;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Implements the DirectiveList class whihc will be used by the
+ * DigestMD5SaslClient class
+ */
+class DirectiveList extends Object
+{
+ private static final int STATE_LOOKING_FOR_FIRST_DIRECTIVE = 1;
+ private static final int STATE_LOOKING_FOR_DIRECTIVE = 2;
+ private static final int STATE_SCANNING_NAME = 3;
+ private static final int STATE_LOOKING_FOR_EQUALS = 4;
+ private static final int STATE_LOOKING_FOR_VALUE = 5;
+ private static final int STATE_LOOKING_FOR_COMMA = 6;
+ private static final int STATE_SCANNING_QUOTED_STRING_VALUE = 7;
+ private static final int STATE_SCANNING_TOKEN_VALUE = 8;
+ private static final int STATE_NO_UTF8_SUPPORT = 9;
+
+ private int m_curPos;
+ private int m_errorPos;
+ private String m_directives;
+ private int m_state;
+ private ArrayList m_directiveList;
+ private String m_curName;
+ private int m_scanStart;
+
+ /**
+ * Constructs a new DirectiveList.
+ */
+ DirectiveList(
+ byte[] directives)
+ {
+ m_curPos = 0;
+ m_state = STATE_LOOKING_FOR_FIRST_DIRECTIVE;
+ m_directiveList = new ArrayList(10);
+ m_scanStart = 0;
+ m_errorPos = -1;
+ try
+ {
+ m_directives = new String(directives, "UTF-8");
+ }
+ catch(UnsupportedEncodingException e)
+ {
+ m_state = STATE_NO_UTF8_SUPPORT;
+ }
+ }
+
+ /**
+ * This function takes a US-ASCII character string containing a list of comma
+ * separated directives, and parses the string into the individual directives
+ * and their values. A directive consists of a token specifying the directive
+ * name followed by an equal sign (=) and the directive value. The value is
+ * either a token or a quoted string
+ *
+ * @exception SaslException If an error Occurs
+ */
+ void parseDirectives() throws SaslException
+ {
+ char prevChar;
+ char currChar;
+ int rc = 0;
+ boolean haveQuotedPair = false;
+ String currentName = "<no name>";
+
+ if (m_state == STATE_NO_UTF8_SUPPORT)
+ throw new SaslException("No UTF-8 support on platform");
+
+ prevChar = 0;
+
+ while (m_curPos < m_directives.length())
+ {
+ currChar = m_directives.charAt(m_curPos);
+ switch (m_state)
+ {
+ case STATE_LOOKING_FOR_FIRST_DIRECTIVE:
+ case STATE_LOOKING_FOR_DIRECTIVE:
+ if (isWhiteSpace(currChar))
+ {
+ break;
+ }
+ else if (isValidTokenChar(currChar))
+ {
+ m_scanStart = m_curPos;
+ m_state = STATE_SCANNING_NAME;
+ }
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Invalid name character");
+ }
+ break;
+
+ case STATE_SCANNING_NAME:
+ if (isValidTokenChar(currChar))
+ {
+ break;
+ }
+ else if (isWhiteSpace(currChar))
+ {
+ currentName = m_directives.substring(m_scanStart, m_curPos);
+ m_state = STATE_LOOKING_FOR_EQUALS;
+ }
+ else if ('=' == currChar)
+ {
+ currentName = m_directives.substring(m_scanStart, m_curPos);
+ m_state = STATE_LOOKING_FOR_VALUE;
+ }
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Invalid name character");
+ }
+ break;
+
+ case STATE_LOOKING_FOR_EQUALS:
+ if (isWhiteSpace(currChar))
+ {
+ break;
+ }
+ else if ('=' == currChar)
+ {
+ m_state = STATE_LOOKING_FOR_VALUE;
+ }
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Expected equals sign '='.");
+ }
+ break;
+
+ case STATE_LOOKING_FOR_VALUE:
+ if (isWhiteSpace(currChar))
+ {
+ break;
+ }
+ else if ('"' == currChar)
+ {
+ m_scanStart = m_curPos+1; /* don't include the quote */
+ m_state = STATE_SCANNING_QUOTED_STRING_VALUE;
+ }
+ else if (isValidTokenChar(currChar))
+ {
+ m_scanStart = m_curPos;
+ m_state = STATE_SCANNING_TOKEN_VALUE;
+ }
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Unexpected character");
+ }
+ break;
+
+ case STATE_SCANNING_TOKEN_VALUE:
+ if (isValidTokenChar(currChar))
+ {
+ break;
+ }
+ else if (isWhiteSpace(currChar))
+ {
+ addDirective(currentName, false);
+ m_state = STATE_LOOKING_FOR_COMMA;
+ }
+ else if (',' == currChar)
+ {
+ addDirective(currentName, false);
+ m_state = STATE_LOOKING_FOR_DIRECTIVE;
+ }
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Invalid value character");
+ }
+ break;
+
+ case STATE_SCANNING_QUOTED_STRING_VALUE:
+ if ('\\' == currChar)
+ haveQuotedPair = true;
+ if ( ('"' == currChar) &&
+ ('\\' != prevChar) )
+ {
+ addDirective(currentName, haveQuotedPair);
+ haveQuotedPair = false;
+ m_state = STATE_LOOKING_FOR_COMMA;
+ }
+ break;
+
+ case STATE_LOOKING_FOR_COMMA:
+ if (isWhiteSpace(currChar))
+ break;
+ else if (currChar == ',')
+ m_state = STATE_LOOKING_FOR_DIRECTIVE;
+ else
+ {
+ m_errorPos = m_curPos;
+ throw new SaslException("Parse error: Expected a comma.");
+ }
+ break;
+ }
+ if (0 != rc)
+ break;
+ prevChar = currChar;
+ m_curPos++;
+ } /* end while loop */
+
+
+ if (rc == 0)
+ {
+ /* check the ending state */
+ switch (m_state)
+ {
+ case STATE_SCANNING_TOKEN_VALUE:
+ addDirective(currentName, false);
+ break;
+
+ case STATE_LOOKING_FOR_FIRST_DIRECTIVE:
+ case STATE_LOOKING_FOR_COMMA:
+ break;
+
+ case STATE_LOOKING_FOR_DIRECTIVE:
+ throw new SaslException("Parse error: Trailing comma.");
+
+ case STATE_SCANNING_NAME:
+ case STATE_LOOKING_FOR_EQUALS:
+ case STATE_LOOKING_FOR_VALUE:
+ throw new SaslException("Parse error: Missing value.");
+
+ case STATE_SCANNING_QUOTED_STRING_VALUE:
+ throw new SaslException("Parse error: Missing closing quote.");
+ }
+ }
+
+ }
+
+ /**
+ * This function returns TRUE if the character is a valid token character.
+ *
+ * token = 1*<any CHAR except CTLs or separators>
+ *
+ * separators = "(" | ")" | "<" | ">" | "@"
+ * | "," | ";" | ":" | "\" | <">
+ * | "/" | "[" | "]" | "?" | "="
+ * | "{" | "}" | SP | HT
+ *
+ * CTL = <any US-ASCII control character
+ * (octets 0 - 31) and DEL (127)>
+ *
+ * CHAR = <any US-ASCII character (octets 0 - 127)>
+ *
+ * @param c character to be tested
+ *
+ * @return Returns TRUE if the character is a valid token character.
+ */
+ boolean isValidTokenChar(
+ char c)
+ {
+ if ( ( (c >= '\u0000') && (c <='\u0020') ) ||
+ ( (c >= '\u003a') && (c <= '\u0040') ) ||
+ ( (c >= '\u005b') && (c <= '\u005d') ) ||
+ ('\u002c' == c) ||
+ ('\u0025' == c) ||
+ ('\u0028' == c) ||
+ ('\u0029' == c) ||
+ ('\u007b' == c) ||
+ ('\u007d' == c) ||
+ ('\u007f' == c) )
+ return false;
+
+ return true;
+ }
+
+ /**
+ * This function returns TRUE if the character is linear white space (LWS).
+ * LWS = [CRLF] 1*( SP | HT )
+ * @param c Input charcter to be tested
+ *
+ * @return Returns TRUE if the character is linear white space (LWS)
+ */
+ boolean isWhiteSpace(
+ char c)
+ {
+ if ( ('\t' == c) || // HORIZONTAL TABULATION.
+ ('\n' == c) || // LINE FEED.
+ ('\r' == c) || // CARRIAGE RETURN.
+ ('\u0020' == c) )
+ return true;
+
+ return false;
+ }
+
+ /**
+ * This function creates a directive record and adds it to the list, the
+ * value will be added later after it is parsed.
+ *
+ * @param name Name
+ * @param haveQuotedPair true if quoted pair is there else false
+ */
+ void addDirective(
+ String name,
+ boolean haveQuotedPair)
+ {
+ String value;
+ int inputIndex;
+ int valueIndex;
+ char valueChar;
+ int type;
+
+ if (!haveQuotedPair)
+ {
+ value = m_directives.substring(m_scanStart, m_curPos);
+ }
+ else
+ { //copy one character at a time skipping backslash excapes.
+ StringBuffer valueBuf = new StringBuffer(m_curPos - m_scanStart);
+ valueIndex = 0;
+ inputIndex = m_scanStart;
+ while (inputIndex < m_curPos)
+ {
+ if ('\\' == (valueChar = m_directives.charAt(inputIndex)))
+ inputIndex++;
+ valueBuf.setCharAt(valueIndex, m_directives.charAt(inputIndex));
+ valueIndex++;
+ inputIndex++;
+ }
+ value = new String(valueBuf);
+ }
+
+ if (m_state == STATE_SCANNING_QUOTED_STRING_VALUE)
+ type = ParsedDirective.QUOTED_STRING_VALUE;
+ else
+ type = ParsedDirective.TOKEN_VALUE;
+ m_directiveList.add(new ParsedDirective(name, value, type));
+ }
+
+
+ /**
+ * Returns the List iterator.
+ *
+ * @return Returns the Iterator Object for the List.
+ */
+ Iterator getIterator()
+ {
+ return m_directiveList.iterator();
+ }
+}
+
diff --git a/src/com/novell/sasl/client/ParsedDirective.java b/src/com/novell/sasl/client/ParsedDirective.java
new file mode 100644
index 0000000..17bf70e
--- /dev/null
+++ b/src/com/novell/sasl/client/ParsedDirective.java
@@ -0,0 +1,56 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/ParsedDirective.java,v 1.1 2003/08/21 10:06:26 kkanil Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+/**
+ * Implements the ParsedDirective class which will be used in the
+ * DigestMD5SaslClient mechanism.
+ */
+class ParsedDirective
+{
+ public static final int QUOTED_STRING_VALUE = 1;
+ public static final int TOKEN_VALUE = 2;
+
+ private int m_valueType;
+ private String m_name;
+ private String m_value;
+
+ ParsedDirective(
+ String name,
+ String value,
+ int type)
+ {
+ m_name = name;
+ m_value = value;
+ m_valueType = type;
+ }
+
+ String getValue()
+ {
+ return m_value;
+ }
+
+ String getName()
+ {
+ return m_name;
+ }
+
+ int getValueType()
+ {
+ return m_valueType;
+ }
+
+}
+
diff --git a/src/com/novell/sasl/client/ResponseAuth.java b/src/com/novell/sasl/client/ResponseAuth.java
new file mode 100644
index 0000000..0aef955
--- /dev/null
+++ b/src/com/novell/sasl/client/ResponseAuth.java
@@ -0,0 +1,83 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/ResponseAuth.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import java.util.*;
+import org.apache.harmony.javax.security.sasl.*;
+
+/**
+ * Implements the ResponseAuth class used by the DigestMD5SaslClient mechanism
+ */
+class ResponseAuth
+{
+
+ private String m_responseValue;
+
+ ResponseAuth(
+ byte[] responseAuth)
+ throws SaslException
+ {
+ m_responseValue = null;
+
+ DirectiveList dirList = new DirectiveList(responseAuth);
+ try
+ {
+ dirList.parseDirectives();
+ checkSemantics(dirList);
+ }
+ catch (SaslException e)
+ {
+ }
+ }
+
+ /**
+ * Checks the semantics of the directives in the directive list as parsed
+ * from the digest challenge byte array.
+ *
+ * @param dirList the list of directives parsed from the digest challenge
+ *
+ * @exception SaslException If a semantic error occurs
+ */
+ void checkSemantics(
+ DirectiveList dirList) throws SaslException
+ {
+ Iterator directives = dirList.getIterator();
+ ParsedDirective directive;
+ String name;
+
+ while (directives.hasNext())
+ {
+ directive = (ParsedDirective)directives.next();
+ name = directive.getName();
+ if (name.equals("rspauth"))
+ m_responseValue = directive.getValue();
+ }
+
+ /* post semantic check */
+ if (m_responseValue == null)
+ throw new SaslException("Missing response-auth directive.");
+ }
+
+ /**
+ * returns the ResponseValue
+ *
+ * @return the ResponseValue as a String.
+ */
+ public String getResponseValue()
+ {
+ return m_responseValue;
+ }
+}
+
diff --git a/src/com/novell/sasl/client/TokenParser.java b/src/com/novell/sasl/client/TokenParser.java
new file mode 100644
index 0000000..3d3491d
--- /dev/null
+++ b/src/com/novell/sasl/client/TokenParser.java
@@ -0,0 +1,208 @@
+/* **************************************************************************
+ * $OpenLDAP: /com/novell/sasl/client/TokenParser.java,v 1.3 2005/01/17 15:00:54 sunilk Exp $
+ *
+ * Copyright (C) 2002 Novell, Inc. All Rights Reserved.
+ *
+ * THIS WORK IS SUBJECT TO U.S. AND INTERNATIONAL COPYRIGHT LAWS AND
+ * TREATIES. USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
+ * TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF WHICH IS
+ * AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR IN THE FILE "LICENSE"
+ * IN THE TOP-LEVEL DIRECTORY OF THE DISTRIBUTION. ANY USE OR EXPLOITATION
+ * OF THIS WORK OTHER THAN AS AUTHORIZED IN VERSION 2.0.1 OF THE OPENLDAP
+ * PUBLIC LICENSE, OR OTHER PRIOR WRITTEN CONSENT FROM NOVELL, COULD SUBJECT
+ * THE PERPETRATOR TO CRIMINAL AND CIVIL LIABILITY.
+ ******************************************************************************/
+package com.novell.sasl.client;
+
+import org.apache.harmony.javax.security.sasl.*;
+/**
+ * The TokenParser class will parse individual tokens from a list of tokens that
+ * are a directive value for a DigestMD5 authentication.The tokens are separated
+ * commas.
+ */
+class TokenParser extends Object
+{
+ private static final int STATE_LOOKING_FOR_FIRST_TOKEN = 1;
+ private static final int STATE_LOOKING_FOR_TOKEN = 2;
+ private static final int STATE_SCANNING_TOKEN = 3;
+ private static final int STATE_LOOKING_FOR_COMMA = 4;
+ private static final int STATE_PARSING_ERROR = 5;
+ private static final int STATE_DONE = 6;
+
+ private int m_curPos;
+ private int m_scanStart;
+ private int m_state;
+ private String m_tokens;
+
+
+ TokenParser(
+ String tokens)
+ {
+ m_tokens = tokens;
+ m_curPos = 0;
+ m_scanStart = 0;
+ m_state = STATE_LOOKING_FOR_FIRST_TOKEN;
+ }
+
+ /**
+ * This function parses the next token from the tokens string and returns
+ * it as a string. If there are no more tokens a null reference is returned.
+ *
+ * @return the parsed token or a null reference if there are no more
+ * tokens
+ *
+ * @exception SASLException if an error occurs while parsing
+ */
+ String parseToken() throws SaslException
+ {
+ char currChar;
+ String token = null;
+
+
+ if (m_state == STATE_DONE)
+ return null;
+
+ while (m_curPos < m_tokens.length() && (token == null))
+ {
+ currChar = m_tokens.charAt(m_curPos);
+ switch (m_state)
+ {
+ case STATE_LOOKING_FOR_FIRST_TOKEN:
+ case STATE_LOOKING_FOR_TOKEN:
+ if (isWhiteSpace(currChar))
+ {
+ break;
+ }
+ else if (isValidTokenChar(currChar))
+ {
+ m_scanStart = m_curPos;
+ m_state = STATE_SCANNING_TOKEN;
+ }
+ else
+ {
+ m_state = STATE_PARSING_ERROR;
+ throw new SaslException("Invalid token character at position " + m_curPos);
+ }
+ break;
+
+ case STATE_SCANNING_TOKEN:
+ if (isValidTokenChar(currChar))
+ {
+ break;
+ }
+ else if (isWhiteSpace(currChar))
+ {
+ token = m_tokens.substring(m_scanStart, m_curPos);
+ m_state = STATE_LOOKING_FOR_COMMA;
+ }
+ else if (',' == currChar)
+ {
+ token = m_tokens.substring(m_scanStart, m_curPos);
+ m_state = STATE_LOOKING_FOR_TOKEN;
+ }
+ else
+ {
+ m_state = STATE_PARSING_ERROR;
+ throw new SaslException("Invalid token character at position " + m_curPos);
+ }
+ break;
+
+
+ case STATE_LOOKING_FOR_COMMA:
+ if (isWhiteSpace(currChar))
+ break;
+ else if (currChar == ',')
+ m_state = STATE_LOOKING_FOR_TOKEN;
+ else
+ {
+ m_state = STATE_PARSING_ERROR;
+ throw new SaslException("Expected a comma, found '" +
+ currChar + "' at postion " +
+ m_curPos);
+ }
+ break;
+ }
+ m_curPos++;
+ } /* end while loop */
+
+ if (token == null)
+ { /* check the ending state */
+ switch (m_state)
+ {
+ case STATE_SCANNING_TOKEN:
+ token = m_tokens.substring(m_scanStart);
+ m_state = STATE_DONE;
+ break;
+
+ case STATE_LOOKING_FOR_FIRST_TOKEN:
+ case STATE_LOOKING_FOR_COMMA:
+ break;
+
+ case STATE_LOOKING_FOR_TOKEN:
+ throw new SaslException("Trialing comma");
+ }
+ }
+
+ return token;
+ }
+
+ /**
+ * This function returns TRUE if the character is a valid token character.
+ *
+ * token = 1*<any CHAR except CTLs or separators>
+ *
+ * separators = "(" | ")" | "<" | ">" | "@"
+ * | "," | ";" | ":" | "\" | <">
+ * | "/" | "[" | "]" | "?" | "="
+ * | "{" | "}" | SP | HT
+ *
+ * CTL = <any US-ASCII control character
+ * (octets 0 - 31) and DEL (127)>
+ *
+ * CHAR = <any US-ASCII character (octets 0 - 127)>
+ *
+ * @param c character to be validated
+ *
+ * @return True if character is valid Token character else it returns
+ * false
+ */
+ boolean isValidTokenChar(
+ char c)
+ {
+ if ( ( (c >= '\u0000') && (c <='\u0020') ) ||
+ ( (c >= '\u003a') && (c <= '\u0040') ) ||
+ ( (c >= '\u005b') && (c <= '\u005d') ) ||
+ ('\u002c' == c) ||
+ ('\u0025' == c) ||
+ ('\u0028' == c) ||
+ ('\u0029' == c) ||
+ ('\u007b' == c) ||
+ ('\u007d' == c) ||
+ ('\u007f' == c) )
+ return false;
+
+ return true;
+ }
+
+ /**
+ * This function returns TRUE if the character is linear white space (LWS).
+ * LWS = [CRLF] 1*( SP | HT )
+ *
+ * @param c character to be validated
+ *
+ * @return True if character is liner whitespace else it returns false
+ */
+ boolean isWhiteSpace(
+ char c)
+ {
+ if ( ('\t' == c) || // HORIZONTAL TABULATION.
+ ('\n' == c) || // LINE FEED.
+ ('\r' == c) || // CARRIAGE RETURN.
+ ('\u0020' == c) )
+ return true;
+
+ return false;
+ }
+
+}
+
diff --git a/src/de/measite/smack/AndroidDebugger.java b/src/de/measite/smack/AndroidDebugger.java
new file mode 100644
index 0000000..4dfc622
--- /dev/null
+++ b/src/de/measite/smack/AndroidDebugger.java
@@ -0,0 +1,185 @@
+package de.measite.smack;
+
+import org.jivesoftware.smack.debugger.SmackDebugger;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketListener;
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.util.*;
+
+import android.util.Log;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Very simple debugger that prints to the android log the sent and received stanzas. Use
+ * this debugger with caution since printing to the console is an expensive operation that may
+ * even block the thread since only one thread may print at a time.<p>
+ * <p/>
+ * It is possible to not only print the raw sent and received stanzas but also the interpreted
+ * packets by Smack. By default interpreted packets won't be printed. To enable this feature
+ * just change the <tt>printInterpreted</tt> static variable to <tt>true</tt>.
+ *
+ * @author Gaston Dombiak
+ */
+public class AndroidDebugger implements SmackDebugger {
+
+ public static boolean printInterpreted = false;
+ private SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa");
+
+ private Connection connection = null;
+
+ private PacketListener listener = null;
+ private ConnectionListener connListener = null;
+
+ private Writer writer;
+ private Reader reader;
+ private ReaderListener readerListener;
+ private WriterListener writerListener;
+
+ public AndroidDebugger(Connection connection, Writer writer, Reader reader) {
+ this.connection = connection;
+ this.writer = writer;
+ this.reader = reader;
+ createDebug();
+ }
+
+ /**
+ * Creates the listeners that will print in the console when new activity is detected.
+ */
+ private void createDebug() {
+ // Create a special Reader that wraps the main Reader and logs data to the GUI.
+ ObservableReader debugReader = new ObservableReader(reader);
+ readerListener = new ReaderListener() {
+ public void read(String str) {
+ Log.d("SMACK",
+ dateFormatter.format(new Date()) + " RCV (" + connection.hashCode() +
+ "): " +
+ str);
+ }
+ };
+ debugReader.addReaderListener(readerListener);
+
+ // Create a special Writer that wraps the main Writer and logs data to the GUI.
+ ObservableWriter debugWriter = new ObservableWriter(writer);
+ writerListener = new WriterListener() {
+ public void write(String str) {
+ Log.d("SMACK",
+ dateFormatter.format(new Date()) + " SENT (" + connection.hashCode() +
+ "): " +
+ str);
+ }
+ };
+ debugWriter.addWriterListener(writerListener);
+
+ // Assign the reader/writer objects to use the debug versions. The packet reader
+ // and writer will use the debug versions when they are created.
+ reader = debugReader;
+ writer = debugWriter;
+
+ // Create a thread that will listen for all incoming packets and write them to
+ // the GUI. This is what we call "interpreted" packet data, since it's the packet
+ // data as Smack sees it and not as it's coming in as raw XML.
+ listener = new PacketListener() {
+ public void processPacket(Packet packet) {
+ if (printInterpreted) {
+ Log.d("SMACK",
+ dateFormatter.format(new Date()) + " RCV PKT (" +
+ connection.hashCode() +
+ "): " +
+ packet.toXML());
+ }
+ }
+ };
+
+ connListener = new ConnectionListener() {
+ public void connectionClosed() {
+ Log.d("SMACK",
+ dateFormatter.format(new Date()) + " Connection closed (" +
+ connection.hashCode() +
+ ")");
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ Log.d("SMACK",
+ dateFormatter.format(new Date()) +
+ " Connection closed due to an exception (" +
+ connection.hashCode() +
+ ")");
+ e.printStackTrace();
+ }
+ public void reconnectionFailed(Exception e) {
+ Log.d("SMACK",
+ dateFormatter.format(new Date()) +
+ " Reconnection failed due to an exception (" +
+ connection.hashCode() +
+ ")");
+ e.printStackTrace();
+ }
+ public void reconnectionSuccessful() {
+ Log.d("SMACK",
+ dateFormatter.format(new Date()) + " Connection reconnected (" +
+ connection.hashCode() +
+ ")");
+ }
+ public void reconnectingIn(int seconds) {
+ Log.d("SMACK",
+ dateFormatter.format(new Date()) + " Connection (" +
+ connection.hashCode() +
+ ") will reconnect in " + seconds);
+ }
+ };
+ }
+
+ public Reader newConnectionReader(Reader newReader) {
+ ((ObservableReader)reader).removeReaderListener(readerListener);
+ ObservableReader debugReader = new ObservableReader(newReader);
+ debugReader.addReaderListener(readerListener);
+ reader = debugReader;
+ return reader;
+ }
+
+ public Writer newConnectionWriter(Writer newWriter) {
+ ((ObservableWriter)writer).removeWriterListener(writerListener);
+ ObservableWriter debugWriter = new ObservableWriter(newWriter);
+ debugWriter.addWriterListener(writerListener);
+ writer = debugWriter;
+ return writer;
+ }
+
+ public void userHasLogged(String user) {
+ boolean isAnonymous = "".equals(StringUtils.parseName(user));
+ String title =
+ "User logged (" + connection.hashCode() + "): "
+ + (isAnonymous ? "" : StringUtils.parseBareAddress(user))
+ + "@"
+ + connection.getServiceName()
+ + ":"
+ + connection.getPort();
+ title += "/" + StringUtils.parseResource(user);
+ Log.d("SMACK", title);
+ // Add the connection listener to the connection so that the debugger can be notified
+ // whenever the connection is closed.
+ connection.addConnectionListener(connListener);
+ }
+
+ public Reader getReader() {
+ return reader;
+ }
+
+ public Writer getWriter() {
+ return writer;
+ }
+
+ public PacketListener getReaderListener() {
+ return listener;
+ }
+
+ public PacketListener getWriterListener() {
+ return null;
+ }
+}
+
diff --git a/src/de/measite/smack/Sasl.java b/src/de/measite/smack/Sasl.java
new file mode 100644
index 0000000..a59135d
--- /dev/null
+++ b/src/de/measite/smack/Sasl.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2009 Rene Treffer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package de.measite.smack;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Map;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslException;
+import org.apache.harmony.javax.security.sasl.SaslServer;
+import org.apache.harmony.javax.security.sasl.SaslServerFactory;
+
+public class Sasl {
+
+ // SaslClientFactory service name
+ private static final String CLIENTFACTORYSRV = "SaslClientFactory"; //$NON-NLS-1$
+
+ // SaslServerFactory service name
+ private static final String SERVERFACTORYSRV = "SaslServerFactory"; //$NON-NLS-1$
+
+ public static final String POLICY_NOPLAINTEXT = "javax.security.sasl.policy.noplaintext"; //$NON-NLS-1$
+
+ public static final String POLICY_NOACTIVE = "javax.security.sasl.policy.noactive"; //$NON-NLS-1$
+
+ public static final String POLICY_NODICTIONARY = "javax.security.sasl.policy.nodictionary"; //$NON-NLS-1$
+
+ public static final String POLICY_NOANONYMOUS = "javax.security.sasl.policy.noanonymous"; //$NON-NLS-1$
+
+ public static final String POLICY_FORWARD_SECRECY = "javax.security.sasl.policy.forward"; //$NON-NLS-1$
+
+ public static final String POLICY_PASS_CREDENTIALS = "javax.security.sasl.policy.credentials"; //$NON-NLS-1$
+
+ public static final String MAX_BUFFER = "javax.security.sasl.maxbuffer"; //$NON-NLS-1$
+
+ public static final String RAW_SEND_SIZE = "javax.security.sasl.rawsendsize"; //$NON-NLS-1$
+
+ public static final String REUSE = "javax.security.sasl.reuse"; //$NON-NLS-1$
+
+ public static final String QOP = "javax.security.sasl.qop"; //$NON-NLS-1$
+
+ public static final String STRENGTH = "javax.security.sasl.strength"; //$NON-NLS-1$
+
+ public static final String SERVER_AUTH = "javax.security.sasl.server.authentication"; //$NON-NLS-1$
+
+ public static Enumeration<SaslClientFactory> getSaslClientFactories() {
+ Hashtable<SaslClientFactory,Object> factories = new Hashtable<SaslClientFactory,Object>();
+ factories.put(new SaslClientFactory(), new Object());
+ return factories.keys();
+ }
+
+ public static Enumeration<SaslServerFactory> getSaslServerFactories() {
+ return org.apache.harmony.javax.security.sasl.Sasl.getSaslServerFactories();
+ }
+
+ public static SaslServer createSaslServer(String mechanism, String protocol,
+ String serverName, Map<String, ?> prop, CallbackHandler cbh) throws SaslException {
+ return org.apache.harmony.javax.security.sasl.Sasl.createSaslServer(mechanism, protocol, serverName, prop, cbh);
+ }
+
+ public static SaslClient createSaslClient(String[] mechanisms, String authanticationID,
+ String protocol, String serverName, Map<String, ?> prop, CallbackHandler cbh)
+ throws SaslException {
+ if (mechanisms == null) {
+ throw new NullPointerException("auth.33"); //$NON-NLS-1$
+ }
+ SaslClientFactory fact = getSaslClientFactories().nextElement();
+ String[] mech = fact.getMechanismNames(null);
+ boolean is = false;
+ if (mech != null) {
+ for (int j = 0; j < mech.length; j++) {
+ for (int n = 0; n < mechanisms.length; n++) {
+ if (mech[j].equals(mechanisms[n])) {
+ is = true;
+ break;
+ }
+ }
+ }
+ }
+ if (is) {
+ return fact.createSaslClient(
+ mechanisms,
+ authanticationID,
+ protocol,
+ serverName,
+ prop,
+ cbh
+ );
+ }
+ return null;
+ }
+
+}
diff --git a/src/de/measite/smack/SaslClientFactory.java b/src/de/measite/smack/SaslClientFactory.java
new file mode 100644
index 0000000..2fa1ebd
--- /dev/null
+++ b/src/de/measite/smack/SaslClientFactory.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2009 Rene Treffer
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+package de.measite.smack;
+
+import java.util.Map;
+
+import com.novell.sasl.client.DigestMD5SaslClient;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslException;
+import org.apache.qpid.management.common.sasl.PlainSaslClient;
+
+public class SaslClientFactory implements
+ org.apache.harmony.javax.security.sasl.SaslClientFactory {
+
+ @Override
+ public SaslClient createSaslClient(String[] mechanisms,
+ String authorizationId, String protocol, String serverName,
+ Map<String, ?> props, CallbackHandler cbh) throws SaslException {
+ for (String mech: mechanisms) {
+ if ("PLAIN".equals(mech)) {
+ return new PlainSaslClient(authorizationId, cbh);
+ } else
+ if ("DIGEST-MD5".equals(mech)) {
+ return DigestMD5SaslClient.getClient(
+ authorizationId,
+ protocol,
+ serverName,
+ props,
+ cbh
+ );
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String[] getMechanismNames(Map<String, ?> props) {
+ return new String[]{
+ "PLAIN",
+ "DIGEST-MD5"
+ };
+ }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/AuthPermission.java b/src/org/apache/harmony/javax/security/auth/AuthPermission.java
new file mode 100644
index 0000000..bb12554
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/AuthPermission.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+import java.security.BasicPermission;
+
+
+
+/**
+ * Governs the use of methods in this package and also its subpackages. A
+ * <i>target name</i> of the permission specifies which methods are allowed
+ * without specifying the concrete action lists. Possible target names and
+ * associated authentication permissions are:
+ *
+ * <pre>
+ * doAs invoke Subject.doAs methods.
+ * doAsPrivileged invoke the Subject.doAsPrivileged methods.
+ * getSubject invoke Subject.getSubject().
+ * getSubjectFromDomainCombiner invoke SubjectDomainCombiner.getSubject().
+ * setReadOnly invoke Subject.setReadonly().
+ * modifyPrincipals modify the set of principals
+ * associated with a Subject.
+ * modifyPublicCredentials modify the set of public credentials
+ * associated with a Subject.
+ * modifyPrivateCredentials modify the set of private credentials
+ * associated with a Subject.
+ * refreshCredential invoke the refresh method on a credential of a
+ * refreshable credential class.
+ * destroyCredential invoke the destroy method on a credential of a
+ * destroyable credential class.
+ * createLoginContext.<i>name</i> instantiate a LoginContext with the
+ * specified name. The wildcard name ('*')
+ * allows to a LoginContext of any name.
+ * getLoginConfiguration invoke the getConfiguration method of
+ * javax.security.auth.login.Configuration.
+ * refreshLoginConfiguration Invoke the refresh method of
+ * javax.security.auth.login.Configuration.
+ * </pre>
+ */
+public final class AuthPermission extends BasicPermission {
+
+ private static final long serialVersionUID = 5806031445061587174L;
+
+ private static final String CREATE_LOGIN_CONTEXT = "createLoginContext"; //$NON-NLS-1$
+
+ private static final String CREATE_LOGIN_CONTEXT_ANY = "createLoginContext.*"; //$NON-NLS-1$
+
+ // inits permission name.
+ private static String init(String name) {
+
+ if (name == null) {
+ throw new NullPointerException("auth.13"); //$NON-NLS-1$
+ }
+
+ if (CREATE_LOGIN_CONTEXT.equals(name)) {
+ return CREATE_LOGIN_CONTEXT_ANY;
+ }
+ return name;
+ }
+
+ /**
+ * Creates an authentication permission with the specified target name.
+ *
+ * @param name
+ * the target name of this authentication permission.
+ */
+ public AuthPermission(String name) {
+ super(init(name));
+ }
+
+ /**
+ * Creates an authentication permission with the specified target name.
+ *
+ * @param name
+ * the target name of this authentication permission.
+ * @param actions
+ * this parameter is ignored and should be {@code null}.
+ */
+ public AuthPermission(String name, String actions) {
+ super(init(name), actions);
+ }
+}
\ No newline at end of file
diff --git a/src/org/apache/harmony/javax/security/auth/DestroyFailedException.java b/src/org/apache/harmony/javax/security/auth/DestroyFailedException.java
new file mode 100644
index 0000000..7c7ea79
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/DestroyFailedException.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+/**
+ * Signals that the {@link Destroyable#destroy()} method failed.
+ */
+public class DestroyFailedException extends Exception {
+
+ private static final long serialVersionUID = -7790152857282749162L;
+
+ /**
+ * Creates an exception of type {@code DestroyFailedException}.
+ */
+ public DestroyFailedException() {
+ super();
+ }
+
+ /**
+ * Creates an exception of type {@code DestroyFailedException}.
+ *
+ * @param message
+ * A detail message that describes the reason for this exception.
+ */
+ public DestroyFailedException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/Destroyable.java b/src/org/apache/harmony/javax/security/auth/Destroyable.java
new file mode 100644
index 0000000..12a107b
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/Destroyable.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+/**
+ * Allows for special treatment of sensitive information, when it comes to
+ * destroying or clearing of the data.
+ */
+public interface Destroyable {
+
+ /**
+ * Erases the sensitive information. Once an object is destroyed any calls
+ * to its methods will throw an {@code IllegalStateException}. If it does
+ * not succeed a DestroyFailedException is thrown.
+ *
+ * @throws DestroyFailedException
+ * if the information cannot be erased.
+ */
+ void destroy() throws DestroyFailedException;
+
+ /**
+ * Returns {@code true} once an object has been safely destroyed.
+ *
+ * @return whether the object has been safely destroyed.
+ */
+ boolean isDestroyed();
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/PrivateCredentialPermission.java b/src/org/apache/harmony/javax/security/auth/PrivateCredentialPermission.java
new file mode 100644
index 0000000..d62bb24
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/PrivateCredentialPermission.java
@@ -0,0 +1,395 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.Serializable;
+import java.security.Permission;
+import java.security.PermissionCollection;
+import java.security.Principal;
+import java.util.Set;
+
+
+
+/**
+ * Protects private credential objects belonging to a {@code Subject}. It has
+ * only one action which is "read". The target name of this permission has a
+ * special syntax:
+ *
+ * <pre>
+ * targetName = CredentialClass {PrincipalClass "PrincipalName"}*
+ * </pre>
+ *
+ * First it states a credential class and is followed then by a list of one or
+ * more principals identifying the subject.
+ * <p>
+ * The principals on their part are specified as the name of the {@code
+ * Principal} class followed by the principal name in quotes. For example, the
+ * following file may define permission to read the private credentials of a
+ * principal named "Bob": "com.sun.PrivateCredential com.sun.Principal \"Bob\""
+ * <p>
+ * The syntax also allows the use of the wildcard "*" in place of {@code
+ * CredentialClass} or {@code PrincipalClass} and/or {@code PrincipalName}.
+ *
+ * @see Principal
+ */
+public final class PrivateCredentialPermission extends Permission {
+
+ private static final long serialVersionUID = 5284372143517237068L;
+
+ // allowed action
+ private static final String READ = "read"; //$NON-NLS-1$
+
+ private String credentialClass;
+
+ // current offset
+ private transient int offset;
+
+ // owners set
+ private transient CredOwner[] set;
+
+ /**
+ * Creates a new permission for private credentials specified by the target
+ * name {@code name} and an {@code action}. The action is always
+ * {@code "read"}.
+ *
+ * @param name
+ * the target name of the permission.
+ * @param action
+ * the action {@code "read"}.
+ */
+ public PrivateCredentialPermission(String name, String action) {
+ super(name);
+ if (READ.equalsIgnoreCase(action)) {
+ initTargetName(name);
+ } else {
+ throw new IllegalArgumentException("auth.11"); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Creates a {@code PrivateCredentialPermission} from the {@code Credential}
+ * class and set of principals.
+ *
+ * @param credentialClass
+ * the credential class name.
+ * @param principals
+ * the set of principals.
+ */
+ PrivateCredentialPermission(String credentialClass, Set<Principal> principals) {
+ super(credentialClass);
+ this.credentialClass = credentialClass;
+
+ set = new CredOwner[principals.size()];
+ for (Principal p : principals) {
+ CredOwner element = new CredOwner(p.getClass().getName(), p.getName());
+ // check for duplicate elements
+ boolean found = false;
+ for (int ii = 0; ii < offset; ii++) {
+ if (set[ii].equals(element)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ set[offset++] = element;
+ }
+ }
+ }
+
+ /**
+ * Initialize a PrivateCredentialPermission object and checks that a target
+ * name has a correct format: CredentialClass 1*(PrincipalClass
+ * "PrincipalName")
+ */
+ private void initTargetName(String name) {
+
+ if (name == null) {
+ throw new NullPointerException("auth.0E"); //$NON-NLS-1$
+ }
+
+ // check empty string
+ name = name.trim();
+ if (name.length() == 0) {
+ throw new IllegalArgumentException("auth.0F"); //$NON-NLS-1$
+ }
+
+ // get CredentialClass
+ int beg = name.indexOf(' ');
+ if (beg == -1) {
+ throw new IllegalArgumentException("auth.10"); //$NON-NLS-1$
+ }
+ credentialClass = name.substring(0, beg);
+
+ // get a number of pairs: PrincipalClass "PrincipalName"
+ beg++;
+ int count = 0;
+ int nameLength = name.length();
+ for (int i, j = 0; beg < nameLength; beg = j + 2, count++) {
+ i = name.indexOf(' ', beg);
+ j = name.indexOf('"', i + 2);
+
+ if (i == -1 || j == -1 || name.charAt(i + 1) != '"') {
+ throw new IllegalArgumentException("auth.10"); //$NON-NLS-1$
+ }
+ }
+
+ // name MUST have one pair at least
+ if (count < 1) {
+ throw new IllegalArgumentException("auth.10"); //$NON-NLS-1$
+ }
+
+ beg = name.indexOf(' ');
+ beg++;
+
+ // populate principal set with instances of CredOwner class
+ String principalClass;
+ String principalName;
+
+ set = new CredOwner[count];
+ for (int index = 0, i, j; index < count; beg = j + 2, index++) {
+ i = name.indexOf(' ', beg);
+ j = name.indexOf('"', i + 2);
+
+ principalClass = name.substring(beg, i);
+ principalName = name.substring(i + 2, j);
+
+ CredOwner element = new CredOwner(principalClass, principalName);
+ // check for duplicate elements
+ boolean found = false;
+ for (int ii = 0; ii < offset; ii++) {
+ if (set[ii].equals(element)) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ set[offset++] = element;
+ }
+ }
+ }
+
+ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
+ ois.defaultReadObject();
+ initTargetName(getName());
+ }
+
+ /**
+ * Returns the principal's classes and names associated with this {@code
+ * PrivateCredentialPermission} as a two dimensional array. The first
+ * dimension of the array corresponds to the number of principals. The
+ * second dimension defines either the name of the {@code PrincipalClass}
+ * [x][0] or the value of {@code PrincipalName} [x][1].
+ * <p>
+ * This corresponds to the the target name's syntax:
+ *
+ * <pre>
+ * targetName = CredentialClass {PrincipalClass "PrincipalName"}*
+ * </pre>
+ *
+ * @return the principal classes and names associated with this {@code
+ * PrivateCredentialPermission}.
+ */
+ public String[][] getPrincipals() {
+
+ String[][] s = new String[offset][2];
+
+ for (int i = 0; i < s.length; i++) {
+ s[i][0] = set[i].principalClass;
+ s[i][1] = set[i].principalName;
+ }
+ return s;
+ }
+
+ @Override
+ public String getActions() {
+ return READ;
+ }
+
+ /**
+ * Returns the class name of the credential associated with this permission.
+ *
+ * @return the class name of the credential associated with this permission.
+ */
+ public String getCredentialClass() {
+ return credentialClass;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 0;
+ for (int i = 0; i < offset; i++) {
+ hash = hash + set[i].hashCode();
+ }
+ return getCredentialClass().hashCode() + hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ if (obj == null || this.getClass() != obj.getClass()) {
+ return false;
+ }
+
+ PrivateCredentialPermission that = (PrivateCredentialPermission) obj;
+
+ return credentialClass.equals(that.credentialClass) && (offset == that.offset)
+ && sameMembers(set, that.set, offset);
+ }
+
+ @Override
+ public boolean implies(Permission permission) {
+
+ if (permission == null || this.getClass() != permission.getClass()) {
+ return false;
+ }
+
+ PrivateCredentialPermission that = (PrivateCredentialPermission) permission;
+
+ if (!("*".equals(credentialClass) || credentialClass //$NON-NLS-1$
+ .equals(that.getCredentialClass()))) {
+ return false;
+ }
+
+ if (that.offset == 0) {
+ return true;
+ }
+
+ CredOwner[] thisCo = set;
+ CredOwner[] thatCo = that.set;
+ int thisPrincipalsSize = offset;
+ int thatPrincipalsSize = that.offset;
+ for (int i = 0, j; i < thisPrincipalsSize; i++) {
+ for (j = 0; j < thatPrincipalsSize; j++) {
+ if (thisCo[i].implies(thatCo[j])) {
+ break;
+ }
+ }
+ if (j == thatCo.length) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public PermissionCollection newPermissionCollection() {
+ return null;
+ }
+
+ /**
+ * Returns true if the two arrays have the same length, and every member of
+ * one array is contained in another array
+ */
+ private boolean sameMembers(Object[] ar1, Object[] ar2, int length) {
+ if (ar1 == null && ar2 == null) {
+ return true;
+ }
+ if (ar1 == null || ar2 == null) {
+ return false;
+ }
+ boolean found;
+ for (int i = 0; i < length; i++) {
+ found = false;
+ for (int j = 0; j < length; j++) {
+ if (ar1[i].equals(ar2[j])) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static final class CredOwner implements Serializable {
+
+ private static final long serialVersionUID = -5607449830436408266L;
+
+ String principalClass;
+
+ String principalName;
+
+ // whether class name contains wildcards
+ private transient boolean isClassWildcard;
+
+ // whether pname contains wildcards
+ private transient boolean isPNameWildcard;
+
+ // Creates a new CredOwner with the specified Principal Class and Principal Name
+ CredOwner(String principalClass, String principalName) {
+ super();
+ if ("*".equals(principalClass)) { //$NON-NLS-1$
+ isClassWildcard = true;
+ }
+
+ if ("*".equals(principalName)) { //$NON-NLS-1$
+ isPNameWildcard = true;
+ }
+
+ if (isClassWildcard && !isPNameWildcard) {
+ throw new IllegalArgumentException("auth.12"); //$NON-NLS-1$
+ }
+
+ this.principalClass = principalClass;
+ this.principalName = principalName;
+ }
+
+ // Checks if this CredOwner implies the specified Object.
+ boolean implies(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+
+ CredOwner co = (CredOwner) obj;
+
+ if (isClassWildcard || principalClass.equals(co.principalClass)) {
+ if (isPNameWildcard || principalName.equals(co.principalName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Checks two CredOwner objects for equality.
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj instanceof CredOwner) {
+ CredOwner that = (CredOwner) obj;
+ return principalClass.equals(that.principalClass)
+ && principalName.equals(that.principalName);
+ }
+ return false;
+ }
+
+ // Returns the hash code value for this object.
+ @Override
+ public int hashCode() {
+ return principalClass.hashCode() + principalName.hashCode();
+ }
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/RefreshFailedException.java b/src/org/apache/harmony/javax/security/auth/RefreshFailedException.java
new file mode 100644
index 0000000..71bcc6b
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/RefreshFailedException.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+public class RefreshFailedException extends Exception {
+
+ private static final long serialVersionUID = 5058444488565265840L;
+
+ public RefreshFailedException() {
+ super();
+ }
+
+ public RefreshFailedException(String message) {
+ super(message);
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/Refreshable.java b/src/org/apache/harmony/javax/security/auth/Refreshable.java
new file mode 100644
index 0000000..90b00cb
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/Refreshable.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+public interface Refreshable {
+
+ void refresh() throws RefreshFailedException;
+
+ boolean isCurrent();
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/Subject.java b/src/org/apache/harmony/javax/security/auth/Subject.java
new file mode 100644
index 0000000..142686e
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/Subject.java
@@ -0,0 +1,782 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.security.AccessControlContext;
+import java.security.AccessController;
+import java.security.DomainCombiner;
+import java.security.Permission;
+import java.security.Principal;
+import java.security.PrivilegedAction;
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
+import java.security.ProtectionDomain;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Set;
+
+
+
+/**
+ * The central class of the {@code javax.security.auth} package representing an
+ * authenticated user or entity (both referred to as "subject"). IT defines also
+ * the static methods that allow code to be run, and do modifications according
+ * to the subject's permissions.
+ * <p>
+ * A subject has the following features:
+ * <ul>
+ * <li>A set of {@code Principal} objects specifying the identities bound to a
+ * {@code Subject} that distinguish it.</li>
+ * <li>Credentials (public and private) such as certificates, keys, or
+ * authentication proofs such as tickets</li>
+ * </ul>
+ */
+public final class Subject implements Serializable {
+
+ private static final long serialVersionUID = -8308522755600156056L;
+
+ private static final AuthPermission _AS = new AuthPermission("doAs"); //$NON-NLS-1$
+
+ private static final AuthPermission _AS_PRIVILEGED = new AuthPermission(
+ "doAsPrivileged"); //$NON-NLS-1$
+
+ private static final AuthPermission _SUBJECT = new AuthPermission(
+ "getSubject"); //$NON-NLS-1$
+
+ private static final AuthPermission _PRINCIPALS = new AuthPermission(
+ "modifyPrincipals"); //$NON-NLS-1$
+
+ private static final AuthPermission _PRIVATE_CREDENTIALS = new AuthPermission(
+ "modifyPrivateCredentials"); //$NON-NLS-1$
+
+ private static final AuthPermission _PUBLIC_CREDENTIALS = new AuthPermission(
+ "modifyPublicCredentials"); //$NON-NLS-1$
+
+ private static final AuthPermission _READ_ONLY = new AuthPermission(
+ "setReadOnly"); //$NON-NLS-1$
+
+ private final Set<Principal> principals;
+
+ private boolean readOnly;
+
+ // set of private credentials
+ private transient SecureSet<Object> privateCredentials;
+
+ // set of public credentials
+ private transient SecureSet<Object> publicCredentials;
+
+ /**
+ * The default constructor initializing the sets of public and private
+ * credentials and principals with the empty set.
+ */
+ public Subject() {
+ super();
+ principals = new SecureSet<Principal>(_PRINCIPALS);
+ publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS);
+ privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS);
+
+ readOnly = false;
+ }
+
+ /**
+ * The constructor for the subject, setting its public and private
+ * credentials and principals according to the arguments.
+ *
+ * @param readOnly
+ * {@code true} if this {@code Subject} is read-only, thus
+ * preventing any modifications to be done.
+ * @param subjPrincipals
+ * the set of Principals that are attributed to this {@code
+ * Subject}.
+ * @param pubCredentials
+ * the set of public credentials that distinguish this {@code
+ * Subject}.
+ * @param privCredentials
+ * the set of private credentials that distinguish this {@code
+ * Subject}.
+ */
+ public Subject(boolean readOnly, Set<? extends Principal> subjPrincipals,
+ Set<?> pubCredentials, Set<?> privCredentials) {
+
+ if (subjPrincipals == null || pubCredentials == null || privCredentials == null) {
+ throw new NullPointerException();
+ }
+
+ principals = new SecureSet<Principal>(_PRINCIPALS, subjPrincipals);
+ publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS, pubCredentials);
+ privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS, privCredentials);
+
+ this.readOnly = readOnly;
+ }
+
+ /**
+ * Runs the code defined by {@code action} using the permissions granted to
+ * the {@code Subject} itself and to the code as well.
+ *
+ * @param subject
+ * the distinguished {@code Subject}.
+ * @param action
+ * the code to be run.
+ * @return the {@code Object} returned when running the {@code action}.
+ */
+ @SuppressWarnings("unchecked")
+ public static Object doAs(Subject subject, PrivilegedAction action) {
+
+ checkPermission(_AS);
+
+ return doAs_PrivilegedAction(subject, action, AccessController.getContext());
+ }
+
+ /**
+ * Run the code defined by {@code action} using the permissions granted to
+ * the {@code Subject} and to the code itself, additionally providing a more
+ * specific context.
+ *
+ * @param subject
+ * the distinguished {@code Subject}.
+ * @param action
+ * the code to be run.
+ * @param context
+ * the specific context in which the {@code action} is invoked.
+ * if {@code null} a new {@link AccessControlContext} is
+ * instantiated.
+ * @return the {@code Object} returned when running the {@code action}.
+ */
+ @SuppressWarnings("unchecked")
+ public static Object doAsPrivileged(Subject subject, PrivilegedAction action,
+ AccessControlContext context) {
+
+ checkPermission(_AS_PRIVILEGED);
+
+ if (context == null) {
+ return doAs_PrivilegedAction(subject, action, new AccessControlContext(
+ new ProtectionDomain[0]));
+ }
+ return doAs_PrivilegedAction(subject, action, context);
+ }
+
+ // instantiates a new context and passes it to AccessController
+ @SuppressWarnings("unchecked")
+ private static Object doAs_PrivilegedAction(Subject subject, PrivilegedAction action,
+ final AccessControlContext context) {
+
+ AccessControlContext newContext;
+
+ final SubjectDomainCombiner combiner;
+ if (subject == null) {
+ // performance optimization
+ // if subject is null there is nothing to combine
+ combiner = null;
+ } else {
+ combiner = new SubjectDomainCombiner(subject);
+ }
+
+ PrivilegedAction dccAction = new PrivilegedAction() {
+ public Object run() {
+
+ return new AccessControlContext(context, combiner);
+ }
+ };
+
+ newContext = (AccessControlContext) AccessController.doPrivileged(dccAction);
+
+ return AccessController.doPrivileged(action, newContext);
+ }
+
+ /**
+ * Runs the code defined by {@code action} using the permissions granted to
+ * the subject and to the code itself.
+ *
+ * @param subject
+ * the distinguished {@code Subject}.
+ * @param action
+ * the code to be run.
+ * @return the {@code Object} returned when running the {@code action}.
+ * @throws PrivilegedActionException
+ * if running the {@code action} throws an exception.
+ */
+ @SuppressWarnings("unchecked")
+ public static Object doAs(Subject subject, PrivilegedExceptionAction action)
+ throws PrivilegedActionException {
+
+ checkPermission(_AS);
+
+ return doAs_PrivilegedExceptionAction(subject, action, AccessController.getContext());
+ }
+
+ /**
+ * Runs the code defined by {@code action} using the permissions granted to
+ * the subject and to the code itself, additionally providing a more
+ * specific context.
+ *
+ * @param subject
+ * the distinguished {@code Subject}.
+ * @param action
+ * the code to be run.
+ * @param context
+ * the specific context in which the {@code action} is invoked.
+ * if {@code null} a new {@link AccessControlContext} is
+ * instantiated.
+ * @return the {@code Object} returned when running the {@code action}.
+ * @throws PrivilegedActionException
+ * if running the {@code action} throws an exception.
+ */
+ @SuppressWarnings("unchecked")
+ public static Object doAsPrivileged(Subject subject,
+ PrivilegedExceptionAction action, AccessControlContext context)
+ throws PrivilegedActionException {
+
+ checkPermission(_AS_PRIVILEGED);
+
+ if (context == null) {
+ return doAs_PrivilegedExceptionAction(subject, action,
+ new AccessControlContext(new ProtectionDomain[0]));
+ }
+ return doAs_PrivilegedExceptionAction(subject, action, context);
+ }
+
+ // instantiates a new context and passes it to AccessController
+ @SuppressWarnings("unchecked")
+ private static Object doAs_PrivilegedExceptionAction(Subject subject,
+ PrivilegedExceptionAction action, final AccessControlContext context)
+ throws PrivilegedActionException {
+
+ AccessControlContext newContext;
+
+ final SubjectDomainCombiner combiner;
+ if (subject == null) {
+ // performance optimization
+ // if subject is null there is nothing to combine
+ combiner = null;
+ } else {
+ combiner = new SubjectDomainCombiner(subject);
+ }
+
+ PrivilegedAction<AccessControlContext> dccAction = new PrivilegedAction<AccessControlContext>() {
+ public AccessControlContext run() {
+ return new AccessControlContext(context, combiner);
+ }
+ };
+
+ newContext = AccessController.doPrivileged(dccAction);
+
+ return AccessController.doPrivileged(action, newContext);
+ }
+
+ /**
+ * Checks two Subjects for equality. More specifically if the principals,
+ * public and private credentials are equal, equality for two {@code
+ * Subjects} is implied.
+ *
+ * @param obj
+ * the {@code Object} checked for equality with this {@code
+ * Subject}.
+ * @return {@code true} if the specified {@code Subject} is equal to this
+ * one.
+ */
+ @Override
+ public boolean equals(Object obj) {
+
+ if (this == obj) {
+ return true;
+ }
+
+ if (obj == null || this.getClass() != obj.getClass()) {
+ return false;
+ }
+
+ Subject that = (Subject) obj;
+
+ if (principals.equals(that.principals)
+ && publicCredentials.equals(that.publicCredentials)
+ && privateCredentials.equals(that.privateCredentials)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns this {@code Subject}'s {@link Principal}.
+ *
+ * @return this {@code Subject}'s {@link Principal}.
+ */
+ public Set<Principal> getPrincipals() {
+ return principals;
+ }
+
+
+ /**
+ * Returns this {@code Subject}'s {@link Principal} which is a subclass of
+ * the {@code Class} provided.
+ *
+ * @param c
+ * the {@code Class} as a criteria which the {@code Principal}
+ * returned must satisfy.
+ * @return this {@code Subject}'s {@link Principal}. Modifications to the
+ * returned set of {@code Principal}s do not affect this {@code
+ * Subject}'s set.
+ */
+ public <T extends Principal> Set<T> getPrincipals(Class<T> c) {
+ return ((SecureSet<Principal>) principals).get(c);
+ }
+
+ /**
+ * Returns the private credentials associated with this {@code Subject}.
+ *
+ * @return the private credentials associated with this {@code Subject}.
+ */
+ public Set<Object> getPrivateCredentials() {
+ return privateCredentials;
+ }
+
+ /**
+ * Returns this {@code Subject}'s private credentials which are a subclass
+ * of the {@code Class} provided.
+ *
+ * @param c
+ * the {@code Class} as a criteria which the private credentials
+ * returned must satisfy.
+ * @return this {@code Subject}'s private credentials. Modifications to the
+ * returned set of credentials do not affect this {@code Subject}'s
+ * credentials.
+ */
+ public <T> Set<T> getPrivateCredentials(Class<T> c) {
+ return privateCredentials.get(c);
+ }
+
+ /**
+ * Returns the public credentials associated with this {@code Subject}.
+ *
+ * @return the public credentials associated with this {@code Subject}.
+ */
+ public Set<Object> getPublicCredentials() {
+ return publicCredentials;
+ }
+
+
+ /**
+ * Returns this {@code Subject}'s public credentials which are a subclass of
+ * the {@code Class} provided.
+ *
+ * @param c
+ * the {@code Class} as a criteria which the public credentials
+ * returned must satisfy.
+ * @return this {@code Subject}'s public credentials. Modifications to the
+ * returned set of credentials do not affect this {@code Subject}'s
+ * credentials.
+ */
+ public <T> Set<T> getPublicCredentials(Class<T> c) {
+ return publicCredentials.get(c);
+ }
+
+ /**
+ * Returns a hash code of this {@code Subject}.
+ *
+ * @return a hash code of this {@code Subject}.
+ */
+ @Override
+ public int hashCode() {
+ return principals.hashCode() + privateCredentials.hashCode()
+ + publicCredentials.hashCode();
+ }
+
+ /**
+ * Prevents from modifications being done to the credentials and {@link
+ * Principal} sets. After setting it to read-only this {@code Subject} can
+ * not be made writable again. The destroy method on the credentials still
+ * works though.
+ */
+ public void setReadOnly() {
+ checkPermission(_READ_ONLY);
+
+ readOnly = true;
+ }
+
+ /**
+ * Returns whether this {@code Subject} is read-only or not.
+ *
+ * @return whether this {@code Subject} is read-only or not.
+ */
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ /**
+ * Returns a {@code String} representation of this {@code Subject}.
+ *
+ * @return a {@code String} representation of this {@code Subject}.
+ */
+ @Override
+ public String toString() {
+
+ StringBuilder buf = new StringBuilder("Subject:\n"); //$NON-NLS-1$
+
+ Iterator<?> it = principals.iterator();
+ while (it.hasNext()) {
+ buf.append("\tPrincipal: "); //$NON-NLS-1$
+ buf.append(it.next());
+ buf.append('\n');
+ }
+
+ it = publicCredentials.iterator();
+ while (it.hasNext()) {
+ buf.append("\tPublic Credential: "); //$NON-NLS-1$
+ buf.append(it.next());
+ buf.append('\n');
+ }
+
+ int offset = buf.length() - 1;
+ it = privateCredentials.iterator();
+ try {
+ while (it.hasNext()) {
+ buf.append("\tPrivate Credential: "); //$NON-NLS-1$
+ buf.append(it.next());
+ buf.append('\n');
+ }
+ } catch (SecurityException e) {
+ buf.delete(offset, buf.length());
+ buf.append("\tPrivate Credentials: no accessible information\n"); //$NON-NLS-1$
+ }
+ return buf.toString();
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+
+ in.defaultReadObject();
+
+ publicCredentials = new SecureSet<Object>(_PUBLIC_CREDENTIALS);
+ privateCredentials = new SecureSet<Object>(_PRIVATE_CREDENTIALS);
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.defaultWriteObject();
+ }
+
+ /**
+ * Returns the {@code Subject} that was last associated with the {@code
+ * context} provided as argument.
+ *
+ * @param context
+ * the {@code context} that was associated with the
+ * {@code Subject}.
+ * @return the {@code Subject} that was last associated with the {@code
+ * context} provided as argument.
+ */
+ public static Subject getSubject(final AccessControlContext context) {
+ checkPermission(_SUBJECT);
+ if (context == null) {
+ throw new NullPointerException("auth.09"); //$NON-NLS-1$
+ }
+ PrivilegedAction<DomainCombiner> action = new PrivilegedAction<DomainCombiner>() {
+ public DomainCombiner run() {
+ return context.getDomainCombiner();
+ }
+ };
+ DomainCombiner combiner = AccessController.doPrivileged(action);
+
+ if ((combiner == null) || !(combiner instanceof SubjectDomainCombiner)) {
+ return null;
+ }
+ return ((SubjectDomainCombiner) combiner).getSubject();
+ }
+
+ // checks passed permission
+ private static void checkPermission(Permission p) {
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ sm.checkPermission(p);
+ }
+ }
+
+ // FIXME is used only in two places. remove?
+ private void checkState() {
+ if (readOnly) {
+ throw new IllegalStateException("auth.0A"); //$NON-NLS-1$
+ }
+ }
+
+ private final class SecureSet<SST> extends AbstractSet<SST> implements Serializable {
+
+ /**
+ * Compatibility issue: see comments for setType variable
+ */
+ private static final long serialVersionUID = 7911754171111800359L;
+
+ private LinkedList<SST> elements;
+
+ /*
+ * Is used to define a set type for serialization.
+ *
+ * A type can be principal, priv. or pub. credential set. The spec.
+ * doesn't clearly says that priv. and pub. credential sets can be
+ * serialized and what classes they are. It is only possible to figure
+ * out from writeObject method comments that priv. credential set is
+ * serializable and it is an instance of SecureSet class. So pub.
+ * credential was implemented by analogy
+ *
+ * Compatibility issue: the class follows its specified serial form.
+ * Also according to the serialization spec. adding new field is a
+ * compatible change. So is ok for principal set (because the default
+ * value for integer is zero). But priv. or pub. credential set it is
+ * not compatible because most probably other implementations resolve
+ * this issue in other way
+ */
+ private int setType;
+
+ // Defines principal set for serialization.
+ private static final int SET_Principal = 0;
+
+ // Defines private credential set for serialization.
+ private static final int SET_PrivCred = 1;
+
+ // Defines public credential set for serialization.
+ private static final int SET_PubCred = 2;
+
+ // permission required to modify set
+ private transient AuthPermission permission;
+
+ protected SecureSet(AuthPermission perm) {
+ permission = perm;
+ elements = new LinkedList<SST>();
+ }
+
+ // creates set from specified collection with specified permission
+ // all collection elements are verified before adding
+ protected SecureSet(AuthPermission perm, Collection<? extends SST> s) {
+ this(perm);
+
+ // Subject's constructor receives a Set, we can trusts if a set is from bootclasspath,
+ // and not to check whether it contains duplicates or not
+ boolean trust = s.getClass().getClassLoader() == null;
+
+ Iterator<? extends SST> it = s.iterator();
+ while (it.hasNext()) {
+ SST o = it.next();
+ verifyElement(o);
+ if (trust || !elements.contains(o)) {
+ elements.add(o);
+ }
+ }
+ }
+
+ // verifies new set element
+ private void verifyElement(Object o) {
+
+ if (o == null) {
+ throw new NullPointerException();
+ }
+ if (permission == _PRINCIPALS && !(Principal.class.isAssignableFrom(o.getClass()))) {
+ throw new IllegalArgumentException("auth.0B"); //$NON-NLS-1$
+ }
+ }
+
+ /*
+ * verifies specified element, checks set state, and security permission
+ * to modify set before adding new element
+ */
+ @Override
+ public boolean add(SST o) {
+
+ verifyElement(o);
+
+ checkState();
+ checkPermission(permission);
+
+ if (!elements.contains(o)) {
+ elements.add(o);
+ return true;
+ }
+ return false;
+ }
+
+ // returns an instance of SecureIterator
+ @Override
+ public Iterator<SST> iterator() {
+
+ if (permission == _PRIVATE_CREDENTIALS) {
+ /*
+ * private credential set requires iterator with additional
+ * security check (PrivateCredentialPermission)
+ */
+ return new SecureIterator(elements.iterator()) {
+ /*
+ * checks permission to access next private credential moves
+ * to the next element even SecurityException was thrown
+ */
+ @Override
+ public SST next() {
+ SST obj = iterator.next();
+ checkPermission(new PrivateCredentialPermission(obj
+ .getClass().getName(), principals));
+ return obj;
+ }
+ };
+ }
+ return new SecureIterator(elements.iterator());
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+
+ if (c == null) {
+ throw new NullPointerException();
+ }
+ return super.retainAll(c);
+ }
+
+ @Override
+ public int size() {
+ return elements.size();
+ }
+
+ /**
+ * return set with elements that are instances or subclasses of the
+ * specified class
+ */
+ protected final <E> Set<E> get(final Class<E> c) {
+
+ if (c == null) {
+ throw new NullPointerException();
+ }
+
+ AbstractSet<E> s = new AbstractSet<E>() {
+ private LinkedList<E> elements = new LinkedList<E>();
+
+ @Override
+ public boolean add(E o) {
+
+ if (!c.isAssignableFrom(o.getClass())) {
+ throw new IllegalArgumentException(
+ "auth.0C " + c.getName()); //$NON-NLS-1$
+ }
+
+ if (elements.contains(o)) {
+ return false;
+ }
+ elements.add(o);
+ return true;
+ }
+
+ @Override
+ public Iterator<E> iterator() {
+ return elements.iterator();
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+
+ if (c == null) {
+ throw new NullPointerException();
+ }
+ return super.retainAll(c);
+ }
+
+ @Override
+ public int size() {
+ return elements.size();
+ }
+ };
+
+ // FIXME must have permissions for requested priv. credentials
+ for (Iterator<SST> it = iterator(); it.hasNext();) {
+ SST o = it.next();
+ if (c.isAssignableFrom(o.getClass())) {
+ s.add(c.cast(o));
+ }
+ }
+ return s;
+ }
+
+ private void readObject(ObjectInputStream in) throws IOException,
+ ClassNotFoundException {
+ in.defaultReadObject();
+
+ switch (setType) {
+ case SET_Principal:
+ permission = _PRINCIPALS;
+ break;
+ case SET_PrivCred:
+ permission = _PRIVATE_CREDENTIALS;
+ break;
+ case SET_PubCred:
+ permission = _PUBLIC_CREDENTIALS;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+
+ Iterator<SST> it = elements.iterator();
+ while (it.hasNext()) {
+ verifyElement(it.next());
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+
+ if (permission == _PRIVATE_CREDENTIALS) {
+ // does security check for each private credential
+ for (Iterator<SST> it = iterator(); it.hasNext();) {
+ it.next();
+ }
+ setType = SET_PrivCred;
+ } else if (permission == _PRINCIPALS) {
+ setType = SET_Principal;
+ } else {
+ setType = SET_PubCred;
+ }
+
+ out.defaultWriteObject();
+ }
+
+ /**
+ * Represents iterator for subject's secure set
+ */
+ private class SecureIterator implements Iterator<SST> {
+ protected Iterator<SST> iterator;
+
+ protected SecureIterator(Iterator<SST> iterator) {
+ this.iterator = iterator;
+ }
+
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ public SST next() {
+ return iterator.next();
+ }
+
+ /**
+ * checks set state, and security permission to modify set before
+ * removing current element
+ */
+ public void remove() {
+ checkState();
+ checkPermission(permission);
+ iterator.remove();
+ }
+ }
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/SubjectDomainCombiner.java b/src/org/apache/harmony/javax/security/auth/SubjectDomainCombiner.java
new file mode 100644
index 0000000..edbb672
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/SubjectDomainCombiner.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth;
+
+import java.security.DomainCombiner;
+import java.security.Principal;
+import java.security.ProtectionDomain;
+import java.util.Set;
+
+/**
+ * Merges permissions based on code source and code signers with permissions
+ * granted to the specified {@link Subject}.
+ */
+public class SubjectDomainCombiner implements DomainCombiner {
+
+ // subject to be associated
+ private Subject subject;
+
+ // permission required to get a subject object
+ private static final AuthPermission _GET = new AuthPermission(
+ "getSubjectFromDomainCombiner"); //$NON-NLS-1$
+
+ /**
+ * Creates a domain combiner for the entity provided in {@code subject}.
+ *
+ * @param subject
+ * the entity to which this domain combiner is associated.
+ */
+ public SubjectDomainCombiner(Subject subject) {
+ super();
+ if (subject == null) {
+ throw new NullPointerException();
+ }
+ this.subject = subject;
+ }
+
+ /**
+ * Returns the entity to which this domain combiner is associated.
+ *
+ * @return the entity to which this domain combiner is associated.
+ */
+ public Subject getSubject() {
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ sm.checkPermission(_GET);
+ }
+
+ return subject;
+ }
+
+ /**
+ * Merges the {@code ProtectionDomain} with the {@code Principal}s
+ * associated with the subject of this {@code SubjectDomainCombiner}.
+ *
+ * @param currentDomains
+ * the {@code ProtectionDomain}s associated with the context of
+ * the current thread. The domains must be sorted according to
+ * the execution order, the most recent residing at the
+ * beginning.
+ * @param assignedDomains
+ * the {@code ProtectionDomain}s from the parent thread based on
+ * code source and signers.
+ * @return a single {@code ProtectionDomain} array computed from the two
+ * provided arrays, or {@code null}.
+ * @see ProtectionDomain
+ */
+ public ProtectionDomain[] combine(ProtectionDomain[] currentDomains,
+ ProtectionDomain[] assignedDomains) {
+ // get array length for combining protection domains
+ int len = 0;
+ if (currentDomains != null) {
+ len += currentDomains.length;
+ }
+ if (assignedDomains != null) {
+ len += assignedDomains.length;
+ }
+ if (len == 0) {
+ return null;
+ }
+
+ ProtectionDomain[] pd = new ProtectionDomain[len];
+
+ // for each current domain substitute set of principal with subject's
+ int cur = 0;
+ if (currentDomains != null) {
+
+ Set<Principal> s = subject.getPrincipals();
+ Principal[] p = s.toArray(new Principal[s.size()]);
+
+ for (cur = 0; cur < currentDomains.length; cur++) {
+ if (currentDomains[cur] != null) {
+ ProtectionDomain newPD;
+ newPD = new ProtectionDomain(currentDomains[cur].getCodeSource(),
+ currentDomains[cur].getPermissions(), currentDomains[cur]
+ .getClassLoader(), p);
+ pd[cur] = newPD;
+ }
+ }
+ }
+
+ // copy assigned domains
+ if (assignedDomains != null) {
+ System.arraycopy(assignedDomains, 0, pd, cur, assignedDomains.length);
+ }
+
+ return pd;
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/Callback.java b/src/org/apache/harmony/javax/security/auth/callback/Callback.java
new file mode 100644
index 0000000..8fd745c
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/Callback.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+/**
+ * Defines an empty base interface for all {@code Callback}s used during
+ * authentication.
+ */
+public interface Callback {
+}
\ No newline at end of file
diff --git a/src/org/apache/harmony/javax/security/auth/callback/CallbackHandler.java b/src/org/apache/harmony/javax/security/auth/callback/CallbackHandler.java
new file mode 100644
index 0000000..d09fafa
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/CallbackHandler.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.IOException;
+
+/**
+ * Needs to be implemented by classes that want to handle authentication
+ * {@link Callback}s. A single method {@link #handle(Callback[])} must be
+ * provided that checks the type of the incoming {@code Callback}s and reacts
+ * accordingly. {@code CallbackHandler}s can be installed per application. It is
+ * also possible to configure a system-default {@code CallbackHandler} by
+ * setting the {@code auth.login.defaultCallbackHandler} property in the
+ * standard {@code security.properties} file.
+ */
+public interface CallbackHandler {
+
+ /**
+ * Handles the actual {@link Callback}. A {@code CallbackHandler} needs to
+ * implement this method. In the method, it is free to select which {@code
+ * Callback}s it actually wants to handle and in which way. For example, a
+ * console-based {@code CallbackHandler} might choose to sequentially ask
+ * the user for login and password, if it implements these {@code Callback}
+ * s, whereas a GUI-based one might open a single dialog window for both
+ * values. If a {@code CallbackHandler} is not able to handle a specific
+ * {@code Callback}, it needs to throw an
+ * {@link UnsupportedCallbackException}.
+ *
+ * @param callbacks
+ * the array of {@code Callback}s that need handling
+ * @throws IOException
+ * if an I/O related error occurs
+ * @throws UnsupportedCallbackException
+ * if the {@code CallbackHandler} is not able to handle a
+ * specific {@code Callback}
+ */
+ void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException;
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/ChoiceCallback.java b/src/org/apache/harmony/javax/security/auth/callback/ChoiceCallback.java
new file mode 100644
index 0000000..1e53fb6
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/ChoiceCallback.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+
+
+
+public class ChoiceCallback implements Callback, Serializable {
+
+ private static final long serialVersionUID = -3975664071579892167L;
+
+ private int defaultChoice;
+
+ private String prompt;
+
+ private boolean multipleSelectionsAllowed;
+
+ private String[] choices;
+
+ private int[] selections;
+
+ private void setChoices(String[] choices) {
+ if (choices == null || choices.length == 0) {
+ throw new IllegalArgumentException("auth.1C"); //$NON-NLS-1$
+ }
+ for (int i = 0; i < choices.length; i++) {
+ if (choices[i] == null || choices[i].length() == 0) {
+ throw new IllegalArgumentException("auth.1C"); //$NON-NLS-1$
+ }
+ }
+ //FIXME: System.arraycopy(choices, 0 , new String[choices.length], 0, choices.length);
+ this.choices = choices;
+
+ }
+
+ private void setPrompt(String prompt) {
+ if (prompt == null || prompt.length() == 0) {
+ throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+ }
+ this.prompt = prompt;
+ }
+
+ private void setDefaultChoice(int defaultChoice) {
+ if (0 > defaultChoice || defaultChoice >= choices.length) {
+ throw new IllegalArgumentException("auth.1D"); //$NON-NLS-1$
+ }
+ this.defaultChoice = defaultChoice;
+ }
+
+ public ChoiceCallback(String prompt, String[] choices, int defaultChoice,
+ boolean multipleSelectionsAllowed) {
+ super();
+ setPrompt(prompt);
+ setChoices(choices);
+ setDefaultChoice(defaultChoice);
+ this.multipleSelectionsAllowed = multipleSelectionsAllowed;
+ }
+
+ public boolean allowMultipleSelections() {
+ return multipleSelectionsAllowed;
+ }
+
+ public String[] getChoices() {
+ return choices;
+ }
+
+ public int getDefaultChoice() {
+ return defaultChoice;
+ }
+
+ public String getPrompt() {
+ return prompt;
+ }
+
+ public int[] getSelectedIndexes() {
+ return selections;
+ }
+
+ public void setSelectedIndex(int selection) {
+ this.selections = new int[1];
+ this.selections[0] = selection;
+ }
+
+ public void setSelectedIndexes(int[] selections) {
+ if (!multipleSelectionsAllowed) {
+ throw new UnsupportedOperationException();
+ }
+ this.selections = selections;
+ //FIXME:
+ // this.selections = new int[selections.length]
+ //System.arraycopy(selections, 0, this.selections, 0, this.selections.length);
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/ConfirmationCallback.java b/src/org/apache/harmony/javax/security/auth/callback/ConfirmationCallback.java
new file mode 100644
index 0000000..a1893f3
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/ConfirmationCallback.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+
+
+
+public class ConfirmationCallback implements Callback, Serializable {
+
+ private static final long serialVersionUID = -9095656433782481624L;
+
+ public static final int YES = 0; // default options
+
+ public static final int NO = 1;
+
+ public static final int CANCEL = 2;
+
+ public static final int OK = 3;
+
+ public static final int YES_NO_OPTION = 0; // options type
+
+ public static final int YES_NO_CANCEL_OPTION = 1;
+
+ public static final int OK_CANCEL_OPTION = 2;
+
+ public static final int UNSPECIFIED_OPTION = -1;
+
+ public static final int INFORMATION = 0; // messages type
+
+ public static final int WARNING = 1;
+
+ public static final int ERROR = 2;
+
+ private String prompt;
+
+ private int messageType;
+
+ private int optionType = UNSPECIFIED_OPTION;
+
+ private int defaultOption;
+
+ private String[] options;
+
+ private int selection;
+
+ public ConfirmationCallback(int messageType, int optionType, int defaultOption) {
+ super();
+ if (messageType > ERROR || messageType < INFORMATION) {
+ throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$
+ }
+
+ switch (optionType) {
+ case YES_NO_OPTION:
+ if (defaultOption != YES && defaultOption != NO) {
+ throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+ }
+ break;
+ case YES_NO_CANCEL_OPTION:
+ if (defaultOption != YES && defaultOption != NO && defaultOption != CANCEL) {
+ throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+ }
+ break;
+ case OK_CANCEL_OPTION:
+ if (defaultOption != OK && defaultOption != CANCEL) {
+ throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("auth.18"); //$NON-NLS-1$
+ }
+ this.messageType = messageType;
+ this.optionType = optionType;
+ this.defaultOption = defaultOption;
+ }
+
+ public ConfirmationCallback(int messageType, String[] options, int defaultOption) {
+ super();
+ if (messageType > ERROR || messageType < INFORMATION) {
+ throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$
+ }
+
+ if (options == null || options.length == 0) {
+ throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$
+ }
+ for (int i = 0; i < options.length; i++) {
+ if (options[i] == null || options[i].length() == 0) {
+ throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$
+ }
+ }
+ if (0 > defaultOption || defaultOption >= options.length) {
+ throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+ }
+ // FIXME:System.arraycopy(options, 0 , new String[this.options.length],
+ // 0, this.options.length);
+ this.options = options;
+ this.defaultOption = defaultOption;
+ this.messageType = messageType;
+ }
+
+ public ConfirmationCallback(String prompt, int messageType, int optionType,
+ int defaultOption) {
+ super();
+ if (prompt == null || prompt.length() == 0) {
+ throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+ }
+
+ if (messageType > ERROR || messageType < INFORMATION) {
+ throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$
+ }
+
+ switch (optionType) {
+ case YES_NO_OPTION:
+ if (defaultOption != YES && defaultOption != NO) {
+ throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+ }
+ break;
+ case YES_NO_CANCEL_OPTION:
+ if (defaultOption != YES && defaultOption != NO && defaultOption != CANCEL) {
+ throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+ }
+ break;
+ case OK_CANCEL_OPTION:
+ if (defaultOption != OK && defaultOption != CANCEL) {
+ throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("auth.18"); //$NON-NLS-1$
+ }
+ this.prompt = prompt;
+ this.messageType = messageType;
+ this.optionType = optionType;
+ this.defaultOption = defaultOption;
+ }
+
+ public ConfirmationCallback(String prompt, int messageType, String[] options,
+ int defaultOption) {
+ super();
+ if (prompt == null || prompt.length() == 0) {
+ throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+ }
+
+ if (messageType > ERROR || messageType < INFORMATION) {
+ throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$
+ }
+
+ if (options == null || options.length == 0) {
+ throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$
+ }
+ for (int i = 0; i < options.length; i++) {
+ if (options[i] == null || options[i].length() == 0) {
+ throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$
+ }
+ }
+ if (0 > defaultOption || defaultOption >= options.length) {
+ throw new IllegalArgumentException("auth.17"); //$NON-NLS-1$
+ }
+ // FIXME:System.arraycopy(options, 0 , new String[this.options.length],
+ // 0, this.options.length);
+ this.options = options;
+ this.defaultOption = defaultOption;
+ this.messageType = messageType;
+ this.prompt = prompt;
+ }
+
+ public String getPrompt() {
+ return prompt;
+ }
+
+ public int getMessageType() {
+ return messageType;
+ }
+
+ public int getDefaultOption() {
+ return defaultOption;
+ }
+
+ public String[] getOptions() {
+ return options;
+ }
+
+ public int getOptionType() {
+ return optionType;
+ }
+
+ public int getSelectedIndex() {
+ return selection;
+ }
+
+ public void setSelectedIndex(int selection) {
+ if (options != null) {
+ if (0 <= selection && selection <= options.length) {
+ this.selection = selection;
+ } else {
+ throw new ArrayIndexOutOfBoundsException("auth.1B"); //$NON-NLS-1$
+ }
+ } else {
+ switch (optionType) {
+ case YES_NO_OPTION:
+ if (selection != YES && selection != NO) {
+ throw new IllegalArgumentException("auth.19"); //$NON-NLS-1$
+ }
+ break;
+ case YES_NO_CANCEL_OPTION:
+ if (selection != YES && selection != NO && selection != CANCEL) {
+ throw new IllegalArgumentException("auth.19"); //$NON-NLS-1$
+ }
+ break;
+ case OK_CANCEL_OPTION:
+ if (selection != OK && selection != CANCEL) {
+ throw new IllegalArgumentException("auth.19"); //$NON-NLS-1$
+ }
+ break;
+ }
+ this.selection = selection;
+ }
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/LanguageCallback.java b/src/org/apache/harmony/javax/security/auth/callback/LanguageCallback.java
new file mode 100644
index 0000000..729bb49
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/LanguageCallback.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+import java.util.Locale;
+
+public class LanguageCallback implements Callback, Serializable {
+
+ private static final long serialVersionUID = 2019050433478903213L;
+
+ private Locale locale;
+
+ public LanguageCallback() {
+ super();
+ }
+
+ public Locale getLocale() {
+ return locale;
+ }
+
+ public void setLocale(Locale locale) {
+ this.locale = locale;
+ }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/NameCallback.java b/src/org/apache/harmony/javax/security/auth/callback/NameCallback.java
new file mode 100644
index 0000000..97264ef
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/NameCallback.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+
+
+
+public class NameCallback implements Callback, Serializable {
+
+ private static final long serialVersionUID = 3770938795909392253L;
+
+ private String prompt;
+
+ private String defaultName;
+
+ private String inputName;
+
+ private void setPrompt(String prompt) {
+ if (prompt == null || prompt.length() == 0) {
+ throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+ }
+ this.prompt = prompt;
+ }
+
+ private void setDefaultName(String defaultName) {
+ if (defaultName == null || defaultName.length() == 0) {
+ throw new IllegalArgumentException("auth.1E"); //$NON-NLS-1$
+ }
+ this.defaultName = defaultName;
+ }
+
+ public NameCallback(String prompt) {
+ super();
+ setPrompt(prompt);
+ }
+
+ public NameCallback(String prompt, String defaultName) {
+ super();
+ setPrompt(prompt);
+ setDefaultName(defaultName);
+ }
+
+ public String getPrompt() {
+ return prompt;
+ }
+
+ public String getDefaultName() {
+ return defaultName;
+ }
+
+ public void setName(String name) {
+ this.inputName = name;
+ }
+
+ public String getName() {
+ return inputName;
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/PasswordCallback.java b/src/org/apache/harmony/javax/security/auth/callback/PasswordCallback.java
new file mode 100644
index 0000000..bd142fc
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/PasswordCallback.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+import java.util.Arrays;
+
+
+
+/**
+ * Is used in conjunction with a {@link CallbackHandler} to retrieve a password
+ * when needed.
+ */
+public class PasswordCallback implements Callback, Serializable {
+
+ private static final long serialVersionUID = 2267422647454909926L;
+
+ private String prompt;
+
+ boolean echoOn;
+
+ private char[] inputPassword;
+
+ private void setPrompt(String prompt) throws IllegalArgumentException {
+ if (prompt == null || prompt.length() == 0) {
+ throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+ }
+ this.prompt = prompt;
+ }
+
+ /**
+ * Creates a new {@code PasswordCallback} instance.
+ *
+ * @param prompt
+ * the message that should be displayed to the user
+ * @param echoOn
+ * determines whether the user input should be echoed
+ */
+ public PasswordCallback(String prompt, boolean echoOn) {
+ super();
+ setPrompt(prompt);
+ this.echoOn = echoOn;
+ }
+
+ /**
+ * Returns the prompt that was specified when creating this {@code
+ * PasswordCallback}
+ *
+ * @return the prompt
+ */
+ public String getPrompt() {
+ return prompt;
+ }
+
+ /**
+ * Queries whether this {@code PasswordCallback} expects user input to be
+ * echoed, which is specified during the creation of the object.
+ *
+ * @return {@code true} if (and only if) user input should be echoed
+ */
+ public boolean isEchoOn() {
+ return echoOn;
+ }
+
+ /**
+ * Sets the password. The {@link CallbackHandler} that performs the actual
+ * provisioning or input of the password needs to call this method to hand
+ * back the password to the security service that requested it.
+ *
+ * @param password
+ * the password. A copy of this is stored, so subsequent changes
+ * to the input array do not affect the {@code PasswordCallback}.
+ */
+ public void setPassword(char[] password) {
+ if (password == null) {
+ this.inputPassword = password;
+ } else {
+ inputPassword = new char[password.length];
+ System.arraycopy(password, 0, inputPassword, 0, inputPassword.length);
+ }
+ }
+
+ /**
+ * Returns the password. The security service that needs the password
+ * usually calls this method once the {@link CallbackHandler} has finished
+ * its work.
+ *
+ * @return the password. A copy of the internal password is created and
+ * returned, so subsequent changes to the internal password do not
+ * affect the result.
+ */
+ public char[] getPassword() {
+ if (inputPassword != null) {
+ char[] tmp = new char[inputPassword.length];
+ System.arraycopy(inputPassword, 0, tmp, 0, tmp.length);
+ return tmp;
+ }
+ return null;
+ }
+
+ /**
+ * Clears the password stored in this {@code PasswordCallback}.
+ */
+ public void clearPassword() {
+ if (inputPassword != null) {
+ Arrays.fill(inputPassword, '\u0000');
+ }
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/TextInputCallback.java b/src/org/apache/harmony/javax/security/auth/callback/TextInputCallback.java
new file mode 100644
index 0000000..c7de222
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/TextInputCallback.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+
+
+
+public class TextInputCallback implements Callback, Serializable {
+
+ private static final long serialVersionUID = -8064222478852811804L;
+
+ private String defaultText;
+
+ private String prompt;
+
+ private String inputText;
+
+ private void setPrompt(String prompt) {
+ if (prompt == null || prompt.length() == 0) {
+ throw new IllegalArgumentException("auth.14"); //$NON-NLS-1$
+ }
+ this.prompt = prompt;
+ }
+
+ private void setDefaultText(String defaultText) {
+ if (defaultText == null || defaultText.length() == 0) {
+ throw new IllegalArgumentException("auth.15"); //$NON-NLS-1$
+ }
+ this.defaultText = defaultText;
+ }
+
+ public TextInputCallback(String prompt) {
+ super();
+ setPrompt(prompt);
+ }
+
+ public TextInputCallback(String prompt, String defaultText) {
+ super();
+ setPrompt(prompt);
+ setDefaultText(defaultText);
+ }
+
+ public String getDefaultText() {
+ return defaultText;
+ }
+
+ public String getPrompt() {
+ return prompt;
+ }
+
+ public String getText() {
+ return inputText;
+ }
+
+ public void setText(String text) {
+ this.inputText = text;
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/TextOutputCallback.java b/src/org/apache/harmony/javax/security/auth/callback/TextOutputCallback.java
new file mode 100644
index 0000000..23a72fa
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/TextOutputCallback.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+import java.io.Serializable;
+
+
+
+public class TextOutputCallback implements Callback, Serializable {
+
+ private static final long serialVersionUID = 1689502495511663102L;
+
+ public static final int INFORMATION = 0;
+
+ public static final int WARNING = 1;
+
+ public static final int ERROR = 2;
+
+ private String message;
+
+ private int messageType;
+
+ public TextOutputCallback(int messageType, String message) {
+ if (messageType > ERROR || messageType < INFORMATION) {
+ throw new IllegalArgumentException("auth.16"); //$NON-NLS-1$
+ }
+ if (message == null || message.length() == 0) {
+ throw new IllegalArgumentException("auth.1F"); //$NON-NLS-1$
+ }
+ this.messageType = messageType;
+ this.message = message;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public int getMessageType() {
+ return messageType;
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/callback/UnsupportedCallbackException.java b/src/org/apache/harmony/javax/security/auth/callback/UnsupportedCallbackException.java
new file mode 100644
index 0000000..19f6e40
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/callback/UnsupportedCallbackException.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.callback;
+
+/**
+ * Thrown when a {@link CallbackHandler} does not support a particular {@link
+ * Callback}.
+ */
+public class UnsupportedCallbackException extends Exception {
+
+ private static final long serialVersionUID = -6873556327655666839L;
+
+ private Callback callback;
+
+ /**
+ * Creates a new exception instance and initializes it with just the
+ * unsupported {@code Callback}, but no error message.
+ *
+ * @param callback
+ * the {@code Callback}
+ */
+ public UnsupportedCallbackException(Callback callback) {
+ super();
+ this.callback = callback;
+ }
+
+ /**
+ * Creates a new exception instance and initializes it with both the
+ * unsupported {@code Callback} and an error message.
+ *
+ * @param callback
+ * the {@code Callback}
+ * @param message
+ * the error message
+ */
+ public UnsupportedCallbackException(Callback callback, String message) {
+ super(message);
+ this.callback = callback;
+ }
+
+ /**
+ * Returns the unsupported {@code Callback} that triggered this exception.
+ *
+ * @return the {@code Callback}
+ */
+ public Callback getCallback() {
+ return callback;
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/AccountException.java b/src/org/apache/harmony/javax/security/auth/login/AccountException.java
new file mode 100644
index 0000000..c86e801
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/AccountException.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class AccountException extends LoginException {
+
+ private static final long serialVersionUID = -2112878680072211787L;
+
+ public AccountException() {
+ super();
+ }
+
+ public AccountException(String message) {
+ super(message);
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/AccountExpiredException.java b/src/org/apache/harmony/javax/security/auth/login/AccountExpiredException.java
new file mode 100644
index 0000000..1e0ce4d
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/AccountExpiredException.java
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class AccountExpiredException extends AccountException {
+
+ private static final long serialVersionUID = -6064064890162661560L;
+
+ public AccountExpiredException() {
+ super();
+ }
+
+ public AccountExpiredException(String message) {
+ super(message);
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/AccountLockedException.java b/src/org/apache/harmony/javax/security/auth/login/AccountLockedException.java
new file mode 100644
index 0000000..47913a5
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/AccountLockedException.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class AccountLockedException extends AccountException {
+
+ private static final long serialVersionUID = 8280345554014066334L;
+
+ public AccountLockedException() {
+ super();
+ }
+
+ public AccountLockedException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/AccountNotFoundException.java b/src/org/apache/harmony/javax/security/auth/login/AccountNotFoundException.java
new file mode 100644
index 0000000..8ca9b96
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/AccountNotFoundException.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class AccountNotFoundException extends AccountException {
+
+ private static final long serialVersionUID = 1498349563916294614L;
+
+ public AccountNotFoundException() {
+ super();
+ }
+
+ public AccountNotFoundException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/AppConfigurationEntry.java b/src/org/apache/harmony/javax/security/auth/login/AppConfigurationEntry.java
new file mode 100644
index 0000000..2a735dc
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/AppConfigurationEntry.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+import java.util.Collections;
+import java.util.Map;
+
+
+
+public class AppConfigurationEntry {
+
+ // the login module options
+ private final Map<String, ?> options;
+
+ // the control flag
+ private final AppConfigurationEntry.LoginModuleControlFlag controlFlag;
+
+ // the login module name
+ private final String loginModuleName;
+
+ public AppConfigurationEntry(String loginModuleName,
+ AppConfigurationEntry.LoginModuleControlFlag controlFlag, Map<String, ?> options) {
+
+ if (loginModuleName == null || loginModuleName.length() == 0) {
+ throw new IllegalArgumentException("auth.26"); //$NON-NLS-1$
+ }
+
+ if (controlFlag == null) {
+ throw new IllegalArgumentException("auth.27"); //$NON-NLS-1$
+ }
+
+ if (options == null) {
+ throw new IllegalArgumentException("auth.1A"); //$NON-NLS-1$
+ }
+
+ this.loginModuleName = loginModuleName;
+ this.controlFlag = controlFlag;
+ this.options = Collections.unmodifiableMap(options);
+ }
+
+ public String getLoginModuleName() {
+ return loginModuleName;
+ }
+
+ public LoginModuleControlFlag getControlFlag() {
+ return controlFlag;
+ }
+
+ public Map<java.lang.String, ?> getOptions() {
+ return options;
+ }
+
+ public static class LoginModuleControlFlag {
+
+ // the control flag
+ private final String flag;
+
+ public static final LoginModuleControlFlag REQUIRED = new LoginModuleControlFlag(
+ "LoginModuleControlFlag: required"); //$NON-NLS-1$
+
+ public static final LoginModuleControlFlag REQUISITE = new LoginModuleControlFlag(
+ "LoginModuleControlFlag: requisite"); //$NON-NLS-1$
+
+ public static final LoginModuleControlFlag OPTIONAL = new LoginModuleControlFlag(
+ "LoginModuleControlFlag: optional"); //$NON-NLS-1$
+
+ public static final LoginModuleControlFlag SUFFICIENT = new LoginModuleControlFlag(
+ "LoginModuleControlFlag: sufficient"); //$NON-NLS-1$
+
+ // Creates the LoginModuleControlFlag object with specified a flag
+ private LoginModuleControlFlag(String flag) {
+ this.flag = flag;
+ }
+
+ @Override
+ public String toString() {
+ return flag;
+ }
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/Configuration.java b/src/org/apache/harmony/javax/security/auth/login/Configuration.java
new file mode 100644
index 0000000..74c371f
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/Configuration.java
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+import java.security.AccessController;
+import org.apache.harmony.javax.security.auth.AuthPermission;
+
+public abstract class Configuration {
+
+ // the current configuration
+ private static Configuration configuration;
+
+ // creates a AuthPermission object with a specify property
+ private static final AuthPermission GET_LOGIN_CONFIGURATION = new AuthPermission(
+ "getLoginConfiguration"); //$NON-NLS-1$
+
+ // creates a AuthPermission object with a specify property
+ private static final AuthPermission SET_LOGIN_CONFIGURATION = new AuthPermission(
+ "setLoginConfiguration"); //$NON-NLS-1$
+
+ // Key to security properties, defining default configuration provider.
+ private static final String LOGIN_CONFIGURATION_PROVIDER = "login.configuration.provider"; //$NON-NLS-1$
+
+ protected Configuration() {
+ super();
+ }
+
+ public static Configuration getConfiguration() {
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ sm.checkPermission(GET_LOGIN_CONFIGURATION);
+ }
+ return getAccessibleConfiguration();
+ }
+
+ /**
+ * Reads name of default configuration provider from security.properties,
+ * loads the class and instantiates the provider.<br> In case of any
+ * exception, wraps it with SecurityException and throws further.
+ */
+ private static final Configuration getDefaultProvider() {
+ return new Configuration() {
+
+ @Override
+ public void refresh() {
+ }
+
+ @Override
+ public AppConfigurationEntry[] getAppConfigurationEntry(
+ String applicationName) {
+ return new AppConfigurationEntry[0];
+ }
+ };
+ }
+
+ /**
+ * Shortcut accessor for friendly classes, to skip security checks.
+ * If active configuration was set to <code>null</code>, tries to load a default
+ * provider, so this method never returns <code>null</code>. <br>
+ * This method is synchronized with setConfiguration()
+ */
+ static Configuration getAccessibleConfiguration() {
+ Configuration current = configuration;
+ if (current == null) {
+ synchronized (Configuration.class) {
+ if (configuration == null) {
+ configuration = getDefaultProvider();
+ }
+ return configuration;
+ }
+ }
+ return current;
+ }
+
+ public static void setConfiguration(Configuration configuration) {
+ SecurityManager sm = System.getSecurityManager();
+ if (sm != null) {
+ sm.checkPermission(SET_LOGIN_CONFIGURATION);
+ }
+ Configuration.configuration = configuration;
+ }
+
+ public abstract AppConfigurationEntry[] getAppConfigurationEntry(String applicationName);
+
+ public abstract void refresh();
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/CredentialException.java b/src/org/apache/harmony/javax/security/auth/login/CredentialException.java
new file mode 100644
index 0000000..e74e866
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/CredentialException.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class CredentialException extends LoginException {
+
+ private static final long serialVersionUID = -4772893876810601859L;
+
+ public CredentialException() {
+ super();
+ }
+
+ public CredentialException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/CredentialExpiredException.java b/src/org/apache/harmony/javax/security/auth/login/CredentialExpiredException.java
new file mode 100644
index 0000000..3ca3ad7
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/CredentialExpiredException.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class CredentialExpiredException extends CredentialException {
+
+ private static final long serialVersionUID = -5344739593859737937L;
+
+ public CredentialExpiredException() {
+ super();
+ }
+
+ public CredentialExpiredException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/CredentialNotFoundException.java b/src/org/apache/harmony/javax/security/auth/login/CredentialNotFoundException.java
new file mode 100644
index 0000000..ffd529f
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/CredentialNotFoundException.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class CredentialNotFoundException extends CredentialException {
+
+ private static final long serialVersionUID = -7779934467214319475L;
+
+ public CredentialNotFoundException() {
+ super();
+ }
+
+ public CredentialNotFoundException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/FailedLoginException.java b/src/org/apache/harmony/javax/security/auth/login/FailedLoginException.java
new file mode 100644
index 0000000..f689d99
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/FailedLoginException.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+public class FailedLoginException extends LoginException {
+
+ private static final long serialVersionUID = 802556922354616286L;
+
+ public FailedLoginException() {
+ super();
+ }
+
+ public FailedLoginException(String message) {
+ super(message);
+ }
+
+}
\ No newline at end of file
diff --git a/src/org/apache/harmony/javax/security/auth/login/LoginContext.java b/src/org/apache/harmony/javax/security/auth/login/LoginContext.java
new file mode 100644
index 0000000..7d46278
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/LoginContext.java
@@ -0,0 +1,548 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+import java.io.IOException;
+import java.security.AccessController;
+import java.security.AccessControlContext;
+import java.security.PrivilegedExceptionAction;
+import java.security.PrivilegedActionException;
+
+import java.security.Security;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.harmony.javax.security.auth.Subject;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
+import org.apache.harmony.javax.security.auth.spi.LoginModule;
+import org.apache.harmony.javax.security.auth.AuthPermission;
+
+import org.apache.harmony.javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
+
+
+
+public class LoginContext {
+
+ private static final String DEFAULT_CALLBACK_HANDLER_PROPERTY = "auth.login.defaultCallbackHandler"; //$NON-NLS-1$
+
+ /*
+ * Integer constants which serve as a replacement for the corresponding
+ * LoginModuleControlFlag.* constants. These integers are used later as
+ * index in the arrays - see loginImpl() and logoutImpl() methods
+ */
+ private static final int OPTIONAL = 0;
+
+ private static final int REQUIRED = 1;
+
+ private static final int REQUISITE = 2;
+
+ private static final int SUFFICIENT = 3;
+
+ // Subject to be used for this LoginContext's operations
+ private Subject subject;
+
+ /*
+ * Shows whether the subject was specified by user (true) or was created by
+ * this LoginContext itself (false).
+ */
+ private boolean userProvidedSubject;
+
+ // Shows whether we use installed or user-provided Configuration
+ private boolean userProvidedConfig;
+
+ // An user's AccessControlContext, used when user specifies
+ private AccessControlContext userContext;
+
+ /*
+ * Either a callback handler passed by the user or a wrapper for the user's
+ * specified handler - see init() below.
+ */
+ private CallbackHandler callbackHandler;
+
+ /*
+ * An array which keeps the instantiated and init()-ialized login modules
+ * and their states
+ */
+ private Module[] modules;
+
+ // Stores a shared state
+ private Map<String, ?> sharedState;
+
+ // A context class loader used to load [mainly] LoginModules
+ private ClassLoader contextClassLoader;
+
+ // Shows overall status - whether this LoginContext was successfully logged
+ private boolean loggedIn;
+
+ public LoginContext(String name) throws LoginException {
+ super();
+ init(name, null, null, null);
+ }
+
+ public LoginContext(String name, CallbackHandler cbHandler) throws LoginException {
+ super();
+ if (cbHandler == null) {
+ throw new LoginException("auth.34"); //$NON-NLS-1$
+ }
+ init(name, null, cbHandler, null);
+ }
+
+ public LoginContext(String name, Subject subject) throws LoginException {
+ super();
+ if (subject == null) {
+ throw new LoginException("auth.03"); //$NON-NLS-1$
+ }
+ init(name, subject, null, null);
+ }
+
+ public LoginContext(String name, Subject subject, CallbackHandler cbHandler)
+ throws LoginException {
+ super();
+ if (subject == null) {
+ throw new LoginException("auth.03"); //$NON-NLS-1$
+ }
+ if (cbHandler == null) {
+ throw new LoginException("auth.34"); //$NON-NLS-1$
+ }
+ init(name, subject, cbHandler, null);
+ }
+
+ public LoginContext(String name, Subject subject, CallbackHandler cbHandler,
+ Configuration config) throws LoginException {
+ super();
+ init(name, subject, cbHandler, config);
+ }
+
+ // Does all the machinery needed for the initialization.
+ private void init(String name, Subject subject, final CallbackHandler cbHandler,
+ Configuration config) throws LoginException {
+ userProvidedSubject = (this.subject = subject) != null;
+
+ //
+ // Set config
+ //
+ if (name == null) {
+ throw new LoginException("auth.00"); //$NON-NLS-1$
+ }
+
+ if (config == null) {
+ config = Configuration.getAccessibleConfiguration();
+ } else {
+ userProvidedConfig = true;
+ }
+
+ SecurityManager sm = System.getSecurityManager();
+
+ if (sm != null && !userProvidedConfig) {
+ sm.checkPermission(new AuthPermission("createLoginContext." + name));//$NON-NLS-1$
+ }
+
+ AppConfigurationEntry[] entries = config.getAppConfigurationEntry(name);
+ if (entries == null) {
+ if (sm != null && !userProvidedConfig) {
+ sm.checkPermission(new AuthPermission("createLoginContext.other")); //$NON-NLS-1$
+ }
+ entries = config.getAppConfigurationEntry("other"); //$NON-NLS-1$
+ if (entries == null) {
+ throw new LoginException("auth.35 " + name); //$NON-NLS-1$
+ }
+ }
+
+ modules = new Module[entries.length];
+ for (int i = 0; i < modules.length; i++) {
+ modules[i] = new Module(entries[i]);
+ }
+ //
+ // Set CallbackHandler and this.contextClassLoader
+ //
+
+ /*
+ * as some of the operations to be executed (i.e. get*ClassLoader,
+ * getProperty, class loading) are security-checked, then combine all of
+ * them into a single doPrivileged() call.
+ */
+ try {
+ AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
+ public Void run() throws Exception {
+ // First, set the 'contextClassLoader'
+ contextClassLoader = Thread.currentThread().getContextClassLoader();
+ if (contextClassLoader == null) {
+ contextClassLoader = ClassLoader.getSystemClassLoader();
+ }
+ // then, checks whether the cbHandler is set
+ if (cbHandler == null) {
+ // well, let's try to find it
+ String klassName = Security
+ .getProperty(DEFAULT_CALLBACK_HANDLER_PROPERTY);
+ if (klassName == null || klassName.length() == 0) {
+ return null;
+ }
+ Class<?> klass = Class.forName(klassName, true, contextClassLoader);
+ callbackHandler = (CallbackHandler) klass.newInstance();
+ } else {
+ callbackHandler = cbHandler;
+ }
+ return null;
+ }
+ });
+ } catch (PrivilegedActionException ex) {
+ Throwable cause = ex.getCause();
+ throw (LoginException) new LoginException("auth.36").initCause(cause);//$NON-NLS-1$
+ }
+
+ if (userProvidedConfig) {
+ userContext = AccessController.getContext();
+ } else if (callbackHandler != null) {
+ userContext = AccessController.getContext();
+ callbackHandler = new ContextedCallbackHandler(callbackHandler);
+ }
+ }
+
+ public Subject getSubject() {
+ if (userProvidedSubject || loggedIn) {
+ return subject;
+ }
+ return null;
+ }
+
+ /**
+ * Warning: calling the method more than once may result in undefined
+ * behaviour if logout() method is not invoked before.
+ */
+ public void login() throws LoginException {
+ PrivilegedExceptionAction<Void> action = new PrivilegedExceptionAction<Void>() {
+ public Void run() throws LoginException {
+ loginImpl();
+ return null;
+ }
+ };
+ try {
+ if (userProvidedConfig) {
+ AccessController.doPrivileged(action, userContext);
+ } else {
+ AccessController.doPrivileged(action);
+ }
+ } catch (PrivilegedActionException ex) {
+ throw (LoginException) ex.getException();
+ }
+ }
+
+ /**
+ * The real implementation of login() method whose calls are wrapped into
+ * appropriate doPrivileged calls in login().
+ */
+ private void loginImpl() throws LoginException {
+ if (subject == null) {
+ subject = new Subject();
+ }
+
+ if (sharedState == null) {
+ sharedState = new HashMap<String, Object>();
+ }
+
+ // PHASE 1: Calling login()-s
+ Throwable firstProblem = null;
+
+ int[] logged = new int[4];
+ int[] total = new int[4];
+
+ for (Module module : modules) {
+ try {
+ // if a module fails during Class.forName(), then it breaks overall
+ // attempt - see catch() below
+ module.create(subject, callbackHandler, sharedState);
+
+ if (module.module.login()) {
+ ++total[module.getFlag()];
+ ++logged[module.getFlag()];
+ if (module.getFlag() == SUFFICIENT) {
+ break;
+ }
+ }
+ } catch (Throwable ex) {
+ if (firstProblem == null) {
+ firstProblem = ex;
+ }
+ if (module.klass == null) {
+ /*
+ * an exception occurred during class lookup - overall
+ * attempt must fail a little trick: increase the REQUIRED's
+ * number - this will look like a failed REQUIRED module
+ * later, so overall attempt will fail
+ */
+ ++total[REQUIRED];
+ break;
+ }
+ ++total[module.getFlag()];
+ // something happened after the class was loaded
+ if (module.getFlag() == REQUISITE) {
+ // ... and no need to walk down anymore
+ break;
+ }
+ }
+ }
+ // end of PHASE1,
+
+ // Let's decide whether we have either overall success or a total failure
+ boolean fail = true;
+
+ /*
+ * Note: 'failed[xxx]!=0' is not enough to check.
+ *
+ * Use 'logged[xx] != total[xx]' instead. This is because some modules
+ * might not be counted as 'failed' if an exception occurred during
+ * preload()/Class.forName()-ing. But, such modules still get counted in
+ * the total[].
+ */
+
+ // if any REQ* module failed - then it's failure
+ if (logged[REQUIRED] != total[REQUIRED] || logged[REQUISITE] != total[REQUISITE]) {
+ // fail = true;
+ } else {
+ if (total[REQUIRED] == 0 && total[REQUISITE] == 0) {
+ // neither REQUIRED nor REQUISITE was configured.
+ // must have at least one SUFFICIENT or OPTIONAL
+ if (logged[OPTIONAL] != 0 || logged[SUFFICIENT] != 0) {
+ fail = false;
+ }
+ //else { fail = true; }
+ } else {
+ fail = false;
+ }
+ }
+
+ int commited[] = new int[4];
+ // clear it
+ total[0] = total[1] = total[2] = total[3] = 0;
+ if (!fail) {
+ // PHASE 2:
+
+ for (Module module : modules) {
+ if (module.klass != null) {
+ ++total[module.getFlag()];
+ try {
+ module.module.commit();
+ ++commited[module.getFlag()];
+ } catch (Throwable ex) {
+ if (firstProblem == null) {
+ firstProblem = ex;
+ }
+ }
+ }
+ }
+ }
+
+ // need to decide once again
+ fail = true;
+ if (commited[REQUIRED] != total[REQUIRED] || commited[REQUISITE] != total[REQUISITE]) {
+ //fail = true;
+ } else {
+ if (total[REQUIRED] == 0 && total[REQUISITE] == 0) {
+ /*
+ * neither REQUIRED nor REQUISITE was configured. must have at
+ * least one SUFFICIENT or OPTIONAL
+ */
+ if (commited[OPTIONAL] != 0 || commited[SUFFICIENT] != 0) {
+ fail = false;
+ } else {
+ //fail = true;
+ }
+ } else {
+ fail = false;
+ }
+ }
+
+ if (fail) {
+ // either login() or commit() failed. aborting...
+
+ for (Module module : modules) {
+ try {
+ module.module.abort();
+ } catch ( /*LoginException*/Throwable ex) {
+ if (firstProblem == null) {
+ firstProblem = ex;
+ }
+ }
+ }
+ if (firstProblem instanceof PrivilegedActionException
+ && firstProblem.getCause() != null) {
+ firstProblem = firstProblem.getCause();
+ }
+ if (firstProblem instanceof LoginException) {
+ throw (LoginException) firstProblem;
+ }
+ throw (LoginException) new LoginException("auth.37").initCause(firstProblem); //$NON-NLS-1$
+ }
+ loggedIn = true;
+ }
+
+ public void logout() throws LoginException {
+ PrivilegedExceptionAction<Void> action = new PrivilegedExceptionAction<Void>() {
+ public Void run() throws LoginException {
+ logoutImpl();
+ return null;
+ }
+ };
+ try {
+ if (userProvidedConfig) {
+ AccessController.doPrivileged(action, userContext);
+ } else {
+ AccessController.doPrivileged(action);
+ }
+ } catch (PrivilegedActionException ex) {
+ throw (LoginException) ex.getException();
+ }
+ }
+
+ /**
+ * The real implementation of logout() method whose calls are wrapped into
+ * appropriate doPrivileged calls in logout().
+ */
+ private void logoutImpl() throws LoginException {
+ if (subject == null) {
+ throw new LoginException("auth.38"); //$NON-NLS-1$
+ }
+ loggedIn = false;
+ Throwable firstProblem = null;
+ int total = 0;
+ for (Module module : modules) {
+ try {
+ module.module.logout();
+ ++total;
+ } catch (Throwable ex) {
+ if (firstProblem == null) {
+ firstProblem = ex;
+ }
+ }
+ }
+ if (firstProblem != null || total == 0) {
+ if (firstProblem instanceof PrivilegedActionException
+ && firstProblem.getCause() != null) {
+ firstProblem = firstProblem.getCause();
+ }
+ if (firstProblem instanceof LoginException) {
+ throw (LoginException) firstProblem;
+ }
+ throw (LoginException) new LoginException("auth.37").initCause(firstProblem); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * <p>A class that servers as a wrapper for the CallbackHandler when we use
+ * installed Configuration, but not a passed one. See API docs on the
+ * LoginContext.</p>
+ *
+ * <p>Simply invokes the given handler with the given AccessControlContext.</p>
+ */
+ private class ContextedCallbackHandler implements CallbackHandler {
+ private final CallbackHandler hiddenHandlerRef;
+
+ ContextedCallbackHandler(CallbackHandler handler) {
+ super();
+ this.hiddenHandlerRef = handler;
+ }
+
+ public void handle(final Callback[] callbacks) throws IOException,
+ UnsupportedCallbackException {
+ try {
+ AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
+ public Void run() throws IOException, UnsupportedCallbackException {
+ hiddenHandlerRef.handle(callbacks);
+ return null;
+ }
+ }, userContext);
+ } catch (PrivilegedActionException ex) {
+ if (ex.getCause() instanceof UnsupportedCallbackException) {
+ throw (UnsupportedCallbackException) ex.getCause();
+ }
+ throw (IOException) ex.getCause();
+ }
+ }
+ }
+
+ /**
+ * A private class that stores an instantiated LoginModule.
+ */
+ private final class Module {
+
+ // An initial info about the module to be used
+ AppConfigurationEntry entry;
+
+ // A mapping of LoginModuleControlFlag onto a simple int constant
+ int flag;
+
+ // The LoginModule itself
+ LoginModule module;
+
+ // A class of the module
+ Class<?> klass;
+
+ Module(AppConfigurationEntry entry) {
+ this.entry = entry;
+ LoginModuleControlFlag flg = entry.getControlFlag();
+ if (flg == LoginModuleControlFlag.OPTIONAL) {
+ flag = OPTIONAL;
+ } else if (flg == LoginModuleControlFlag.REQUISITE) {
+ flag = REQUISITE;
+ } else if (flg == LoginModuleControlFlag.SUFFICIENT) {
+ flag = SUFFICIENT;
+ } else {
+ flag = REQUIRED;
+ //if(flg!=LoginModuleControlFlag.REQUIRED) throw new Error()
+ }
+ }
+
+ int getFlag() {
+ return flag;
+ }
+
+ /**
+ * Loads class of the LoginModule, instantiates it and then calls
+ * initialize().
+ */
+ void create(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState)
+ throws LoginException {
+ String klassName = entry.getLoginModuleName();
+ if (klass == null) {
+ try {
+ klass = Class.forName(klassName, false, contextClassLoader);
+ } catch (ClassNotFoundException ex) {
+ throw (LoginException) new LoginException(
+ "auth.39 " + klassName).initCause(ex); //$NON-NLS-1$
+ }
+ }
+
+ if (module == null) {
+ try {
+ module = (LoginModule) klass.newInstance();
+ } catch (IllegalAccessException ex) {
+ throw (LoginException) new LoginException(
+ "auth.3A " + klassName) //$NON-NLS-1$
+ .initCause(ex);
+ } catch (InstantiationException ex) {
+ throw (LoginException) new LoginException(
+ "auth.3A" + klassName) //$NON-NLS-1$
+ .initCause(ex);
+ }
+ module.initialize(subject, callbackHandler, sharedState, entry.getOptions());
+ }
+ }
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/auth/login/LoginException.java b/src/org/apache/harmony/javax/security/auth/login/LoginException.java
new file mode 100644
index 0000000..e9ea566
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/login/LoginException.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.login;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Base class for exceptions that are thrown when a login error occurs.
+ */
+public class LoginException extends GeneralSecurityException {
+
+ private static final long serialVersionUID = -4679091624035232488L;
+
+ /**
+ * Creates a new exception instance and initializes it with default values.
+ */
+ public LoginException() {
+ super();
+ }
+
+ /**
+ * Creates a new exception instance and initializes it with a given message.
+ *
+ * @param message the error message
+ */
+ public LoginException(String message) {
+ super(message);
+ }
+
+}
diff --git a/src/org/apache/harmony/javax/security/auth/spi/LoginModule.java b/src/org/apache/harmony/javax/security/auth/spi/LoginModule.java
new file mode 100644
index 0000000..3ed9eb2
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/auth/spi/LoginModule.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.auth.spi;
+
+import java.util.Map;
+
+import org.apache.harmony.javax.security.auth.Subject;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.login.LoginException;
+
+public interface LoginModule {
+
+ void initialize(Subject subject, CallbackHandler callbackHandler,
+ Map<String, ?> sharedState, Map<String, ?> options);
+
+ boolean login() throws LoginException;
+
+ boolean commit() throws LoginException;
+
+ boolean abort() throws LoginException;
+
+ boolean logout() throws LoginException;
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/AuthenticationException.java b/src/org/apache/harmony/javax/security/sasl/AuthenticationException.java
new file mode 100644
index 0000000..38703ef
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/AuthenticationException.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+public class AuthenticationException extends SaslException {
+
+ private static final long serialVersionUID = -3579708765071815007L;
+
+ public AuthenticationException() {
+ super();
+ }
+
+ public AuthenticationException(String detail) {
+ super(detail);
+ }
+
+ public AuthenticationException(String detail, Throwable ex) {
+ super(detail, ex);
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/AuthorizeCallback.java b/src/org/apache/harmony/javax/security/sasl/AuthorizeCallback.java
new file mode 100644
index 0000000..2ba90a2
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/AuthorizeCallback.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import java.io.Serializable;
+import org.apache.harmony.javax.security.auth.callback.Callback;
+
+public class AuthorizeCallback implements Callback, Serializable {
+
+ private static final long serialVersionUID = -2353344186490470805L;
+
+ /**
+ * Serialized field for storing authenticationID.
+ */
+ private final String authenticationID;
+
+ /**
+ * Serialized field for storing authorizationID.
+ */
+ private final String authorizationID;
+
+ /**
+ * Serialized field for storing authorizedID.
+ */
+ private String authorizedID;
+
+ /**
+ * Store authorized Serialized field.
+ */
+ private boolean authorized;
+
+ public AuthorizeCallback(String authnID, String authzID) {
+ super();
+ authenticationID = authnID;
+ authorizationID = authzID;
+ authorizedID = authzID;
+ }
+
+ public String getAuthenticationID() {
+ return authenticationID;
+ }
+
+ public String getAuthorizationID() {
+ return authorizationID;
+ }
+
+ public String getAuthorizedID() {
+ return (authorized ? authorizedID : null);
+ }
+
+ public boolean isAuthorized() {
+ return authorized;
+ }
+
+ public void setAuthorized(boolean ok) {
+ authorized = ok;
+ }
+
+ public void setAuthorizedID(String id) {
+ if (id != null) {
+ authorizedID = id;
+ }
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/RealmCallback.java b/src/org/apache/harmony/javax/security/sasl/RealmCallback.java
new file mode 100644
index 0000000..65b5d15
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/RealmCallback.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.TextInputCallback;
+
+public class RealmCallback extends TextInputCallback {
+
+ private static final long serialVersionUID = -4342673378785456908L;
+
+ public RealmCallback(String prompt) {
+ super(prompt);
+ }
+
+ public RealmCallback(String prompt, String defaultRealmInfo) {
+ super(prompt, defaultRealmInfo);
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/RealmChoiceCallback.java b/src/org/apache/harmony/javax/security/sasl/RealmChoiceCallback.java
new file mode 100644
index 0000000..079ea07
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/RealmChoiceCallback.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.ChoiceCallback;
+
+public class RealmChoiceCallback extends ChoiceCallback {
+
+ private static final long serialVersionUID = -8588141348846281332L;
+
+ public RealmChoiceCallback(String prompt, String[] choices, int defaultChoice,
+ boolean multiple) {
+ super(prompt, choices, defaultChoice, multiple);
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/Sasl.java b/src/org/apache/harmony/javax/security/sasl/Sasl.java
new file mode 100644
index 0000000..4d827f8
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/Sasl.java
@@ -0,0 +1,204 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import java.security.Provider;
+import java.security.Security;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+
+
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.HashSet;
+import java.util.Iterator;
+
+public class Sasl {
+ // SaslClientFactory service name
+ private static final String CLIENTFACTORYSRV = "SaslClientFactory"; //$NON-NLS-1$
+
+ // SaslServerFactory service name
+ private static final String SERVERFACTORYSRV = "SaslServerFactory"; //$NON-NLS-1$
+
+ public static final String POLICY_NOPLAINTEXT = "javax.security.sasl.policy.noplaintext"; //$NON-NLS-1$
+
+ public static final String POLICY_NOACTIVE = "javax.security.sasl.policy.noactive"; //$NON-NLS-1$
+
+ public static final String POLICY_NODICTIONARY = "javax.security.sasl.policy.nodictionary"; //$NON-NLS-1$
+
+ public static final String POLICY_NOANONYMOUS = "javax.security.sasl.policy.noanonymous"; //$NON-NLS-1$
+
+ public static final String POLICY_FORWARD_SECRECY = "javax.security.sasl.policy.forward"; //$NON-NLS-1$
+
+ public static final String POLICY_PASS_CREDENTIALS = "javax.security.sasl.policy.credentials"; //$NON-NLS-1$
+
+ public static final String MAX_BUFFER = "javax.security.sasl.maxbuffer"; //$NON-NLS-1$
+
+ public static final String RAW_SEND_SIZE = "javax.security.sasl.rawsendsize"; //$NON-NLS-1$
+
+ public static final String REUSE = "javax.security.sasl.reuse"; //$NON-NLS-1$
+
+ public static final String QOP = "javax.security.sasl.qop"; //$NON-NLS-1$
+
+ public static final String STRENGTH = "javax.security.sasl.strength"; //$NON-NLS-1$
+
+ public static final String SERVER_AUTH = "javax.security.sasl.server.authentication"; //$NON-NLS-1$
+
+ // Default public constructor is overridden
+ private Sasl() {
+ super();
+ }
+
+ // Forms new instance of factory
+ private static Object newInstance(String factoryName, Provider prv) throws SaslException {
+ String msg = "auth.31"; //$NON-NLS-1$
+ Object factory;
+ ClassLoader cl = prv.getClass().getClassLoader();
+ if (cl == null) {
+ cl = ClassLoader.getSystemClassLoader();
+ }
+ try {
+ factory = (Class.forName(factoryName, true, cl)).newInstance();
+ return factory;
+ } catch (IllegalAccessException e) {
+ throw new SaslException(msg + factoryName, e);
+ } catch (ClassNotFoundException e) {
+ throw new SaslException(msg + factoryName, e);
+ } catch (InstantiationException e) {
+ throw new SaslException(msg + factoryName, e);
+ }
+ }
+
+ /**
+ * This method forms the list of SaslClient/SaslServer factories which are
+ * implemented in used providers
+ */
+ private static Collection<?> findFactories(String service) {
+ HashSet<Object> fact = new HashSet<Object>();
+ Provider[] pp = Security.getProviders();
+ if ((pp == null) || (pp.length == 0)) {
+ return fact;
+ }
+ HashSet<String> props = new HashSet<String>();
+ for (int i = 0; i < pp.length; i++) {
+ String prName = pp[i].getName();
+ Enumeration<Object> keys = pp[i].keys();
+ while (keys.hasMoreElements()) {
+ String s = (String) keys.nextElement();
+ if (s.startsWith(service)) {
+ String prop = pp[i].getProperty(s);
+ try {
+ if (props.add(prName.concat(prop))) {
+ fact.add(newInstance(prop, pp[i]));
+ }
+ } catch (SaslException e) {
+ // ignore this factory
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ return fact;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Enumeration<SaslClientFactory> getSaslClientFactories() {
+ Collection<SaslClientFactory> res = (Collection<SaslClientFactory>) findFactories(CLIENTFACTORYSRV);
+ return Collections.enumeration(res);
+
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Enumeration<SaslServerFactory> getSaslServerFactories() {
+ Collection<SaslServerFactory> res = (Collection<SaslServerFactory>) findFactories(SERVERFACTORYSRV);
+ return Collections.enumeration(res);
+ }
+
+ public static SaslServer createSaslServer(String mechanism, String protocol,
+ String serverName, Map<String, ?> prop, CallbackHandler cbh) throws SaslException {
+ if (mechanism == null) {
+ throw new NullPointerException("auth.32"); //$NON-NLS-1$
+ }
+ Collection<?> res = findFactories(SERVERFACTORYSRV);
+ if (res.isEmpty()) {
+ return null;
+ }
+
+ Iterator<?> iter = res.iterator();
+ while (iter.hasNext()) {
+ SaslServerFactory fact = (SaslServerFactory) iter.next();
+ String[] mech = fact.getMechanismNames(null);
+ boolean is = false;
+ if (mech != null) {
+ for (int j = 0; j < mech.length; j++) {
+ if (mech[j].equals(mechanism)) {
+ is = true;
+ break;
+ }
+ }
+ }
+ if (is) {
+ SaslServer saslS = fact.createSaslServer(mechanism, protocol, serverName, prop,
+ cbh);
+ if (saslS != null) {
+ return saslS;
+ }
+ }
+ }
+ return null;
+ }
+
+ public static SaslClient createSaslClient(String[] mechanisms, String authanticationID,
+ String protocol, String serverName, Map<String, ?> prop, CallbackHandler cbh)
+ throws SaslException {
+ if (mechanisms == null) {
+ throw new NullPointerException("auth.33"); //$NON-NLS-1$
+ }
+ Collection<?> res = findFactories(CLIENTFACTORYSRV);
+ if (res.isEmpty()) {
+ return null;
+ }
+
+ Iterator<?> iter = res.iterator();
+ while (iter.hasNext()) {
+ SaslClientFactory fact = (SaslClientFactory) iter.next();
+ String[] mech = fact.getMechanismNames(null);
+ boolean is = false;
+ if (mech != null) {
+ for (int j = 0; j < mech.length; j++) {
+ for (int n = 0; n < mechanisms.length; n++) {
+ if (mech[j].equals(mechanisms[n])) {
+ is = true;
+ break;
+ }
+ }
+ }
+ }
+ if (is) {
+ SaslClient saslC = fact.createSaslClient(mechanisms, authanticationID,
+ protocol, serverName, prop, cbh);
+ if (saslC != null) {
+ return saslC;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/SaslClient.java b/src/org/apache/harmony/javax/security/sasl/SaslClient.java
new file mode 100644
index 0000000..e07ff53
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/SaslClient.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+public interface SaslClient {
+
+ void dispose() throws SaslException;
+
+ byte[] evaluateChallenge(byte[] challenge) throws SaslException;
+
+ String getMechanismName();
+
+ Object getNegotiatedProperty(String propName);
+
+ boolean hasInitialResponse();
+
+ boolean isComplete();
+
+ byte[] unwrap(byte[] incoming, int offset, int len) throws SaslException;
+
+ byte[] wrap(byte[] outgoing, int offset, int len) throws SaslException;
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/SaslClientFactory.java b/src/org/apache/harmony/javax/security/sasl/SaslClientFactory.java
new file mode 100644
index 0000000..e567ed3
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/SaslClientFactory.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import java.util.Map;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+
+public interface SaslClientFactory {
+
+ SaslClient createSaslClient(String[] mechanisms, String authorizationId, String protocol,
+ String serverName, Map<String, ?> props, CallbackHandler cbh) throws SaslException;
+
+ String[] getMechanismNames(Map<String, ?> props);
+
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/SaslException.java b/src/org/apache/harmony/javax/security/sasl/SaslException.java
new file mode 100644
index 0000000..1ab7b12
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/SaslException.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import java.io.IOException;
+
+public class SaslException extends IOException {
+
+ private static final long serialVersionUID = 4579784287983423626L;
+
+ /**
+ * Serialized field for storing initial cause
+ */
+ private Throwable _exception;
+
+ public SaslException() {
+ super();
+ }
+
+ public SaslException(String detail) {
+ super(detail);
+ }
+
+ public SaslException(String detail, Throwable ex) {
+ super(detail);
+ if (ex != null) {
+ super.initCause(ex);
+ _exception = ex;
+ }
+ }
+
+ @Override
+ public Throwable getCause() {
+ return _exception;
+ }
+
+ @Override
+ public Throwable initCause(Throwable cause) {
+ super.initCause(cause);
+ _exception = cause;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ if (_exception == null) {
+ return super.toString();
+ }
+ StringBuilder sb = new StringBuilder(super.toString());
+ sb.append(", caused by: "); //$NON-NLS-1$
+ sb.append(_exception.toString());
+ return sb.toString();
+ }
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/SaslServer.java b/src/org/apache/harmony/javax/security/sasl/SaslServer.java
new file mode 100644
index 0000000..f057a4b
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/SaslServer.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+public interface SaslServer {
+
+ void dispose() throws SaslException;
+
+ byte[] evaluateResponse(byte[] response) throws SaslException;
+
+ String getAuthorizationID();
+
+ String getMechanismName();
+
+ Object getNegotiatedProperty(String propName);
+
+ boolean isComplete();
+
+ byte[] unwrap(byte[] incoming, int offset, int len) throws SaslException;
+
+ byte[] wrap(byte[] outgoing, int offset, int len) throws SaslException;
+}
diff --git a/src/org/apache/harmony/javax/security/sasl/SaslServerFactory.java b/src/org/apache/harmony/javax/security/sasl/SaslServerFactory.java
new file mode 100644
index 0000000..d59530e
--- /dev/null
+++ b/src/org/apache/harmony/javax/security/sasl/SaslServerFactory.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.harmony.javax.security.sasl;
+
+import java.util.Map;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+
+public interface SaslServerFactory {
+
+ SaslServer createSaslServer(String mechanisms, String protocol, String serverName,
+ Map<String, ?> props, CallbackHandler cbh) throws SaslException;
+
+ String[] getMechanismNames(Map<String, ?> props);
+
+}
diff --git a/src/org/apache/qpid/management/common/sasl/CRAMMD5HashedSaslClientFactory.java b/src/org/apache/qpid/management/common/sasl/CRAMMD5HashedSaslClientFactory.java
new file mode 100644
index 0000000..5c33e40
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/CRAMMD5HashedSaslClientFactory.java
@@ -0,0 +1,59 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import de.measite.smack.Sasl;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslClientFactory;
+import org.apache.harmony.javax.security.sasl.SaslException;
+import java.util.Map;
+
+public class CRAMMD5HashedSaslClientFactory implements SaslClientFactory
+{
+ /** The name of this mechanism */
+ public static final String MECHANISM = "CRAM-MD5-HASHED";
+
+ public SaslClient createSaslClient(String[] mechanisms, String authorizationId, String protocol,
+ String serverName, Map<String, ?> props, CallbackHandler cbh)
+ throws SaslException
+ {
+ for (int i = 0; i < mechanisms.length; i++)
+ {
+ if (mechanisms[i].equals(MECHANISM))
+ {
+ if (cbh == null)
+ {
+ throw new SaslException("CallbackHandler must not be null");
+ }
+
+ String[] mechs = {"CRAM-MD5"};
+ return Sasl.createSaslClient(mechs, authorizationId, protocol, serverName, props, cbh);
+ }
+ }
+ return null;
+ }
+
+ public String[] getMechanismNames(Map props)
+ {
+ return new String[]{MECHANISM};
+ }
+}
diff --git a/src/org/apache/qpid/management/common/sasl/ClientSaslFactory.java b/src/org/apache/qpid/management/common/sasl/ClientSaslFactory.java
new file mode 100644
index 0000000..19162d8
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/ClientSaslFactory.java
@@ -0,0 +1,53 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslClientFactory;
+import org.apache.harmony.javax.security.sasl.SaslException;
+import java.util.Map;
+
+public class ClientSaslFactory implements SaslClientFactory
+{
+ public SaslClient createSaslClient(String[] mechs, String authorizationId, String protocol,
+ String serverName, Map props, CallbackHandler cbh)
+ throws SaslException
+ {
+ for (int i = 0; i < mechs.length; i++)
+ {
+ if (mechs[i].equals("PLAIN"))
+ {
+ return new PlainSaslClient(authorizationId, cbh);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Simple-minded implementation that ignores props
+ */
+ public String[] getMechanismNames(Map props)
+ {
+ return new String[]{"PLAIN"};
+ }
+
+}
diff --git a/src/org/apache/qpid/management/common/sasl/Constants.java b/src/org/apache/qpid/management/common/sasl/Constants.java
new file mode 100644
index 0000000..31010ba
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/Constants.java
@@ -0,0 +1,33 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+
+package org.apache.qpid.management.common.sasl;
+
+public class Constants
+{
+
+ public final static String MECH_CRAMMD5 = "CRAM-MD5";
+ public final static String MECH_PLAIN = "PLAIN";
+ public final static String SASL_CRAMMD5 = "SASL/CRAM-MD5";
+ public final static String SASL_PLAIN = "SASL/PLAIN";
+
+}
+
diff --git a/src/org/apache/qpid/management/common/sasl/JCAProvider.java b/src/org/apache/qpid/management/common/sasl/JCAProvider.java
new file mode 100644
index 0000000..5793dae
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/JCAProvider.java
@@ -0,0 +1,55 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.sasl.SaslClientFactory;
+import java.security.Provider;
+import java.util.Map;
+
+public class JCAProvider extends Provider
+{
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Creates the security provider with a map from SASL mechanisms to implementing factories.
+ *
+ * @param providerMap The map from SASL mechanims to implementing factory classes.
+ */
+ public JCAProvider(Map<String, Class<? extends SaslClientFactory>> providerMap)
+ {
+ super("AMQSASLProvider", 1.0, "A JCA provider that registers all "
+ + "AMQ SASL providers that want to be registered");
+ register(providerMap);
+ }
+
+ /**
+ * Registers client factory classes for a map of mechanism names to client factory classes.
+ *
+ * @param providerMap The map from SASL mechanims to implementing factory classes.
+ */
+ private void register(Map<String, Class<? extends SaslClientFactory>> providerMap)
+ {
+ for (Map.Entry<String, Class<? extends SaslClientFactory>> me : providerMap.entrySet())
+ {
+ put("SaslClientFactory." + me.getKey(), me.getValue().getName());
+ }
+ }
+}
diff --git a/src/org/apache/qpid/management/common/sasl/PlainSaslClient.java b/src/org/apache/qpid/management/common/sasl/PlainSaslClient.java
new file mode 100644
index 0000000..99a1d43
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/PlainSaslClient.java
@@ -0,0 +1,210 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.NameCallback;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
+import de.measite.smack.Sasl;
+import org.apache.harmony.javax.security.sasl.SaslClient;
+import org.apache.harmony.javax.security.sasl.SaslException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+
+public class PlainSaslClient implements SaslClient
+{
+
+ private boolean completed;
+ private CallbackHandler cbh;
+ private String authorizationID;
+ private String authenticationID;
+ private byte password[];
+ private static byte SEPARATOR = 0;
+
+ public PlainSaslClient(String authorizationID, CallbackHandler cbh) throws SaslException
+ {
+ completed = false;
+ this.cbh = cbh;
+ Object[] userInfo = getUserInfo();
+ this.authorizationID = authorizationID;
+ this.authenticationID = (String) userInfo[0];
+ this.password = (byte[]) userInfo[1];
+ if (authenticationID == null || password == null)
+ {
+ throw new SaslException("PLAIN: authenticationID and password must be specified");
+ }
+ }
+
+ public byte[] evaluateChallenge(byte[] challenge) throws SaslException
+ {
+ if (completed)
+ {
+ throw new IllegalStateException("PLAIN: authentication already " +
+ "completed");
+ }
+ completed = true;
+ try
+ {
+ byte authzid[] =
+ authorizationID == null ? null : authorizationID.getBytes("UTF8");
+ byte authnid[] = authenticationID.getBytes("UTF8");
+ byte response[] =
+ new byte[
+ password.length +
+ authnid.length +
+ 2 + // SEPARATOR
+ (authzid != null ? authzid.length : 0)
+ ];
+ int size = 0;
+ if (authzid != null) {
+ System.arraycopy(authzid, 0, response, 0, authzid.length);
+ size = authzid.length;
+ }
+ response[size++] = SEPARATOR;
+ System.arraycopy(authnid, 0, response, size, authnid.length);
+ size += authnid.length;
+ response[size++] = SEPARATOR;
+ System.arraycopy(password, 0, response, size, password.length);
+ clearPassword();
+ return response;
+ } catch (UnsupportedEncodingException e) {
+ throw new SaslException("PLAIN: Cannot get UTF-8 encoding of ids",
+ e);
+ }
+ }
+
+ public String getMechanismName()
+ {
+ return "PLAIN";
+ }
+
+ public boolean hasInitialResponse()
+ {
+ return true;
+ }
+
+ public boolean isComplete()
+ {
+ return completed;
+ }
+
+ public byte[] unwrap(byte[] incoming, int offset, int len) throws SaslException
+ {
+ if (completed) {
+ throw new IllegalStateException("PLAIN: this mechanism supports " +
+ "neither integrity nor privacy");
+ } else {
+ throw new IllegalStateException("PLAIN: authentication not " +
+ "completed");
+ }
+ }
+
+ public byte[] wrap(byte[] outgoing, int offset, int len) throws SaslException
+ {
+ if (completed)
+ {
+ throw new IllegalStateException("PLAIN: this mechanism supports " +
+ "neither integrity nor privacy");
+ }
+ else
+ {
+ throw new IllegalStateException("PLAIN: authentication not " +
+ "completed");
+ }
+ }
+
+ public Object getNegotiatedProperty(String propName)
+ {
+ if (completed)
+ {
+ if (propName.equals(Sasl.QOP))
+ {
+ return "auth";
+ }
+ else
+ {
+ return null;
+ }
+ }
+ else
+ {
+ throw new IllegalStateException("PLAIN: authentication not " +
+ "completed");
+ }
+ }
+
+ private void clearPassword()
+ {
+ if (password != null)
+ {
+ for (int i = 0 ; i < password.length ; i++)
+ {
+ password[i] = 0;
+ }
+ password = null;
+ }
+ }
+
+ public void dispose() throws SaslException
+ {
+ clearPassword();
+ }
+
+ protected void finalize()
+ {
+ clearPassword();
+ }
+
+ private Object[] getUserInfo() throws SaslException
+ {
+ try
+ {
+ final String userPrompt = "PLAIN authentication id: ";
+ final String pwPrompt = "PLAIN password: ";
+ NameCallback nameCb = new NameCallback(userPrompt);
+ PasswordCallback passwordCb = new PasswordCallback(pwPrompt, false);
+ cbh.handle(new Callback[] { nameCb, passwordCb });
+ String userid = nameCb.getName();
+ char pwchars[] = passwordCb.getPassword();
+ byte pwbytes[];
+ if (pwchars != null)
+ {
+ pwbytes = (new String(pwchars)).getBytes("UTF8");
+ passwordCb.clearPassword();
+ }
+ else
+ {
+ pwbytes = null;
+ }
+ return (new Object[] { userid, pwbytes });
+ }
+ catch (IOException e)
+ {
+ throw new SaslException("Cannot get password", e);
+ }
+ catch (UnsupportedCallbackException e)
+ {
+ throw new SaslException("Cannot get userid/password", e);
+ }
+ }
+}
diff --git a/src/org/apache/qpid/management/common/sasl/SaslProvider.java b/src/org/apache/qpid/management/common/sasl/SaslProvider.java
new file mode 100644
index 0000000..1eb44e3
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/SaslProvider.java
@@ -0,0 +1,35 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import java.security.Provider;
+
+public class SaslProvider extends Provider
+{
+ private static final long serialVersionUID = -6978096016899676466L;
+
+ public SaslProvider()
+ {
+ super("SaslClientFactory", 1.0, "SASL PLAIN CLIENT MECHANISM");
+ put("SaslClientFactory.PLAIN", "ClientSaslFactory");
+ }
+
+}
diff --git a/src/org/apache/qpid/management/common/sasl/UserPasswordCallbackHandler.java b/src/org/apache/qpid/management/common/sasl/UserPasswordCallbackHandler.java
new file mode 100644
index 0000000..a7886cf
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/UserPasswordCallbackHandler.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.NameCallback;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
+import java.io.IOException;
+
+public class UserPasswordCallbackHandler implements CallbackHandler
+{
+ private String user;
+ private char[] pwchars;
+
+ public UserPasswordCallbackHandler(String user, String password)
+ {
+ this.user = user;
+ this.pwchars = password.toCharArray();
+ }
+
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
+ {
+ for (int i = 0; i < callbacks.length; i++)
+ {
+ if (callbacks[i] instanceof NameCallback)
+ {
+ NameCallback ncb = (NameCallback) callbacks[i];
+ ncb.setName(user);
+ }
+ else if (callbacks[i] instanceof PasswordCallback)
+ {
+ PasswordCallback pcb = (PasswordCallback) callbacks[i];
+ pcb.setPassword(pwchars);
+ }
+ else
+ {
+ throw new UnsupportedCallbackException(callbacks[i]);
+ }
+ }
+ }
+
+ private void clearPassword()
+ {
+ if (pwchars != null)
+ {
+ for (int i = 0 ; i < pwchars.length ; i++)
+ {
+ pwchars[i] = 0;
+ }
+ pwchars = null;
+ }
+ }
+
+ protected void finalize()
+ {
+ clearPassword();
+ }
+}
diff --git a/src/org/apache/qpid/management/common/sasl/UsernameHashedPasswordCallbackHandler.java b/src/org/apache/qpid/management/common/sasl/UsernameHashedPasswordCallbackHandler.java
new file mode 100644
index 0000000..54d7374
--- /dev/null
+++ b/src/org/apache/qpid/management/common/sasl/UsernameHashedPasswordCallbackHandler.java
@@ -0,0 +1,107 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ */
+package org.apache.qpid.management.common.sasl;
+
+import org.apache.harmony.javax.security.auth.callback.Callback;
+import org.apache.harmony.javax.security.auth.callback.CallbackHandler;
+import org.apache.harmony.javax.security.auth.callback.NameCallback;
+import org.apache.harmony.javax.security.auth.callback.PasswordCallback;
+import org.apache.harmony.javax.security.auth.callback.UnsupportedCallbackException;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+
+public class UsernameHashedPasswordCallbackHandler implements CallbackHandler
+{
+ private String user;
+ private char[] pwchars;
+
+ public UsernameHashedPasswordCallbackHandler(String user, String password) throws Exception
+ {
+ this.user = user;
+ this.pwchars = getHash(password);
+ }
+
+ public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
+ {
+ for (int i = 0; i < callbacks.length; i++)
+ {
+ if (callbacks[i] instanceof NameCallback)
+ {
+ NameCallback ncb = (NameCallback) callbacks[i];
+ ncb.setName(user);
+ }
+ else if (callbacks[i] instanceof PasswordCallback)
+ {
+ PasswordCallback pcb = (PasswordCallback) callbacks[i];
+ pcb.setPassword(pwchars);
+ }
+ else
+ {
+ throw new UnsupportedCallbackException(callbacks[i]);
+ }
+ }
+ }
+
+
+ private void clearPassword()
+ {
+ if (pwchars != null)
+ {
+ for (int i = 0 ; i < pwchars.length ; i++)
+ {
+ pwchars[i] = 0;
+ }
+ pwchars = null;
+ }
+ }
+
+ protected void finalize()
+ {
+ clearPassword();
+ }
+
+ public static char[] getHash(String text) throws NoSuchAlgorithmException, UnsupportedEncodingException
+ {
+ byte[] data = text.getBytes("utf-8");
+
+ MessageDigest md = MessageDigest.getInstance("MD5");
+
+ for (byte b : data)
+ {
+ md.update(b);
+ }
+
+ byte[] digest = md.digest();
+
+ char[] hash = new char[digest.length ];
+
+ int index = 0;
+ for (byte b : digest)
+ {
+ hash[index++] = (char) b;
+ }
+
+ return hash;
+ }
+}
diff --git a/src/org/jivesoftware/smack/AbstractConnectionListener.java b/src/org/jivesoftware/smack/AbstractConnectionListener.java
new file mode 100644
index 0000000..69acf90
--- /dev/null
+++ b/src/org/jivesoftware/smack/AbstractConnectionListener.java
@@ -0,0 +1,46 @@
+/**
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jivesoftware.smack;
+
+/**
+ * The AbstractConnectionListener class provides an empty implementation for all
+ * methods defined by the {@link ConnectionListener} interface. This is a
+ * convenience class which should be used in case you do not need to implement
+ * all methods.
+ *
+ * @author Henning Staib
+ */
+public class AbstractConnectionListener implements ConnectionListener {
+
+ public void connectionClosed() {
+ // do nothing
+ }
+
+ public void connectionClosedOnError(Exception e) {
+ // do nothing
+ }
+
+ public void reconnectingIn(int seconds) {
+ // do nothing
+ }
+
+ public void reconnectionFailed(Exception e) {
+ // do nothing
+ }
+
+ public void reconnectionSuccessful() {
+ // do nothing
+ }
+
+}
diff --git a/src/org/jivesoftware/smack/AccountManager.java b/src/org/jivesoftware/smack/AccountManager.java
new file mode 100644
index 0000000..4d9faa5
--- /dev/null
+++ b/src/org/jivesoftware/smack/AccountManager.java
@@ -0,0 +1,337 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2003-2007 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smack;
+
+import org.jivesoftware.smack.filter.AndFilter;
+import org.jivesoftware.smack.filter.PacketFilter;
+import org.jivesoftware.smack.filter.PacketIDFilter;
+import org.jivesoftware.smack.filter.PacketTypeFilter;
+import org.jivesoftware.smack.packet.IQ;
+import org.jivesoftware.smack.packet.Registration;
+import org.jivesoftware.smack.util.StringUtils;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Allows creation and management of accounts on an XMPP server.
+ *
+ * @see Connection#getAccountManager()
+ * @author Matt Tucker
+ */
+public class AccountManager {
+
+ private Connection connection;
+ private Registration info = null;
+
+ /**
+ * Flag that indicates whether the server supports In-Band Registration.
+ * In-Band Registration may be advertised as a stream feature. If no stream feature
+ * was advertised from the server then try sending an IQ packet to discover if In-Band
+ * Registration is available.
+ */
+ private boolean accountCreationSupported = false;
+
+ /**
+ * Creates a new AccountManager instance.
+ *
+ * @param connection a connection to a XMPP server.
+ */
+ public AccountManager(Connection connection) {
+ this.connection = connection;
+ }
+
+ /**
+ * Sets whether the server supports In-Band Registration. In-Band Registration may be
+ * advertised as a stream feature. If no stream feature was advertised from the server
+ * then try sending an IQ packet to discover if In-Band Registration is available.
+ *
+ * @param accountCreationSupported true if the server supports In-Band Registration.
+ */
+ void setSupportsAccountCreation(boolean accountCreationSupported) {
+ this.accountCreationSupported = accountCreationSupported;
+ }
+
+ /**
+ * Returns true if the server supports creating new accounts. Many servers require
+ * that you not be currently authenticated when creating new accounts, so the safest
+ * behavior is to only create new accounts before having logged in to a server.
+ *
+ * @return true if the server support creating new accounts.
+ */
+ public boolean supportsAccountCreation() {
+ // Check if we already know that the server supports creating new accounts
+ if (accountCreationSupported) {
+ return true;
+ }
+ // No information is known yet (e.g. no stream feature was received from the server
+ // indicating that it supports creating new accounts) so send an IQ packet as a way
+ // to discover if this feature is supported
+ try {
+ if (info == null) {
+ getRegistrationInfo();
+ accountCreationSupported = info.getType() != IQ.Type.ERROR;
+ }
+ return accountCreationSupported;
+ }
+ catch (XMPPException xe) {
+ return false;
+ }
+ }
+
+ /**
+ * Returns an unmodifiable collection of the names of the required account attributes.
+ * All attributes must be set when creating new accounts. The standard set of possible
+ * attributes are as follows: <ul>
+ * <li>name -- the user's name.
+ * <li>first -- the user's first name.
+ * <li>last -- the user's last name.
+ * <li>email -- the user's email address.
+ * <li>city -- the user's city.
+ * <li>state -- the user's state.
+ * <li>zip -- the user's ZIP code.
+ * <li>phone -- the user's phone number.
+ * <li>url -- the user's website.
+ * <li>date -- the date the registration took place.
+ * <li>misc -- other miscellaneous information to associate with the account.
+ * <li>text -- textual information to associate with the account.
+ * <li>remove -- empty flag to remove account.
+ * </ul><p>
+ *
+ * Typically, servers require no attributes when creating new accounts, or just
+ * the user's email address.
+ *
+ * @return the required account attributes.
+ */
+ public Collection<String> getAccountAttributes() {
+ try {
+ if (info == null) {
+ getRegistrationInfo();
+ }
+ Map<String, String> attributes = info.getAttributes();
+ if (attributes != null) {
+ return Collections.unmodifiableSet(attributes.keySet());
+ }
+ }
+ catch (XMPPException xe) {
+ xe.printStackTrace();
+ }
+ return Collections.emptySet();
+ }
+
+ /**
+ * Returns the value of a given account attribute or <tt>null</tt> if the account
+ * attribute wasn't found.
+ *
+ * @param name the name of the account attribute to return its value.
+ * @return the value of the account attribute or <tt>null</tt> if an account
+ * attribute wasn't found for the requested name.
+ */
+ public String getAccountAttribute(String name) {
+ try {
+ if (info == null) {
+ getRegistrationInfo();
+ }
+ return info.getAttributes().get(name);
+ }
+ catch (XMPPException xe) {
+ xe.printStackTrace();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the instructions for creating a new account, or <tt>null</tt> if there
+ * are no instructions. If present, instructions should be displayed to the end-user
+ * that will complete the registration process.
+ *
+ * @return the account creation instructions, or <tt>null</tt> if there are none.
+ */
+ public String getAccountInstructions() {
+ try {
+ if (info == null) {
+ getRegistrationInfo();
+ }
+ return info.getInstructions();
+ }
+ catch (XMPPException xe) {
+ return null;
+ }
+ }
+
+ /**
+ * Creates a new account using the specified username and password. The server may
+ * require a number of extra account attributes such as an email address and phone
+ * number. In that case, Smack will attempt to automatically set all required
+ * attributes with blank values, which may or may not be accepted by the server.
+ * Therefore, it's recommended to check the required account attributes and to let
+ * the end-user populate them with real values instead.
+ *
+ * @param username the username.
+ * @param password the password.
+ * @throws XMPPException if an error occurs creating the account.
+ */
+ public void createAccount(String username, String password) throws XMPPException {
+ if (!supportsAccountCreation()) {
+ throw new XMPPException("Server does not support account creation.");
+ }
+ // Create a map for all the required attributes, but give them blank values.
+ Map<String, String> attributes = new HashMap<String, String>();
+ for (String attributeName : getAccountAttributes()) {
+ attributes.put(attributeName, "");
+ }
+ createAccount(username, password, attributes);
+ }
+
+ /**
+ * Creates a new account using the specified username, password and account attributes.
+ * The attributes Map must contain only String name/value pairs and must also have values
+ * for all required attributes.
+ *
+ * @param username the username.
+ * @param password the password.
+ * @param attributes the account attributes.
+ * @throws XMPPException if an error occurs creating the account.
+ * @see #getAccountAttributes()
+ */
+ public void createAccount(String username, String password, Map<String, String> attributes)
+ throws XMPPException
+ {
+ if (!supportsAccountCreation()) {
+ throw new XMPPException("Server does not support account creation.");
+ }
+ Registration reg = new Registration();
+ reg.setType(IQ.Type.SET);
+ reg.setTo(connection.getServiceName());
+ attributes.put("username",username);
+ attributes.put("password",password);
+ reg.setAttributes(attributes);
+ PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+ new PacketTypeFilter(IQ.class));
+ PacketCollector collector = connection.createPacketCollector(filter);
+ connection.sendPacket(reg);
+ IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ }
+
+ /**
+ * Changes the password of the currently logged-in account. This operation can only
+ * be performed after a successful login operation has been completed. Not all servers
+ * support changing passwords; an XMPPException will be thrown when that is the case.
+ *
+ * @throws IllegalStateException if not currently logged-in to the server.
+ * @throws XMPPException if an error occurs when changing the password.
+ */
+ public void changePassword(String newPassword) throws XMPPException {
+ Registration reg = new Registration();
+ reg.setType(IQ.Type.SET);
+ reg.setTo(connection.getServiceName());
+ Map<String, String> map = new HashMap<String, String>();
+ map.put("username",StringUtils.parseName(connection.getUser()));
+ map.put("password",newPassword);
+ reg.setAttributes(map);
+ PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+ new PacketTypeFilter(IQ.class));
+ PacketCollector collector = connection.createPacketCollector(filter);
+ connection.sendPacket(reg);
+ IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ }
+
+ /**
+ * Deletes the currently logged-in account from the server. This operation can only
+ * be performed after a successful login operation has been completed. Not all servers
+ * support deleting accounts; an XMPPException will be thrown when that is the case.
+ *
+ * @throws IllegalStateException if not currently logged-in to the server.
+ * @throws XMPPException if an error occurs when deleting the account.
+ */
+ public void deleteAccount() throws XMPPException {
+ if (!connection.isAuthenticated()) {
+ throw new IllegalStateException("Must be logged in to delete a account.");
+ }
+ Registration reg = new Registration();
+ reg.setType(IQ.Type.SET);
+ reg.setTo(connection.getServiceName());
+ Map<String, String> attributes = new HashMap<String, String>();
+ // To delete an account, we add a single attribute, "remove", that is blank.
+ attributes.put("remove", "");
+ reg.setAttributes(attributes);
+ PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+ new PacketTypeFilter(IQ.class));
+ PacketCollector collector = connection.createPacketCollector(filter);
+ connection.sendPacket(reg);
+ IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ }
+
+ /**
+ * Gets the account registration info from the server.
+ *
+ * @throws XMPPException if an error occurs.
+ */
+ private synchronized void getRegistrationInfo() throws XMPPException {
+ Registration reg = new Registration();
+ reg.setTo(connection.getServiceName());
+ PacketFilter filter = new AndFilter(new PacketIDFilter(reg.getPacketID()),
+ new PacketTypeFilter(IQ.class));
+ PacketCollector collector = connection.createPacketCollector(filter);
+ connection.sendPacket(reg);
+ IQ result = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());
+ // Stop queuing results
+ collector.cancel();
+ if (result == null) {
+ throw new XMPPException("No response from server.");
+ }
+ else if (result.getType() == IQ.Type.ERROR) {
+ throw new XMPPException(result.getError());
+ }
+ else {
+ info = (Registration)result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/org/jivesoftware/smack/AndroidConnectionConfiguration.java b/src/org/jivesoftware/smack/AndroidConnectionConfiguration.java
new file mode 100644
index 0000000..6ec05e0
--- /dev/null
+++ b/src/org/jivesoftware/smack/AndroidConnectionConfiguration.java
@@ -0,0 +1,113 @@
+package org.jivesoftware.smack;
+
+import java.io.File;
+
+import android.os.Build;
+
+import org.jivesoftware.smack.proxy.ProxyInfo;
+import org.jivesoftware.smack.util.DNSUtil;
+import org.jivesoftware.smack.util.dns.HostAddress;
+
+import java.util.List;
+
+/**
+ * This class wraps DNS SRV lookups for a new ConnectionConfiguration in a
+ * new thread, since Android API >= 11 (Honeycomb) does not allow network
+ * activity in the main thread.
+ *
+ * @author Florian Schmaus fschmaus@gmail.com
+ *
+ */
+public class AndroidConnectionConfiguration extends ConnectionConfiguration {
+ private static final int DEFAULT_TIMEOUT = 10000;
+
+ /**
+ * Creates a new ConnectionConfiguration for the specified service name.
+ * A DNS SRV lookup will be performed to find out the actual host address
+ * and port to use for the connection.
+ *
+ * @param serviceName the name of the service provided by an XMPP server.
+ */
+ public AndroidConnectionConfiguration(String serviceName) throws XMPPException {
+ super();
+ AndroidInit(serviceName, DEFAULT_TIMEOUT);
+ }
+
+ /**
+ *
+ * @param serviceName
+ * @param timeout
+ * @throws XMPPException
+ */
+ public AndroidConnectionConfiguration(String serviceName, int timeout) throws XMPPException {
+ super();
+ AndroidInit(serviceName, timeout);
+ }
+
+ public AndroidConnectionConfiguration(String host, int port, String name) {
+ super(host, port, name);
+ AndroidInit();
+ }
+
+ private void AndroidInit() {
+ // API 14 is Ice Cream Sandwich
+ if (Build.VERSION.SDK_INT >= 14) {
+ setTruststoreType("AndroidCAStore");
+ setTruststorePassword(null);
+ setTruststorePath(null);
+ } else {
+ setTruststoreType("BKS");
+ String path = System.getProperty("javax.net.ssl.trustStore");
+ if (path == null)
+ path = System.getProperty("java.home") + File.separator + "etc"
+ + File.separator + "security" + File.separator
+ + "cacerts.bks";
+ setTruststorePath(path);
+ }
+ }
+
+ /**
+ *
+ * @param serviceName
+ * @param timeout
+ * @throws XMPPException
+ */
+ private void AndroidInit(String serviceName, int timeout) throws XMPPException {
+ AndroidInit();
+ class DnsSrvLookupRunnable implements Runnable {
+ String serviceName;
+ List<HostAddress> addresses;
+
+ public DnsSrvLookupRunnable(String serviceName) {
+ this.serviceName = serviceName;
+ }
+
+ @Override
+ public void run() {
+ addresses = DNSUtil.resolveXMPPDomain(serviceName);
+ }
+
+ public List<HostAddress> getHostAddresses() {
+ return addresses;
+ }
+ }
+
+ DnsSrvLookupRunnable dnsSrv = new DnsSrvLookupRunnable(serviceName);
+ Thread t = new Thread(dnsSrv, "dns-srv-lookup");
+ t.start();
+ try {
+ t.join(timeout);
+ } catch (InterruptedException e) {
+ throw new XMPPException("DNS lookup timeout after " + timeout + "ms", e);
+ }
+
+ hostAddresses = dnsSrv.getHostAddresses();
+ if (hostAddresses == null) {
+ throw new XMPPException("DNS lookup failure");
+ }
+
+ ProxyInfo proxy = ProxyInfo.forDefaultProxy();
+
+ init(serviceName, proxy);
+ }
+}
diff --git a/src/org/jivesoftware/smack/BOSHConfiguration.java b/src/org/jivesoftware/smack/BOSHConfiguration.java
new file mode 100644
index 0000000..0b033b4
--- /dev/null
+++ b/src/org/jivesoftware/smack/BOSHConfiguration.java
@@ -0,0 +1,124 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smack;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.jivesoftware.smack.ConnectionConfiguration;
+import org.jivesoftware.smack.proxy.ProxyInfo;
+
+/**
+ * Configuration to use while establishing the connection to the XMPP server via
+ * HTTP binding.
+ *
+ * @see BOSHConnection
+ * @author Guenther Niess
+ */
+public class BOSHConfiguration extends ConnectionConfiguration {
+
+ private boolean ssl;
+ private String file;
+
+ public BOSHConfiguration(String xmppDomain) {
+ super(xmppDomain, 7070);
+ setSASLAuthenticationEnabled(true);
+ ssl = false;
+ file = "/http-bind/";
+ }
+
+ public BOSHConfiguration(String xmppDomain, int port) {
+ super(xmppDomain, port);
+ setSASLAuthenticationEnabled(true);
+ ssl = false;
+ file = "/http-bind/";
+ }
+
+ /**
+ * Create a HTTP Binding configuration.
+ *
+ * @param https true if you want to use SSL
+ * (e.g. false for http://domain.lt:7070/http-bind).
+ * @param host the hostname or IP address of the connection manager
+ * (e.g. domain.lt for http://domain.lt:7070/http-bind).
+ * @param port the port of the connection manager
+ * (e.g. 7070 for http://domain.lt:7070/http-bind).
+ * @param filePath the file which is described by the URL
+ * (e.g. /http-bind for http://domain.lt:7070/http-bind).
+ * @param xmppDomain the XMPP service name
+ * (e.g. domain.lt for the user alice@domain.lt)
+ */
+ public BOSHConfiguration(boolean https, String host, int port, String filePath, String xmppDomain) {
+ super(host, port, xmppDomain);
+ setSASLAuthenticationEnabled(true);
+ ssl = https;
+ file = (filePath != null ? filePath : "/");
+ }
+
+ /**
+ * Create a HTTP Binding configuration.
+ *
+ * @param https true if you want to use SSL
+ * (e.g. false for http://domain.lt:7070/http-bind).
+ * @param host the hostname or IP address of the connection manager
+ * (e.g. domain.lt for http://domain.lt:7070/http-bind).
+ * @param port the port of the connection manager
+ * (e.g. 7070 for http://domain.lt:7070/http-bind).
+ * @param filePath the file which is described by the URL
+ * (e.g. /http-bind for http://domain.lt:7070/http-bind).
+ * @param proxy the configuration of a proxy server.
+ * @param xmppDomain the XMPP service name
+ * (e.g. domain.lt for the user alice@domain.lt)
+ */
+ public BOSHConfiguration(boolean https, String host, int port, String filePath, ProxyInfo proxy, String xmppDomain) {
+ super(host, port, xmppDomain, proxy);
+ setSASLAuthenticationEnabled(true);
+ ssl = https;
+ file = (filePath != null ? filePath : "/");
+ }
+
+ public boolean isProxyEnabled() {
+ return (proxy != null && proxy.getProxyType() != ProxyInfo.ProxyType.NONE);
+ }
+
+ public ProxyInfo getProxyInfo() {
+ return proxy;
+ }
+
+ public String getProxyAddress() {
+ return (proxy != null ? proxy.getProxyAddress() : null);
+ }
+
+ public int getProxyPort() {
+ return (proxy != null ? proxy.getProxyPort() : 8080);
+ }
+
+ public boolean isUsingSSL() {
+ return ssl;
+ }
+
+ public URI getURI() throws URISyntaxException {
+ if (file.charAt(0) != '/') {
+ file = '/' + file;
+ }
+ return new URI((ssl ? "https://" : "http://") + getHost() + ":" + getPort() + file);
+ }
+}
diff --git a/src/org/jivesoftware/smack/BOSHConnection.java b/src/org/jivesoftware/smack/BOSHConnection.java
new file mode 100644
index 0000000..594cf9d
--- /dev/null
+++ b/src/org/jivesoftware/smack/BOSHConnection.java
@@ -0,0 +1,779 @@
+/**
+ * $RCSfile$
+ * $Revision$
+ * $Date$
+ *
+ * Copyright 2009 Jive Software.
+ *
+ * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.jivesoftware.smack;
+
+import java.io.IOException;
+import java.io.PipedReader;
+import java.io.PipedWriter;
+import java.io.Writer;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import org.jivesoftware.smack.Connection;
+import org.jivesoftware.smack.ConnectionCreationListener;
+import org.jivesoftware.smack.ConnectionListener;
+import org.jivesoftware.smack.PacketCollector;
+import org.jivesoftware.smack.Roster;
+import org.jivesoftware.smack.XMPPException;
+import org.jivesoftware.smack.packet.Packet;
+import org.jivesoftware.smack.packet.Presence;
+import org.jivesoftware.smack.packet.XMPPError;
+import org.jivesoftware.smack.util.StringUtils;
+
+import com.kenai.jbosh.BOSHClient;
+import com.kenai.jbosh.BOSHClientConfig;
+import com.kenai.jbosh.BOSHClientConnEvent;
+import com.kenai.jbosh.BOSHClientConnListener;
+import com.kenai.jbosh.BOSHClientRequestListener;
+import com.kenai.jbosh.BOSHClientResponseListener;
+import com.kenai.jbosh.BOSHException;
+import com.kenai.jbosh.BOSHMessageEvent;
+import com.kenai.jbosh.BodyQName;
+import com.kenai.jbosh.ComposableBody;
+
+/**
+ * Creates a connection to a XMPP server via HTTP binding.
+ * This is specified in the XEP-0206: XMPP Over BOSH.
+ *
+ * @see Connection
+ * @author Guenther Niess
+ */
+public class BOSHConnection extends Connection {
+
+ /**
+ * The XMPP Over Bosh namespace.
+ */
+ public static final String XMPP_BOSH_NS = "urn:xmpp:xbosh";
+
+ /**
+ * The BOSH namespace from XEP-0124.
+ */
+ public static final String BOSH_URI = "http://jabber.org/protocol/httpbind";
+
+ /**
+ * The used BOSH client from the jbosh library.
+ */
+ private BOSHClient client;
+
+ /**
+ * Holds the initial configuration used while creating the connection.
+ */
+ private final BOSHConfiguration config;
+
+ // Some flags which provides some info about the current state.
+ private boolean connected = false;
+ private boolean authenticated = false;
+ private boolean anonymous = false;
+ private boolean isFirstInitialization = true;
+ private boolean wasAuthenticated = false;
+ private boolean done = false;
+
+ /**
+ * The Thread environment for sending packet listeners.
+ */
+ private ExecutorService listenerExecutor;
+
+ // The readerPipe and consumer thread are used for the debugger.
+ private PipedWriter readerPipe;
+ private Thread readerConsumer;
+
+ /**
+ * The BOSH equivalent of the stream ID which is used for DIGEST authentication.
+ */
+ protected String authID = null;
+
+ /**
+ * The session ID for the BOSH session with the connection manager.
+ */
+ protected String sessionID = null;
+
+ /**
+ * The full JID of the authenticated user.
+ */
+ private String user = null;
+
+ /**
+ * The roster maybe also called buddy list holds the list of the users contacts.
+ */
+ private Roster roster = null;
+
+
+ /**
+ * Create a HTTP Binding connection to a XMPP server.
+ *
+ * @param https true if you want to use SSL
+ * (e.g. false for http://domain.lt:7070/http-bind).
+ * @param host the hostname or IP address of the connection manager
+ * (e.g. domain.lt for http://domain.lt:7070/http-bind).
+ * @param port the port of the connection manager
+ * (e.g. 7070 for http://domain.lt:7070/http-bind).
+ * @param filePath the file which is described by the URL
+ * (e.g. /http-bind for http://domain.lt:7070/http-bind).
+ * @param xmppDomain the XMPP service name
+ * (e.g. domain.lt for the user alice@domain.lt)
+ */
+ public BOSHConnection(boolean https, String host, int port, String filePath, String xmppDomain) {
+ super(new BOSHConfiguration(https, host, port, filePath, xmppDomain));
+ this.config = (BOSHConfiguration) getConfiguration();
+ }
+
+ /**
+ * Create a HTTP Binding connection to a XMPP server.
+ *
+ * @param config The configuration which is used for this connection.
+ */
+ public BOSHConnection(BOSHConfiguration config) {
+ super(config);
+ this.config = config;
+ }
+
+ public void connect() throws XMPPException {
+ if (connected) {
+ throw new IllegalStateException("Already connected to a server.");
+ }
+ done = false;
+ try {
+ // Ensure a clean starting state
+ if (client != null) {
+ client.close();
+ client = null;
+ }
+ saslAuthentication.init();
+ sessionID = null;
+ authID = null;
+
+ // Initialize BOSH client
+ BOSHClientConfig.Builder cfgBuilder = BOSHClientConfig.Builder
+ .create(config.getURI(), config.getServiceName());
+ if (config.isProxyEnabled()) {
+ cfgBuilder.setProxy(config.getProxyAddress(), config.getProxyPort());
+ }
+ client = BOSHClient.create(cfgBuilder.build());
+
+ // Create an executor to deliver incoming packets to listeners.
+ // We'll use a single thread with an unbounded queue.
+ listenerExecutor = Executors
+ .newSingleThreadExecutor(new ThreadFactory() {
+ public Thread newThread(Runnable runnable) {
+ Thread thread = new Thread(runnable,
+ "Smack Listener Processor ("
+ + connectionCounterValue + ")");
+ thread.setDaemon(true);
+ return thread;
+ }
+ });
+ client.addBOSHClientConnListener(new BOSHConnectionListener(this));
+ client.addBOSHClientResponseListener(new BOSHPacketReader(this));
+
+ // Initialize the debugger
+ if (config.isDebuggerEnabled()) {
+ initDebugger();
+ if (isFirstInitialization) {
+ if (debugger.getReaderListener() != null) {
+ addPacketListener(debugger.getReaderListener(), null);
+ }
+ if (debugger.getWriterListener() != null) {
+ addPacketSendingListener(debugger.getWriterListener(), null);
+ }
+ }
+ }
+
+ // Send the session creation request
+ client.send(ComposableBody.builder()
+ .setNamespaceDefinition("xmpp", XMPP_BOSH_NS)
+ .setAttribute(BodyQName.createWithPrefix(XMPP_BOSH_NS, "version", "xmpp"), "1.0")
+ .build());
+ } catch (Exception e) {
+ throw new XMPPException("Can't connect to " + getServiceName(), e);
+ }
+
+ // Wait for the response from the server
+ synchronized (this) {
+ long endTime = System.currentTimeMillis() +
+ SmackConfiguration.getPacketReplyTimeout() * 6;
+ while ((!connected) && (System.currentTimeMillis() < endTime)) {
+ try {
+ wait(Math.abs(endTime - System.currentTimeMillis()));
+ }
+ catch (InterruptedException e) {}
+ }
+ }
+
+ // If there is no feedback, throw an remote server timeout error