+ * {@link URLStreamHandlerFactory} is a singleton that is registered using the
+ * static method
+ * {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)}. Also,
+ * the {@link URL} objects can only be created for the following protocols:
+ * -- http, https, file, ftp, and jar. In order to install protocol handlers
+ * for other protocols, one has to hijack or override the system's singleton
+ * {@link URLStreamHandlerFactory} instance with a custom implementation. The
+ * objective of this class is to make the {@link URLStreamHandler} registration
+ * for other protocols such as ws, wss, etc. feasible without hijacking the
+ * system's {@link URLStreamHandlerFactory}.
+ *
+ */
+public abstract class URLStreamHandlerFactorySpi implements URLStreamHandlerFactory {
+
+ /**
+ * Returns a list of supported protocols. This can be used to instantiate
+ * appropriate {@link URLStreamHandler} objects based on the protocol.
+ *
+ * @return list of supported protocols
+ */
+ public abstract Collection getSupportedProtocols();
+}
+
diff --git a/android/src/main/java/org/kaazing/net/auth/BasicChallengeHandler.java b/android/src/main/java/org/kaazing/net/auth/BasicChallengeHandler.java
new file mode 100755
index 0000000..21ee8a9
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/auth/BasicChallengeHandler.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.auth;
+
+
+/**
+ * Challenge handler for Basic authentication as defined in RFC 2617.
+ *
+ * This BasicChallengeHandler can be loaded and instantiated using
+ * {@link BasicChallengeHandler#create()}, and registered
+ * at a location using {@link DispatchChallengeHandler#register(String, ChallengeHandler)}.
+ *
+ * In addition, one can install general and realm-specific {@link LoginHandler} objects onto this
+ * {@link BasicChallengeHandler} to assist in handling challenges associated
+ * with any or specific realms. This can be achieved using {@link #setLoginHandler(LoginHandler)} and
+ * {@link #setRealmLoginHandler(String, LoginHandler)} methods.
+ *
+ * The following example loads an instance of a {@link BasicChallengeHandler}, sets a login
+ * handler onto it and registers the basic handler at a URI location. In this way, all attempts to access
+ * that URI for which the server issues "Basic" challenges are handled by the registered {@link BasicChallengeHandler}.
+ *
+ *
+ * @see RFC 2616 - HTTP 1.1
+ * @see RFC 2617 Section 2 - Basic Authentication
+ */
+public abstract class BasicChallengeHandler extends ChallengeHandler {
+
+ /**
+ * Creates a new instance of {@link BasicChallengeHandler} using the
+ * {@link ServiceLoader} API with the implementation specified under
+ * META-INF/services.
+ *
+ * @return BasicChallengeHandler
+ */
+ public static BasicChallengeHandler create() {
+ return create(BasicChallengeHandler.class);
+ }
+
+ /**
+ * Creates a new instance of {@link BasicChallengeHandler} with the
+ * specified {@link ClassLoader} using the {@link ServiceLoader} API with
+ * the implementation specified under META-INF/services.
+ *
+ * @param classLoader ClassLoader to be used to instantiate
+ * @return BasicChallengeHandler
+ */
+ public static BasicChallengeHandler create(ClassLoader classLoader) {
+ return create(BasicChallengeHandler.class, classLoader);
+ }
+
+ /**
+ * Set a Login Handler to be used if and only if a challenge request has
+ * a realm parameter matching the provided realm.
+ *
+ * @param realm the realm upon which to apply the {@code loginHandler}.
+ * @param loginHandler the login handler to use for the provided realm.
+ */
+ public abstract void setRealmLoginHandler(String realm, LoginHandler loginHandler);
+
+
+ /**
+ * Provide a general (non-realm-specific) login handler to be used in association with this challenge handler.
+ * The login handler is used to assist in obtaining credentials to respond to any Basic
+ * challenge requests when no realm-specific login handler matches the realm parameter of the request (if any).
+ *
+ * @param loginHandler a login handler for credentials.
+ */
+ public abstract BasicChallengeHandler setLoginHandler(LoginHandler loginHandler);
+
+ /**
+ * Get the general (non-realm-specific) login handler associated with this challenge handler.
+ * A login handler is used to assist in obtaining credentials to respond to challenge requests.
+ *
+ * @return a login handler to assist in providing credentials, or {@code null} if none has been established yet.
+ */
+ public abstract LoginHandler getLoginHandler() ;
+}
diff --git a/android/src/main/java/org/kaazing/net/auth/ChallengeHandler.java b/android/src/main/java/org/kaazing/net/auth/ChallengeHandler.java
new file mode 100755
index 0000000..b57578b
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/auth/ChallengeHandler.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.auth;
+
+import java.util.ServiceLoader;
+
+/**
+ * A ChallengeHandler is responsible for producing responses to authentication challenges.
+ *
+ * When an attempt to access a protected URI is made, the server responsible for serving the resource
+ * may respond with a challenge, indicating that credentials need be provided before access to the
+ * resource is granted. The specific type of challenge is indicated in a HTTP header called "WWW-Authenticate".
+ * This response and that header are converted into a {@link ChallengeRequest} and sent to a
+ * registered ChallengeHandler for authentication challenge responses. The {@link ChallengeResponse} credentials
+ * generated by a registered challenge handler are included in a replay of the original HTTP request to the server, which
+ * (assuming the credentials are sufficient) allows access to the resource.
+ *
+ * Public subclasses of ChallengeHandler can be loaded and instantiated using {@link ChallengeHandlers},
+ * and registered to handle server challenges for specific URI locations
+ * using {@link DispatchChallengeHandler#register(String, ChallengeHandler)}.
+ *
+ * Any challenge responses to requests matching the registered location may be handled by the registered {@link ChallengeHandler}
+ * as long as {@link #canHandle(ChallengeRequest)} returns true. In the case where multiple registered challenge handlers
+ * could respond to a challenge request, the earliest challenge handler registered at the most specific location matching
+ * the protected URI is selected.
+ *
+ */
+public abstract class ChallengeHandler {
+ /**
+ * Creates a new instance of the sub-class of {@link ChallengeHandler} using
+ * the {@link ServiceLoader} API with the implementation specified under
+ * META-INF/services.
+ *
+ * @param T sub-class of ChallengeHandler
+ * @param clazz Class object of the sub-type
+ * @return ChallengeHandler
+ */
+ protected static T create(Class clazz) {
+ return load0(clazz, ServiceLoader.load(clazz));
+ }
+
+ /**
+ * Creates a new instance of the sub-class of {@link ChallengeHandler} with
+ * specified {@link ClassLoader} using the {@link ServiceLoader} API with
+ * the implementation specified under META-INF/services.
+ *
+ * @param T sub-type of ChallengeHandler
+ * @param clazz Class object of the sub-type
+ * @param classLoader ClassLoader to be used to instantiate
+ * @return ChallengeHandler
+ */
+ protected static T create(Class clazz,
+ ClassLoader clazzLoader) {
+ return load0(clazz, ServiceLoader.load(clazz, clazzLoader));
+ }
+
+ /**
+ * Can the presented challenge be potentially handled by this challenge handler?
+ *
+ * @param challengeRequest a challenge request object containing a challenge
+ * @return true iff this challenge handler could potentially respond meaningfully to the challenge.
+ */
+ public abstract boolean canHandle(ChallengeRequest challengeRequest);
+
+ /**
+ * Handle the presented challenge by creating a challenge response or returning {@code null}.
+ * This responsibility is usually achieved
+ * by using the associated {@link LoginHandler} to obtain user credentials, and transforming those credentials
+ * into a {@link ChallengeResponse}.
+ *
+ * When it is not possible to create a {@link ChallengeResponse}, this method MUST return {@code null}.
+ *
+ * @param challengeRequest a challenge object
+ * @return a challenge response object or {@code null} if no response is possible.
+ */
+ public abstract ChallengeResponse handle(ChallengeRequest challengeRequest);
+
+ // ----------------------- Private Methods -------------------------------
+
+ private static T load0(Class clazz,
+ ServiceLoader serviceLoader) {
+ for (ChallengeHandler challengeHandler: serviceLoader) {
+ Class> c = challengeHandler.getClass();
+ if ((clazz != null) && clazz.isAssignableFrom(c)) {
+ try {
+ return clazz.cast(c.newInstance());
+ } catch (InstantiationException e) {
+ String s = "Failed to instantiate class " + c;
+ throw new RuntimeException(s,e);
+ } catch (IllegalAccessException e) {
+ String s = "Failed to access and instantiate class " + c;
+ throw new RuntimeException(s, e);
+ }
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/android/src/main/java/org/kaazing/net/auth/ChallengeRequest.java b/android/src/main/java/org/kaazing/net/auth/ChallengeRequest.java
new file mode 100755
index 0000000..077a9e7
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/auth/ChallengeRequest.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.auth;
+
+/**
+ * An immutable object representing the challenge presented by the server when the client accessed
+ * the URI represented by a location.
+ *
+ * According to RFC 2617,
+ *
+ * challenge = auth-scheme 1*SP 1#auth-param
+ *
+ * so we model the authentication scheme and parameters in this class.
+ *
+ * This class is also responsible for detecting and adapting the {@code Application Basic}
+ * and {@code Application Negotiate} authentication schemes into their {@code Basic} and {@code Negotiate}
+ * counterpart authentication schemes.
+ */
+public class ChallengeRequest {
+
+ private static final String APPLICATION_PREFIX = "Application ";
+
+
+ String location;
+ String authenticationScheme;
+ String authenticationParameters;
+
+
+ /**
+ * Constructor from the protected URI location triggering the challenge,
+ * and an entire server-provided 'WWW-Authenticate:' string.
+ *
+ * @param location the protected URI location triggering the challenge
+ * @param challenge an entire server-provided 'WWW-Authenticate:' string
+ */
+ public ChallengeRequest(String location, String challenge) {
+ if ( location == null ) {
+ throw new NullPointerException("location");
+ }
+ if ( challenge == null ) {
+ return;
+ }
+
+ if (challenge.startsWith(APPLICATION_PREFIX)) {
+ challenge = challenge.substring(APPLICATION_PREFIX.length());
+ }
+
+ this.location = location;
+ this.authenticationParameters = null;
+
+ int space = challenge.indexOf(' ');
+ if ( space == -1 ) {
+ this.authenticationScheme = challenge;
+ } else {
+ this.authenticationScheme = challenge.substring(0, space);
+ if ( challenge.length() > (space+1)) {
+ this.authenticationParameters = challenge.substring(space+1);
+ }
+ }
+ }
+
+ /**
+ * Return the protected URI the access of which triggered this challenge as a {@code String}.
+ *
+ * For some authentication schemes, the production of a response to the challenge may require
+ * access to the location of the URI triggering the challenge.
+ *
+ * @return the protected URI the access of which triggered this challenge as a {@code String}
+ */
+ public String getLocation() {
+ return location;
+ }
+
+ /**
+ * Return the authentication scheme with which the server is challenging.
+ *
+ * @return the authentication scheme with which the server is challenging.
+ */
+ public String getAuthenticationScheme() {
+ return authenticationScheme;
+ }
+
+ /**
+ * Return the string after the space separator, not including the authentication scheme nor the space itself,
+ * or {@code null} if no such string exists.
+ *
+ * @return the string after the space separator, not including the authentication scheme nor the space itself,
+ * or {@code null} if no such string exists.
+ */
+ public String getAuthenticationParameters() {
+ return authenticationParameters;
+ }
+
+
+ @Override
+ public String toString() {
+ return String.format("%s: %s: %s %s", super.toString(), location, authenticationScheme, authenticationParameters);
+ }
+}
diff --git a/android/src/main/java/org/kaazing/net/auth/ChallengeResponse.java b/android/src/main/java/org/kaazing/net/auth/ChallengeResponse.java
new file mode 100755
index 0000000..354a272
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/auth/ChallengeResponse.java
@@ -0,0 +1,95 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.auth;
+
+import java.util.Arrays;
+
+/**
+ * A challenge response contains a character array representing the response to the server,
+ * and a reference to the next challenge handler to handle any further challenges for the request.
+ *
+ */
+public class ChallengeResponse {
+
+ private char[] credentials;
+ private ChallengeHandler nextChallengeHandler;
+
+ /**
+ * Constructor from a set of credentials to send to the server in an 'Authorization:' header
+ * and the next challenge handler responsible for handling any further challenges for the request.
+ *
+ * @param credentials a set of credentials to send to the server in an 'Authorization:' header
+ * @param nextChallengeHandler the next challenge handler responsible for handling any further challenges for the request.
+ */
+ public ChallengeResponse(char[] credentials, ChallengeHandler nextChallengeHandler) {
+ this.credentials = credentials;
+ this.nextChallengeHandler = nextChallengeHandler;
+ }
+
+ /**
+ * Return the next challenge handler responsible for handling any further challenges for the request.
+ *
+ * @return the next challenge handler responsible for handling any further challenges for the request.
+ */
+ public ChallengeHandler getNextChallengeHandler() {
+ return nextChallengeHandler;
+ }
+
+ /**
+ * Return a set of credentials to send to the server in an 'Authorization:' header.
+ *
+ * @return a set of credentials to send to the server in an 'Authorization:' header.
+ */
+ public char[] getCredentials() {
+ return credentials;
+ }
+
+ /**
+ * Establish the credentials for this response.
+ *
+ * @param credentials the credentials to be used for this challenge response.
+ */
+ public void setCredentials(char[] credentials) {
+ this.credentials = credentials;
+ }
+
+ /**
+ * Establish the next challenge handler responsible for handling any further challenges for the request.
+ *
+ * @param nextChallengeHandler the next challenge handler responsible for handling any further challenges for the request.
+ */
+ public void setNextChallengeHandler(ChallengeHandler nextChallengeHandler) {
+ this.nextChallengeHandler = nextChallengeHandler;
+ }
+
+ /**
+ * Clear the credentials of this response.
+ *
+ * Calling this method once the credentials have been communicated to the network layer
+ * protects credentials in memory.
+ */
+ public void clearCredentials() {
+ if (getCredentials() != null) {
+ Arrays.fill(getCredentials(), (char) 0);
+ }
+ }
+}
diff --git a/android/src/main/java/org/kaazing/net/auth/DispatchChallengeHandler.java b/android/src/main/java/org/kaazing/net/auth/DispatchChallengeHandler.java
new file mode 100755
index 0000000..c319343
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/auth/DispatchChallengeHandler.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.auth;
+
+/**
+ * A DispatchChallengeHandler is responsible for dispatching challenge requests
+ * to appropriate challenge handlers when challenges
+ * arrive from specific URI locations in challenge responses.
+ *
+ * This allows clients to use specific challenge handlers to handle specific
+ * types of challenges at different URI locations.
+ *
+ */
+public abstract class DispatchChallengeHandler extends ChallengeHandler {
+
+ /**
+ * Creates a new instance of {@link DispatchChallengeHandler} using the
+ * {@link ServiceLoader} API with the implementation specified under
+ * META-INF/services.
+ *
+ * @return DispatchChallengeHandler
+ */
+ public static DispatchChallengeHandler create() {
+ return create(DispatchChallengeHandler.class);
+ }
+
+ /**
+ * Creates a new instance of {@link DispatchChallengeHandler} with the
+ * specified {@link ClassLoader} using the {@link ServiceLoader} API with
+ * the implementation specified under META-INF/services.
+ *
+ * @param classLoader ClassLoader to be used to instantiate
+ * @return DispatchChallengeHandler
+ */
+ public static DispatchChallengeHandler create(ClassLoader classLoader) {
+ return create(DispatchChallengeHandler.class, classLoader);
+ }
+
+ @Override
+ public abstract boolean canHandle(ChallengeRequest challengeRequest) ;
+
+ @Override
+ public abstract ChallengeResponse handle(ChallengeRequest challengeRequest) ;
+
+ /**
+ * Register a challenge handler to respond to challenges at one or more locations.
+ *
+ * When a challenge response is received for a protected URI, the {@code locationDescription}
+ * matches against elements of the protected URI; if a match is found, one
+ * consults the challenge handler(s) registered at that {@code locationDescription} to find
+ * a challenge handler suitable to respond to the challenge.
+ *
+ * A {@code locationDescription} comprises a username, password, host, port and paths,
+ * any of which can be wild-carded with the "*" character to match any number of request URIs.
+ * If no port is explicitly mentioned in a {@code locationDescription}, a default port will be inferred
+ * based on the scheme mentioned in the location description, according to the following table:
+ *
+ *
scheme
default port
Sample locationDescription
+ *
http
80
foo.example.com or http://foo.example.com
+ *
ws
80
foo.example.com or ws://foo.example.com
+ *
https
443
https://foo.example.com
+ *
wss
443
wss://foo.example.com
+ *
+ *
+ * The protocol scheme (e.g. http or ws) if present in {@code locationDescription} will not be used to
+ * match {@code locationDescription} with the protected URI, because authentication challenges are
+ * implemented on top of one of the HTTP/s protocols always, whether one is initiating web socket
+ * connections or regular HTTP connections. That is to say for example, the locationDescription {@code "foo.example.com"}
+ * matches both URIs {@code http://foo.example.com} and {@code ws://foo.example.com}.
+ *
+ * Some examples of {@code locationDescription} values with wildcards are:
+ *
+ *
{@code *}/ -- matches all requests to any host on port 80 (default port), with no user info or path specified.
+ *
{@code *.hostname.com:8000} -- matches all requests to port 8000 on any sub-domain of {@code hostname.com},
+ * but not {@code hostname.com} itself.
+ *
{@code server.hostname.com:*}/{@code *} -- matches all requests to a particular server on any port on any path but not the empty path.
+ *
+ * @param locationDescription the (possibly wild-carded) location(s) at which to register a handler.
+ * @param challengeHandler the challenge handler to register at the location(s).
+ *
+ * @return a reference to this challenge handler for chained calls
+ */
+ public abstract DispatchChallengeHandler register(String locationDescription, ChallengeHandler challengeHandler) ;
+
+ /**
+ * If the provided challengeHandler is registered at the provided location, clear that
+ * association such that any future challenge requests matching the location will never
+ * be handled by the provided challenge handler.
+ *
+ * If no such location or challengeHandler registration exists, this method silently succeeds.
+ * @param locationDescription the exact location description at which the challenge handler was originally registered
+ * @param challengeHandler the challenge handler to de-register.
+ *
+ * @return a reference to this object for chained call support
+ */
+ public abstract DispatchChallengeHandler unregister(String locationDescription, ChallengeHandler challengeHandler) ;
+}
diff --git a/android/src/main/java/org/kaazing/net/auth/LoginHandler.java b/android/src/main/java/org/kaazing/net/auth/LoginHandler.java
new file mode 100755
index 0000000..c5aae4c
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/auth/LoginHandler.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.auth;
+
+import java.net.PasswordAuthentication;
+
+/**
+ * A login handler is responsible for obtaining credentials from an arbitrary
+ * source.
+ *
+ * Login Handlers can be associated with one or more {@link ChallengeHandler}
+ * objects, to ensure that when a Challenge Handler requires credentials for a {@link ChallengeResponse},
+ * the work is delegated to a {@link LoginHandler}.
+ *
+ * At client configuration time, a {@link LoginHandler} can be associated with a {@link ChallengeHandler} as follows:
+ *
+ */
+public abstract class LoginHandler {
+
+ /**
+ * Default constructor.
+ */
+ protected LoginHandler() {
+ }
+
+ /**
+ * Gets the password authentication credentials from an arbitrary source.
+ * @return the password authentication obtained.
+ */
+ public abstract PasswordAuthentication getCredentials();
+
+}
\ No newline at end of file
diff --git a/android/src/main/java/org/kaazing/net/auth/NegotiableChallengeHandler.java b/android/src/main/java/org/kaazing/net/auth/NegotiableChallengeHandler.java
new file mode 100755
index 0000000..a7ea216
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/auth/NegotiableChallengeHandler.java
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.auth;
+
+
+import java.util.Collection;
+
+/**
+ * A NegotiableChallengeHandler can be used to directly respond to
+ * "Negotiate" challenges, and in addition, can be used indirectly in conjunction
+ * with a {@link NegotiateChallengeHandler}
+ * to assist in the construction of a challenge response using object identifiers.
+ *
+ * @see RFC 4178 Section 4.2.1 for details
+ * about how the supported object identifiers contribute towards the initial context token in the challenge response.
+ *
+ *
+ *
+ */
+public abstract class NegotiableChallengeHandler extends ChallengeHandler {
+
+ /**
+ * Creates a new instance of {@link NegotiableChallengeHandler} using the
+ * {@link ServiceLoader} API with the implementation specified under
+ * META-INF/services.
+ *
+ * @return NegotiableChallengeHandler
+ */
+ public static NegotiableChallengeHandler create() {
+ return create(NegotiableChallengeHandler.class);
+ }
+
+ /**
+ * Creates a new instance of {@link NegotiableChallengeHandler} with the
+ * specified {@link ClassLoader} using the {@link ServiceLoader} API with
+ * the implementation specified under META-INF/services.
+ *
+ * @param classLoader ClassLoader to be used to instantiate
+ * @return NegotiableChallengeHandler
+ */
+ public static NegotiableChallengeHandler create(ClassLoader classLoader) {
+ return create(NegotiableChallengeHandler.class, classLoader);
+ }
+
+ /**
+ * Default constructor.
+ */
+ protected NegotiableChallengeHandler() {
+ }
+
+ /**
+ * Return a collection of string representations of object identifiers
+ * supported by this challenge handler implementation, in dot-separated notation.
+ * For example, {@literal 1.3.5.1.5.2}.
+ *
+ * @return a collection of string representations of object identifiers
+ * supported by this challenge handler implementation.
+ */
+ public abstract Collection getSupportedOids();
+
+ /**
+ * Provide a general login handler to be used in association with this challenge handler.
+ * The login handler is used to assist in obtaining credentials to respond to any
+ * challenge requests when this challenge handler handles the request.
+ *
+ * @param loginHandler a login handler for credentials.
+ *
+ * @return this challenge handler object, to support chained calls
+ */
+ public abstract NegotiableChallengeHandler setLoginHandler(LoginHandler loginHandler);
+
+ /**
+ * Get the general login handler associated with this challenge handler.
+ * A login handler is used to assist in obtaining credentials to respond to challenge requests.
+ *
+ * @return a login handler to assist in providing credentials, or {@code null} if none has been established yet.
+ */
+ public abstract LoginHandler getLoginHandler() ;
+}
diff --git a/android/src/main/java/org/kaazing/net/auth/NegotiateChallengeHandler.java b/android/src/main/java/org/kaazing/net/auth/NegotiateChallengeHandler.java
new file mode 100755
index 0000000..cdbe45e
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/auth/NegotiateChallengeHandler.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.auth;
+
+/**
+ * A Negotiate Challenge Handler handles initial empty "Negotiate" challenges from the
+ * server. It uses other "candidate" challenger handlers to assemble an initial context token
+ * to send to the server, and is responsible for creating a challenge response that can delegate
+ * to the winning candidate.
+ *
+ * This NegotiateChallengeHandler can be loaded and instantiated using
+ * {@link #create()}, and registered at a location using
+ * {@link DispatchChallengeHandler#register(String, ChallengeHandler)}.
+ *
+ * In addition, one can register more specific {@link NegotiableChallengeHandler} objects with
+ * this initial {@link NegotiateChallengeHandler} to handle initial Negotiate challenges and subsequent challenges associated
+ * with specific Negotiation mechanism types / object identifiers.
+ *
+ * The following example establishes a Negotiation strategy at a specific URL location.
+ * We show the use of a {@link DispatchChallengeHandler} to register a {@link NegotiateChallengeHandler} at
+ * a specific location. The {@link NegotiateChallengeHandler} has a {@link NegotiableChallengeHandler}
+ * instance registered as one of the potential negotiable alternative challenge handlers.
+ *
+ *
+ * @see RFC 2616 - HTTP 1.1
+ * @see RFC 2617 - HTTP Authentication
+ */
+public abstract class NegotiateChallengeHandler extends ChallengeHandler {
+
+ /**
+ * Creates a new instance of {@link NegotiateChallengeHandler} using the
+ * {@link ServiceLoader} API with the implementation specified under
+ * META-INF/services.
+ *
+ * @return NegotiateChallengeHandler
+ */
+ public static NegotiateChallengeHandler create() {
+ return create(NegotiateChallengeHandler.class);
+ }
+
+ /**
+ * Creates a new instance of {@link NegotiateChallengeHandler} with the
+ * specified {@link ClassLoader} using the {@link ServiceLoader} API with
+ * the implementation specified under META-INF/services.
+ *
+ * @param classLoader ClassLoader to be used to instantiate
+ * @return NegotiateChallengeHandler
+ */
+ public static NegotiateChallengeHandler create(ClassLoader classLoader) {
+ return create(NegotiateChallengeHandler.class, classLoader);
+ }
+
+ /**
+ * Register a candidate negotiable challenge handler that will be used to respond
+ * to an initial "Negotiate" server challenge and can then potentially be
+ * a winning candidate in the race to handle the subsequent server challenge.
+ *
+ * @param handler the mechanism-type-specific challenge handler.
+ *
+ * @return a reference to this handler, to support chained calls
+ */
+ public abstract NegotiateChallengeHandler register(NegotiableChallengeHandler handler);
+}
diff --git a/android/src/main/java/org/kaazing/net/http/HttpRedirectPolicy.java b/android/src/main/java/org/kaazing/net/http/HttpRedirectPolicy.java
new file mode 100755
index 0000000..e8b6df8
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/http/HttpRedirectPolicy.java
@@ -0,0 +1,317 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.http;
+
+import java.net.URI;
+import java.util.Comparator;
+
+/**
+ * Options for following HTTP redirect requests with response code 3xx.
+ */
+public enum HttpRedirectPolicy implements Comparator {
+ /**
+ * Do not follow HTTP redirects.
+ */
+ NEVER() {
+ @Override
+ public int compare(URI current, URI redirect) {
+ if ((current == null) || (redirect == null)) {
+ String s = "Null URI passed in to compare()";
+ throw new IllegalArgumentException(s);
+ }
+
+ // URIs don't matter as this option indicates never to follow
+ // redirects.
+ return -1;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpRedirectOption.NEVER";
+ }
+ },
+
+ /**
+ * Follow HTTP redirect requests always regardless of the origin, domain, etc.
+ */
+ ALWAYS() {
+ @Override
+ public int compare(URI current, URI redirect) {
+ if ((current == null) || (redirect == null)) {
+ String s = "Null URI passed in to compare()";
+ throw new IllegalArgumentException(s);
+ }
+
+ // URIs don't matter as this option indicates always to follow
+ // redirects.
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpRedirectOption.ALWAYS";
+ }
+ },
+
+ /**
+ * Follow HTTP redirect only if the redirected request is for the same
+ * origin. This implies that both the scheme/protocol and the
+ * authority should match between the current and the redirect URIs.
+ * Note that authority includes the hostname and the port.
+ */
+ SAME_ORIGIN() {
+ @Override
+ public int compare(URI current, URI redirect) {
+ if ((current == null) || (redirect == null)) {
+ String s = "Null URI passed in to compare()";
+ throw new IllegalArgumentException(s);
+ }
+
+ // Two URIs have same origin if the protocol and authority
+ // are the same.
+ if (current.getScheme().equalsIgnoreCase(redirect.getScheme()) &&
+ current.getAuthority().equalsIgnoreCase(redirect.getAuthority())) {
+ return 0;
+ }
+
+ return -1;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpRedirectOption.SAME_ORIGIN";
+ }
+ },
+
+ /**
+ * Follow HTTP redirect only if the redirected request is for the same
+ * domain. This implies that both the scheme/protocol and the
+ * hostname should match between the current and the redirect URIs.
+ *
+ * URIs that satisfy HttpRedirectPolicy.SAME_ORIGIN policy will implicitly
+ * satisfy HttpRedirectPolicy.SAME_DOMAIN policy.
+ *
+ * URIs with identical domains would be ws://production.example.com:8001 and
+ * ws://production.example.com:8002.
+ */
+ SAME_DOMAIN() {
+ @Override
+ public int compare(URI current, URI redirect) {
+ if (HttpRedirectPolicy.SAME_ORIGIN.compare(current, redirect) == 0) {
+ // If the URIs have same origin, then they implicitly have same
+ // domain.
+ return 0;
+ }
+
+ if ((current == null) || (redirect == null)) {
+ String s = "Null URI passed in to compare()";
+ throw new IllegalArgumentException(s);
+ }
+
+ // We should allow redirecting to a more secure scheme from a less
+ // secure scheme. For example, we should allow redirecting from
+ // ws -> wss.
+ String currScheme = current.getScheme();
+ String newScheme = redirect.getScheme();
+ if (newScheme.equalsIgnoreCase(currScheme) ||
+ newScheme.contains(currScheme)) {
+ // Check if the host names are the same between the two URIs.
+ String currHost = current.getHost();
+ String newHost = redirect.getHost();
+
+ if (currHost.equalsIgnoreCase(newHost)) {
+ return 0;
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpRedirectOption.SAME_DOMAIN";
+ }
+ },
+
+ /**
+ * Follow HTTP redirect only if the redirected request is for a peer-domain.
+ * This implies that both the scheme/protocol and the domain should
+ * match between the current and the redirect URIs.
+ *
+ * URIs that satisfy HttpRedirectPolicy.SAME_DOMAIN policy will implicitly
+ * satisfy HttpRedirectPolicy.PEER_DOMAIN policy.
+ *
+ * To determine if the two URIs have peer-domains, we do the following:
+ *
+ *
compute base-domain by removing the token before the first '.' in
+ * the hostname of the original URI and check if the hostname of the
+ * redirected URI ends with the computed base-domain
+ *
compute base-domain by removing the token before the first '.' in
+ * the hostname of the redirected URI and check if the hostname of the
+ * original URI ends with the computed base-domain
+ *
+ *
+ * If both the conditions are satisfied, then we conclude that the URIs are
+ * for peer-domains. However, if the host in the URI has no '.'(for eg.,
+ * ws://localhost:8000), then we just use the entire hostname as the
+ * computed base-domain.
+ *
+ * If you are using this policy, it is recommended that the number of tokens
+ * in the hostname be atleast 2 + number_of_tokens(top-level-domain). For
+ * example, if the top-level-domain(TLD) is "com", then the URIs should have
+ * atleast 3 tokens in the hostname. So, ws://marketing.example.com:8001 and
+ * ws://sales.example.com:8002 are examples of URIs with peer-domains. Similarly,
+ * if the TLD is "co.uk", then the URIs should have atleast 4 tokens in the
+ * hostname. So, ws://marketing.example.co.uk:8001 and
+ * ws://sales.example.co.uk:8002 are examples of URIs with peer-domains.
+ */
+ PEER_DOMAIN() {
+ @Override
+ public int compare(URI current, URI redirect) {
+ if (HttpRedirectPolicy.SAME_DOMAIN.compare(current, redirect) == 0) {
+ // If the domains are the same, then they are peers.
+ return 0;
+ }
+
+ if ((current == null) || (redirect == null)) {
+ String s = "Null URI passed in to compare()";
+ throw new IllegalArgumentException(s);
+ }
+
+ // We should allow redirecting to a more secure scheme from a less
+ // secure scheme. For example, we should allow redirecting from
+ // ws -> wss.
+ String currScheme = current.getScheme();
+ String newScheme = redirect.getScheme();
+ if (newScheme.equalsIgnoreCase(currScheme) ||
+ newScheme.contains(currScheme)) {
+ String currHost = current.getHost();
+ String redirectHost = redirect.getHost();
+ String currBaseDomain = getBaseDomain(currHost);
+ String redirectBaseDomain = getBaseDomain(redirectHost);
+
+ if (currHost.endsWith(redirectBaseDomain) &&
+ redirectHost.endsWith(currBaseDomain)) {
+ return 0;
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpRedirectOption.PEER_DOMAIN";
+ }
+
+ // Get the base domain for a given hostname. For example, jms.kaazing.com
+ // will return kaazing.com.
+ private String getBaseDomain(String hostname) {
+ String[] tokens = hostname.split("\\.");
+
+ if (tokens.length <= 2) {
+ return hostname;
+ }
+
+ String baseDomain = "";
+ for (int i = 1; i < tokens.length; i++) {
+ baseDomain += "." + tokens[i];
+ }
+
+ return baseDomain;
+ }
+ },
+
+ /**
+ * Follow HTTP redirect only if the redirected request is for child-domain
+ * or sub-domain of the original request.
+ *
+ * URIs that satisfy HttpRedirectPolicy.SAME_DOMAIN policy will implicitly
+ * satisfy HttpRedirectPolicy.SUB_DOMAIN policy.
+ *
+ * To determine if the domain of the redirected URI is sub-domain/child-domain
+ * of the domain of the original URI, we check if the hostname of the
+ * redirected URI ends with the hostname of the original URI.
+ *
+ * Domain of the redirected URI ws://benefits.hr.example.com:8002 is a
+ * sub-domain/child-domain of the domain of the original URI
+ * ws://hr.example.com:8001. Note that domain in ws://example.com:9001 is a
+ * sub-domain of the domain in ws://example.com:9001.
+ */
+ SUB_DOMAIN() {
+ @Override
+ public int compare(URI current, URI redirect) {
+ if (HttpRedirectPolicy.SAME_DOMAIN.compare(current, redirect) == 0) {
+ // If the domains are the same, then one can be a sub-domain
+ // of the other.
+ return 0;
+ }
+
+ if ((current == null) || (redirect == null)) {
+ String s = "Null URI passed in to compare()";
+ throw new IllegalArgumentException(s);
+ }
+
+ // We should allow redirecting to a more secure scheme from a less
+ // secure scheme. For example, we should allow redirecting from
+ // ws -> wss.
+ String currScheme = current.getScheme();
+ String newScheme = redirect.getScheme();
+ if (newScheme.equalsIgnoreCase(currScheme) ||
+ newScheme.contains(currScheme)) {
+ // If the current host is gateway.example.com, and the new
+ // is child.gateway.example.com, then allow redirect.
+ String currHost = current.getHost();
+ String newHost = redirect.getHost();
+
+ if (newHost.length() < currHost.length()) {
+ return -1;
+ }
+
+ if (newHost.endsWith("." + currHost)) {
+ return 0;
+ }
+ }
+
+ return -1;
+ }
+
+ @Override
+ public String toString() {
+ return "HttpRedirectOption.SUB_DOMAIN";
+ }
+ };
+
+ /**
+ * Returns 0, if the aspects of current and the redirected URIs match as per
+ * the option. Otherwise, -1 is returned.
+ *
+ * @param current URI of the current request
+ * @param redirect URI of the redirected request
+ * @return 0, for a successful match; otherwise -1
+ */
+ @Override
+ public abstract int compare(URI current, URI redirect);
+
+ @Override
+ public abstract String toString();
+}
\ No newline at end of file
diff --git a/android/src/main/java/org/kaazing/net/impl/auth/BasicChallengeResponseFactory.java b/android/src/main/java/org/kaazing/net/impl/auth/BasicChallengeResponseFactory.java
new file mode 100755
index 0000000..765920f
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/impl/auth/BasicChallengeResponseFactory.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.impl.auth;
+
+import org.kaazing.gateway.client.util.Base64Util;
+import org.kaazing.gateway.client.util.WrappedByteBuffer;
+import org.kaazing.net.auth.ChallengeHandler;
+import org.kaazing.net.auth.ChallengeResponse;
+
+import java.net.PasswordAuthentication;
+import java.util.Arrays;
+
+public class BasicChallengeResponseFactory {
+
+ public static ChallengeResponse create(PasswordAuthentication creds, ChallengeHandler nextChallengeHandler) {
+ String unencoded = String.format("%s:%s", creds.getUserName(), new String(creds.getPassword()));
+ String response = String.format("Basic %s", Base64Util.encode(WrappedByteBuffer.wrap(unencoded.getBytes())));
+ Arrays.fill(creds.getPassword(), (char) 0);
+ return new ChallengeResponse(response.toCharArray(), nextChallengeHandler);
+ }
+}
diff --git a/android/src/main/java/org/kaazing/net/impl/auth/DefaultBasicChallengeHandler.java b/android/src/main/java/org/kaazing/net/impl/auth/DefaultBasicChallengeHandler.java
new file mode 100755
index 0000000..3dfd062
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/impl/auth/DefaultBasicChallengeHandler.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.impl.auth;
+
+
+import java.net.PasswordAuthentication;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.logging.Logger;
+
+import org.kaazing.gateway.client.util.auth.LoginHandlerProvider;
+import org.kaazing.net.auth.BasicChallengeHandler;
+import org.kaazing.net.auth.ChallengeRequest;
+import org.kaazing.net.auth.ChallengeResponse;
+import org.kaazing.net.auth.LoginHandler;
+
+/**
+ * Challenge handler for Basic authentication. See RFC 2617.
+ */
+public class DefaultBasicChallengeHandler extends BasicChallengeHandler implements LoginHandlerProvider {
+
+// ------------------------------ FIELDS ------------------------------
+
+ private static final String CLASS_NAME = DefaultBasicChallengeHandler.class.getName();
+ private static final Logger LOG = Logger.getLogger(CLASS_NAME);
+
+ private Map loginHandlersByRealm = new ConcurrentHashMap();
+
+ @Override
+ public void setRealmLoginHandler(String realm, LoginHandler loginHandler) {
+ if ( realm == null) {
+ throw new NullPointerException("realm");
+ }
+ if ( loginHandler == null ) {
+ throw new NullPointerException("loginHandler");
+ }
+
+ loginHandlersByRealm.put(realm, loginHandler);
+ }
+
+ /**
+ * If specified, this login handler is responsible for assisting in the
+ * production of challenge responses.
+ */
+ private LoginHandler loginHandler;
+
+ /**
+ * Provide a login handler to be used in association with this challenge handler.
+ * The login handler is used to assist in obtaining credentials to respond to challenge requests.
+ *
+ * @param loginHandler a login handler for credentials.
+ */
+ public BasicChallengeHandler setLoginHandler(LoginHandler loginHandler) {
+ this.loginHandler = loginHandler;
+ return this;
+ }
+
+ /**
+ * Get the login handler associated with this challenge handler.
+ * A login handler is used to assist in obtaining credentials to respond to challenge requests.
+ *
+ * @return a login handler to assist in providing credentials, or {@code null} if none has been established yet.
+ */
+ public LoginHandler getLoginHandler() {
+ return loginHandler;
+ }
+
+ @Override
+ public boolean canHandle(ChallengeRequest challengeRequest) {
+ return challengeRequest != null &&
+ "Basic".equals(challengeRequest.getAuthenticationScheme());
+ }
+
+ @Override
+ public ChallengeResponse handle(ChallengeRequest challengeRequest) {
+
+ LOG.entering(CLASS_NAME, "handle", new String[]{challengeRequest.getLocation(),
+ challengeRequest.getAuthenticationParameters()});
+
+ if (challengeRequest.getLocation() != null) {
+
+
+
+ // Start by using this generic Basic handler
+ LoginHandler loginHandler = getLoginHandler();
+
+ // Try to delegate to a realm-specific login handler if we can
+ String realm = RealmUtils.getRealm(challengeRequest);
+ if ( realm != null && loginHandlersByRealm.get(realm) != null) {
+ loginHandler = loginHandlersByRealm.get(realm);
+ }
+ LOG.finest("BasicChallengeHandler.getResponse: login handler = " + loginHandler);
+ if (loginHandler != null) {
+ PasswordAuthentication creds = loginHandler.getCredentials();
+ if (creds != null && creds.getUserName() != null && creds.getPassword() != null) {
+ return BasicChallengeResponseFactory.create(creds, this);
+ }
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/android/src/main/java/org/kaazing/net/impl/auth/DefaultDispatchChallengeHandler.java b/android/src/main/java/org/kaazing/net/impl/auth/DefaultDispatchChallengeHandler.java
new file mode 100755
index 0000000..fff10e2
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/impl/auth/DefaultDispatchChallengeHandler.java
@@ -0,0 +1,778 @@
+/**
+ * Copyright 2007-2015, Kaazing Corporation. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.kaazing.net.impl.auth;
+
+import org.kaazing.net.auth.ChallengeHandler;
+import org.kaazing.net.auth.ChallengeRequest;
+import org.kaazing.net.auth.ChallengeResponse;
+import org.kaazing.net.auth.DispatchChallengeHandler;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * The DefaultDispatchChallengeHandler is responsible for defining and using appropriate challenge handlers when challenges
+ * arrive from specific URI locations. This allows clients to use specific challenge handlers to handle specific
+ * types of challenges at different URI locations.
+ *
+ */
+public class DefaultDispatchChallengeHandler extends DispatchChallengeHandler {
+// ------------------------------ FIELDS ------------------------------
+
+ static final String SCHEME_URI = "^(.*)://(.*)";
+ static final Pattern SCHEME_URI_PATTERN = Pattern.compile(SCHEME_URI);
+
+ enum UriElement {
+ HOST,
+ USERINFO,
+ PORT,
+ PATH
+ }
+ private Node rootNode;
+
+ public void clear() {
+ rootNode = new Node();
+ }
+
+ @Override
+ public boolean canHandle(ChallengeRequest challengeRequest) {
+ return lookup(challengeRequest) != null;
+ }
+
+ @Override
+ public ChallengeResponse handle(ChallengeRequest challengeRequest) {
+ ChallengeHandler challengeHandler = lookup(challengeRequest);
+ if (challengeHandler == null) {
+ return null;
+ }
+ return challengeHandler.handle(challengeRequest);
+ }
+
+
+
+
+// --------------------------- CONSTRUCTORS ---------------------------
+
+ public DefaultDispatchChallengeHandler() {
+ rootNode = new Node();
+ }
+
+ @Override
+ public DispatchChallengeHandler register(String locationDescription, ChallengeHandler challengeHandler) {
+ if (locationDescription == null || locationDescription.length() == 0) {
+ throw new IllegalArgumentException("Must specify a location to handle challenges upon.");
+ }
+
+ if (challengeHandler == null) {
+ throw new IllegalArgumentException("Must specify a handler to handle challenges.");
+ }
+
+ addChallengeHandlerAtLocation(locationDescription, challengeHandler);
+ return this;
+ }
+
+ @Override
+ public DispatchChallengeHandler unregister(String locationDescription, ChallengeHandler challengeHandler) {
+ if (locationDescription == null || locationDescription.length() == 0) {
+ throw new IllegalArgumentException("Must specify a location to un-register challenge handlers upon.");
+ }
+
+ if (challengeHandler == null) {
+ throw new IllegalArgumentException("Must specify a handler to un-register.");
+ }
+
+ delChallengeHandlerAtLocation(locationDescription, challengeHandler);
+
+ return this;
+ }
+
+ private void delChallengeHandlerAtLocation(String locationDescription, ChallengeHandler challengeHandler) {
+ List> tokens = tokenize(locationDescription);
+ Node cursor = rootNode;
+ for (Token token : tokens) {
+ if (!cursor.hasChild(token.getName(), token.getKind())) {
+ return; // silently remove nothing
+ } else {
+ cursor = cursor.getChild(token.getName());
+ }
+ }
+ cursor.removeValue(challengeHandler);
+ }
+
+ private void addChallengeHandlerAtLocation(String locationDescription, ChallengeHandler challengeHandler) {
+ List> tokens = tokenize(locationDescription);
+ Node cursor = rootNode;
+
+ for (Token token : tokens) {
+ if (!cursor.hasChild(token.getName(), token.getKind())) {
+ cursor = cursor.addChild(token.getName(), token.getKind());
+ } else {
+ cursor = cursor.getChild(token.getName());
+ }
+ }
+ cursor.appendValues(challengeHandler);
+ }
+
+
+
+// -------------------------- OTHER METHODS --------------------------
+ /**
+ * Locate all challenge handlers to serve the given location.
+ *
+ * @param location a location
+ * @return a collection of {@link ChallengeHandler}s if found registered at a matching location or an empty list if none are found.
+ */
+ public List lookup(String location) {
+ List result = Collections.emptyList();
+ if (location != null) {
+ Node resultNode = findBestMatchingNode(location);
+ if (resultNode != null) {
+ return resultNode.getValues();
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Locate a challenge handler factory to serve the given location and challenge type.
+ *
+ *
+ * @param challengeRequest A challenge string from the server.
+ * @return a challenge handler registered to handle the challenge at the location,
+ * or null if none could be found.
+ */
+ ChallengeHandler lookup(ChallengeRequest challengeRequest) {
+ ChallengeHandler result = null;
+ String location = challengeRequest.getLocation();
+ if (location != null) {
+ Node resultNode = findBestMatchingNode(location);
+
+ //
+ // If we found an exact or wildcard match, try to find a handler
+ // for the requested challenge.
+ //
+ if (resultNode != null) {
+ List handlers = resultNode.getValues();
+ if (handlers != null) {
+ for (ChallengeHandler challengeHandler : handlers) {
+ if (challengeHandler.canHandle(challengeRequest)) {
+ result = challengeHandler;
+ break;
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Return the Node corresponding to ("matching") a location, or null if none can be found.
+ *
+ * @param location the location at which to find a Node
+ * @return the Node corresponding to ("matching") a location, or null if none can be found.
+ */
+ private Node findBestMatchingNode(String location) {
+ List> tokens = tokenize(location);
+ int tokenIdx = 0;
+
+ return rootNode.findBestMatchingNode(tokens, tokenIdx);
+ }
+
+ /**
+ * Tokenize a given string assuming it is like a URL.
+ *
+ * @param s the string to be parsed as a wildcard-able URI
+ * @return the array of tokens of URI parts.
+ * @throws IllegalArgumentException when the string cannot be parsed as a wildcard-able URI
+ */
+ List> tokenize(String s) throws IllegalArgumentException {
+ if (s == null || s.length() == 0) {
+ return new ArrayList>();
+ }
+
+ //
+ // Make sure if a scheme is not specified, we default one before we parse as a URI.
+ //
+ if ( !SCHEME_URI_PATTERN.matcher(s).matches()) {
+ s = ("http://")+s;
+ }
+
+ //
+ // Parse as a URI
+ //
+ URI uri = URI.create(s);
+
+ //
+ // Detect what the scheme is, if any.
+ //
+ List> result = new ArrayList>(10);
+ String scheme = "http";
+ if (uri.getScheme() != null) {
+ scheme = uri.getScheme();
+ }
+
+
+ //
+ // A wildcard-ed hostname is parsed as an authority.
+ //
+ String host = uri.getHost();
+ String parsedPortFromAuthority = null;
+ String parsedUserInfoFromAuthority = null;
+ String userFromAuthority = null;
+ String passwordFromAuthority = null;
+ if (host == null) {
+ String authority = uri.getAuthority();
+ if (authority != null) {
+ host = authority;
+ int asteriskIdx = host.indexOf("@");
+ if ( asteriskIdx >= 0) {
+ parsedUserInfoFromAuthority = host.substring(0, asteriskIdx);
+ host = host.substring(asteriskIdx+1);
+ int colonIdx = parsedUserInfoFromAuthority.indexOf(":");
+ if ( colonIdx >= 0) {
+ userFromAuthority = parsedUserInfoFromAuthority.substring(0, colonIdx);
+ passwordFromAuthority = parsedUserInfoFromAuthority.substring(colonIdx+1);
+ }
+ }
+ int colonIdx = host.indexOf(":");
+ if ( colonIdx >=0 ) {
+ parsedPortFromAuthority = host.substring(colonIdx + 1);
+ host = host.substring(0, colonIdx);
+ }
+ } else {
+ throw new IllegalArgumentException("Hostname is required.");
+ }
+ }
+
+ //
+ // Split the host and reverse it for the tokenization.
+ //
+ List hostParts = Arrays.asList(host.split("\\."));
+ Collections.reverse(hostParts);
+ for (String hostPart: hostParts) {
+ result.add(new Token(hostPart, UriElement.HOST));
+ }
+
+ if (parsedPortFromAuthority != null) {
+ result.add(new Token(parsedPortFromAuthority, UriElement.PORT));
+ } else if (uri.getPort() > 0) {
+ result.add(new Token(String.valueOf(uri.getPort()), UriElement.PORT));
+ } else if (getDefaultPort(scheme) > 0) {
+ result.add(new Token(String.valueOf(getDefaultPort(scheme)), UriElement.PORT));
+ }
+
+
+ if ( parsedUserInfoFromAuthority != null ) {
+ if ( userFromAuthority != null) {
+ result.add(new Token(userFromAuthority, UriElement.USERINFO));
+ }
+ if ( passwordFromAuthority != null ) {
+ result.add(new Token(passwordFromAuthority, UriElement.USERINFO));
+ }
+ if ( userFromAuthority == null && passwordFromAuthority == null) {
+ result.add(new Token(parsedUserInfoFromAuthority, UriElement.USERINFO));
+ }
+ } else if (uri.getUserInfo() != null) {
+ String userInfo = uri.getUserInfo();
+ int colonIdx = userInfo.indexOf(":");
+ if ( colonIdx >= 0) {
+ result.add(new Token(userInfo.substring(0, colonIdx), UriElement.USERINFO));
+ result.add(new Token(userInfo.substring(colonIdx+1), UriElement.USERINFO));
+ } else {
+ result.add(new Token(uri.getUserInfo(), UriElement.USERINFO));
+ }
+ }
+
+ if (isNotBlank(uri.getPath())) {
+ String path = uri.getPath();
+ if (path.startsWith("/")) {
+ path = path.substring(1);
+ }
+ if (isNotBlank(path)) {
+ for (String p: path.split("/")) {
+ result.add(new Token(p, UriElement.PATH));
+ }
+ }
+ }
+ return result;
+ }
+
+ int getDefaultPort(String scheme) {
+ if ( defaultPortsByScheme.containsKey(scheme.toLowerCase())) {
+ return defaultPortsByScheme.get(scheme);
+ } else {
+ return -1;
+ }
+ }
+
+ static Map defaultPortsByScheme = new HashMap();
+ static {
+ defaultPortsByScheme.put("http", 80);
+ defaultPortsByScheme.put("ws", 80);
+ defaultPortsByScheme.put("wss", 443);
+ defaultPortsByScheme.put("https", 443);
+ }
+
+
+ private boolean isNotBlank(String s) {
+ return s != null && s.length() > 0;
+ }
+
+ /**
+ * A Node instance has a kind, holds a list of {@link #values} (parameterized type instances),
+ * and a sub-tree of nodes called {@link #children}. It is used as a model for
+ * holding typed {@link org.kaazing.net.auth.ChallengeHandler} instances at "locations".
+ *
+ * {@link org.kaazing.net.auth.impl.DefaultDispatchChallengeHandler.Node} instances are mutable. Nodes have {@link #name}s. One can add children
+ * with distinct names, add {@link #values} and recall them.
+ *
+ * {@link org.kaazing.net.auth.impl.DefaultDispatchChallengeHandler.Node} instances are considered to be "wildcard" nodes when their name is equal
+ * to {@link #getWildcardChar()}. Nodes are considered to have a wildcard defined if they or
+ * any of their children are named {@link #getWildcardChar()}. Wildcard nodes are treated
+ * as matching one or multiple elements during {@link org.kaazing.net.auth.impl.DefaultDispatchChallengeHandler.Node searches}.
+ *
+ * The concept of a Node has the following restrictions in the following cases.
+ * An {@link IllegalArgumentException} will result in each case.
+ *
+ *
One is not permitted to add values to the root node. Use a wildcard node instead.
+ *
One is not permitted to create Node instances with null or empty names.
+ *
+ *
+ * @private
+ */
+ static class Node> {
+
+ /**
+ * The name of this Node instance.
+ * Must not be null or empty.
+ */
+ private String name;
+
+ /**
+ * The parameterized type instances.
+ * Optimized for fewer values per node.
+ */
+ private List values = new ArrayList(3);
+
+ /**
+ * An up-link to this Node instance's parent.
+ */
+ private Node parent;
+
+ /**
+ * An enumerated value representing the "kind" of this node.
+ */
+ private E kind;
+
+ /**
+ * The down-links to children sub-nodes of this node.
+ * Each link is accessed through the child Node's name.
+ * This means that child names must be unique.
+ */
+ private Map> children = new LinkedHashMap>();
+
+ /**
+ * A method to access the wildcard character.
+ * Making this a method rather than a constant will allow
+ * this value to change without recompilation of client classes.
+ * @return the character assumed to be the wildcard character for this tree.
+ */
+ public static String getWildcardChar() {
+ return "*";
+ }
+
+ /**
+ * Create a new root node, with a null name and parent.
+ */
+ Node() {
+ this.name=null;
+ this.parent=null;
+ this.kind = null;
+ }
+
+
+ /**
+ * Create a new node with the provided name and parent node.
+ * @param name the name of the node instance to create.
+ * @param parent the parent of the new node to establish the new node's place in the tree.
+ * @param kind the kind of the node instance to create.
+ */
+ private Node(String name, Node parent, E kind) {
+ this.name = name;
+ this.parent = parent;
+ this.kind = kind;
+ }
+
+ /**
+ * Add a new node with the given name of the given kind to this node.
+ *
+ * If an existing child has that name, replace the existing named sub-tree with
+ * a new single node with the provided name.
+ *
+ *
+ * @param name the name of the new node
+ * @param kind the kind of the new node
+ * @return the freshly added Node, for chained calls if needed.
+ * @throws IllegalArgumentException if the name is null or empty
+ */
+ public Node addChild(String name, E kind) {
+ if ( name == null || name.length() == 0) {
+ throw new IllegalArgumentException("A node may not have a null name.");
+ }
+
+ Node result = new Node(name, this, kind);
+ children.put(name, result);
+ return result;
+ }
+
+ /**
+ * Return whether this node has a child with the name and kind provided.
+ *
+ *
+ * @param name the name of the child node sought
+ * @param kind the kind of the child node sought
+ * @return true iff this instance has a child with that name and kind
+ */
+ public boolean hasChild(String name, E kind) {
+ return null != getChild(name) && kind == getChild(name).getKind();
+ }
+
+ /**
+ * Return the child node instance corresponding to the provided name,
+ * or null if no such node can be found.
+ *
+ *
+ * @param name the name of the node sought
+ * @return the child node instance corresponding to the provided name,
+ * or null if no such node can be found.
+ */
+ public Node getChild(String name) {
+ return children.get(name);
+ }
+
+ /**
+ * Return the distance that this token is away from the root.
+ * @return 0 if this node is the root node,
+ * otherwise the number of nodes from this node to the root node including this node.
+ */
+ public int getDistanceFromRoot() {
+ int result = 0;
+ Node cursor = this;
+ while (!cursor.isRootNode()) {
+ result++;
+ cursor = cursor.getParent();
+ }
+ return result;
+ }
+
+ /**
+ * Add the provided values to the current node.
+ *
+ * @param values the values to add to this node instance
+ * @throws IllegalArgumentException when attempting to add values to the root node.
+ */
+ public void appendValues(T... values) {
+ if ( isRootNode() ) {
+ throw new IllegalArgumentException("Cannot set a values on the root node.");
+ }
+ if ( values != null ) {
+ this.values.addAll(Arrays.asList(values));
+ }
+ }
+
+ /**
+ * Remove the provided value from the current node.
+ * @param value the value to remove from this node instance.
+ */
+ public void removeValue(T value) {
+ if ( isRootNode() ) {
+ return;
+ }
+ this.values.remove(value);
+ }
+
+ /**
+ * Return the collection of stored values for this node instance.
+ * If no values have been stored, we return an empty list.
+ * @return the collection of stored values for this node instance; an empty list if none have been stored.
+ */
+ public List getValues() {
+ return values;
+ }
+
+ /**
+ * Returns whether this node instance contains any values.
+ * @return true iff values have been stored in this node.
+ */
+ public boolean hasValues() {
+ return values != null && values.size()>0;
+ }
+
+ /**
+ * Return a link to the parent instance of this node, or null
+ * when invoked on the root node.
+ *
+ * @return a link to the parent instance of this node, or null
+ * when invoked on the root node.
+ */
+ public Node getParent() {
+ return parent;
+ }
+
+ /**
+ * Return the enumerated value from E for this kind of this node, or null
+ * when invoked on the root node.
+ *
+ * @return the enumerated value from E for this kind of this node, or null
+ * when invoked on the root node.
+ */
+ public E getKind() {
+ return this.kind;
+ }
+
+ /**
+ * Is this node the root node?
+ * @return true iff this node instance is the root node.
+ */
+ public boolean isRootNode() {
+ return this.parent == null;
+ }
+
+ /**
+ * Return the name of this node.
+ * @return the name of this node. null iff this node is the root node.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Has at least one child been added to this node instance explicitly?
+ * @return true iff one child been added to this node instance explicitly.
+ */
+ public boolean hasChildren() {
+ return children != null && children.size() > 0;
+ }
+
+ /**
+ * Return whether this node instance's name is equal to {@link #getWildcardChar()}.
+ *
+ * @return true iff this node instance's name is equal to {@link #getWildcardChar()}.
+ */
+ public boolean isWildcard() {
+ return name!=null && name.equals(getWildcardChar());
+ }
+
+
+
+ boolean hasWildcardChild() {
+ return hasChildren() && children.keySet().contains(getWildcardChar());
+ }
+
+ /**
+ * Get a fully qualified name from the root node down to this node.
+ *
+ * Implemented as: Walk up to the root, gathering names, then emit a dot-separated list of names.
+ * Useful for debugging.
+ *
+ * @return a fully qualified name from the root node down to this node.
+ */
+ String getFullyQualifiedName() {
+ StringBuilder b = new StringBuilder();
+ List name = new ArrayList();
+ Node cursor = this;
+ while (!cursor.isRootNode()) {
+ name.add(cursor.name);
+ cursor = cursor.parent;
+ }
+ Collections.reverse(name);
+ for(String s: name) {
+ b.append(s).append('.');
+ }
+
+ if ( b.length() >= 1 && b.charAt(b.length()-1) == '.') {
+ b.deleteCharAt(b.length()-1);
+ }
+
+ return b.toString();
+ }
+
+ public List> getChildrenAsList() {
+ return new ArrayList>(children.values());
+ }
+
+ /**
+ * Find the best matching node with respect to the tokens underneath this node.
+ * @param tokens the tokenized location to query.
+ * @param tokenIdx the index into the tokens to commence matching at.
+ * @return the best matching node or {@code null} if no matching node could be found.
+ */
+ Node findBestMatchingNode(List> tokens, int tokenIdx) {
+ List> matches = findAllMatchingNodes(tokens, tokenIdx);
+
+ Node resultNode = null;
+ int score = 0;
+ for (Node node : matches) {
+ if (node.getDistanceFromRoot() > score) {
+ score = node.getDistanceFromRoot();
+ resultNode = node;
+ }
+ }
+ return resultNode;
+ }
+
+ /**
+ * Find all matching nodes with respect to the tokens underneath this node.
+ * @param tokens the tokenized location to query.
+ * @param tokenIdx the index into the tokens to commence matching at.
+ * @return a collection of all matching nodes, which may be empty if no matching nodes were found.
+ */
+ private List> findAllMatchingNodes(List> tokens, int tokenIdx) {
+ List> result = new ArrayList>();
+
+ //
+ // Iterate over this node's children.
+ //
+ List> nodes = this.getChildrenAsList();
+ for (Node node : nodes) {
+
+ //
+ // Do any tokens match the child node?
+ //
+ int matchResult = node.matches(tokens, tokenIdx);
+ if (matchResult < 0) {
+ // The node matched no tokens.
+ continue;
+ }
+ if (matchResult >= tokens.size()) {
+ // This node matched all remaining tokens.
+
+ //
+ // Make sure we walk down further wildcard node(s) of the same kind
+ // as the node that matched all tokens and gather all values.
+ //
+ do {
+ if (node.hasValues()) {
+ result.add(node);
+ }
+ if ( node.hasWildcardChild()) {
+ Node child = node.getChild(getWildcardChar());
+ if (child.getKind() != getKind()) {
+ node = null;
+ } else {
+ node = child;
+ }
+
+ } else {
+ node = null;
+ }
+ } while (node != null);
+
+ } else {
+ //
+ // This node matched some of the remaining tokens.
+ // So continue to find matching nodes for the remaining tokens (from matchResult onwards).
+ //
+ result.addAll(node.findAllMatchingNodes(tokens, matchResult));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Does this node match one or more tokens starting at tokenIdx?
+ *
+ * If not, returns {@code -1}.
+ * If so, return the index of the first non-matching token in the provided
+ * tokens, or {@code tokens.length} when all tokens in the array match the node.
+ *
+ * @param tokens the tokenized lcoation query
+ * @param tokenIdx the index of interest into the tokens
+ * @return the index of the first non-matching token starting at tokenIdx,
+ * {@code -1} when this node does not match any tokens starting at tokenIdx,
+ * {@code tokens.length} when this node matches all tokens starting at tokenIdx.
+ */
+ private int matches(List> tokens, int tokenIdx) {
+ // Return no match (-1) for bad token indices
+ if (tokenIdx < 0 || tokenIdx >= tokens.size()) {
+ return -1;
+ }
+
+ // For exact name matches return the next token index
+ if (matchesToken(tokens.get(tokenIdx))) {
+ return tokenIdx + 1;
+ }
+
+ // Return no match (-1) since we are not a wildcard and not an exact match
+ if (!this.isWildcard()) {
+ return -1;
+ } else {
+
+ // Return no match because wildcards match within Node kinds
+ if ( this.kind != tokens.get(tokenIdx).getKind()) {
+ return -1;
+ }
+
+ do {
+ tokenIdx++;
+ } while ( tokenIdx < tokens.size() && this.kind == tokens.get(tokenIdx).getKind());
+ return tokenIdx;
+ }
+ }
+
+ private boolean matchesToken(Token token) {
+ return this.getName().equals(token.getName()) &&
+ this.kind == token.getKind();
+ }
+
+
+ }
+
+
+ static class Token> {
+ E kind;
+ String name;
+
+ Token(String name, E element) {
+ this.kind = element;
+ this.name = name;
+ }
+
+ public E getKind() {
+ return kind;
+ }
+
+ public void setKind(E kind) {
+ this.kind = kind;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/java/org/kaazing/net/impl/auth/RealmUtils.java b/android/src/main/java/org/kaazing/net/impl/auth/RealmUtils.java
new file mode 100755
index 0000000..a1cfc5f
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/impl/auth/RealmUtils.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.impl.auth;
+
+import org.kaazing.net.auth.ChallengeRequest;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class RealmUtils {
+
+ private RealmUtils() {
+ // prevent object creation
+ }
+
+ private static final String REALM_REGEX = "(.*)\\s?(?i:realm=)(\"(.*)\")(.*)";
+ private static final Pattern REALM_PATTERN= Pattern.compile(REALM_REGEX);
+
+ /**
+ * A realm parameter is a valid authentication parameter for all authentication schemes
+ * according to RFC 2617 Section 2.1".
+ *
+ *
+ * The realm directive (case-insensitive) is required for all
+ * authentication schemes that issue a challenge. The realm value
+ * (case-sensitive), in combination with the canonical root URL (the
+ * absoluteURI for the server whose abs_path is empty) of the server
+ * being accessed, defines the protection space.
+ *
+ *
+ * @param challengeRequest the challenge request to extract a realm from
+ *
+ * @return the unquoted realm parameter value if present, or {@code null} if no such parameter exists.
+ */
+ public static String getRealm(ChallengeRequest challengeRequest) {
+ String authenticationParameters = challengeRequest.getAuthenticationParameters();
+ if ( authenticationParameters == null) {
+ return null;
+ }
+ Matcher m = REALM_PATTERN.matcher(authenticationParameters);
+ if ( m.matches() && m.groupCount()>=3) {
+ return m.group(3);
+ }
+ return null;
+ }
+}
diff --git a/android/src/main/java/org/kaazing/net/impl/util/BlockingQueueImpl.java b/android/src/main/java/org/kaazing/net/impl/util/BlockingQueueImpl.java
new file mode 100755
index 0000000..1e6faa7
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/impl/util/BlockingQueueImpl.java
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.impl.util;
+
+import java.util.concurrent.ArrayBlockingQueue;
+
+/**
+ * ArrayBlockingQueue extension with ability to interrupt or end-of-stream.
+ * This will be used by producer(ie. the listener) and the consumer(ie. the
+ * WebSocketMessageReader). To match the 3.X event-listener behavior, the
+ * capacity of the queue can be set to 1.
+ *
+ * @param element type
+ */
+public class BlockingQueueImpl extends ArrayBlockingQueue {
+ private static final long serialVersionUID = 1L;
+
+ // ### TODO: Maybe expose an API on WebSocket/WsURLConnection for developers
+ // to specify the number of incoming messages that can be held
+ // before we start pushing on the network.
+ private static final int _QUEUE_CAPACITY = 32;
+
+ private boolean _done = false;
+
+ public BlockingQueueImpl() {
+ super(_QUEUE_CAPACITY, true);
+ }
+
+ public synchronized void done() {
+ _done = true;
+ notifyAll();
+ }
+
+ public boolean isDone() {
+ return _done;
+ }
+
+ public synchronized void reset() {
+ // Wake up threads that maybe blocked to retrieve data.
+ notifyAll();
+ clear();
+ _done = false;
+ }
+
+ // Override to make peek() a blocking call.
+ @Override
+ public E peek() {
+ E el;
+
+ while (((el = super.peek()) == null) && !isDone()) {
+ synchronized (this) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ String s = "Reader has been interrupted maybe the connection is closed";
+ throw new RuntimeException(s);
+ }
+ }
+ }
+
+ if ((el == null) && isDone()) {
+ String s = "Reader has been interrupted maybe the connection is closed";
+ throw new RuntimeException(s);
+ }
+
+ return el;
+ }
+
+ @Override
+ public void put(E el) throws InterruptedException {
+ synchronized (this) {
+ while ((size() == _QUEUE_CAPACITY) && !isDone()) {
+ // Push on the network as the messages are not being retrieved.
+ wait();
+ }
+
+ if (isDone()) {
+ notifyAll();
+ return;
+ }
+ }
+
+ super.put(el);
+
+ synchronized (this) {
+ notifyAll();
+ }
+ }
+
+ @Override
+ public E take() throws InterruptedException {
+ E el = null;
+
+ synchronized (this) {
+ while (isEmpty() && !isDone()) {
+ wait();
+ }
+
+ if (isDone()) {
+ notifyAll();
+
+ if (size() == 0) {
+ String s = "Reader has been interrupted maybe the connection is closed";
+ throw new InterruptedException(s);
+ }
+ }
+ }
+
+ el = super.take();
+
+ synchronized (this) {
+ notifyAll();
+ }
+
+ return el;
+ }
+}
+
diff --git a/android/src/main/java/org/kaazing/net/impl/util/ResumableTimer.java b/android/src/main/java/org/kaazing/net/impl/util/ResumableTimer.java
new file mode 100755
index 0000000..cb1547d
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/impl/util/ResumableTimer.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.impl.util;
+
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class ResumableTimer {
+ public enum PauseStrategy { UPDATE_DELAY, DO_NOT_UPDATE_DELAY };
+
+ private final Runnable runnable;
+ private volatile boolean taskExecuted = false;
+
+ private AtomicLong delay; // milliseconds
+ private AtomicLong startTime; // milliseconds since epoch
+ private Timer timer;
+ private boolean updateDelayWhenPaused;
+
+ public ResumableTimer(Runnable runnable, long delay, boolean updateDelayWhenPaused) {
+ if (runnable == null) {
+ throw new IllegalArgumentException("runnable is null");
+ }
+
+ if (delay < 0) {
+ throw new IllegalArgumentException("Timer delay cannot be negative");
+ }
+
+ this.delay = new AtomicLong(delay);
+ this.startTime = new AtomicLong(0L);
+ this.runnable = runnable;
+ this.updateDelayWhenPaused = updateDelayWhenPaused;
+ }
+
+ public synchronized void cancel() {
+ if (timer != null) {
+ timer.cancel();
+ }
+
+ timer = null;
+ delay.set(-1L);
+ startTime.set(-1L);
+ taskExecuted = false;
+ }
+
+ public boolean didTaskExecute() {
+ return taskExecuted;
+ }
+
+ public synchronized long getDelay() {
+ return delay.get();
+ }
+
+ public synchronized void pause() {
+ long elapsedTime = System.currentTimeMillis() - startTime.get();
+
+ if (timer == null) {
+ // throw new IllegalStateException("Timer is not running");
+ return;
+ }
+
+ timer.cancel();
+ timer = null;
+
+ // If updateDelayWhenPaused is true, then update this.delay by
+ // subtracting the elapsed time. Otherwise, this.delay is not modified.
+ if (this.updateDelayWhenPaused) {
+ assert(elapsedTime < delay.get());
+ delay.compareAndSet(delay.get(), (delay.get() - elapsedTime));
+ }
+ }
+
+ public synchronized void resume() {
+ if (timer != null) {
+ // throw new IllegalStateException("Timer is already running");
+ return;
+ }
+
+ if (delay.get() < 0) {
+ throw new IllegalStateException("Timer delay cannot be negative");
+ }
+
+ timer = new Timer("ResumableTimer", true);
+ startTime.compareAndSet(startTime.get(), System.currentTimeMillis());
+ timer.schedule(new RunnableTask(runnable), delay.get());
+ }
+
+ public synchronized void start() {
+ resume();
+ }
+
+ private synchronized void cleanup() {
+ taskExecuted = true;
+ startTime.set(-1L);
+ timer = null;
+ }
+
+ private class RunnableTask extends TimerTask {
+ private final Runnable runnable;
+
+ public RunnableTask(Runnable runnable) {
+ if (runnable == null) {
+ throw new NullPointerException("runnable is null");
+ }
+
+ this.runnable = runnable;
+ }
+
+ public void run() {
+ runnable.run();
+ ResumableTimer.this.cleanup();
+ }
+ }
+}
diff --git a/android/src/main/java/org/kaazing/net/sse/SseEventReader.java b/android/src/main/java/org/kaazing/net/sse/SseEventReader.java
new file mode 100755
index 0000000..3627456
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/sse/SseEventReader.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.sse;
+
+import java.io.IOException;
+
+public abstract class SseEventReader {
+ /**
+ * Returns the payload of the last received event. This method returns a
+ * null until the first event is received. Note that this is not a blocking
+ * call. Typically, this method should be invoked after {@link #next()} ONLY
+ * if the returned type is {@link SseEventType#DATA}. Otherwise, an
+ * IOException is thrown.
+ *
+ * If {@link #next()} returns any other event type, then subsequent
+ * invocation of this method returns a null.
+ *
+ * @return String event payload; a null is returned if
+ * invoked when not connected or before the first
+ * event is received
+ * @throws IOException if the event type is not SseEventType.DATA
+ */
+ public abstract CharSequence getData() throws IOException;
+
+ /**
+ * Returns the name of the last received event. This method returns a null
+ * until the first event is received. Note that this is not a blocking
+ * call. Typically, this method should be invoked after {@link #next()}.
+ * It's perfectly legal for an event name to be null even if it contains
+ * data. Similarly, it is perfectly legal for an event of type
+ * {@link SseEvent#EMPTY} to have an event name.
+ *
+ * @return String event name; a null is returned if invoked
+ * when not connected or before the first event is
+ * received
+ */
+ public abstract String getName();
+
+ /**
+ * Returns the {@link SseEventType} of the already received event. This
+ * method returns a null until the first event is received. Note that
+ * this is not a blocking call. When connected, if this method is invoked
+ * immediately after {@link #next()}, then they will return the same value.
+ *
+ * Based on the returned {@link SseEventType}, the application developer can
+ * decide whether to read the data. This method will continue to return the
+ * same {@link SseEventType} till the next event arrives. When the next
+ * event arrives, this method will return the the {@link SseEventType}
+ * associated with that event.
+ *
+ * @return SseEventType SseEventType.DATA for an event that contain
+ * data; SseEventType.EMPTY for an event that
+ * is empty with no data; WebSocketMessageType.EOS
+ * if the connection is closed; a null is
+ * returned if not connected or before the first
+ * event is received
+ */
+ public abstract SseEventType getType();
+
+ /**
+ * Invoking this method will cause the thread to block until an event is
+ * received. When the event is received, it will return the type of the
+ * newly received event. Based on {@link SseEventType}, the application
+ * developer can decide whether to invoke the {@link #readData()} method.
+ * When the connection is closed, this method returns
+ * {@link SseEventType#EOS}.
+ *
+ * An IOException is thrown if this method is invoked before the connection
+ * has been established.
+ *
+ * @return SseEventType SseEventType.DATA for an event that contain
+ * data; SseEventType.EMPTY for an event that
+ * is empty with no data; WebSocketMessageType.EOS
+ * if the connection is closed
+ * @throws IOException if invoked before the connection is established
+ */
+ public abstract SseEventType next() throws IOException;
+}
diff --git a/android/src/main/java/org/kaazing/net/sse/SseEventSource.java b/android/src/main/java/org/kaazing/net/sse/SseEventSource.java
new file mode 100755
index 0000000..0b00dcb
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/sse/SseEventSource.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.sse;
+
+import java.io.IOException;
+
+import org.kaazing.net.http.HttpRedirectPolicy;
+
+/**
+ * SseEventSource provides an implementation of HTML5 Server-sent Events.
+ * Refer to HTML5 EventSource at
+ * {@link http://www.whatwg.org/specs/web-apps/current-work/#server-sent-events}
+ * {@link http://www.whatwg.org/specs/web-apps/current-work/#the-event-source}
+ */
+public abstract class SseEventSource {
+ /**
+ * Disconnects with the server. This is a blocking call that returns only
+ * when the shutdown is complete.
+ *
+ * @throws IOException if the disconnect did not succeed
+ */
+ public abstract void close() throws IOException;
+
+ /**
+ * Connects with the server using an end-point. This is a blocking call. The
+ * thread invoking this method will be blocked till a successful connection
+ * is established. If the connection cannot be established, then an
+ * IOException is thrown and the thread is unblocked.
+ *
+ * @throws IOException if the connection cannot be established
+ */
+ public abstract void connect() throws IOException;
+
+ /**
+ * Returns a {@link SseEventReader} that can be used to receive
+ * events based on the {@link SseEventType}.
+ *
+ * If this method is invoked before a connection is established successfully,
+ * then an IOException is thrown.
+ *
+ * @return SseEventReader to receive events
+ * @throws IOException if invoked before the connection is opened
+ */
+ public abstract SseEventReader getEventReader() throws IOException;
+
+ /**
+ * Returns {@link HttpRedirectPolicy} indicating the policy for
+ * following HTTP redirects (3xx). The default option is
+ * {@link HttpRedirectPolicy#NONE}.
+ *
+ * @return HttpRedirectOption indicating the
+ */
+ public abstract HttpRedirectPolicy getFollowRedirect();
+
+ /**
+ * Returns the retry timeout in milliseconds. The default is 3000ms.
+ *
+ * @return retry timeout in milliseconds
+ */
+ public abstract long getRetryTimeout();
+
+ /**
+ * Sets {@link HttpRedirectPolicy} indicating the policy for
+ * following HTTP redirects (3xx).
+ *
+ * @param option HttpRedirectOption to used for following the
+ * redirects
+ */
+ public abstract void setFollowRedirect(HttpRedirectPolicy option);
+
+ /**
+ * Sets the retry timeout specified in milliseconds.
+ *
+ * @param millis retry timeout
+ */
+ public abstract void setRetryTimeout(long millis);
+}
diff --git a/android/src/main/java/org/kaazing/net/sse/SseEventSourceFactory.java b/android/src/main/java/org/kaazing/net/sse/SseEventSourceFactory.java
new file mode 100755
index 0000000..bffcb45
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/sse/SseEventSourceFactory.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.sse;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ServiceLoader;
+
+import org.kaazing.net.http.HttpRedirectPolicy;
+
+/**
+ * {@link SseEventSourceFactory} is an abstract class that can be used to create
+ * {@link SseEventSource}s by specifying the end-point. It may be extended to
+ * instantiate particular subclasses of {@link SseEventSource} and thus provide
+ * a general framework for the addition of public SSE-level functionality.
+ */
+public abstract class SseEventSourceFactory {
+
+ protected SseEventSourceFactory() {
+
+ }
+
+ /**
+ * Creates and returns a new instance of the default implementation of the
+ * {@link SseEventSourceFactory}.
+ *
+ * @return SseEventSourceFactory
+ */
+ public static SseEventSourceFactory createEventSourceFactory() {
+ Class clazz = SseEventSourceFactory.class;
+ ServiceLoader loader = ServiceLoader.load(clazz);
+ return loader.iterator().next();
+ }
+
+ /**
+ * Creates a {@link SseEventSource} to connect to the target location.
+ *
+ *
+ * @param location URI of the SSE provider for the connection
+ * @throws URISyntaxException
+ */
+ public abstract SseEventSource createEventSource(URI location)
+ throws URISyntaxException;
+
+ /**
+ * Returns the default {@link HttpRedirectPolicy} that was specified at
+ * on the factory.
+ *
+ * ### TODO: If this wasn't set, should we return HttpRedirectOption.NONE or
+ * null.
+ *
+ * @return HttpRedirectOption
+ */
+ public abstract HttpRedirectPolicy getDefaultFollowRedirect();
+
+ /**
+ * Returns the default retry timeout. The default is 3000 milliseconds.
+ *
+ * @return retry timeout in milliseconds
+ */
+ public abstract long getDefaultRetryTimeout();
+
+ /**
+ * Sets the default {@link HttpRedirectPolicy} that is to be inherited by
+ * all the {@link EventSource}s created using this factory instance.
+ *
+ * @param option HttpRedirectOption
+ */
+ public abstract void setDefaultFollowRedirect(HttpRedirectPolicy option);
+
+ /**
+ * Sets the default retry timeout that is to be inherited by all the
+ * {@link EventSource}s created using this factory instance.
+ *
+ * @param millis retry timeout
+ */
+ public abstract void setDefaultRetryTimeout(long millis);
+}
diff --git a/android/src/main/java/org/kaazing/net/sse/SseEventType.java b/android/src/main/java/org/kaazing/net/sse/SseEventType.java
new file mode 100755
index 0000000..9f9c177
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/sse/SseEventType.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.sse;
+
+public enum SseEventType {
+ EOS, EMPTY, DATA;
+}
\ No newline at end of file
diff --git a/android/src/main/java/org/kaazing/net/sse/SseException.java b/android/src/main/java/org/kaazing/net/sse/SseException.java
new file mode 100755
index 0000000..11fce00
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/sse/SseException.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.sse;
+
+import java.io.IOException;
+
+public class SseException extends IOException {
+
+ private static final long serialVersionUID = 1L;
+
+ public SseException(String reason) {
+ super(reason);
+ }
+
+ public SseException(Exception ex) {
+ super(ex);
+ }
+}
diff --git a/android/src/main/java/org/kaazing/net/sse/impl/DefaultEventSourceFactory.java b/android/src/main/java/org/kaazing/net/sse/impl/DefaultEventSourceFactory.java
new file mode 100755
index 0000000..513c477
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/sse/impl/DefaultEventSourceFactory.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.sse.impl;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.kaazing.net.http.HttpRedirectPolicy;
+import org.kaazing.net.sse.SseEventSource;
+import org.kaazing.net.sse.SseEventSourceFactory;
+
+public class DefaultEventSourceFactory extends SseEventSourceFactory {
+
+ private long _retryTimeout;
+ private HttpRedirectPolicy _redirectOption;
+
+ public DefaultEventSourceFactory() {
+ _retryTimeout = 3000;
+
+ // ### TODO: Should _redirectOption be null or
+ // HttpRedirectOption.ALWAYS by default. Note that in
+ // HttpURLConnection, followRedirects is true by default.
+ _redirectOption = HttpRedirectPolicy.ALWAYS;
+ }
+
+ @Override
+ public SseEventSource createEventSource(URI location)
+ throws URISyntaxException {
+
+ String scheme = location.getScheme();
+ if (!scheme.toLowerCase().equals("sse") &&
+ !scheme.toLowerCase().equals("http") &&
+ !scheme.toLowerCase().equals("https")) {
+ String s = String.format("Incorrect scheme or protocol '%s'", scheme);
+ throw new URISyntaxException(location.toString(), s);
+ }
+
+ SseEventSourceImpl eventSource = new SseEventSourceImpl(location);
+
+ // Set up the defaults from the factory.
+ eventSource.setFollowRedirect(_redirectOption);
+ eventSource.setRetryTimeout(_retryTimeout);
+
+ return eventSource;
+ }
+
+ @Override
+ public HttpRedirectPolicy getDefaultFollowRedirect() {
+ return _redirectOption;
+ }
+
+ @Override
+ public long getDefaultRetryTimeout() {
+ return _retryTimeout;
+ }
+
+ @Override
+ public void setDefaultFollowRedirect(HttpRedirectPolicy redirectOption) {
+ _redirectOption = redirectOption;
+ }
+
+ @Override
+ public void setDefaultRetryTimeout(long millis) {
+ _retryTimeout = millis;
+ }
+}
diff --git a/android/src/main/java/org/kaazing/net/sse/impl/SseEventReaderImpl.java b/android/src/main/java/org/kaazing/net/sse/impl/SseEventReaderImpl.java
new file mode 100755
index 0000000..2bd5c13
--- /dev/null
+++ b/android/src/main/java/org/kaazing/net/sse/impl/SseEventReaderImpl.java
@@ -0,0 +1,205 @@
+/**
+ * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved.
+ *
+ * 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.kaazing.net.sse.impl;
+
+import java.io.IOException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.kaazing.net.impl.util.BlockingQueueImpl;
+import org.kaazing.net.sse.SseEventReader;
+import org.kaazing.net.sse.SseEventType;
+import org.kaazing.net.sse.SseException;
+
+public class SseEventReaderImpl extends SseEventReader {
+ private static final String _CLASS_NAME = SseEventReaderImpl.class.getName();
+ private static final Logger _LOG = Logger.getLogger(_CLASS_NAME);
+
+ private final BlockingQueueImpl